Merge pull request #312 from loathingKernel/fixups
https://github.com/RareDevs/Rare/pull/312#issue-2002162120
This commit is contained in:
commit
c7efe3615a
7
.github/workflows/release-tests.yml
vendored
7
.github/workflows/release-tests.yml
vendored
|
@ -1,5 +1,5 @@
|
|||
|
||||
name: "Development Snapshot"
|
||||
name: "Snapshot"
|
||||
|
||||
|
||||
on:
|
||||
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
jobs:
|
||||
version:
|
||||
name: "Describe version"
|
||||
name: "Version"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag_abbrev: ${{ steps.version.outputs.tag_abbrev }}
|
||||
|
@ -239,7 +239,8 @@ jobs:
|
|||
git clone https://github.com/create-dmg/create-dmg
|
||||
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: Upload to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.dmg
|
||||
path: Rare.dmg
|
||||
|
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
|
@ -14,8 +14,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -65,8 +63,6 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
|
@ -105,8 +101,6 @@ jobs:
|
|||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
|
@ -170,8 +164,6 @@ jobs:
|
|||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
|
@ -202,8 +194,6 @@ jobs:
|
|||
runs-on: "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
|
@ -236,8 +226,6 @@ jobs:
|
|||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "release"
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
cache: pip
|
||||
|
@ -264,7 +252,7 @@ jobs:
|
|||
git clone https://github.com/create-dmg/create-dmg
|
||||
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
|
||||
|
||||
- name: upload to GitHub
|
||||
- name: Upload to Releases
|
||||
uses: svenstaro/upload-release-action@2.2.1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
__version__ = "1.10.3"
|
||||
__version__ = "1.10.4"
|
||||
__codename__ = "Garlic Crab"
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import Tuple, List, Union, Optional
|
|||
|
||||
from PyQt5.QtCore import Qt, QThreadPool, QSettings, QCoreApplication
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent, QShowEvent
|
||||
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox, QLayout, QWidget, QVBoxLayout
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
|
@ -66,6 +66,8 @@ class InstallDialog(QDialog):
|
|||
header = self.tr("Repair and update")
|
||||
elif options.update:
|
||||
header = self.tr("Update")
|
||||
elif options.reset_sdl:
|
||||
header = self.tr("Modify")
|
||||
else:
|
||||
header = self.tr("Install")
|
||||
self.ui.install_dialog_label.setText(f'<h3>{header} "{self.rgame.app_title}"</h3>')
|
||||
|
@ -95,12 +97,8 @@ class InstallDialog(QDialog):
|
|||
|
||||
self.error_box()
|
||||
|
||||
platforms = ["Windows"]
|
||||
if self.rgame.is_win32:
|
||||
platforms.append("Win32")
|
||||
if self.rgame.is_mac:
|
||||
platforms.append("Mac")
|
||||
self.ui.platform_combo.addItems(platforms)
|
||||
platforms = self.rgame.platforms
|
||||
self.ui.platform_combo.addItems(reversed(platforms))
|
||||
self.ui.platform_combo.currentIndexChanged.connect(lambda: self.option_changed(None))
|
||||
self.ui.platform_combo.currentIndexChanged.connect(lambda: self.error_box())
|
||||
self.ui.platform_combo.currentIndexChanged.connect(
|
||||
|
@ -113,8 +111,11 @@ class InstallDialog(QDialog):
|
|||
if (self.ui.platform_combo.currentText() == "Mac" and pf.system() != "Darwin")
|
||||
else None
|
||||
)
|
||||
if pf.system() == "Darwin" and "Mac" in platforms:
|
||||
self.ui.platform_combo.setCurrentIndex(platforms.index("Mac"))
|
||||
self.ui.platform_combo.setCurrentIndex(
|
||||
self.ui.platform_combo.findText(
|
||||
"Mac" if (pf.system() == "Darwin" and "Mac" in platforms) else "Windows"
|
||||
)
|
||||
)
|
||||
self.ui.platform_combo.currentTextChanged.connect(self.setup_sdl_list)
|
||||
|
||||
self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
|
||||
|
@ -135,7 +136,7 @@ class InstallDialog(QDialog):
|
|||
|
||||
self.selectable_checks: List[TagCheckBox] = []
|
||||
self.config_tags: Optional[List[str]] = None
|
||||
self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows")
|
||||
self.setup_sdl_list(self.ui.platform_combo.currentText())
|
||||
|
||||
self.ui.install_button.setEnabled(False)
|
||||
|
||||
|
@ -171,6 +172,12 @@ class InstallDialog(QDialog):
|
|||
|
||||
self.ui.install_dialog_layout.setSizeConstraint(QLayout.SetFixedSize)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.save_install_edit(self.install_dir_edit.text())
|
||||
super().showEvent(a0)
|
||||
|
||||
def execute(self):
|
||||
if self.options.silent:
|
||||
self.reject_close = False
|
||||
|
@ -192,7 +199,6 @@ class InstallDialog(QDialog):
|
|||
config_disable_sdl = self.core.lgd.config.getboolean(self.rgame.app_name, 'disable_sdl', fallback=False)
|
||||
sdl_name = get_sdl_appname(self.rgame.app_name)
|
||||
if not config_disable_sdl and sdl_name is not None:
|
||||
# FIXME: this should be updated whenever platform changes
|
||||
sdl_data = self.core.get_sdl_data(sdl_name, platform=platform)
|
||||
if sdl_data:
|
||||
widget = QWidget(self.selectable)
|
||||
|
|
|
@ -143,15 +143,6 @@ class MainWindow(QMainWindow):
|
|||
self.resize(window_size)
|
||||
self.move(screen_rect.center() - self.rect().adjusted(0, 0, decor_width, decor_height).center())
|
||||
|
||||
# lk: For the gritty details see `RareCore.load_pixmaps()` method
|
||||
# Just before the window is shown, fire a timer to load game icons
|
||||
# This is by nature a little iffy because we don't really know if the
|
||||
# has been shown, and it might make the window delay as widgets being are updated.
|
||||
# Still better than showing a hanged window frame for a few seconds.
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if not self._window_launched:
|
||||
QTimer.singleShot(100, self.rcore.load_pixmaps)
|
||||
|
||||
@pyqtSlot()
|
||||
def show(self) -> None:
|
||||
super(MainWindow, self).show()
|
||||
|
|
|
@ -102,7 +102,7 @@ class DownloadsTab(QWidget):
|
|||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(RareGame)
|
||||
def __add_update(self, update: Union[str,RareGame]):
|
||||
def __add_update(self, update: Union[str, RareGame]):
|
||||
if isinstance(update, str):
|
||||
update = self.rcore.get_game(update)
|
||||
if update.metadata.auto_update or QSettings().value("auto_update", False, bool):
|
||||
|
@ -192,12 +192,12 @@ class DownloadsTab(QWidget):
|
|||
if item.expired:
|
||||
self.__refresh_download(item)
|
||||
return
|
||||
thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug)
|
||||
thread.result.connect(self.__on_download_result)
|
||||
thread.progress.connect(self.__on_download_progress)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.start()
|
||||
self.__thread = thread
|
||||
dl_thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug)
|
||||
dl_thread.result.connect(self.__on_download_result)
|
||||
dl_thread.progress.connect(self.__on_download_progress)
|
||||
dl_thread.finished.connect(dl_thread.deleteLater)
|
||||
dl_thread.start()
|
||||
self.__thread = dl_thread
|
||||
self.download_widget.ui.kill_button.setDisabled(False)
|
||||
self.download_widget.ui.dl_name.setText(item.download.game.app_title)
|
||||
self.download_widget.setPixmap(
|
||||
|
|
|
@ -9,7 +9,7 @@ from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
|
|||
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
|
||||
from rare.widgets.image_widget import ImageWidget, ImageSize
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from rare.utils.misc import widget_object_name
|
||||
from rare.utils.misc import widget_object_name, icon
|
||||
|
||||
|
||||
class GameDlcWidget(QFrame):
|
||||
|
@ -48,6 +48,7 @@ class InstalledGameDlcWidget(GameDlcWidget):
|
|||
self.ui.action_button.setObjectName("UninstallButton")
|
||||
self.ui.action_button.clicked.connect(self.uninstall_dlc)
|
||||
self.ui.action_button.setText(self.tr("Uninstall DLC"))
|
||||
self.ui.action_button.setIcon(icon("ri.uninstall-line"))
|
||||
# lk: don't reference `self.rdlc` here because the object has been deleted
|
||||
rdlc.signals.game.uninstalled.connect(self.__uninstalled)
|
||||
|
||||
|
@ -68,6 +69,8 @@ class AvailableGameDlcWidget(GameDlcWidget):
|
|||
self.ui.action_button.setObjectName("InstallButton")
|
||||
self.ui.action_button.clicked.connect(self.install_dlc)
|
||||
self.ui.action_button.setText(self.tr("Install DLC"))
|
||||
self.ui.action_button.setIcon(icon("ri.install-line"))
|
||||
|
||||
# lk: don't reference `self.rdlc` here because the object has been deleted
|
||||
rdlc.signals.game.installed.connect(self.__installed)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from rare.models.game import RareGame
|
|||
from rare.shared import RareCore
|
||||
from rare.shared.workers import VerifyWorker, MoveWorker
|
||||
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
||||
from rare.utils.misc import format_size
|
||||
from rare.utils.misc import format_size, icon
|
||||
from rare.widgets.image_widget import ImageWidget, ImageSize
|
||||
from rare.widgets.side_tab import SideTabContents
|
||||
from .move_game import MoveGamePopUp, is_game_dir
|
||||
|
@ -38,8 +38,18 @@ class GameInfo(QWidget, SideTabContents):
|
|||
self.ui.setupUi(self)
|
||||
# lk: set object names for CSS properties
|
||||
self.ui.install_button.setObjectName("InstallButton")
|
||||
self.ui.modify_button.setObjectName("InstallButton")
|
||||
self.ui.uninstall_button.setObjectName("UninstallButton")
|
||||
|
||||
self.ui.install_button.setIcon(icon("ri.install-line"))
|
||||
self.ui.import_button.setIcon(icon("mdi.file-import"))
|
||||
|
||||
self.ui.modify_button.setIcon(icon("fa.gear"))
|
||||
self.ui.verify_button.setIcon(icon("fa.check"))
|
||||
self.ui.repair_button.setIcon(icon("fa.wrench"))
|
||||
self.ui.move_button.setIcon(icon("mdi.folder-move"))
|
||||
self.ui.uninstall_button.setIcon(icon("ri.uninstall-line"))
|
||||
|
||||
self.rcore = RareCore.instance()
|
||||
self.core = RareCore.instance().core()
|
||||
self.args = RareCore.instance().args()
|
||||
|
@ -53,6 +63,7 @@ class GameInfo(QWidget, SideTabContents):
|
|||
|
||||
self.ui.install_button.clicked.connect(self.__on_install)
|
||||
self.ui.import_button.clicked.connect(self.__on_import)
|
||||
self.ui.modify_button.clicked.connect(self.__on_modify)
|
||||
self.ui.verify_button.clicked.connect(self.__on_verify)
|
||||
self.ui.repair_button.clicked.connect(self.__on_repair)
|
||||
self.ui.uninstall_button.clicked.connect(self.__on_uninstall)
|
||||
|
@ -78,7 +89,7 @@ class GameInfo(QWidget, SideTabContents):
|
|||
"na": self.tr("Not applicable"),
|
||||
}
|
||||
|
||||
# lk: requirements is unused so hide it
|
||||
# lk: hide unfinished things
|
||||
self.ui.requirements_group.setVisible(False)
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -97,6 +108,11 @@ class GameInfo(QWidget, SideTabContents):
|
|||
""" This method is to be called from the button only """
|
||||
self.rgame.uninstall()
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_modify(self):
|
||||
""" This method is to be called from the button only """
|
||||
self.rgame.modify()
|
||||
|
||||
@pyqtSlot()
|
||||
def __on_repair(self):
|
||||
""" This method is to be called from the button only """
|
||||
|
@ -286,6 +302,13 @@ class GameInfo(QWidget, SideTabContents):
|
|||
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle
|
||||
)
|
||||
|
||||
self.ui.modify_button.setEnabled(
|
||||
self.rgame.is_installed
|
||||
and (not self.rgame.is_non_asset)
|
||||
and self.rgame.is_idle
|
||||
and self.rgame.sdl_name is not None
|
||||
)
|
||||
|
||||
self.ui.verify_button.setEnabled(
|
||||
self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle
|
||||
)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import platform
|
||||
import random
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent, QTimer
|
||||
from PyQt5.QtGui import QMouseEvent, QShowEvent
|
||||
from PyQt5.QtWidgets import QMessageBox, QAction
|
||||
|
||||
from rare.models.game import RareGame
|
||||
|
@ -38,9 +39,6 @@ class GameWidget(LibraryWidget):
|
|||
self.install_action = QAction(self.tr("Install"), self)
|
||||
self.install_action.triggered.connect(self._install)
|
||||
|
||||
# self.sync_action = QAction(self.tr("Sync with cloud"), self)
|
||||
# self.sync_action.triggered.connect(self.sync_saves)
|
||||
|
||||
self.desktop_link_action = QAction(self)
|
||||
self.desktop_link_action.triggered.connect(
|
||||
lambda: self._create_link(self.rgame.folder_name, "desktop")
|
||||
|
@ -108,6 +106,13 @@ class GameWidget(LibraryWidget):
|
|||
# lk: attributes as `GameWidgetUi` class
|
||||
__slots__ = "ui"
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
if self.rgame.pixmap.isNull():
|
||||
QTimer.singleShot(random.randrange(42, 361, 7), self.rgame.load_pixmap)
|
||||
super().showEvent(a0)
|
||||
|
||||
@pyqtSlot()
|
||||
def update_state(self):
|
||||
if self.rgame.is_idle:
|
||||
|
@ -144,9 +149,6 @@ class GameWidget(LibraryWidget):
|
|||
else:
|
||||
self.addAction(self.install_action)
|
||||
|
||||
# if self.rgame.game.supports_cloud_saves:
|
||||
# self.addAction(self.sync_action)
|
||||
|
||||
if desktop_links_supported() and self.rgame.is_installed:
|
||||
if desktop_link_path(self.rgame.folder_name, "desktop").exists():
|
||||
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
|
||||
|
@ -209,8 +211,6 @@ class GameWidget(LibraryWidget):
|
|||
def _launch(self, offline=False, skip_version_check=False):
|
||||
if offline or (self.rgame.is_foreign and self.rgame.can_run_offline):
|
||||
offline = True
|
||||
# if self.rgame.game.supports_cloud_saves and not offline:
|
||||
# self.syncing_cloud_saves = True
|
||||
if self.rgame.has_update:
|
||||
skip_version_check = True
|
||||
self.rgame.launch(
|
||||
|
@ -261,21 +261,3 @@ class GameWidget(LibraryWidget):
|
|||
self.desktop_link_action.setText(self.tr("Create Desktop link"))
|
||||
elif link_type == "start_menu":
|
||||
self.menu_link_action.setText(self.tr("Create Start Menu link"))
|
||||
|
||||
# def sync_finished(self, app_name):
|
||||
# self.syncing_cloud_saves = False
|
||||
|
||||
# def sync_game(self):
|
||||
# try:
|
||||
# sync = self.game_utils.cloud_save_utils.sync_before_launch_game(
|
||||
# self.rgame.app_name, True
|
||||
# )
|
||||
# except Exception:
|
||||
# sync = False
|
||||
# if sync:
|
||||
# self.syncing_cloud_saves = True
|
||||
|
||||
# def game_finished(self, app_name, error):
|
||||
# if error:
|
||||
# QMessageBox.warning(self, "Error", error)
|
||||
# self.game_running = False
|
||||
|
|
|
@ -32,7 +32,10 @@ class ProgressLabel(QLabel):
|
|||
return super().event(e)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.__center_on_parent()
|
||||
super().showEvent(a0)
|
||||
|
||||
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
|
||||
if a0 is self.parent() and a1.type() == QEvent.Resize:
|
||||
|
|
|
@ -36,8 +36,8 @@ class IntegrationsTabs(SideTabWidget):
|
|||
)
|
||||
self.ubisoft_group = UbisoftGroup(self.eos_ubisoft)
|
||||
self.eos_group = EOSGroup(self.eos_ubisoft)
|
||||
self.eos_ubisoft.addWidget(self.ubisoft_group)
|
||||
self.eos_ubisoft.addWidget(self.eos_group)
|
||||
self.eos_ubisoft.addWidget(self.ubisoft_group)
|
||||
self.eos_ubisoft_index = self.addTab(self.eos_ubisoft, self.tr("Epic Overlay and Ubisoft"))
|
||||
|
||||
self.setCurrentIndex(self.import_index)
|
||||
|
|
|
@ -5,6 +5,7 @@ from logging import getLogger
|
|||
from typing import Tuple, Iterable, List, Union
|
||||
|
||||
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox, QFrame, QLabel
|
||||
from legendary.models.egl import EGLManifest
|
||||
from legendary.models.game import InstalledGame
|
||||
|
@ -15,8 +16,8 @@ from rare.shared import RareCore
|
|||
from rare.shared.workers.wine_resolver import WineResolver
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
|
||||
|
||||
logger = getLogger("EGLSync")
|
||||
|
||||
|
@ -28,8 +29,20 @@ class EGLSyncGroup(QGroupBox):
|
|||
self.ui.setupUi(self)
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
self.egl_path_edit = PathEdit(
|
||||
path=self.core.egl.programdata_path,
|
||||
placeholder=self.tr(
|
||||
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
|
||||
),
|
||||
file_mode=QFileDialog.DirectoryOnly,
|
||||
edit_func=self.egl_path_edit_edit_cb,
|
||||
save_func=self.egl_path_edit_save_cb,
|
||||
parent=self,
|
||||
)
|
||||
self.ui.egl_path_edit_layout.addWidget(self.egl_path_edit)
|
||||
|
||||
self.egl_path_info_label = QLabel(self.tr("Estimated path"), self)
|
||||
self.egl_path_info = ElideLabel("", parent=self)
|
||||
self.egl_path_info = ElideLabel(parent=self)
|
||||
self.egl_path_info.setProperty("infoLabel", 1)
|
||||
self.ui.egl_sync_layout.insertRow(
|
||||
self.ui.egl_sync_layout.indexOf(self.ui.egl_path_edit_label) + 1,
|
||||
|
@ -37,33 +50,15 @@ class EGLSyncGroup(QGroupBox):
|
|||
)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
self.ui.egl_path_edit_label.setVisible(False)
|
||||
self.egl_path_info_label.setVisible(False)
|
||||
self.egl_path_info.setVisible(False)
|
||||
self.ui.egl_path_edit_label.setEnabled(False)
|
||||
self.egl_path_edit.setEnabled(False)
|
||||
self.egl_path_info_label.setEnabled(False)
|
||||
self.egl_path_info.setEnabled(False)
|
||||
else:
|
||||
self.egl_path_edit = PathEdit(
|
||||
path=self.core.egl.programdata_path,
|
||||
placeholder=self.tr(
|
||||
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
|
||||
),
|
||||
file_mode=QFileDialog.DirectoryOnly,
|
||||
edit_func=self.egl_path_edit_edit_cb,
|
||||
save_func=self.egl_path_edit_save_cb,
|
||||
parent=self,
|
||||
)
|
||||
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
|
||||
self.ui.egl_path_edit_layout.addWidget(self.egl_path_edit)
|
||||
|
||||
if not self.core.egl.programdata_path:
|
||||
self.egl_path_info.setText(self.tr("Updating..."))
|
||||
wine_resolver = WineResolver(
|
||||
self.core, PathSpec.egl_programdata, "default"
|
||||
)
|
||||
wine_resolver.signals.result_ready.connect(self.wine_resolver_cb)
|
||||
QThreadPool.globalInstance().start(wine_resolver)
|
||||
else:
|
||||
self.egl_path_info_label.setVisible(False)
|
||||
self.egl_path_info.setVisible(False)
|
||||
if self.core.egl.programdata_path:
|
||||
self.egl_path_info_label.setEnabled(True)
|
||||
self.egl_path_info.setEnabled(True)
|
||||
|
||||
self.ui.egl_sync_check.setChecked(self.core.egl_sync_enabled)
|
||||
self.ui.egl_sync_check.stateChanged.connect(self.egl_sync_changed)
|
||||
|
@ -76,9 +71,25 @@ class EGLSyncGroup(QGroupBox):
|
|||
# self.egl_watcher = QFileSystemWatcher([self.egl_path_edit.text()], self)
|
||||
# self.egl_watcher.directoryChanged.connect(self.update_lists)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
if not self.core.egl.programdata_path:
|
||||
self.__run_wine_resolver()
|
||||
self.update_lists()
|
||||
super().showEvent(a0)
|
||||
|
||||
def wine_resolver_cb(self, path):
|
||||
def __run_wine_resolver(self):
|
||||
self.egl_path_info.setText(self.tr("Updating..."))
|
||||
wine_resolver = WineResolver(
|
||||
self.core,
|
||||
PathSpec.egl_programdata,
|
||||
"default"
|
||||
)
|
||||
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
|
||||
QThreadPool.globalInstance().start(wine_resolver)
|
||||
|
||||
def __on_wine_resolver_result(self, path):
|
||||
self.egl_path_info.setText(path)
|
||||
if not path:
|
||||
self.egl_path_info.setText(
|
||||
|
@ -162,9 +173,13 @@ class EGLSyncGroup(QGroupBox):
|
|||
|
||||
def update_lists(self):
|
||||
# self.egl_watcher.blockSignals(True)
|
||||
if have_path := bool(self.core.egl.programdata_path) and os.path.exists(
|
||||
self.core.egl.programdata_path
|
||||
):
|
||||
have_path = False
|
||||
if self.core.egl.programdata_path:
|
||||
have_path = os.path.exists(self.core.egl.programdata_path)
|
||||
if not have_path and os.path.isdir(os.path.dirname(self.core.egl.programdata_path)):
|
||||
# NOTE: This might happen if EGL is installed but no games have been installed through it
|
||||
os.mkdir(self.core.egl.programdata_path)
|
||||
have_path = os.path.isdir(self.core.egl.programdata_path)
|
||||
# NOTE: need to clear known manifests to force refresh
|
||||
self.core.egl.manifests.clear()
|
||||
self.ui.egl_sync_check_label.setEnabled(have_path)
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
import time
|
||||
import webbrowser
|
||||
from logging import getLogger
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSize
|
||||
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QPushButton, QGroupBox, QVBoxLayout
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSize, pyqtSlot, Qt
|
||||
from PyQt5.QtGui import QShowEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QFrame,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QSizePolicy,
|
||||
QPushButton,
|
||||
QGroupBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from legendary.models.game import Game
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.shared import RareCore
|
||||
from rare.shared.workers.worker import Worker
|
||||
from rare.utils.metrics import timelogger
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
from rare.widgets.loading_widget import LoadingWidget
|
||||
|
||||
logger = getLogger("Ubisoft")
|
||||
|
||||
|
@ -17,15 +31,16 @@ class UbiGetInfoWorker(Worker):
|
|||
class Signals(QObject):
|
||||
worker_finished = pyqtSignal(set, set, str)
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, core: LegendaryCore):
|
||||
super(UbiGetInfoWorker, self).__init__()
|
||||
self.signals = UbiGetInfoWorker.Signals()
|
||||
self.setAutoDelete(True)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.core = core
|
||||
|
||||
def run_real(self) -> None:
|
||||
try:
|
||||
external_auths = self.core.egs.get_external_auths()
|
||||
with timelogger(logger, "Request external auths"):
|
||||
external_auths = self.core.egs.get_external_auths()
|
||||
for ext_auth in external_auths:
|
||||
if ext_auth["type"] != "ubisoft":
|
||||
continue
|
||||
|
@ -35,12 +50,17 @@ class UbiGetInfoWorker(Worker):
|
|||
self.signals.worker_finished.emit(set(), set(), "")
|
||||
return
|
||||
|
||||
uplay_keys = self.core.egs.store_get_uplay_codes()
|
||||
with timelogger(logger, "Request uplay codes"):
|
||||
uplay_keys = self.core.egs.store_get_uplay_codes()
|
||||
key_list = uplay_keys["data"]["PartnerIntegration"]["accountUplayCodes"]
|
||||
redeemed = {k["gameId"] for k in key_list if k["redeemedOnUplay"]}
|
||||
|
||||
entitlements = self.core.egs.get_user_entitlements()
|
||||
if (entitlements := self.core.lgd.entitlements) is None:
|
||||
with timelogger(logger, "Request entitlements"):
|
||||
entitlements = self.core.egs.get_user_entitlements()
|
||||
self.core.lgd.entitlements = 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))
|
||||
|
@ -51,11 +71,11 @@ class UbiConnectWorker(Worker):
|
|||
class Signals(QObject):
|
||||
linked = pyqtSignal(str)
|
||||
|
||||
def __init__(self, ubi_account_id, partner_link_id):
|
||||
def __init__(self, core: LegendaryCore, ubi_account_id, partner_link_id):
|
||||
super(UbiConnectWorker, self).__init__()
|
||||
self.signals = UbiConnectWorker.Signals()
|
||||
self.setAutoDelete(True)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.core = core
|
||||
self.ubi_account_id = ubi_account_id
|
||||
self.partner_link_id = partner_link_id
|
||||
|
||||
|
@ -65,9 +85,7 @@ class UbiConnectWorker(Worker):
|
|||
self.signals.linked.emit("")
|
||||
return
|
||||
try:
|
||||
self.core.egs.store_claim_uplay_code(
|
||||
self.ubi_account_id, self.partner_link_id
|
||||
)
|
||||
self.core.egs.store_claim_uplay_code(self.ubi_account_id, self.partner_link_id)
|
||||
self.core.egs.store_redeem_uplay_codes(self.ubi_account_id)
|
||||
except Exception as e:
|
||||
self.signals.linked.emit(str(e))
|
||||
|
@ -76,55 +94,58 @@ class UbiConnectWorker(Worker):
|
|||
self.signals.linked.emit("")
|
||||
|
||||
|
||||
class UbiLinkWidget(QWidget):
|
||||
def __init__(self, game: Game, ubi_account_id):
|
||||
super(UbiLinkWidget, self).__init__()
|
||||
self.args = ArgumentsSingleton()
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
class UbiLinkWidget(QFrame):
|
||||
def __init__(self, game: Game, ubi_account_id, activated: bool = False, parent=None):
|
||||
super(UbiLinkWidget, self).__init__(parent=parent)
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
self.args = RareCore.instance().args()
|
||||
self.game = game
|
||||
self.ubi_account_id = ubi_account_id
|
||||
|
||||
self.title_label = QLabel(game.app_title)
|
||||
layout.addWidget(self.title_label, stretch=1)
|
||||
|
||||
self.ok_indicator = QLabel()
|
||||
self.ok_indicator.setPixmap(icon("fa.info-circle", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator = QLabel(parent=self)
|
||||
self.ok_indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20))
|
||||
self.ok_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
|
||||
layout.addWidget(self.ok_indicator)
|
||||
|
||||
self.link_button = QPushButton(
|
||||
self.tr("Redeem to Ubisoft") + ": Test" if self.args.debug else ""
|
||||
)
|
||||
layout.addWidget(self.link_button)
|
||||
self.title_label = ElideLabel(game.app_title, parent=self)
|
||||
|
||||
self.link_button = QPushButton(self.tr("Redeem in Ubisoft"), parent=self)
|
||||
self.link_button.setMinimumWidth(150)
|
||||
self.link_button.clicked.connect(self.activate)
|
||||
self.setLayout(layout)
|
||||
|
||||
if activated:
|
||||
self.link_button.setText(self.tr("Already activated"))
|
||||
self.link_button.setDisabled(True)
|
||||
self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(-1, 0, 0, 0)
|
||||
layout.addWidget(self.ok_indicator)
|
||||
layout.addWidget(self.title_label, stretch=1)
|
||||
layout.addWidget(self.link_button)
|
||||
|
||||
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 self.args.debug:
|
||||
worker = UbiConnectWorker(None, None)
|
||||
worker = UbiConnectWorker(RareCore.instance().core(), None, None)
|
||||
else:
|
||||
worker = UbiConnectWorker(self.ubi_account_id, self.game.partner_link_id)
|
||||
worker = UbiConnectWorker(
|
||||
RareCore.instance().core(), self.ubi_account_id, self.game.partner_link_id
|
||||
)
|
||||
worker.signals.linked.connect(self.worker_finished)
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
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("fa.check-circle-o", 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.times-circle-o", color="red").pixmap(QSize(20, 20)))
|
||||
self.ok_indicator.setToolTip(error)
|
||||
self.link_button.setText(self.tr("Try again"))
|
||||
self.link_button.setDisabled(False)
|
||||
|
@ -134,86 +155,120 @@ class UbisoftGroup(QGroupBox):
|
|||
def __init__(self, parent=None):
|
||||
super(UbisoftGroup, self).__init__(parent=parent)
|
||||
self.setTitle(self.tr("Link Ubisoft Games"))
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.args = ArgumentsSingleton()
|
||||
self.rcore = RareCore.instance()
|
||||
self.core = RareCore.instance().core()
|
||||
self.args = RareCore.instance().args()
|
||||
|
||||
self.thread_pool = QThreadPool.globalInstance()
|
||||
worker = UbiGetInfoWorker()
|
||||
worker.signals.worker_finished.connect(self.show_ubi_games)
|
||||
self.thread_pool.start(worker)
|
||||
self.worker: Optional[UbiGetInfoWorker] = None
|
||||
|
||||
self.info_label = QLabel(parent=self)
|
||||
self.info_label.setText(self.tr("Getting information about your redeemable Ubisoft games."))
|
||||
self.browser_button = QPushButton(self.tr("Link Ubisoft acccount"), parent=self)
|
||||
self.browser_button.setMinimumWidth(140)
|
||||
self.browser_button.clicked.connect(
|
||||
lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")
|
||||
)
|
||||
self.browser_button.setEnabled(False)
|
||||
|
||||
self.loading_widget = LoadingWidget(self)
|
||||
self.loading_widget.stop()
|
||||
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.addWidget(self.info_label, stretch=1)
|
||||
header_layout.addWidget(self.browser_button)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addLayout(header_layout)
|
||||
layout.addWidget(self.loading_widget)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
|
||||
if self.worker is not None:
|
||||
return
|
||||
|
||||
for widget in self.findChildren(UbiLinkWidget, options=Qt.FindDirectChildrenOnly):
|
||||
widget.deleteLater()
|
||||
self.loading_widget.start()
|
||||
|
||||
self.worker = UbiGetInfoWorker(self.core)
|
||||
self.worker.signals.worker_finished.connect(self.show_ubi_games)
|
||||
self.thread_pool.start(self.worker)
|
||||
super().showEvent(a0)
|
||||
|
||||
@pyqtSlot(set, set, str)
|
||||
def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
|
||||
self.worker = None
|
||||
if not redeemed and ubi_account_id != "error":
|
||||
logger.error(
|
||||
"No linked ubisoft account found! Link your accounts via your browser and try again."
|
||||
)
|
||||
self.layout().addWidget(
|
||||
QLabel(
|
||||
self.tr(
|
||||
"Your account is not linked with Ubisoft. Please link your account first"
|
||||
)
|
||||
)
|
||||
self.info_label.setText(
|
||||
self.tr("Your account is not linked with Ubisoft. Please link your account and try again.")
|
||||
)
|
||||
open_browser_button = QPushButton(self.tr("Open link page"))
|
||||
open_browser_button.clicked.connect(
|
||||
lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")
|
||||
)
|
||||
self.layout().addWidget(open_browser_button)
|
||||
return
|
||||
self.browser_button.setEnabled(True)
|
||||
elif ubi_account_id == "error":
|
||||
self.layout().addWidget(QLabel(self.tr("An error occurred")))
|
||||
return
|
||||
self.info_label.setText(
|
||||
self.tr("An error has occurred while requesting your account's Ubisoft information.")
|
||||
)
|
||||
self.browser_button.setEnabled(True)
|
||||
else:
|
||||
self.browser_button.setEnabled(False)
|
||||
|
||||
games = self.core.get_game_list(False)
|
||||
uplay_games = []
|
||||
activated = 0
|
||||
for game in games:
|
||||
for dlc_data in game.metadata.get("dlcItemList", []):
|
||||
if dlc_data["entitlementName"] not in entitlements:
|
||||
uplay_games = []
|
||||
activated = 0
|
||||
for rgame in self.rcore.ubisoft_games:
|
||||
game = rgame.game
|
||||
for dlc_data in game.metadata.get("dlcItemList", []):
|
||||
if dlc_data["entitlementName"] not in entitlements:
|
||||
continue
|
||||
try:
|
||||
app_name = dlc_data["releaseInfo"][0]["appId"]
|
||||
except (IndexError, KeyError):
|
||||
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":
|
||||
continue
|
||||
if dlc_game.partner_link_id in redeemed:
|
||||
continue
|
||||
uplay_games.append(dlc_game)
|
||||
|
||||
if game.partner_link_type != "ubisoft":
|
||||
continue
|
||||
if game.partner_link_id in redeemed:
|
||||
activated += 1
|
||||
uplay_games.append(game)
|
||||
|
||||
try:
|
||||
app_name = dlc_data["releaseInfo"][0]["appId"]
|
||||
except (IndexError, KeyError):
|
||||
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":
|
||||
continue
|
||||
if dlc_game.partner_link_id in redeemed:
|
||||
continue
|
||||
uplay_games.append(dlc_game)
|
||||
|
||||
if game.partner_link_type != "ubisoft":
|
||||
continue
|
||||
if game.partner_link_id in redeemed:
|
||||
activated += 1
|
||||
continue
|
||||
uplay_games.append(game)
|
||||
|
||||
if not uplay_games:
|
||||
if activated >= 1:
|
||||
self.layout().addWidget(
|
||||
QLabel(
|
||||
self.tr("All your Ubisoft games have already been activated")
|
||||
)
|
||||
)
|
||||
if not uplay_games:
|
||||
self.info_label.setText(self.tr("You don't own any Ubisoft games."))
|
||||
else:
|
||||
self.layout().addWidget(
|
||||
QLabel(self.tr("You don't own any Ubisoft games"))
|
||||
)
|
||||
if self.args.debug:
|
||||
if activated == len(uplay_games):
|
||||
self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
|
||||
else:
|
||||
self.info_label.setText(
|
||||
self.tr("You have <b>{}</b> games available to redeem.").format(
|
||||
len(uplay_games) - activated
|
||||
)
|
||||
)
|
||||
logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
|
||||
|
||||
self.loading_widget.stop()
|
||||
|
||||
for game in uplay_games:
|
||||
widget = UbiLinkWidget(
|
||||
Game(app_name="Test", app_title="This is a test game"),
|
||||
ubi_account_id,
|
||||
game, ubi_account_id, activated=game.partner_link_id in redeemed, parent=self
|
||||
)
|
||||
self.layout().addWidget(widget)
|
||||
return
|
||||
logger.info(f"Found {len(uplay_games)} game(s) to redeem")
|
||||
|
||||
for game in uplay_games:
|
||||
widget = UbiLinkWidget(game, ubi_account_id)
|
||||
self.layout().addWidget(widget)
|
||||
if self.args.debug:
|
||||
widget = UbiLinkWidget(
|
||||
Game(app_name="RareTestGame", app_title="Super Fake Super Rare Super Test (Super?) Game"),
|
||||
ubi_account_id,
|
||||
activated=False,
|
||||
parent=self,
|
||||
)
|
||||
self.layout().addWidget(widget)
|
||||
self.browser_button.setEnabled(True)
|
||||
|
|
|
@ -14,6 +14,7 @@ class DxvkSettings(OverlaySettings):
|
|||
("devinfo", QCoreApplication.translate("DxvkSettings", "Show Device info")),
|
||||
("version", QCoreApplication.translate("DxvkSettings", "DXVK Version")),
|
||||
("api", QCoreApplication.translate("DxvkSettings", "D3D feature level")),
|
||||
("compiler", QCoreApplication.translate("DxvkSettings", "Compiler activity")),
|
||||
],
|
||||
[
|
||||
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
|
||||
|
|
|
@ -3,10 +3,11 @@ import sys
|
|||
from collections import ChainMap
|
||||
from typing import Any, Union
|
||||
|
||||
from rare.utils.misc import icon
|
||||
from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel, pyqtSlot
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class EnvVarsTableModel(QAbstractTableModel):
|
||||
|
@ -54,7 +55,6 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
try:
|
||||
return list(self.__data_map)[index]
|
||||
except Exception as e:
|
||||
print(e, index)
|
||||
return ""
|
||||
|
||||
def __is_local(self, index: Union[QModelIndex, int]):
|
||||
|
@ -220,7 +220,6 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
self.core.lgd.save_config()
|
||||
self.dataChanged.emit(self.index(index.row(), 0), index, [])
|
||||
self.headerDataChanged.emit(Qt.Vertical, index.row(), index.row())
|
||||
# pprint([item for item in self.__data_map.items()])
|
||||
return True
|
||||
|
||||
def removeRow(self, row: int, parent: QModelIndex = None) -> bool:
|
||||
|
@ -244,7 +243,6 @@ class EnvVarsTableModel(QAbstractTableModel):
|
|||
del self.__data_map[self.__key(row)]
|
||||
self.core.lgd.save_config()
|
||||
self.endRemoveRows()
|
||||
# pprint([item for item in self.__data_map.items()])
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from rare.shared import LegendaryCoreSingleton
|
|||
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
|
||||
from rare.utils import config_helper
|
||||
|
||||
logger = getLogger("Overlay")
|
||||
logger = getLogger("GameOverlays")
|
||||
|
||||
|
||||
class TextInputField(QLineEdit):
|
||||
|
|
|
@ -124,7 +124,6 @@ class ProtonSettings(QGroupBox):
|
|||
proton = proton.replace('"', "")
|
||||
self.proton_prefix.setEnabled(bool(proton))
|
||||
if proton:
|
||||
print(proton)
|
||||
self.ui.proton_combo.setCurrentText(
|
||||
f'"{proton.replace(" run", "")}" run'
|
||||
)
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import re
|
||||
import shutil
|
||||
from logging import getLogger
|
||||
from typing import Dict, List
|
||||
from typing import Dict, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
|
||||
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont
|
||||
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QInputDialog,
|
||||
QFrame,
|
||||
QMessageBox,
|
||||
QSizePolicy,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
QAction,
|
||||
QToolButton,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
from rare.shared import RareCore
|
||||
|
@ -30,60 +32,97 @@ extra_wrapper_regex = {
|
|||
}
|
||||
|
||||
|
||||
class Wrapper:
|
||||
pass
|
||||
|
||||
|
||||
class WrapperWidget(QFrame):
|
||||
update_wrapper = pyqtSignal(str, str)
|
||||
delete_wrapper = pyqtSignal(str)
|
||||
|
||||
def __init__(self, text: str, show_text=None, parent=None):
|
||||
super(WrapperWidget, self).__init__(parent=parent)
|
||||
if not show_text:
|
||||
show_text = text
|
||||
show_text = text.split()[0]
|
||||
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
|
||||
self.text = text
|
||||
self.text_lbl = QLabel(show_text, parent=self)
|
||||
self.text_lbl.setFont(QFont("monospace"))
|
||||
self.image_lbl = QLabel(parent=self)
|
||||
self.image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
|
||||
self.setToolTip(text)
|
||||
|
||||
self.delete_button = QPushButton(icon("ei.remove"), "", parent=self)
|
||||
if show_text in extra_wrapper_regex.keys():
|
||||
self.delete_button.setDisabled(True)
|
||||
self.delete_button.setToolTip(self.tr("Disable it in settings"))
|
||||
self.delete_button.clicked.connect(self.delete)
|
||||
unmanaged = show_text in extra_wrapper_regex.keys()
|
||||
|
||||
layout = QHBoxLayout()
|
||||
text_lbl = QLabel(show_text, parent=self)
|
||||
text_lbl.setFont(QFont("monospace"))
|
||||
text_lbl.setDisabled(unmanaged)
|
||||
|
||||
image_lbl = QLabel(parent=self)
|
||||
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
|
||||
|
||||
edit_action = QAction("Edit", parent=self)
|
||||
edit_action.triggered.connect(self.__edit)
|
||||
delete_action = QAction("Delete", parent=self)
|
||||
delete_action.triggered.connect(self.__delete)
|
||||
|
||||
manage_menu = QMenu(parent=self)
|
||||
manage_menu.addActions([edit_action, delete_action])
|
||||
|
||||
manage_button = QToolButton(parent=self)
|
||||
manage_button.setIcon(icon("mdi.menu"))
|
||||
manage_button.setMenu(manage_menu)
|
||||
manage_button.setPopupMode(QToolButton.InstantPopup)
|
||||
manage_button.setDisabled(unmanaged)
|
||||
if unmanaged:
|
||||
manage_button.setToolTip(self.tr("Manage through settings"))
|
||||
else:
|
||||
manage_button.setToolTip(self.tr("Manage"))
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.image_lbl)
|
||||
layout.addWidget(self.text_lbl)
|
||||
layout.addWidget(self.delete_button)
|
||||
layout.addWidget(image_lbl)
|
||||
layout.addWidget(text_lbl)
|
||||
layout.addWidget(manage_button)
|
||||
self.setLayout(layout)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.delete_button.setObjectName(f"{self.objectName()}Button")
|
||||
manage_button.setObjectName(f"{self.objectName()}Button")
|
||||
|
||||
def delete(self):
|
||||
@pyqtSlot()
|
||||
def __delete(self):
|
||||
self.delete_wrapper.emit(self.text)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
if e.buttons() == Qt.LeftButton:
|
||||
def __edit(self) -> None:
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
|
||||
dialog.setLabelText(self.tr("Edit wrapper command"))
|
||||
dialog.setTextValue(self.text)
|
||||
accepted = dialog.exec()
|
||||
wrapper = dialog.textValue()
|
||||
dialog.deleteLater()
|
||||
if accepted and wrapper:
|
||||
self.update_wrapper.emit(self.text, wrapper)
|
||||
|
||||
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
|
||||
if a0.buttons() == Qt.LeftButton:
|
||||
a0.accept()
|
||||
drag = QDrag(self)
|
||||
mime = QMimeData()
|
||||
drag.setMimeData(mime)
|
||||
drag.exec_(Qt.MoveAction)
|
||||
|
||||
|
||||
class WrapperSettings(QWidget, Ui_WrapperSettings):
|
||||
class WrapperSettings(QWidget):
|
||||
def __init__(self):
|
||||
super(WrapperSettings, self).__init__()
|
||||
self.setupUi(self)
|
||||
self.ui = Ui_WrapperSettings()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.wrappers: Dict[str, WrapperWidget] = {}
|
||||
self.app_name: str
|
||||
self.app_name: str = "default"
|
||||
|
||||
self.wrapper_scroll = QScrollArea(self.widget_stack)
|
||||
self.wrapper_scroll = QScrollArea(self.ui.widget_stack)
|
||||
self.wrapper_scroll.setWidgetResizable(True)
|
||||
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
|
||||
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
@ -92,18 +131,18 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
|
|||
save_cb=self.save, parent=self.wrapper_scroll
|
||||
)
|
||||
self.wrapper_scroll.setWidget(self.scroll_content)
|
||||
self.widget_stack.insertWidget(0, self.wrapper_scroll)
|
||||
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
|
||||
|
||||
self.core = RareCore.instance().core()
|
||||
|
||||
self.add_button.clicked.connect(self.add_button_pressed)
|
||||
self.ui.add_button.clicked.connect(self.add_button_pressed)
|
||||
self.settings = QSettings()
|
||||
|
||||
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
|
||||
self.ui.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
|
||||
self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll")
|
||||
self.wrapper_scroll.horizontalScrollBar().setObjectName(
|
||||
f"{self.wrapper_scroll.objectName()}Bar")
|
||||
|
@ -135,78 +174,104 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
|
|||
return " ".join(self.get_wrapper_list())
|
||||
|
||||
def get_wrapper_list(self):
|
||||
data: List[str] = []
|
||||
for w in self.wrappers.values():
|
||||
# Get the widget at each index in turn.
|
||||
try:
|
||||
data.append(w.text)
|
||||
except AttributeError:
|
||||
pass
|
||||
return data
|
||||
wrappers = list(self.wrappers.values())
|
||||
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
|
||||
return [w.text for w in wrappers]
|
||||
|
||||
def add_button_pressed(self):
|
||||
header = self.tr("Add wrapper")
|
||||
wrapper, done = QInputDialog.getText(
|
||||
self, f"{header} - {QCoreApplication.instance().applicationName()}", self.tr("Insert wrapper executable")
|
||||
)
|
||||
if not done:
|
||||
return
|
||||
self.add_wrapper(wrapper)
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
|
||||
dialog.setLabelText(self.tr("Enter wrapper command"))
|
||||
accepted = dialog.exec()
|
||||
wrapper = dialog.textValue()
|
||||
dialog.deleteLater()
|
||||
if accepted:
|
||||
self.add_wrapper(wrapper)
|
||||
|
||||
def add_wrapper(self, text: str, from_load=False):
|
||||
def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
|
||||
if text == "mangohud" and self.wrappers.get("mangohud"):
|
||||
return
|
||||
show_text = text
|
||||
show_text = ""
|
||||
for key, extra_wrapper in extra_wrapper_regex.items():
|
||||
if re.match(extra_wrapper, text):
|
||||
show_text = key
|
||||
if not show_text:
|
||||
show_text = text.split()[0]
|
||||
|
||||
# validate
|
||||
if not text.strip(): # is empty
|
||||
return
|
||||
if not from_load:
|
||||
if self.wrappers.get(text):
|
||||
QMessageBox.warning(self, "Warning", self.tr("Wrapper is already in the list"))
|
||||
QMessageBox.warning(
|
||||
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(text)
|
||||
)
|
||||
return
|
||||
|
||||
if show_text != "proton" and not shutil.which(text.split()[0]):
|
||||
if QMessageBox.question(self, "Warning", self.tr("Wrapper is not in $PATH. Ignore? "),
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.No:
|
||||
if (
|
||||
QMessageBox.question(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
== QMessageBox.No
|
||||
):
|
||||
return
|
||||
|
||||
if text == "proton":
|
||||
QMessageBox.warning(self, "Warning", self.tr("Do not insert proton manually. Add it in proton settings"))
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Warning"),
|
||||
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
|
||||
)
|
||||
return
|
||||
|
||||
self.widget_stack.setCurrentIndex(0)
|
||||
self.ui.widget_stack.setCurrentIndex(0)
|
||||
|
||||
if widget := self.wrappers.get(show_text, None):
|
||||
widget.deleteLater()
|
||||
|
||||
widget = WrapperWidget(text, show_text, self.scroll_content)
|
||||
self.scroll_content.layout().addWidget(widget)
|
||||
if position < 0:
|
||||
self.scroll_content.layout().addWidget(widget)
|
||||
else:
|
||||
self.scroll_content.layout().insertWidget(position, widget)
|
||||
self.adjust_scrollarea(
|
||||
self.wrapper_scroll.horizontalScrollBar().minimum(),
|
||||
self.wrapper_scroll.horizontalScrollBar().maximum()
|
||||
self.wrapper_scroll.horizontalScrollBar().maximum(),
|
||||
)
|
||||
widget.update_wrapper.connect(self.update_wrapper)
|
||||
widget.delete_wrapper.connect(self.delete_wrapper)
|
||||
|
||||
self.wrappers[show_text] = widget
|
||||
|
||||
if not from_load:
|
||||
self.save()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def delete_wrapper(self, text: str):
|
||||
text = text.split()[0]
|
||||
widget = self.wrappers.get(text, None)
|
||||
if widget:
|
||||
self.wrappers.pop(text)
|
||||
widget.deleteLater()
|
||||
|
||||
if not self.wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height())
|
||||
self.widget_stack.setCurrentIndex(1)
|
||||
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
|
||||
self.ui.widget_stack.setCurrentIndex(1)
|
||||
|
||||
self.save()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def update_wrapper(self, old: str, new: str):
|
||||
key = old.split()[0]
|
||||
idx = self.scroll_content.layout().indexOf(self.wrappers[key])
|
||||
self.delete_wrapper(key)
|
||||
self.add_wrapper(new, position=idx)
|
||||
|
||||
def save(self):
|
||||
# save wrappers twice, to support wrappers with spaces
|
||||
if len(self.wrappers) == 0:
|
||||
|
@ -231,19 +296,18 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
|
|||
wrappers = pattern.split(cfg)[1::2]
|
||||
|
||||
for wrapper in wrappers:
|
||||
self.add_wrapper(wrapper, True)
|
||||
self.add_wrapper(wrapper, from_load=True)
|
||||
|
||||
if not self.wrappers:
|
||||
self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height())
|
||||
self.widget_stack.setCurrentIndex(1)
|
||||
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
|
||||
self.ui.widget_stack.setCurrentIndex(1)
|
||||
else:
|
||||
self.widget_stack.setCurrentIndex(0)
|
||||
self.ui.widget_stack.setCurrentIndex(0)
|
||||
|
||||
self.save()
|
||||
|
||||
|
||||
class WrapperContainer(QWidget):
|
||||
drag_widget: QWidget
|
||||
|
||||
def __init__(self, save_cb, parent=None):
|
||||
super(WrapperContainer, self).__init__(parent=parent)
|
||||
|
@ -254,6 +318,8 @@ class WrapperContainer(QWidget):
|
|||
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.drag_widget: Optional[QWidget] = None
|
||||
|
||||
# lk: set object names for the stylesheet
|
||||
self.setObjectName(type(self).__name__)
|
||||
|
||||
|
|
|
@ -10,19 +10,19 @@ from logging import getLogger
|
|||
from signal import signal, SIGINT, SIGTERM, strsignal
|
||||
from typing import Union, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt
|
||||
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from legendary.models.game import SaveGameStatus
|
||||
|
||||
from rare.components.dialogs.cloud_save_dialog import CloudSaveDialog
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.models.base_game import RareGameSlim
|
||||
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
|
||||
from rare.widgets.rare_app import RareApp, RareAppException
|
||||
from .console import Console
|
||||
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
|
||||
from ..models.base_game import RareGameSlim
|
||||
from rare.components.dialogs.cloud_save_dialog import CloudSaveDialog
|
||||
|
||||
logger = logging.getLogger("RareLauncher")
|
||||
|
||||
|
@ -41,7 +41,6 @@ class PreLaunchThread(QRunnable):
|
|||
def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None):
|
||||
super(PreLaunchThread, self).__init__()
|
||||
self.core = core
|
||||
self.app_name = args.app_name
|
||||
self.signals = self.Signals()
|
||||
self.args = args
|
||||
self.rgame = rgame
|
||||
|
@ -119,45 +118,30 @@ class RareLauncherException(RareAppException):
|
|||
|
||||
|
||||
class RareLauncher(RareApp):
|
||||
game_process: QProcess
|
||||
server: QLocalServer
|
||||
socket: Optional[QLocalSocket] = None
|
||||
exit_app = pyqtSignal()
|
||||
console: Optional[Console] = None
|
||||
success: bool = True
|
||||
|
||||
def __init__(self, args: InitArgs):
|
||||
log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log"
|
||||
super(RareLauncher, self).__init__(args, log_file)
|
||||
self._hook.deleteLater()
|
||||
self._hook = RareLauncherException(self, args, self)
|
||||
self.game_process = QProcess()
|
||||
self.app_name = args.app_name
|
||||
self.logger = getLogger(self.app_name)
|
||||
self.core = LegendaryCore()
|
||||
self.args = args
|
||||
self.logger = getLogger(f"Launcher_{args.app_name}")
|
||||
|
||||
self.success: bool = True
|
||||
self.no_sync_on_exit = False
|
||||
|
||||
game = self.core.get_game(self.app_name)
|
||||
self.rgame = RareGameSlim(self.core, game)
|
||||
self.args = args
|
||||
self.core = LegendaryCore()
|
||||
self.rgame = RareGameSlim(self.core, self.core.get_game(args.app_name))
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
|
||||
self.console: Optional[Console] = None
|
||||
if QSettings().value("show_console", False, bool):
|
||||
self.console = Console()
|
||||
self.console.show()
|
||||
|
||||
self.server = QLocalServer()
|
||||
ret = self.server.listen(f"rare_{self.app_name}")
|
||||
if not ret:
|
||||
self.logger.error(self.server.errorString())
|
||||
self.logger.info("Server is running")
|
||||
self.server.close()
|
||||
self.success = False
|
||||
return
|
||||
self.server.newConnection.connect(self.new_server_connection)
|
||||
self.game_process: QProcess = QProcess(self)
|
||||
self.game_process.finished.connect(self.game_finished)
|
||||
self.game_process.errorOccurred.connect(
|
||||
lambda err: self.error_occurred(self.game_process.errorString()))
|
||||
|
@ -172,11 +156,30 @@ class RareLauncher(RareApp):
|
|||
self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
|
||||
)
|
||||
)
|
||||
self.console.term.connect(lambda: self.game_process.terminate())
|
||||
self.console.kill.connect(lambda: self.game_process.kill())
|
||||
self.console.term.connect(self.__proc_term)
|
||||
self.console.kill.connect(self.__proc_kill)
|
||||
|
||||
self.socket: Optional[QLocalSocket] = None
|
||||
self.server: QLocalServer = QLocalServer(self)
|
||||
ret = self.server.listen(f"rare_{args.app_name}")
|
||||
if not ret:
|
||||
self.logger.error(self.server.errorString())
|
||||
self.logger.info("Server is running")
|
||||
self.server.close()
|
||||
self.success = False
|
||||
return
|
||||
self.server.newConnection.connect(self.new_server_connection)
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_term(self):
|
||||
self.game_process.terminate()
|
||||
|
||||
@pyqtSlot()
|
||||
def __proc_kill(self):
|
||||
self.game_process.kill()
|
||||
|
||||
def new_server_connection(self):
|
||||
if self.socket is not None:
|
||||
try:
|
||||
|
@ -221,7 +224,6 @@ class RareLauncher(RareApp):
|
|||
else:
|
||||
self.on_exit(exit_code)
|
||||
|
||||
|
||||
def game_finished(self, exit_code):
|
||||
self.logger.info("Game finished")
|
||||
|
||||
|
@ -232,12 +234,12 @@ class RareLauncher(RareApp):
|
|||
|
||||
def on_exit(self, exit_code: int):
|
||||
if self.console:
|
||||
self.console.on_process_exit(self.core.get_game(self.app_name).app_title, exit_code)
|
||||
self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, exit_code)
|
||||
|
||||
self.send_message(
|
||||
FinishedModel(
|
||||
action=Actions.finished,
|
||||
app_name=self.app_name,
|
||||
app_name=self.rgame.app_name,
|
||||
exit_code=exit_code,
|
||||
playtime=int(time.time() - self.start_time)
|
||||
)
|
||||
|
@ -265,11 +267,11 @@ class RareLauncher(RareApp):
|
|||
# send start message after process started
|
||||
self.game_process.started.connect(lambda: self.send_message(
|
||||
StateChangedModel(
|
||||
action=Actions.state_update, app_name=self.app_name,
|
||||
action=Actions.state_update, app_name=self.rgame.app_name,
|
||||
new_state=StateChangedModel.States.started
|
||||
)
|
||||
))
|
||||
if self.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
|
||||
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
|
||||
self.game_process.deleteLater()
|
||||
subprocess.Popen([args.executable] + args.args, cwd=args.cwd,
|
||||
env={i: args.env.value(i) for i in args.env.keys()})
|
||||
|
@ -281,7 +283,7 @@ class RareLauncher(RareApp):
|
|||
logger.info("Dry run activated")
|
||||
if self.console:
|
||||
self.console.log(f"{args.executable} {' '.join(args.args)}")
|
||||
self.console.log(f"Do not start {self.app_name}")
|
||||
self.console.log(f"Do not start {self.rgame.app_name}")
|
||||
self.console.accept_close = True
|
||||
print(args.executable, " ".join(args.args))
|
||||
self.stop()
|
||||
|
@ -291,9 +293,9 @@ class RareLauncher(RareApp):
|
|||
def error_occurred(self, error_str: str):
|
||||
self.logger.warning(error_str)
|
||||
if self.console:
|
||||
self.console.on_process_exit(self.core.get_game(self.app_name).app_title, error_str)
|
||||
self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, error_str)
|
||||
self.send_message(ErrorModel(
|
||||
error_string=error_str, app_name=self.app_name,
|
||||
error_string=error_str, app_name=self.rgame.app_name,
|
||||
action=Actions.error)
|
||||
)
|
||||
self.stop()
|
||||
|
@ -364,7 +366,7 @@ class RareLauncher(RareApp):
|
|||
if not self.console:
|
||||
self.exit()
|
||||
else:
|
||||
self.console.on_process_exit(self.app_name, 0)
|
||||
self.console.on_process_exit(self.rgame.app_name, 0)
|
||||
|
||||
|
||||
def start_game(args: Namespace):
|
||||
|
|
|
@ -84,7 +84,7 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
|
|||
launch_args: LaunchArgs) -> LaunchArgs:
|
||||
if not args.offline: # skip for update
|
||||
if not args.skip_update_check and not core.is_noupdate_game(igame.app_name):
|
||||
print("Checking for updates...")
|
||||
# print("Checking for updates...")
|
||||
# check updates
|
||||
try:
|
||||
latest = core.get_asset(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import functools
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
|
@ -6,6 +7,7 @@ import time
|
|||
from typing import Optional, Union, Tuple
|
||||
|
||||
from legendary.cli import LegendaryCLI as LegendaryCLIReal
|
||||
from legendary.lfs.wine_helpers import case_insensitive_file_search
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
from legendary.models.game import Game, InstalledGame, VerifyResult
|
||||
from legendary.lfs.utils import validate_files
|
||||
|
@ -30,21 +32,35 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# noinspection PyMissingConstructor
|
||||
def __init__(self, core: LegendaryCore):
|
||||
self.core = core
|
||||
self.logger = logging.getLogger('Cli')
|
||||
self.logger = logging.getLogger('cli')
|
||||
self.logging_queue = None
|
||||
self.ql = self.setup_threaded_logging()
|
||||
|
||||
def __del__(self):
|
||||
self.ql.stop()
|
||||
|
||||
@staticmethod
|
||||
def unlock_installed(func):
|
||||
@functools.wraps(func)
|
||||
def unlock(self, *args, **kwargs):
|
||||
ret = func(self, *args, **kwargs)
|
||||
self.core.lgd._installed_lock.release(force=True)
|
||||
return ret
|
||||
return unlock
|
||||
|
||||
def resolve_aliases(self, name):
|
||||
return super(LegendaryCLI, self)._resolve_aliases(name)
|
||||
|
||||
@unlock_installed
|
||||
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
sdl_prompt = args.sdl_prompt
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
if self.core.is_installed(args.app_name):
|
||||
|
@ -131,7 +147,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if config_tags:
|
||||
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||
config_tags = None
|
||||
self.core.lgd.config.set(game.app_name, 'disable_sdl', True)
|
||||
self.core.lgd.config.set(game.app_name, 'disable_sdl', 'true')
|
||||
sdl_enabled = False
|
||||
# just disable SDL, but keep config tags that have been manually specified
|
||||
elif config_disable_sdl or args.disable_sdl:
|
||||
|
@ -205,10 +221,15 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
|
||||
|
||||
# Rare: This is currently handled in DownloadThread, this is a trial
|
||||
@unlock_installed
|
||||
def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game: Game, igame: InstalledGame) -> LgndrInstallGameRealRet:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
ret = LgndrInstallGameRealRet(game.app_name)
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return ret
|
||||
|
||||
start_t = time.time()
|
||||
|
||||
|
@ -286,9 +307,14 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
|
||||
return ret
|
||||
|
||||
@unlock_installed
|
||||
def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return
|
||||
|
||||
old_igame = self.core.get_installed_game(game.app_name)
|
||||
if old_igame and repair_mode and os.path.exists(repair_file):
|
||||
|
@ -310,6 +336,11 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(old_igame.install_tags))
|
||||
self.core.lgd.save_config()
|
||||
|
||||
# check if the version changed, this can happen for DLC that gets a version bump with no actual file changes
|
||||
if old_igame and old_igame.version != igame.version:
|
||||
old_igame.version = igame.version
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
def _handle_postinstall(self, postinstall, igame, skip_prereqs=False, choice=True):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||
|
@ -347,10 +378,16 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
else:
|
||||
logger.info('Automatic installation not available on Linux.')
|
||||
|
||||
@unlock_installed
|
||||
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
get_boolean_choice = args.get_boolean_choice_main
|
||||
# def get_boolean_choice(x, default): return True
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
|
@ -362,6 +399,9 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
|
||||
return
|
||||
|
||||
if os.name == 'nt' and igame.uninstaller and not args.skip_uninstaller:
|
||||
self._handle_uninstaller(igame, args)
|
||||
|
||||
try:
|
||||
if not igame.is_dlc:
|
||||
# Remove DLC first so directory is empty when game uninstall runs
|
||||
|
@ -380,6 +420,31 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
||||
return
|
||||
|
||||
def _handle_uninstaller(self, igame: InstalledGame, args: LgndrUninstallGameArgs):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
yes = args.yes
|
||||
get_boolean_choice = args.get_boolean_choice_handler
|
||||
# def get_boolean_choice(x, default): return True
|
||||
# noinspection PyShadowingBuiltins
|
||||
def print(x): self.logger.info(x) if x else None
|
||||
|
||||
uninstaller = igame.uninstaller
|
||||
|
||||
print('\nThis game provides the following uninstaller:')
|
||||
print(f'- {uninstaller["path"]} {uninstaller["args"]}\n')
|
||||
|
||||
if yes or get_boolean_choice('Do you wish to run the uninstaller?', default=True):
|
||||
logger.info('Running uninstaller...')
|
||||
req_path, req_exec = os.path.split(uninstaller['path'])
|
||||
work_dir = os.path.join(igame.install_path, req_path)
|
||||
fullpath = os.path.join(work_dir, req_exec)
|
||||
try:
|
||||
p = subprocess.Popen([fullpath, uninstaller['args']], cwd=work_dir, shell=True)
|
||||
p.wait()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to run uninstaller: {e!r}')
|
||||
|
||||
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
|
@ -492,10 +557,15 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
||||
return len(failed), len(missing)
|
||||
|
||||
@unlock_installed
|
||||
def import_game(self, args: LgndrImportGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return
|
||||
|
||||
# make sure path is absolute
|
||||
args.app_path = os.path.abspath(args.app_path)
|
||||
|
@ -535,6 +605,8 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# get everything needed for import from core, then run additional checks.
|
||||
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
|
||||
exe_path = os.path.join(args.app_path, manifest.meta.launch_exe.lstrip('/'))
|
||||
if os.name != 'nt':
|
||||
exe_path = case_insensitive_file_search(exe_path)
|
||||
# check if most files at least exist or if user might have specified the wrong directory
|
||||
total = len(manifest.file_manifest_list.elements)
|
||||
found = sum(os.path.exists(os.path.join(args.app_path, f.filename))
|
||||
|
@ -590,9 +662,18 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||
return
|
||||
|
||||
@unlock_installed
|
||||
def egs_sync(self, args):
|
||||
return super(LegendaryCLI, self).egs_sync(args)
|
||||
|
||||
@unlock_installed
|
||||
def move(self, args):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
|
||||
'install/import/move applications at a time.')
|
||||
return
|
||||
|
||||
app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import functools
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from multiprocessing import Queue
|
||||
from uuid import uuid4
|
||||
from requests.exceptions import HTTPError, ConnectionError
|
||||
|
||||
# On Windows the monkeypatching of `run_real` below doesn't work like on Linux
|
||||
# This has the side effect of emitting the UIUpdate in DownloadThread complaining with a TypeError
|
||||
|
@ -11,6 +14,7 @@ from legendary.core import LegendaryCore as LegendaryCoreReal
|
|||
from legendary.lfs.utils import delete_folder
|
||||
from legendary.models.downloading import AnalysisResult
|
||||
from legendary.models.egl import EGLManifest
|
||||
from legendary.models.exceptions import InvalidCredentialsError
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
from legendary.models.manifest import ManifestMeta
|
||||
|
||||
|
@ -28,6 +32,84 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
self.handler = LgndrCoreLogHandler()
|
||||
self.log.addHandler(self.handler)
|
||||
|
||||
@staticmethod
|
||||
def unlock_installed(func):
|
||||
@functools.wraps(func)
|
||||
def unlock(self, *args, **kwargs):
|
||||
ret = func(self, *args, **kwargs)
|
||||
self.lgd._installed_lock.release(force=True)
|
||||
return ret
|
||||
return unlock
|
||||
|
||||
def _login(self, lock, force_refresh=False) -> bool:
|
||||
"""
|
||||
Attempts logging in with existing credentials.
|
||||
|
||||
raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
|
||||
"""
|
||||
if not lock.data:
|
||||
raise ValueError('No saved credentials')
|
||||
elif self.logged_in and lock.data['expires_at']:
|
||||
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
|
||||
dt_now = datetime.utcnow()
|
||||
td = dt_now - dt_exp
|
||||
|
||||
# if session still has at least 10 minutes left we can re-use it.
|
||||
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
|
||||
return True
|
||||
else:
|
||||
self.logged_in = False
|
||||
|
||||
# run update check
|
||||
if self.update_check_enabled():
|
||||
try:
|
||||
self.check_for_updates()
|
||||
except Exception as e:
|
||||
self.log.warning(f'Checking for Legendary updates failed: {e!r}')
|
||||
else:
|
||||
self.apply_lgd_config()
|
||||
|
||||
# check for overlay updates
|
||||
if self.is_overlay_installed():
|
||||
try:
|
||||
self.check_for_overlay_updates()
|
||||
except Exception as e:
|
||||
self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
|
||||
|
||||
if lock.data['expires_at'] and not force_refresh:
|
||||
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
|
||||
dt_now = datetime.utcnow()
|
||||
td = dt_now - dt_exp
|
||||
|
||||
# if session still has at least 10 minutes left we can re-use it.
|
||||
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
|
||||
self.log.info('Trying to re-use existing login session...')
|
||||
try:
|
||||
self.egs.resume_session(lock.data)
|
||||
self.logged_in = True
|
||||
return True
|
||||
except InvalidCredentialsError as e:
|
||||
self.log.warning(f'Resuming failed due to invalid credentials: {e!r}')
|
||||
except Exception as e:
|
||||
self.log.warning(f'Resuming failed for unknown reason: {e!r}')
|
||||
# If verify fails just continue the normal authentication process
|
||||
self.log.info('Falling back to using refresh token...')
|
||||
|
||||
try:
|
||||
self.log.info('Logging in...')
|
||||
userdata = self.egs.start_session(lock.data['refresh_token'])
|
||||
except InvalidCredentialsError:
|
||||
self.log.error('Stored credentials are no longer valid! Please login again.')
|
||||
lock.clear()
|
||||
return False
|
||||
except (HTTPError, ConnectionError) as e:
|
||||
self.log.error(f'HTTP request for login failed: {e!r}, please try again later.')
|
||||
return False
|
||||
|
||||
lock.data = userdata
|
||||
self.logged_in = True
|
||||
return True
|
||||
|
||||
# skip_sync defaults to false but since Rare is persistent, skip by default
|
||||
# def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame:
|
||||
# return super(LegendaryCore, self).get_installed_game(app_name, skip_sync)
|
||||
|
@ -76,6 +158,7 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
finally:
|
||||
pass
|
||||
|
||||
@unlock_installed
|
||||
def egl_import(self, app_name):
|
||||
try:
|
||||
super(LegendaryCore, self).egl_import(app_name)
|
||||
|
@ -121,6 +204,7 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
if delete_files:
|
||||
delete_folder(os.path.join(igame.install_path, '.egstore'))
|
||||
|
||||
@unlock_installed
|
||||
def egl_export(self, app_name):
|
||||
try:
|
||||
super(LegendaryCore, self).egl_export(app_name)
|
||||
|
|
|
@ -72,7 +72,7 @@ class DLManager(DLManagerReal):
|
|||
self.conditions = [shm_cond, task_cond]
|
||||
|
||||
# start threads
|
||||
s_time = time.time()
|
||||
s_time = time.perf_counter()
|
||||
self.threads.append(Thread(target=self.download_job_manager, args=(task_cond, shm_cond)))
|
||||
self.threads.append(Thread(target=self.dl_results_handler, args=(task_cond,)))
|
||||
self.threads.append(Thread(target=self.fw_results_handler, args=(shm_cond,)))
|
||||
|
@ -80,13 +80,13 @@ class DLManager(DLManagerReal):
|
|||
for t in self.threads:
|
||||
t.start()
|
||||
|
||||
last_update = time.time()
|
||||
last_update = time.perf_counter()
|
||||
|
||||
# Rare: kill requested
|
||||
kill_request = False
|
||||
|
||||
while processed_tasks < num_tasks:
|
||||
delta = time.time() - last_update
|
||||
delta = time.perf_counter() - last_update
|
||||
if not delta:
|
||||
time.sleep(self.update_interval)
|
||||
continue
|
||||
|
@ -108,10 +108,10 @@ class DLManager(DLManagerReal):
|
|||
self.bytes_read_since_last = self.bytes_written_since_last = 0
|
||||
self.bytes_downloaded_since_last = self.num_processed_since_last = 0
|
||||
self.bytes_decompressed_since_last = self.num_tasks_processed_since_last = 0
|
||||
last_update = time.time()
|
||||
last_update = time.perf_counter()
|
||||
|
||||
perc = (processed_chunks / num_chunk_tasks) * 100
|
||||
runtime = time.time() - s_time
|
||||
runtime = time.perf_counter() - s_time
|
||||
total_avail = len(self.sms)
|
||||
total_used = (num_shared_memory_segments - total_avail) * (self.analysis.biggest_chunk / 1024 / 1024)
|
||||
|
||||
|
|
|
@ -40,10 +40,12 @@ class LgndrImportGameArgs:
|
|||
class LgndrUninstallGameArgs:
|
||||
app_name: str
|
||||
keep_files: bool = False
|
||||
skip_uninstaller: bool = False
|
||||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
get_boolean_choice_main: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
get_boolean_choice_handler: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -5,8 +5,8 @@ from typing import Optional
|
|||
@dataclass
|
||||
class UIUpdate:
|
||||
"""
|
||||
Status update object sent from the manager to the CLI/GUI to update status indicators
|
||||
Inheritance doesn't work due to optional arguments in UIUpdate proper
|
||||
Status update object sent from the manager to the CLI/GUI to update status indicators
|
||||
Inheritance doesn't work due to optional arguments in UIUpdate proper
|
||||
"""
|
||||
progress: float
|
||||
download_speed: float
|
||||
|
|
|
@ -6,6 +6,7 @@ from logging import getLogger
|
|||
from typing import Optional, List, Tuple
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QSettings
|
||||
from legendary.lfs import eos
|
||||
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
|
@ -116,30 +117,13 @@ class RareGameBase(QObject):
|
|||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_mac(self) -> bool:
|
||||
pass
|
||||
def platforms(self) -> Tuple:
|
||||
"""!
|
||||
@brief Property that holds the platforms a game is available for
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_win32(self) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class RareGameSlim(RareGameBase):
|
||||
|
||||
def __init__(self, legendary_core: LegendaryCore, game: Game):
|
||||
super(RareGameSlim, self).__init__(legendary_core, game)
|
||||
# None if origin or not installed
|
||||
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
|
||||
self.saves: List[RareSaveGame] = []
|
||||
|
||||
@property
|
||||
def is_installed(self) -> bool:
|
||||
return True
|
||||
|
||||
def set_installed(self, installed: bool) -> None:
|
||||
pass
|
||||
@return Tuple
|
||||
"""
|
||||
return tuple(self.game.asset_infos.keys())
|
||||
|
||||
@property
|
||||
def is_mac(self) -> bool:
|
||||
|
@ -159,6 +143,60 @@ class RareGameSlim(RareGameBase):
|
|||
"""
|
||||
return "Win32" in self.game.asset_infos.keys()
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a Game is an Origin game
|
||||
|
||||
Legendary and by extenstion Rare can't launch Origin games directly,
|
||||
it just launches the Origin client and thus requires a bit of a special
|
||||
handling to let the user know.
|
||||
|
||||
@return bool If the game is an Origin game
|
||||
"""
|
||||
return (
|
||||
self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_overlay(self):
|
||||
return self.app_name == eos.EOSOverlayApp.app_name
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""!
|
||||
@brief Reports the currently installed version of the Game
|
||||
|
||||
If InstalledGame reports the currently installed version, which might be
|
||||
different from the remote version available from EGS. For not installed Games
|
||||
it reports the already known version.
|
||||
|
||||
@return str The current version of the game
|
||||
"""
|
||||
return self.igame.version if self.igame is not None else self.game.app_version()
|
||||
|
||||
@property
|
||||
def install_path(self) -> Optional[str]:
|
||||
if self.igame:
|
||||
return self.igame.install_path
|
||||
return None
|
||||
|
||||
|
||||
class RareGameSlim(RareGameBase):
|
||||
|
||||
def __init__(self, legendary_core: LegendaryCore, game: Game):
|
||||
super(RareGameSlim, self).__init__(legendary_core, game)
|
||||
# None if origin or not installed
|
||||
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
|
||||
self.saves: List[RareSaveGame] = []
|
||||
|
||||
@property
|
||||
def is_installed(self) -> bool:
|
||||
return True
|
||||
|
||||
def set_installed(self, installed: bool) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def auto_sync_saves(self):
|
||||
return self.supports_cloud_saves and QSettings().value(
|
||||
|
|
|
@ -10,6 +10,7 @@ from typing import List, Optional, Dict, Set
|
|||
from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.models.install import InstallOptionsModel, UninstallOptionsModel
|
||||
|
@ -93,6 +94,13 @@ class RareGame(RareGameSlim):
|
|||
if self.is_installed and not self.is_dlc:
|
||||
self.game_process.connect_to_server(on_startup=True)
|
||||
|
||||
def add_dlc(self, dlc) -> None:
|
||||
# lk: plug dlc progress signals to the game's
|
||||
dlc.signals.progress.start.connect(self.signals.progress.start)
|
||||
dlc.signals.progress.update.connect(self.signals.progress.update)
|
||||
dlc.signals.progress.finish.connect(self.signals.progress.finish)
|
||||
self.owned_dlcs.add(dlc)
|
||||
|
||||
def __on_progress_update(self, progress: int):
|
||||
self.progress = progress
|
||||
|
||||
|
@ -194,12 +202,10 @@ class RareGame(RareGameSlim):
|
|||
|
||||
@property
|
||||
def install_path(self) -> Optional[str]:
|
||||
if self.igame:
|
||||
return self.igame.install_path
|
||||
elif self.is_origin:
|
||||
if self.is_origin:
|
||||
# TODO Linux is also C:\\...
|
||||
return self.__origin_install_path
|
||||
return None
|
||||
return super(RareGame, self).install_path
|
||||
|
||||
@install_path.setter
|
||||
def install_path(self, path: str) -> None:
|
||||
|
@ -209,19 +215,6 @@ class RareGame(RareGameSlim):
|
|||
elif self.is_origin:
|
||||
self.__origin_install_path = path
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""!
|
||||
@brief Reports the currently installed version of the Game
|
||||
|
||||
If InstalledGame reports the currently installed version, which might be
|
||||
different from the remote version available from EGS. For not installed Games
|
||||
it reports the already known version.
|
||||
|
||||
@return str The current version of the game
|
||||
"""
|
||||
return self.igame.version if self.igame is not None else self.game.app_version()
|
||||
|
||||
@property
|
||||
def remote_version(self) -> str:
|
||||
"""!
|
||||
|
@ -415,21 +408,6 @@ class RareGame(RareGameSlim):
|
|||
# Asset infos are usually None, but there was a bug, that it was an empty GameAsset class
|
||||
return not self.game.asset_infos or not next(iter(self.game.asset_infos.values())).app_name
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a Game is an Origin game
|
||||
|
||||
Legendary and by extenstion Rare can't launch Origin games directly,
|
||||
it just launches the Origin client and thus requires a bit of a special
|
||||
handling to let the user know.
|
||||
|
||||
@return bool If the game is an Origin game
|
||||
"""
|
||||
return (
|
||||
self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_ubisoft(self) -> bool:
|
||||
return (
|
||||
|
@ -444,6 +422,10 @@ class RareGame(RareGameSlim):
|
|||
else self.app_title
|
||||
)
|
||||
|
||||
@property
|
||||
def sdl_name(self) -> Optional[str]:
|
||||
return get_sdl_appname(self.app_name)
|
||||
|
||||
@property
|
||||
def save_path(self) -> Optional[str]:
|
||||
return super(RareGame, self).save_path
|
||||
|
@ -482,12 +464,19 @@ class RareGame(RareGameSlim):
|
|||
grant_date = datetime.fromisoformat(
|
||||
entitlement["grantDate"].replace("Z", "+00:00")
|
||||
) if entitlement else None
|
||||
if force:
|
||||
print(grant_date)
|
||||
self.metadata.grant_date = grant_date
|
||||
self.__save_metadata()
|
||||
return self.metadata.grant_date
|
||||
|
||||
def set_origin_attributes(self, path: str, size: int = 0) -> None:
|
||||
self.__origin_install_path = path
|
||||
self.__origin_install_size = size
|
||||
if self.install_path and self.install_size:
|
||||
self.signals.game.installed.emit(self.app_name)
|
||||
else:
|
||||
self.signals.game.uninstalled.emit(self.app_name)
|
||||
self.set_pixmap()
|
||||
|
||||
@property
|
||||
def can_launch(self) -> bool:
|
||||
if self.is_idle and self.is_origin:
|
||||
|
@ -521,14 +510,15 @@ class RareGame(RareGameSlim):
|
|||
)
|
||||
return True
|
||||
|
||||
def set_origin_attributes(self, path: str, size: int = 0) -> None:
|
||||
self.__origin_install_path = path
|
||||
self.__origin_install_size = size
|
||||
if self.install_path and self.install_size:
|
||||
self.signals.game.installed.emit(self.app_name)
|
||||
else:
|
||||
self.signals.game.uninstalled.emit(self.app_name)
|
||||
self.set_pixmap()
|
||||
def modify(self) -> bool:
|
||||
if not self.is_idle:
|
||||
return False
|
||||
self.signals.game.install.emit(
|
||||
InstallOptionsModel(
|
||||
app_name=self.app_name, reset_sdl=True
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
def repair(self, repair_and_update) -> bool:
|
||||
if not self.is_idle:
|
||||
|
@ -592,11 +582,3 @@ class RareEosOverlay(RareGameBase):
|
|||
else:
|
||||
self.igame = None
|
||||
self.signals.game.uninstalled.emit(self.app_name)
|
||||
|
||||
@property
|
||||
def is_mac(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_win32(self) -> bool:
|
||||
return False
|
||||
|
|
|
@ -24,6 +24,7 @@ class InstallOptionsModel:
|
|||
repair_and_update: bool = False
|
||||
no_install: bool = False
|
||||
ignore_space: bool = False
|
||||
reset_sdl: bool = False
|
||||
skip_dlcs: bool = False
|
||||
with_dlcs: bool = False
|
||||
# Rare's internal arguments
|
||||
|
|
BIN
rare/resources/images/loader.webp
Normal file
BIN
rare/resources/images/loader.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 615 KiB |
Binary file not shown.
|
@ -13,5 +13,6 @@
|
|||
<qresource prefix="images">
|
||||
<file alias="Rare.png">images/Rare.png</file>
|
||||
<file alias="loader.gif">images/loader.gif</file>
|
||||
<file alias="loader.webp">images/loader.webp</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -57,7 +57,7 @@ def processResourceFile(filenamesIn, filenameOut, listFiles):
|
|||
return library.output(filenameOut)
|
||||
|
||||
|
||||
def css_name(widget: Union[wrappertype,QObject,Type], subwidget: str = ""):
|
||||
def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""):
|
||||
return f"#{widget_object_name(widget, '')}{subwidget}"
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -721,10 +721,12 @@ QBalloonTip {
|
|||
}
|
||||
|
||||
/* Wrapper settings styling */
|
||||
QPushButton#WrapperWidgetButton {
|
||||
QPushButton#WrapperWidgetButton,
|
||||
QToolButton#WrapperWidgetButton {
|
||||
border-color: #DADDDE;
|
||||
}
|
||||
QPushButton#WrapperWidgetButton:disabled {
|
||||
QPushButton#WrapperWidgetButton:disabled,
|
||||
QToolButton#WrapperWidgetButton:disabled {
|
||||
border-color: #A8AAAB;
|
||||
}
|
||||
QScrollArea#WrapperSettingsScroll {
|
||||
|
|
Binary file not shown.
|
@ -721,10 +721,12 @@ QBalloonTip {
|
|||
}
|
||||
|
||||
/* Wrapper settings styling */
|
||||
QPushButton#WrapperWidgetButton {
|
||||
QPushButton#WrapperWidgetButton,
|
||||
QToolButton#WrapperWidgetButton {
|
||||
border-color: rgb( 51, 54, 59);
|
||||
}
|
||||
QPushButton#WrapperWidgetButton:disabled {
|
||||
QPushButton#WrapperWidgetButton:disabled,
|
||||
QToolButton#WrapperWidgetButton:disabled {
|
||||
border-color: rgb( 41, 43, 47);
|
||||
}
|
||||
QScrollArea#WrapperSettingsScroll {
|
||||
|
|
|
@ -61,7 +61,7 @@ class ImageManager(QObject):
|
|||
|
||||
def run(self):
|
||||
self.func(self.updates, self.json_data, self.game)
|
||||
logger.debug(f" Emitting singal for {self.game.app_name} ({self.game.app_title})")
|
||||
logger.debug(f"Emitting singal for {self.game.app_name} ({self.game.app_title})")
|
||||
self.signals.completed.emit(self.game)
|
||||
|
||||
def __init__(self, signals: GlobalSignals, core: LegendaryCore):
|
||||
|
@ -82,7 +82,7 @@ class ImageManager(QObject):
|
|||
self.device = ImageSize.Preset(1, QApplication.instance().devicePixelRatio())
|
||||
|
||||
self.threadpool = QThreadPool()
|
||||
self.threadpool.setMaxThreadCount(8)
|
||||
self.threadpool.setMaxThreadCount(6)
|
||||
|
||||
def __img_dir(self, app_name: str) -> Path:
|
||||
return self.image_dir.joinpath(app_name)
|
||||
|
@ -182,8 +182,12 @@ class ImageManager(QObject):
|
|||
logger.info(f"Downloading {image['type']} for {game.app_name} ({game.app_title})")
|
||||
json_data[image["type"]] = image["md5"]
|
||||
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
|
||||
# cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=2).content
|
||||
cache_data[image["type"]] = requests.get(image["url"], params=payload).content
|
||||
try:
|
||||
# cache_data[image["type"]] = requests.get(image["url"], params=payload).content
|
||||
cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=10).content
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False
|
||||
|
||||
self.__convert(game, cache_data)
|
||||
# lk: don't keep the cache if there is no logo (kept for me)
|
||||
|
|
|
@ -238,27 +238,28 @@ class RareCore(QObject):
|
|||
rgame.update_rgame()
|
||||
else:
|
||||
rgame = RareGame(self.__core, self.__image_manager, game)
|
||||
self.__add_game(rgame)
|
||||
return rgame
|
||||
|
||||
def __add_games_and_dlcs(self, games: List[Game], dlcs_dict: Dict[str, List]) -> None:
|
||||
length = len(games)
|
||||
for idx, game in enumerate(games):
|
||||
rgame = self.__create_or_update_rgame(game)
|
||||
# lk: since loading has to know about game state,
|
||||
# validate installation just adding each RareGame
|
||||
# TODO: this should probably be moved into RareGame
|
||||
if rgame.is_installed and not (rgame.is_dlc or rgame.is_non_asset):
|
||||
self.__validate_install(rgame)
|
||||
if game_dlcs := dlcs_dict.get(rgame.game.catalog_item_id, False):
|
||||
for dlc in game_dlcs:
|
||||
rdlc = self.__create_or_update_rgame(dlc)
|
||||
# lk: plug dlc progress signals to the game's
|
||||
rdlc.signals.progress.start.connect(rgame.signals.progress.start)
|
||||
rdlc.signals.progress.update.connect(rgame.signals.progress.update)
|
||||
rdlc.signals.progress.finish.connect(rgame.signals.progress.finish)
|
||||
rgame.owned_dlcs.add(rdlc)
|
||||
self.__add_game(rdlc)
|
||||
self.__add_game(rgame)
|
||||
if rdlc not in rgame.owned_dlcs:
|
||||
rgame.add_dlc(rdlc)
|
||||
# lk: since loading has to know about game state,
|
||||
# validate installation just adding each RareGamesu
|
||||
# TODO: this should probably be moved into RareGame
|
||||
if rgame.is_installed and not (rgame.is_dlc or rgame.is_non_asset):
|
||||
try:
|
||||
self.__validate_install(rgame)
|
||||
except FileNotFoundError as e:
|
||||
logger.info(f'Marking "{rgame.app_title}" as not installed because an exception has occurred...')
|
||||
logger.error(e)
|
||||
rgame.set_installed(False)
|
||||
self.progress.emit(int(idx/length * 80) + 20, self.tr("Loaded <b>{}</b>").format(rgame.app_title))
|
||||
|
||||
@pyqtSlot(object, int)
|
||||
|
@ -329,31 +330,6 @@ class RareCore(QObject):
|
|||
self.fetch_entitlements()
|
||||
self.resolve_origin()
|
||||
|
||||
def load_pixmaps(self) -> None:
|
||||
"""
|
||||
Load pixmaps for all games
|
||||
|
||||
This exists here solely to fight signal and possibly threading issues.
|
||||
The initial image loading at startup should not be done in the RareGame class
|
||||
for two reasons. It will delay startup due to widget updates and the image
|
||||
might become availabe before the UI is brought up. In case of the second, we
|
||||
will get both a long queue of signals to be serviced and some of them might
|
||||
be not connected yet so the widget won't be updated. So do the loading here
|
||||
by calling this after the MainWindow has finished initializing.
|
||||
|
||||
@return: None
|
||||
"""
|
||||
def __load_pixmaps() -> None:
|
||||
# time.sleep(0.1)
|
||||
for rgame in self.__library.values():
|
||||
# self.__image_manager.download_image(rgame.game, rgame.set_pixmap, 0, False)
|
||||
rgame.load_pixmap()
|
||||
# lk: activity perception delay
|
||||
time.sleep(0.0005)
|
||||
|
||||
pixmap_worker = QRunnable.create(__load_pixmaps)
|
||||
QThreadPool.globalInstance().start(pixmap_worker)
|
||||
|
||||
@property
|
||||
def games_and_dlcs(self) -> Iterator[RareGame]:
|
||||
for app_name in self.__library:
|
||||
|
@ -371,6 +347,10 @@ class RareCore(QObject):
|
|||
def origin_games(self) -> Iterator[RareGame]:
|
||||
return self.__filter_games(lambda game: game.is_origin and not game.is_dlc)
|
||||
|
||||
@property
|
||||
def ubisoft_games(self) -> Iterator[RareGame]:
|
||||
return self.__filter_games(lambda game: game.is_ubisoft and not game.is_dlc)
|
||||
|
||||
@property
|
||||
def game_list(self) -> Iterator[Game]:
|
||||
for game in self.games:
|
||||
|
|
|
@ -14,6 +14,7 @@ from .worker import Worker
|
|||
|
||||
logger = getLogger("UninstallWorker")
|
||||
|
||||
|
||||
# TODO: You can use RareGame directly here once this is called inside RareCore and skip metadata fetch
|
||||
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
|
||||
game = core.get_game(app_name)
|
||||
|
@ -32,8 +33,9 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co
|
|||
LgndrUninstallGameArgs(
|
||||
app_name=app_name,
|
||||
keep_files=keep_files,
|
||||
indirect_status=status,
|
||||
skip_uninstaller=False,
|
||||
yes=True,
|
||||
indirect_status=status,
|
||||
)
|
||||
)
|
||||
if not keep_config:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Form implementation generated from reading ui file 'rare/ui/components/dialogs/login/landing_page.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.15.7
|
||||
# Created by: PyQt5 UI code generator 5.15.9
|
||||
#
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
@ -34,12 +34,6 @@ class Ui_LandingPage(object):
|
|||
self.login_browser_radio.setSizePolicy(sizePolicy)
|
||||
self.login_browser_radio.setObjectName("login_browser_radio")
|
||||
self.landing_layout.addWidget(self.login_browser_radio, 1, 0, 1, 1)
|
||||
self.login_browser_label = QtWidgets.QLabel(LandingPage)
|
||||
font = QtGui.QFont()
|
||||
font.setItalic(True)
|
||||
self.login_browser_label.setFont(font)
|
||||
self.login_browser_label.setObjectName("login_browser_label")
|
||||
self.landing_layout.addWidget(self.login_browser_label, 1, 1, 1, 1)
|
||||
self.login_import_radio = QtWidgets.QRadioButton(LandingPage)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
|
@ -48,14 +42,19 @@ class Ui_LandingPage(object):
|
|||
self.login_import_radio.setSizePolicy(sizePolicy)
|
||||
self.login_import_radio.setObjectName("login_import_radio")
|
||||
self.landing_layout.addWidget(self.login_import_radio, 2, 0, 1, 1)
|
||||
self.login_browser_label = QtWidgets.QLabel(LandingPage)
|
||||
font = QtGui.QFont()
|
||||
font.setItalic(True)
|
||||
self.login_browser_label.setFont(font)
|
||||
self.login_browser_label.setObjectName("login_browser_label")
|
||||
self.landing_layout.addWidget(self.login_browser_label, 1, 1, 1, 2)
|
||||
self.login_import_label = QtWidgets.QLabel(LandingPage)
|
||||
font = QtGui.QFont()
|
||||
font.setItalic(True)
|
||||
self.login_import_label.setFont(font)
|
||||
self.login_import_label.setObjectName("login_import_label")
|
||||
self.landing_layout.addWidget(self.login_import_label, 2, 1, 1, 1)
|
||||
spacerItem = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.landing_layout.addItem(spacerItem, 1, 2, 2, 1)
|
||||
self.landing_layout.addWidget(self.login_import_label, 2, 1, 1, 2)
|
||||
self.landing_layout.setColumnStretch(2, 1)
|
||||
self.landing_layout.setRowStretch(1, 1)
|
||||
self.landing_layout.setRowStretch(2, 1)
|
||||
|
||||
|
@ -65,8 +64,8 @@ class Ui_LandingPage(object):
|
|||
_translate = QtCore.QCoreApplication.translate
|
||||
self.login_label.setText(_translate("LandingPage", "Select login method"))
|
||||
self.login_browser_radio.setText(_translate("LandingPage", "Browser"))
|
||||
self.login_browser_label.setText(_translate("LandingPage", "Login using a browser."))
|
||||
self.login_import_radio.setText(_translate("LandingPage", "Import"))
|
||||
self.login_browser_label.setText(_translate("LandingPage", "Login using a browser."))
|
||||
self.login_import_label.setText(_translate("LandingPage", "Import from Epic Games Launcher"))
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<property name="windowTitle">
|
||||
<string notr="true">LandingPage</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="landing_layout" rowstretch="0,1,1">
|
||||
<layout class="QGridLayout" name="landing_layout" rowstretch="0,1,1" columnstretch="0,0,1">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
|
@ -43,18 +43,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="login_browser_label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Login using a browser.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QRadioButton" name="login_import_radio">
|
||||
<property name="sizePolicy">
|
||||
|
@ -68,7 +56,19 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QLabel" name="login_browser_label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Login using a browser.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QLabel" name="login_import_label">
|
||||
<property name="font">
|
||||
<font>
|
||||
|
@ -80,19 +80,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2" rowspan="2">
|
||||
<spacer name="login_hspacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
|
|
@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
|||
class Ui_GameInfo(object):
|
||||
def setupUi(self, GameInfo):
|
||||
GameInfo.setObjectName("GameInfo")
|
||||
GameInfo.resize(408, 340)
|
||||
GameInfo.resize(419, 404)
|
||||
self.main_layout = QtWidgets.QHBoxLayout(GameInfo)
|
||||
self.main_layout.setObjectName("main_layout")
|
||||
self.left_layout = QtWidgets.QVBoxLayout()
|
||||
|
@ -180,6 +180,9 @@ class Ui_GameInfo(object):
|
|||
self.installed_layout = QtWidgets.QVBoxLayout(self.installed_page)
|
||||
self.installed_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.installed_layout.setObjectName("installed_layout")
|
||||
self.modify_button = QtWidgets.QPushButton(self.installed_page)
|
||||
self.modify_button.setObjectName("modify_button")
|
||||
self.installed_layout.addWidget(self.modify_button)
|
||||
self.verify_stack = QtWidgets.QStackedWidget(self.installed_page)
|
||||
self.verify_stack.setObjectName("verify_stack")
|
||||
self.verify_button_page = QtWidgets.QWidget()
|
||||
|
@ -276,7 +279,7 @@ class Ui_GameInfo(object):
|
|||
self.main_layout.setStretch(1, 1)
|
||||
|
||||
self.retranslateUi(GameInfo)
|
||||
self.game_actions_stack.setCurrentIndex(1)
|
||||
self.game_actions_stack.setCurrentIndex(0)
|
||||
self.verify_stack.setCurrentIndex(0)
|
||||
self.move_stack.setCurrentIndex(0)
|
||||
|
||||
|
@ -291,6 +294,7 @@ class Ui_GameInfo(object):
|
|||
self.lbl_install_path.setText(_translate("GameInfo", "Installation Path"))
|
||||
self.lbl_platform.setText(_translate("GameInfo", "Platform"))
|
||||
self.lbl_game_actions.setText(_translate("GameInfo", "Actions"))
|
||||
self.modify_button.setText(_translate("GameInfo", "Modify Installation"))
|
||||
self.verify_button.setText(_translate("GameInfo", "Verify Installation"))
|
||||
self.repair_button.setText(_translate("GameInfo", "Repair Installation"))
|
||||
self.move_button.setText(_translate("GameInfo", "Move Installation"))
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>408</width>
|
||||
<height>340</height>
|
||||
<width>419</width>
|
||||
<height>404</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -309,7 +309,7 @@
|
|||
</size>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="installed_page">
|
||||
<layout class="QVBoxLayout" name="installed_layout">
|
||||
|
@ -325,6 +325,13 @@
|
|||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="modify_button">
|
||||
<property name="text">
|
||||
<string>Modify Installation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="verify_stack">
|
||||
<property name="currentIndex">
|
||||
|
|
10
rare/utils/metrics.py
Normal file
10
rare/utils/metrics.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
import time
|
||||
from contextlib import contextmanager
|
||||
from logging import Logger
|
||||
|
||||
|
||||
@contextmanager
|
||||
def timelogger(logger: Logger, title: str):
|
||||
start = time.perf_counter()
|
||||
yield
|
||||
logger.debug("%s: %s seconds", title, time.perf_counter() - start)
|
|
@ -132,6 +132,8 @@ def get_rare_executable() -> List[str]:
|
|||
# lk: detect if nuitka
|
||||
if "__compiled__" in globals():
|
||||
executable = [sys.executable]
|
||||
elif sys.argv[0].endswith("__main__.py"):
|
||||
executable = [sys.executable, "-m", "rare"]
|
||||
elif platform.system() == "Linux" or platform.system() == "Darwin" or platform.system() == "FreeBSD":
|
||||
if p := os.environ.get("APPIMAGE"):
|
||||
executable = [p]
|
||||
|
|
|
@ -165,6 +165,8 @@ class IndicatorLineEdit(QWidget):
|
|||
self.is_valid = False
|
||||
self.edit_func = edit_func
|
||||
self.save_func = save_func
|
||||
if text:
|
||||
self.line_edit.setText(text)
|
||||
self.line_edit.textChanged.connect(self.__edit)
|
||||
if self.edit_func is None:
|
||||
self.line_edit.textChanged.connect(self.__save)
|
||||
|
@ -175,8 +177,8 @@ class IndicatorLineEdit(QWidget):
|
|||
# lk: however it is going to edit any "understood" bad input to good input
|
||||
# lk: and we might not want that (but the validity check reports on the edited string)
|
||||
# lk: it is also going to trigger this widget's textChanged signal but that gets lost
|
||||
if text:
|
||||
self.line_edit.setText(text)
|
||||
# if text:
|
||||
# self.line_edit.setText(text)
|
||||
|
||||
def deleteLater(self) -> None:
|
||||
if self.__thread is not None:
|
||||
|
|
|
@ -8,8 +8,9 @@ class LoadingWidget(QLabel):
|
|||
super(LoadingWidget, self).__init__(parent=parent)
|
||||
self.setObjectName(type(self).__name__)
|
||||
self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
self.movie = QMovie(":/images/loader.gif", parent=self)
|
||||
self.setFixedSize(128, 128)
|
||||
self.movie = QMovie(":/images/loader.webp", parent=self)
|
||||
# The animation's exact size is 94x94
|
||||
self.setFixedSize(96, 96)
|
||||
self.setMovie(self.movie)
|
||||
if self.parent() is not None:
|
||||
self.parent().installEventFilter(self)
|
||||
|
@ -31,7 +32,10 @@ class LoadingWidget(QLabel):
|
|||
return super().event(e)
|
||||
|
||||
def showEvent(self, a0: QShowEvent) -> None:
|
||||
if a0.spontaneous():
|
||||
return super().showEvent(a0)
|
||||
self.__center_on_parent()
|
||||
super().showEvent(a0)
|
||||
|
||||
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
|
||||
if a0 is self.parent() and a1.type() == QEvent.Resize:
|
||||
|
|
|
@ -3,5 +3,5 @@ requests
|
|||
PyQt5
|
||||
QtAwesome
|
||||
setuptools
|
||||
legendary-gl>=0.20.32
|
||||
legendary-gl>=0.20.33
|
||||
pywin32; platform_system == "Windows"
|
||||
|
|
Loading…
Reference in a new issue