diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3e77a46ae4..41aa2546d5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: ['macos-latest', 'windows-latest', 'ubuntu-slim'] - version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + version: ['3.10', '3.11', '3.12', '3.13'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 diff --git a/misc/requirements.in b/misc/requirements.in index 2049096040..bc5c919445 100644 --- a/misc/requirements.in +++ b/misc/requirements.in @@ -4,7 +4,7 @@ setuptools-scm requests < 3.0 PySide6-Essentials >= 6.8.1 QtAwesome -legendary-gl @ https://github.com/RareDevs/legendary/archive/5b3453c.zip +legendary-gl @ https://github.com/RareDevs/legendary/archive/rare-next.zip orjson vdf @ https://github.com/solsticegamestudios/vdf/archive/be1f7220238022f8b29fe747f0b643f280bfdb6e.zip pywin32 ; platform_system == "Windows" diff --git a/pylintrc b/pylintrc index 71caea947d..c9e02674e7 100644 --- a/pylintrc +++ b/pylintrc @@ -89,7 +89,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.9 +py-version=3.10 # Discover python modules and packages in the file system subtree. recursive=yes diff --git a/pyproject.toml b/pyproject.toml index 29a0cefd7b..685a236dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests < 3.0", "PySide6-Essentials >= 6.8.1", "QtAwesome", - "legendary-gl @ git+https://github.com/RareDevs/legendary@5b3453c", + "legendary-gl @ git+https://github.com/RareDevs/legendary@rare-next", "orjson", "vdf @ https://github.com/solsticegamestudios/vdf/archive/be1f7220238022f8b29fe747f0b643f280bfdb6e.zip", "pywin32 ; platform_system == 'Windows'", @@ -134,15 +134,22 @@ force-exclude = ''' [tool.ruff] include = ["pyproject.toml", "rare/*.py"] -exclude = ["rare/ui", "rare/lgndr"] +exclude = [ + "rare/ui", + "rare/lgndr", + "rare/resources/static_css/__init__.py", + "rare/resources/stylesheets/*/__init__.py" +] + line-length = 130 indent-width = 4 [tool.ruff.lint] -extend-select = ["I"] +extend-select = ["E", "F", "UP", "B", "SIM", "I"] +extend-ignore = [ "UP008", "SIM102", "SIM108" ] [tool.ruff.format] -quote-style = "double" +quote-style = "single" indent-style = "space" [tool.poetry] diff --git a/rare/__init__.py b/rare/__init__.py index 92a408daee..b32aa56d9f 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,13 +1,13 @@ from rare._version import __version__, __version_tuple__ -__codename__ = "Cobia Cormorant" +__codename__ = 'Cobia Cormorant' # For PyCharm profiler -if __name__ == "__main__": +if __name__ == '__main__': import sys from rare.main import main sys.exit(main()) -__all__ = ["__version__", "__version_tuple__", "__codename__"] +__all__ = ['__version__', '__version_tuple__', '__codename__'] diff --git a/rare/__main__.py b/rare/__main__.py index 8a75aaf9c3..3a21060a94 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -1,4 +1,4 @@ -if __name__ == "__main__": +if __name__ == '__main__': import sys from rare.main import main diff --git a/rare/commands/launcher/__init__.py b/rare/commands/launcher/__init__.py index 3c09eea1a5..a87a194f1c 100644 --- a/rare/commands/launcher/__init__.py +++ b/rare/commands/launcher/__init__.py @@ -1,3 +1,4 @@ +import contextlib import json import os import platform @@ -8,7 +9,6 @@ import traceback from argparse import Namespace from logging import getLogger -from typing import Optional import shiboken6 from legendary.models.game import SaveGameStatus @@ -40,13 +40,13 @@ from .lgd_helper import InitParams, LaunchParams, dict_to_qprocenv, get_configured_qprocess, get_launch_params DETACHED_APP_NAMES = { - "0a2d9f6403244d12969e11da6713137b", # Fall Guys - "Fortnite", - "afdb5a85efcc45d8ae8e406e2121d81c", # Fortnite Battle Royale - "09e442f830a341f698b4da42abd98c9b", # Fortnite Festival - "d8f7763e07d74c209d760a679f9ed6ac", # Lego Fortnite - "Fortnite_Studio", # Unreal Editor for Fortnite - "dcfccf8d965a4f2281dddf9fead042de", # Homeworld Remastered Collection (issue#376) + '0a2d9f6403244d12969e11da6713137b', # Fall Guys + 'Fortnite', + 'afdb5a85efcc45d8ae8e406e2121d81c', # Fortnite Battle Royale + '09e442f830a341f698b4da42abd98c9b', # Fortnite Festival + 'd8f7763e07d74c209d760a679f9ed6ac', # Lego Fortnite + 'Fortnite_Studio', # Unreal Editor for Fortnite + 'dcfccf8d965a4f2281dddf9fead042de', # Homeworld Remastered Collection (issue#376) } @@ -67,20 +67,20 @@ def __init__(self, args: InitParams, rgame: RareGameSlim, sync_action=None): self.sync_action = sync_action def run(self) -> None: - self.logger.info(f"Sync action: {self.sync_action}") + self.logger.info(f'Sync action: {self.sync_action}') if self.sync_action == CloudSyncDialogResult.UPLOAD: self.rgame.upload_saves() elif self.sync_action == CloudSyncDialogResult.DOWNLOAD: self.rgame.download_saves() else: - self.logger.info("No sync action") + self.logger.info('No sync action') if args := self.prepare_launch(self.args): self.signals.ready_to_launch.emit(args) self.signals.disconnect(self.signals) self.signals.deleteLater() - def prepare_launch(self, args: InitParams) -> Optional[LaunchParams]: + def prepare_launch(self, args: InitParams) -> LaunchParams | None: try: launch = get_launch_params(self.rgame, args) except Exception as e: @@ -93,13 +93,13 @@ def prepare_launch(self, args: InitParams) -> Optional[LaunchParams]: proc = get_configured_qprocess(shlex.split(launch.pre_launch_command), launch.environment) proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) proc.readyReadStandardOutput.connect( - (lambda obj: obj.logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))).__get__(self) + (lambda obj: obj.logger.debug(str(proc.readAllStandardOutput().data(), 'utf-8', 'ignore'))).__get__(self) ) self.signals.pre_launch_command_started.emit() - self.logger.info("Running pre-launch command %s, %s", proc.program(), proc.arguments()) + self.logger.info('Running pre-launch command %s, %s', proc.program(), proc.arguments()) if launch.pre_launch_wait: proc.start() - self.logger.info("Waiting for pre-launch command to finish") + self.logger.info('Waiting for pre-launch command to finish') proc.waitForFinished(-1) else: proc.startDetached() @@ -131,22 +131,20 @@ def run(self) -> None: class RareLauncherException(RareAppException): - def __init__(self, app: "RareLauncher", args: Namespace, parent=None): + def __init__(self, app: 'RareLauncher', args: Namespace, parent=None): super(RareLauncherException, self).__init__(parent=parent) self.__app = app self.__args = args def _handler(self, exc_type, exc_value, exc_tb) -> bool: - try: + with contextlib.suppress(RuntimeError): self.__app.send_message( ErrorModel( app_name=self.__args.app_name, action=Actions.error, - error_string="".join(traceback.format_exception(exc_type, exc_value, exc_tb)), + error_string=''.join(traceback.format_exception(exc_type, exc_value, exc_tb)), ) ) - except RuntimeError: - pass return False @@ -154,9 +152,9 @@ class RareLauncher(RareApp): exit_app = Signal() def __init__(self, args: InitParams): - super(RareLauncher, self).__init__(args, f"{type(self).__name__}_{args.app_name}_{{0}}.log") - self.socket: Optional[QLocalSocket] = None - self.console: Optional[ConsoleDialog] = None + super(RareLauncher, self).__init__(args, f'{type(self).__name__}_{args.app_name}_{{0}}.log') + self.socket: QLocalSocket | None = None + self.console: ConsoleDialog | None = None self.game_process: QProcess = QProcess(self) self.server: QLocalServer = QLocalServer(self) @@ -169,7 +167,7 @@ def __init__(self, args: InitParams): self.core = LegendaryCore() game = self.core.get_game(args.app_name) if not game: - self.logger.error(f"Game {args.app_name} not found. Exiting") + self.logger.error(f'Game {args.app_name} not found. Exiting') return self.rgame = RareGameSlim(self.settings, self.core, game) @@ -178,7 +176,7 @@ def __init__(self, args: InitParams): self.console.show() self.game_process.stateChanged.connect(self._on_game_process_changed) - self.sync_dialog: Optional[CloudSyncDialog] = None + self.sync_dialog: CloudSyncDialog | None = None self.game_process.finished.connect(self.__process_finished) self.game_process.errorOccurred.connect(self.__process_errored) @@ -188,10 +186,10 @@ def __init__(self, args: InitParams): self.console.term.connect(self._proc_term) self.console.kill.connect(self._proc_kill) - ret = self.server.listen(f"rare_{args.app_name}") + 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.logger.info('Server is running') self.server.close() return self.server.newConnection.connect(self.new_server_connection) @@ -210,44 +208,42 @@ def _on_game_process_changed(self, state: QProcess.ProcessState): @Slot() def _proc_log_stdout(self): - self.console.log_stdout(self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")) + self.console.log_stdout(self.game_process.readAllStandardOutput().data().decode('utf-8', 'ignore')) @Slot() def _proc_log_stderr(self): - self.console.log_stderr(self.game_process.readAllStandardError().data().decode("utf-8", "ignore")) + self.console.log_stderr(self.game_process.readAllStandardError().data().decode('utf-8', 'ignore')) @Slot() def _proc_term(self): - if platform.system() == "Windows": + if platform.system() == 'Windows': self.game_process.terminate() else: os.kill(self.game_process.processId(), signal.SIGINT) @Slot() def _proc_kill(self): - if platform.system() == "Windows": + if platform.system() == 'Windows': self.game_process.kill() else: os.kill(self.game_process.processId(), signal.SIGINT) def new_server_connection(self): if self.socket is not None: - try: + with contextlib.suppress(RuntimeError): self.socket.disconnectFromServer() - except RuntimeError: - pass - self.logger.info("New connection") + self.logger.info('New connection') self.socket = self.server.nextPendingConnection() self.socket.disconnected.connect(self.socket_disconnected) self.socket.flush() def socket_disconnected(self): - self.logger.info("Server disconnected") + self.logger.info('Server disconnected') self.socket = None def send_message(self, message: BaseModel): if self.socket: - self.socket.write(json.dumps(vars(message)).encode("utf-8")) + self.socket.write(json.dumps(vars(message)).encode('utf-8')) self.socket.flush() else: self.logger.error("Can't send message") @@ -279,13 +275,13 @@ def check_saves_finished(self, exit_code, action): action = CloudSyncDialogResult(action) if action == CloudSyncDialogResult.UPLOAD: if self.console: - self.console.log("Uploading saves...") + self.console.log('Uploading saves...') worker = CloudSyncWorker(self.rgame, CloudSyncWorker.Mode.UPLOAD) QThreadPool.globalInstance().start(worker, priority=0) QThreadPool.globalInstance().waitForDone() elif action == CloudSyncDialogResult.DOWNLOAD: if self.console: - self.console.log("Downloading saves...") + self.console.log('Downloading saves...') worker = CloudSyncWorker(self.rgame, CloudSyncWorker.Mode.DOWNLOAD) QThreadPool.globalInstance().start(worker, priority=0) QThreadPool.globalInstance().waitForDone() @@ -294,7 +290,7 @@ def check_saves_finished(self, exit_code, action): @Slot(int, QProcess.ExitStatus) def __process_finished(self, exit_code: int, exit_status: QProcess.ExitStatus): - self.logger.info("Game finished") + self.logger.info('Game finished') if self.rgame.auto_sync_saves: self.check_saves(exit_code) @@ -330,28 +326,28 @@ def launch_game(self, params: LaunchParams): self.start_time = time.time() if self.args.dry_run: - self.logger.info("Dry run %s (%s)", self.rgame.app_title, self.rgame.app_name) - self.logger.info("Command: %s, %s", params.executable, " ".join(params.arguments)) + self.logger.info('Dry run %s (%s)', self.rgame.app_title, self.rgame.app_name) + self.logger.info('Command: %s, %s', params.executable, ' '.join(params.arguments)) if self.console: - self.console.log(f"Dry run {self.rgame.app_title} ({self.rgame.app_name})") - self.console.log(f"{shlex.join((params.executable, *params.arguments))}") + self.console.log(f'Dry run {self.rgame.app_title} ({self.rgame.app_name})') + self.console.log(f'{shlex.join((params.executable, *params.arguments))}') self.console.accept_close = True self.stop() return - if platform.system() == "Windows" and params.is_origin_game: + if platform.system() == 'Windows' and params.is_origin_game: # executable is a protocol link (link2ea://launchgame/...) QDesktopServices.openUrl(QUrl(params.executable)) self.stop() # stop because it is not a subprocess return - self.logger.info("Starting %s (%s)", self.rgame.app_title, self.rgame.app_name) - self.logger.info("Command: %s, %s", params.executable, " ".join(params.arguments)) - self.logger.debug("Working directory %s", params.working_directory) + self.logger.info('Starting %s (%s)', self.rgame.app_title, self.rgame.app_name) + self.logger.info('Command: %s, %s', params.executable, ' '.join(params.arguments)) + self.logger.debug('Working directory %s', params.working_directory) - if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows": + if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == 'Windows': if self.console: - self.console.log(f"Launching {params.executable} as a detached process") + self.console.log(f'Launching {params.executable} as a detached process') subprocess.Popen( (params.executable, *params.arguments), stdin=None, @@ -365,23 +361,21 @@ def launch_game(self, params: LaunchParams): self.stop() # stop because we do not attach to the output return - if platform.system() in {"Linux", "FreeBSD"}: + if platform.system() in {'Linux', 'FreeBSD'}: cmd_line = get_rare_executable() executable, arguments = cmd_line[0], cmd_line[1:] - if appid := os.environ.get("SteamGameId", False): - params.environment["SteamGameId"] = appid - elif params.environment.get("SteamGameId", False): - appid = params.environment["SteamGameId"] + if appid := os.environ.get('SteamGameId', False): # noqa: SIM112 + params.environment['SteamGameId'] = appid + elif params.environment.get('SteamGameId', False): + appid = params.environment['SteamGameId'] self.game_process.setProgram(executable) # TODO: Add "SteamLauch" and "AppId=xxxxxx" here for steamdeck/gamescope - try: + with contextlib.suppress(ValueError): appid = int(appid) >> 32 - except ValueError: - pass self.game_process.setArguments( - [*arguments, "subreaper", "SteamLaunch", f"AppId={appid}", "--", params.executable, *params.arguments] + [*arguments, 'subreaper', 'SteamLaunch', f'AppId={appid}', '--', params.executable, *params.arguments] ) self.game_process.setUnixProcessParameters( QProcess.UnixProcessFlag.ResetSignalHandlers | QProcess.UnixProcessFlag.CreateNewSession @@ -428,7 +422,7 @@ def start_prepare(self, sync_action=None): def sync_ready(self): if self.rgame.is_save_up_to_date: if self.console: - self.console.log("Sync worker ready. Sync not required") + self.console.log('Sync worker ready. Sync not required') self.start_prepare() return @@ -448,23 +442,23 @@ def __sync_ready(self, action: CloudSyncDialogResult): self.no_sync_on_exit = True if self.console: if action == CloudSyncDialogResult.DOWNLOAD: - self.console.log("Downloading saves...") + self.console.log('Downloading saves...') elif action == CloudSyncDialogResult.UPLOAD: - self.console.log("Uploading saves...") + self.console.log('Uploading saves...') self.start_prepare(action) def start(self): if not self.args.offline: try: if not self.core.login(): - raise ValueError("You are not logged in") + raise ValueError('You are not logged in') except ValueError: # automatically launch offline if available - self.logger.error("Not logged in. Trying to launch the game in offline mode") + self.logger.error('Not logged in. Trying to launch the game in offline mode') self.args.offline = True if not self.args.offline and self.rgame.auto_sync_saves: - self.logger.info("Start sync worker") + self.logger.info('Start sync worker') worker = SyncCheckWorker(self.core, self.rgame) worker.signals.error_occurred.connect(self.error_occurred) worker.signals.sync_state_ready.connect(self.sync_ready) @@ -482,7 +476,7 @@ def stop(self, sig: int = signal.SIGINT): if self.game_process.isSignalConnected(QMetaMethod.fromSignal(self.game_process.errorOccurred)): self.game_process.errorOccurred.disconnect() except (TypeError, RuntimeError) as e: - self.logger.error("Failed to disconnect process signals: %s", e) + self.logger.error('Failed to disconnect process signals: %s', e) # FIXME: refactor to avoid this if shiboken6.isValid(self.game_process): # pylint: disable=E1101 @@ -499,12 +493,12 @@ def stop(self, sig: int = signal.SIGINT): # FIXME: refactor to avoid this if shiboken6.isValid(self.server): # pylint: disable=E1101 - self.logger.info("Stopping server %s", self.server.socketDescriptor()) + self.logger.info('Stopping server %s', self.server.socketDescriptor()) try: self.server.close() self.server.deleteLater() except RuntimeError as e: - self.logger.error("Error occurred while stopping server: %s", e) + self.logger.error('Error occurred while stopping server: %s', e) self.processEvents() if not self.console: @@ -525,7 +519,7 @@ def launcher(args: Namespace) -> int: # This prevents ghost QLocalSockets, which block the name, which makes it unable to start # No handling for SIGKILL def signal_handler(sig, frame): - app.logger.info("%s received. Stopping", signal.strsignal(sig)) + app.logger.info('%s received. Stopping', signal.strsignal(sig)) app.stop(sig) app.exit(1) return 1 @@ -541,7 +535,7 @@ def signal_handler(sig, frame): try: exit_code = app.exec() except Exception as e: - app.logger.error("Unhandled error %s", e) + app.logger.error('Unhandled error %s', e) exit_code = 1 finally: pass @@ -550,4 +544,4 @@ def signal_handler(sig, frame): return exit_code -__all__ = ["launcher"] +__all__ = ['launcher'] diff --git a/rare/commands/launcher/cloud_sync_dialog.py b/rare/commands/launcher/cloud_sync_dialog.py index 146868ddda..2a6d3510d6 100644 --- a/rare/commands/launcher/cloud_sync_dialog.py +++ b/rare/commands/launcher/cloud_sync_dialog.py @@ -12,7 +12,7 @@ from rare.utils.misc import qta_icon from rare.widgets.dialogs import ButtonDialog, game_title -logger = getLogger("CloudSyncDialog") +logger = getLogger('CloudSyncDialog') class CloudSyncDialogResult(IntEnum): @@ -27,10 +27,10 @@ class CloudSyncDialog(ButtonDialog): def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime, parent=None): super(CloudSyncDialog, self).__init__(parent=parent) - header = self.tr("Cloud saves for") + header = self.tr('Cloud saves for') self.setWindowTitle(game_title(header, igame.title)) - title_label = QLabel(f"

{game_title(header, igame.title)}

", self) + title_label = QLabel(f'

{game_title(header, igame.title)}

', self) sync_widget = QWidget(self) self.sync_ui = Ui_CloudSyncWidget() @@ -40,17 +40,17 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime layout.addWidget(title_label) layout.addWidget(sync_widget) - self.accept_button.setText(self.tr("Skip")) - self.accept_button.setIcon(qta_icon("fa.chevron-right", "fa5s.chevron-right")) + self.accept_button.setText(self.tr('Skip')) + self.accept_button.setIcon(qta_icon('fa.chevron-right', 'fa5s.chevron-right')) self.setCentralLayout(layout) self.status = CloudSyncDialogResult.CANCEL - newer = self.tr("Newer") + newer = self.tr('Newer') if dt_remote and dt_local: - self.sync_ui.age_label_local.setText(f"{newer}" if dt_remote < dt_local else " ") - self.sync_ui.age_label_remote.setText(f"{newer}" if dt_remote > dt_local else " ") + self.sync_ui.age_label_local.setText(f'{newer}' if dt_remote < dt_local else ' ') + self.sync_ui.age_label_remote.setText(f'{newer}' if dt_remote > dt_local else ' ') # Set status, if one of them is None elif dt_remote and not dt_local: self.status = CloudSyncDialogResult.DOWNLOAD @@ -60,11 +60,11 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime self.status = CloudSyncDialogResult.SKIP local_tz = datetime.now().astimezone().tzinfo - self.sync_ui.date_info_local.setText(dt_local.astimezone(local_tz).strftime("%A, %d %B %Y %X") if dt_local else "None") - self.sync_ui.date_info_remote.setText(dt_remote.astimezone(local_tz).strftime("%A, %d %B %Y %X") if dt_remote else "None") + self.sync_ui.date_info_local.setText(dt_local.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_local else 'None') + self.sync_ui.date_info_remote.setText(dt_remote.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_remote else 'None') - self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa5s.desktop").pixmap(128, 128)) - self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "fa5s.cloud").pixmap(128, 128)) + self.sync_ui.icon_local.setPixmap(qta_icon('mdi.harddisk', 'fa5s.desktop').pixmap(128, 128)) + self.sync_ui.icon_remote.setPixmap(qta_icon('mdi.cloud-outline', 'fa5s.cloud').pixmap(128, 128)) self.sync_ui.upload_button.clicked.connect(self.__on_upload) self.sync_ui.download_button.clicked.connect(self.__on_download) @@ -90,7 +90,7 @@ def reject_handler(self): self.status = CloudSyncDialogResult.CANCEL -if __name__ == "__main__": +if __name__ == '__main__': app = QApplication(sys.argv) core = LegendaryCore() @@ -98,7 +98,7 @@ def reject_handler(self): def __callback(status: int): print(repr(CloudSyncDialogResult(status))) - dlg = CloudSyncDialog(core.get_installed_list()[0], datetime.now(), datetime.strptime("2021,1", "%Y,%M")) + dlg = CloudSyncDialog(core.get_installed_list()[0], datetime.now(), datetime.strptime('2021,1', '%Y,%M')) dlg.result_ready.connect(__callback) dlg.open() app.exec() diff --git a/rare/commands/launcher/console_dialog.py b/rare/commands/launcher/console_dialog.py index 587ff74056..432cb740ba 100644 --- a/rare/commands/launcher/console_dialog.py +++ b/rare/commands/launcher/console_dialog.py @@ -1,5 +1,3 @@ -from typing import Dict, Union - from PySide6.QtCore import QProcessEnvironment, QSize, Qt, Signal from PySide6.QtGui import QCloseEvent, QCursor, QFont, QTextCursor from PySide6.QtWidgets import ( @@ -29,7 +27,7 @@ class ConsoleDialog(QDialog): def __init__(self, app_title: str, parent=None): super(ConsoleDialog, self).__init__(parent=parent) self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) - self.setWindowTitle(dialog_title(game_title(self.tr("Console"), app_title))) + self.setWindowTitle(dialog_title(game_title(self.tr('Console'), app_title))) self.setGeometry(0, 0, 640, 480) layout = QVBoxLayout() @@ -38,27 +36,27 @@ def __init__(self, app_title: str, parent=None): button_layout = QHBoxLayout() - self.env_button = QPushButton(self.tr("Show environment")) + self.env_button = QPushButton(self.tr('Show environment')) button_layout.addWidget(self.env_button) self.env_button.clicked.connect(self.show_env) - self.save_button = QPushButton(self.tr("Save output to file")) + self.save_button = QPushButton(self.tr('Save output to file')) button_layout.addWidget(self.save_button) self.save_button.clicked.connect(self.save) - self.clear_button = QPushButton(self.tr("Clear console")) + self.clear_button = QPushButton(self.tr('Clear console')) button_layout.addWidget(self.clear_button) self.clear_button.clicked.connect(self.console_edit.clear) button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)) - self.terminate_button = QPushButton(self.tr("Terminate")) + self.terminate_button = QPushButton(self.tr('Terminate')) # self.terminate_button.setVisible(platform.system() == "Windows") button_layout.addWidget(self.terminate_button) self.terminate_button.clicked.connect(self.term) self.terminate_button.setEnabled(False) - self.kill_button = QPushButton(self.tr("Kill")) + self.kill_button = QPushButton(self.tr('Kill')) # self.kill_button.setVisible(platform.system() == "Windows") button_layout.addWidget(self.kill_button) self.kill_button.clicked.connect(self.kill) @@ -95,16 +93,16 @@ def center_window(self): self.move(screen_rect.center() - self.rect().adjusted(0, 0, decor_width, decor_height).center()) def save(self): - file, ok = QFileDialog.getSaveFileName(self, "Save output", "", "Log Files (*.log);;All Files (*)") + file, ok = QFileDialog.getSaveFileName(self, 'Save output', '', 'Log Files (*.log);;All Files (*)') if ok: - if "." not in file: - file += ".log" - with open(file, "w", encoding="utf-8") as f: + if '.' not in file: + file += '.log' + with open(file, 'w', encoding='utf-8') as f: f.write(self.console_edit.toPlainText()) f.close() - self.save_button.setText(self.tr("Saved")) + self.save_button.setText(self.tr('Saved')) - def set_env(self, env: Dict): + def set_env(self, env: dict): self.env_variables = dict_to_qprocenv(env) def show_env(self): @@ -112,18 +110,18 @@ def show_env(self): self.env_console.show() def log(self, text: str): - self.console_edit.log(f"Rare: {text}") + self.console_edit.log(f'Rare: {text}') def log_stdout(self, text: str): self.console_edit.log(text) def error(self, text: str): - self.console_edit.error(f"Rare: {text}") + self.console_edit.error(f'Rare: {text}') def log_stderr(self, text: str): self.console_edit.error(text) - def on_process_exit(self, app_title: str, status: Union[int, str]): + def on_process_exit(self, app_title: str, status: int | str): self.error(self.tr('Application finished with exit code "{}"').format(status)) self.accept_close = True @@ -148,13 +146,13 @@ def __init__(self, app_title: str, parent=None): self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) self.ui = Ui_ConsoleEnv() self.ui.setupUi(self) - self.setWindowTitle(dialog_title(game_title(self.tr("Environment"), app_title))) + self.setWindowTitle(dialog_title(game_title(self.tr('Environment'), app_title))) def setTable(self, env: QProcessEnvironment): self.ui.table.clearContents() self.ui.table.setRowCount(len(env.keys())) - for idx, key in enumerate(env.keys()): + for idx, _ in enumerate(env.keys()): self.ui.table.setItem(idx, 0, QTableWidgetItem(env.keys()[idx])) self.ui.table.setItem(idx, 1, QTableWidgetItem(env.value(env.keys()[idx]))) @@ -165,7 +163,7 @@ class ConsoleEdit(QPlainTextEdit): def __init__(self, parent=None): super(ConsoleEdit, self).__init__(parent=parent) self.setReadOnly(True) - self.setFont(QFont("monospace")) + self.setFont(QFont('monospace')) def scroll_to_last_line(self): cursor = self.textCursor() @@ -179,7 +177,7 @@ def print_to_console(self, text: str, color: str): self.scroll_to_last_line() def log(self, text): - self.print_to_console(text, "#aaa") + self.print_to_console(text, '#aaa') def error(self, text): - self.print_to_console(text, "#a33") + self.print_to_console(text, '#a33') diff --git a/rare/commands/launcher/lgd_helper.py b/rare/commands/launcher/lgd_helper.py index 1ea5744992..721fb4703a 100644 --- a/rare/commands/launcher/lgd_helper.py +++ b/rare/commands/launcher/lgd_helper.py @@ -5,7 +5,6 @@ from argparse import Namespace from dataclasses import dataclass, field from logging import getLogger -from typing import Dict, List, Tuple from legendary.models.game import LaunchParameters from PySide6.QtCore import QProcess, QProcessEnvironment @@ -14,7 +13,7 @@ from rare.utils.compat.utils import create_compat_users from rare.utils.paths import setup_compat_shaders_dir -logger = getLogger("RareLauncherUtils") +logger = getLogger('RareLauncherUtils') class GameArgsError(Exception): @@ -28,8 +27,8 @@ class InitParams(Namespace): dry_run: bool = False show_console: bool = False skip_update_check: bool = False - wine_prefix: str = "" - wine_bin: str = "" + wine_prefix: str = '' + wine_bin: str = '' @classmethod def from_argparse(cls, args): @@ -40,18 +39,18 @@ def from_argparse(cls, args): dry_run=args.dry_run, show_console=args.show_console, skip_update_check=args.skip_update_check, - wine_bin=args.wine_bin if hasattr(args, "wine_bin") else "", - wine_prefix=args.wine_pfx if hasattr(args, "wine_prefix") else "", + wine_bin=args.wine_bin if hasattr(args, 'wine_bin') else '', + wine_prefix=args.wine_pfx if hasattr(args, 'wine_prefix') else '', ) @dataclass class LaunchParams: - executable: str = "" - arguments: List[str] = field(default_factory=list) - working_directory: str = "" - environment: Dict[str, str] = field(default_factory=dict) - pre_launch_command: str = "" + executable: str = '' + arguments: list[str] = field(default_factory=list) + working_directory: str = '' + environment: dict[str, str] = field(default_factory=dict) + pre_launch_command: str = '' pre_launch_wait: bool = False is_origin_game: bool = False # only for windows to launch as url @@ -64,7 +63,7 @@ def get_origin_params(rgame: RareGameSlim, init: InitParams, launch: LaunchParam app_name = rgame.app_name origin_uri = core.get_origin_uri(app_name, init.offline) - if platform.system() == "Windows": + if platform.system() == 'Windows': command = [origin_uri] else: command = core.get_app_launch_command(app_name) @@ -87,16 +86,16 @@ def get_game_params(rgame: RareGameSlim, init: InitParams, launch: LaunchParams) if not init.skip_update_check and not rgame.core.is_noupdate_game(rgame.app_name): try: latest = rgame.core.get_asset(rgame.app_name, rgame.igame.platform, update=False) - except ValueError: - raise GameArgsError("Metadata doesn't exist") + except ValueError as exc: + raise GameArgsError("Metadata doesn't exist") from exc else: if latest.build_version != rgame.igame.version: - raise GameArgsError("Game is not up to date. Please update first") + raise GameArgsError('Game is not up to date. Please update first') if (not rgame.igame or not rgame.igame.executable) and rgame.game is not None: # override installed game with base title if rgame.is_launchable_addon: - app_name = rgame.game.metadata["mainGameItem"]["releaseInfo"][0]["appId"] + app_name = rgame.game.metadata['mainGameItem']['releaseInfo'][0]['appId'] rgame.igame = rgame.core.get_installed_game(app_name) try: @@ -104,13 +103,13 @@ def get_game_params(rgame: RareGameSlim, init: InitParams, launch: LaunchParams) app_name=rgame.igame.app_name, offline=init.offline, addon_app_name=rgame.game.app_name ) except TypeError: - logger.warning("Using older get_launch_parameters due to legendary version") + logger.warning('Using older get_launch_parameters due to legendary version') params: LaunchParameters = rgame.core.get_launch_parameters(app_name=rgame.igame.app_name, offline=init.offline) full_params = [] full_params.extend(params.launch_command) - if "LEGENDARY_WRAPPER_EXE" in params.environment: - lgd_wrapper = params.environment.pop("LEGENDARY_WRAPPER_EXE").strip() + if 'LEGENDARY_WRAPPER_EXE' in params.environment: + lgd_wrapper = params.environment.pop('LEGENDARY_WRAPPER_EXE').strip() if os.path.isfile(lgd_wrapper): full_params.append(lgd_wrapper) full_params.append(os.path.join(params.game_directory, params.game_executable)) @@ -132,18 +131,18 @@ def get_launch_params(rgame: RareGameSlim, init: InitParams = None) -> LaunchPar resp = LaunchParams() if not rgame.game: - raise GameArgsError(f"Could not find metadata for {rgame.app_title}") + raise GameArgsError(f'Could not find metadata for {rgame.app_title}') if rgame.is_origin: init.offline = False else: if not rgame.is_installed: - raise GameArgsError("Game is not installed or has unsupported format") + raise GameArgsError('Game is not installed or has unsupported format') if rgame.is_dlc and not rgame.is_launchable_addon: - raise GameArgsError("Game is a DLC") + raise GameArgsError('Game is a DLC') if not os.path.exists(rgame.install_path): - raise GameArgsError("Game path does not exist") + raise GameArgsError('Game path does not exist') if rgame.is_origin: resp = get_origin_params(rgame, init, resp) @@ -155,36 +154,36 @@ def get_launch_params(rgame: RareGameSlim, init: InitParams = None) -> LaunchPar return resp -def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[str], Dict]: - logger.debug("Preparing process: %s", command) +def prepare_process(command: list[str], environment: dict) -> tuple[str, list[str], dict]: + logger.debug('Preparing process: %s', command) _env = environment.copy() # Sanity check environment (mostly for Linux) # ensure shader compat dirs exist - if platform.system() in {"Linux", "FreeBSD"}: + if platform.system() in {'Linux', 'FreeBSD'}: _cmd_line = shlex.join(command) - if os.environ.get("XDG_CURRENT_DESKTOP", None) == "gamescope" or "gamescope" in _cmd_line: + if os.environ.get('XDG_CURRENT_DESKTOP', None) == 'gamescope' or 'gamescope' in _cmd_line: # disable mangohud in gamescope - _env["MANGOHUD"] = "0" - if "STEAM_COMPAT_CLIENT_INSTALL_PATH" not in _env: - _env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" - if "STEAM_COMPAT_DATA_PATH" in _env: - compat_pfx = os.path.join(_env["STEAM_COMPAT_DATA_PATH"], "pfx") + _env['MANGOHUD'] = '0' + if 'STEAM_COMPAT_CLIENT_INSTALL_PATH' not in _env: + _env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = '' + if 'STEAM_COMPAT_DATA_PATH' in _env: + compat_pfx = os.path.join(_env['STEAM_COMPAT_DATA_PATH'], 'pfx') os.makedirs(compat_pfx, exist_ok=True) create_compat_users(compat_pfx) - if "WINEPREFIX" in _env and not os.path.isdir(_env["WINEPREFIX"]): - os.makedirs(_env["WINEPREFIX"], exist_ok=True) - create_compat_users(_env["WINEPREFIX"]) - if "STEAM_COMPAT_SHADER_PATH" in _env: - _env.update(setup_compat_shaders_dir(_env["STEAM_COMPAT_SHADER_PATH"])) - _env["WINEDLLOVERRIDES"] = _env.get("WINEDLLOVERRIDES", "") + ";lsteamclient=d;" + if 'WINEPREFIX' in _env and not os.path.isdir(_env['WINEPREFIX']): + os.makedirs(_env['WINEPREFIX'], exist_ok=True) + create_compat_users(_env['WINEPREFIX']) + if 'STEAM_COMPAT_SHADER_PATH' in _env: + _env.update(setup_compat_shaders_dir(_env['STEAM_COMPAT_SHADER_PATH'])) + _env['WINEDLLOVERRIDES'] = _env.get('WINEDLLOVERRIDES', '') + ';lsteamclient=d;' final_env = os.environ.copy() final_cmd = command.copy() - if os.environ.get("container") == "flatpak": - _flat_cmd = ["flatpak-spawn", "--host"] - _flat_cmd.extend(f"--env={name}={value}" for name, value in _env.items()) + if os.environ.get('container') == 'flatpak': # noqa: SIM112 + _flat_cmd = ['flatpak-spawn', '--host'] + _flat_cmd.extend(f'--env={name}={value}' for name, value in _env.items()) final_cmd = _flat_cmd + command else: final_env.update(_env) @@ -192,14 +191,14 @@ def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[st return final_cmd[0], final_cmd[1:] if len(final_cmd) > 1 else [], final_env -def dict_to_qprocenv(env: Dict) -> QProcessEnvironment: +def dict_to_qprocenv(env: dict) -> QProcessEnvironment: _env = QProcessEnvironment() for name, value in env.items(): _env.insert(name, value) return _env -def get_configured_qprocess(command: List[str], environment: Dict) -> QProcess: +def get_configured_qprocess(command: list[str], environment: dict) -> QProcess: cmd, args, env = prepare_process(command, environment) proc = QProcess() proc.setProcessChannelMode(QProcess.ProcessChannelMode.SeparateChannels) diff --git a/rare/commands/subreaper/__init__.py b/rare/commands/subreaper/__init__.py index 8212ea9568..8c1e67e33f 100644 --- a/rare/commands/subreaper/__init__.py +++ b/rare/commands/subreaper/__init__.py @@ -1,10 +1,10 @@ import platform -if platform.system() == "FreeBSD": +if platform.system() == 'FreeBSD': from .subreaper_bsd import subreaper -elif platform.system() == "Linux": +elif platform.system() == 'Linux': from .subreaper_linux import subreaper else: - raise RuntimeError(f"Unsupported subrepaer platform {platform.system()}") + raise RuntimeError(f'Unsupported subrepaer platform {platform.system()}') -__all__ = ["subreaper"] +__all__ = ['subreaper'] diff --git a/rare/commands/subreaper/subreaper_bsd.py b/rare/commands/subreaper/subreaper_bsd.py index a3f22d26dd..bc78c81df0 100755 --- a/rare/commands/subreaper/subreaper_bsd.py +++ b/rare/commands/subreaper/subreaper_bsd.py @@ -8,11 +8,12 @@ import subprocess import sys from argparse import Namespace +from collections.abc import Generator from ctypes import CDLL, c_int, c_void_p from ctypes.util import find_library from logging import getLogger from pathlib import Path -from typing import Any, Generator, List +from typing import Any # Constants defined in sys/procctl.h P_PID = 0 @@ -21,17 +22,17 @@ def get_libc() -> str: """Find libc.so from the user's system.""" - return find_library("c") or "" + return find_library('c') or '' def _get_pids() -> Generator[int, Any, None]: # FreeBSD's /proc is often not mounted. If it is, entries are just PIDs. - proc_path = Path("/proc") + proc_path = Path('/proc') if proc_path.exists(): - yield from (int(p.name) for p in proc_path.glob("*") if p.name.isdigit()) + yield from (int(p.name) for p in proc_path.glob('*') if p.name.isdigit()) else: # Fallback: Use ps to get PIDs if /proc isn't available - out = subprocess.check_output(["ps", "-ax", "-o", "pid"]) + out = subprocess.check_output(['ps', '-ax', '-o', 'pid']) for line in out.splitlines()[1:]: yield int(line.strip()) @@ -42,8 +43,8 @@ def get_pstree_from_pid(root_pid: int) -> set[int]: try: # -o ppid,pid gets parent and child PIDs - out = subprocess.check_output(["ps", "-ax", "-o", "ppid,pid"], encoding="utf-8") - lines = out.strip().split("\n")[1:] + out = subprocess.check_output(['ps', '-ax', '-o', 'ppid,pid'], encoding='utf-8') + lines = out.strip().split('\n')[1:] pid_to_ppid = {} for line in lines: ppid, pid = map(int, line.split()) @@ -63,16 +64,16 @@ def get_pstree_from_pid(root_pid: int) -> set[int]: return descendants -def subreaper(args: Namespace, other: List[str]) -> int: - logger = getLogger("subreaper") +def subreaper(args: Namespace, other: list[str]) -> int: + logger = getLogger('subreaper') logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", + format='[%(name)s] %(levelname)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO, stream=sys.stderr, ) - logger.debug("command: %s", args) - logger.debug("arguments: %s", other) + logger.debug('command: %s', args) + logger.debug('arguments: %s', other) def signal_handler(sig, frame): logger.info("Caught '%s' signal.", signal.strsignal(sig)) @@ -83,13 +84,13 @@ def signal_handler(sig, frame): except ProcessLookupError: continue - command: List[str] = [args.command, *other] + command: list[str] = [args.command, *other] workdir: str = args.workdir child_status: int = 0 libc_path: str = get_libc() if not libc_path: - logger.error("Could not find libc") + logger.error('Could not find libc') return 1 libc: CDLL = CDLL(libc_path) @@ -107,18 +108,18 @@ def signal_handler(sig, frame): # Set Process Name (FreeBSD specific) # FreeBSD prefers setproctitle over prctl for naming - proc_name = b"reaper" + proc_name = b'reaper' try: libc.setproctitle(proc_name) except AttributeError: - logger.debug("setproctitle not found in libc") + logger.debug('setproctitle not found in libc') procctl_res = procctl(P_PID, os.getpid(), PROC_REAP_ACQUIRE, None) - logger.debug("procctl PROC_REAP_ACQUIRE exited with status: %s", procctl_res) + logger.debug('procctl PROC_REAP_ACQUIRE exited with status: %s', procctl_res) pid = os.fork() # pylint: disable=E1101 if pid == -1: - logger.error("Fork failed") + logger.error('Fork failed') if pid == 0: os.chdir(workdir) @@ -130,7 +131,7 @@ def signal_handler(sig, frame): while True: try: child_pid, child_status = os.wait() # pylint: disable=E1101 - logger.info("Child %s exited with status: %s", child_pid, child_status) + logger.info('Child %s exited with status: %s', child_pid, child_status) except ChildProcessError as e: logger.info(e) break @@ -138,11 +139,11 @@ def signal_handler(sig, frame): return child_status -if __name__ == "__main__": - sep = sys.argv.index("--") +if __name__ == '__main__': + sep = sys.argv.index('--') argv = sys.argv[sep + 1 :] args = Namespace(command=argv.pop(0), workdir=os.getcwd(), debug=True) subreaper(args, argv) -__all__ = ["subreaper"] +__all__ = ['subreaper'] diff --git a/rare/commands/subreaper/subreaper_linux.py b/rare/commands/subreaper/subreaper_linux.py index 3d2a4d415f..809638959c 100755 --- a/rare/commands/subreaper/subreaper_linux.py +++ b/rare/commands/subreaper/subreaper_linux.py @@ -5,11 +5,12 @@ import signal import sys from argparse import Namespace +from collections.abc import Generator from ctypes import CDLL, byref, c_int, create_string_buffer from ctypes.util import find_library from logging import getLogger from pathlib import Path -from typing import Any, Generator, List +from typing import Any from .util import find_mangohud_bin, find_mangohud_shim @@ -21,11 +22,11 @@ def get_libc() -> str: """Find libc.so from the user's system.""" - return find_library("c") or "" + return find_library('c') or '' def _get_pids() -> Generator[int, Any, None]: - yield from (int(pid.name) for pid in Path("/proc").glob("*") if pid.name.isdigit()) + yield from (int(pid.name) for pid in Path('/proc').glob('*') if pid.name.isdigit()) def get_pstree_from_pid(root_pid: int) -> set[int]: @@ -35,10 +36,10 @@ def get_pstree_from_pid(root_pid: int) -> set[int]: for pid in _get_pids(): try: - path = Path(f"/proc/{pid}/status") - with path.open(mode="r", encoding="utf-8") as file: - st_ppid = next(line for line in file if line.startswith("PPid:")) - st_ppid = st_ppid.removeprefix("PPid:").strip() + path = Path(f'/proc/{pid}/status') + with path.open(mode='r', encoding='utf-8') as file: + st_ppid = next(line for line in file if line.startswith('PPid:')) + st_ppid = st_ppid.removeprefix('PPid:').strip() pid_to_ppid[pid] = int(st_ppid) except (FileNotFoundError, ProcessLookupError, ValueError): continue @@ -55,16 +56,16 @@ def get_pstree_from_pid(root_pid: int) -> set[int]: return descendants -def subreaper(args: Namespace, other: List[str]) -> int: - logger = getLogger("subreaper") +def subreaper(args: Namespace, other: list[str]) -> int: + logger = getLogger('subreaper') logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", + format='[%(name)s] %(levelname)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO, stream=sys.stderr, ) - logger.debug("command: %s", args) - logger.debug("arguments: %s", other) + logger.debug('command: %s', args) + logger.debug('arguments: %s', other) def signal_handler(sig, frame): logger.info("Caught '%s' signal.", signal.strsignal(sig)) @@ -72,13 +73,13 @@ def signal_handler(sig, frame): for p in pstree: os.kill(p, sig) - command: List[str] = [args.command, *other] + command: list[str] = [args.command, *other] workdir: str = args.workdir child_status: int = 0 libc_path: str = get_libc() if not libc_path: - logger.error("Could not find libc") + logger.error('Could not find libc') return 1 libc: CDLL = CDLL(libc_path) @@ -92,24 +93,24 @@ def signal_handler(sig, frame): # c_ulong, ] - proc_name = b"reaper" + proc_name = b'reaper' buff = create_string_buffer(len(proc_name) + 1) buff.value = proc_name prctl_ret = prctl(PR_SET_NAME, byref(buff), 0, 0, 0) - logger.debug("prctl PR_SET_NAME exited with status: %s", prctl_ret) + logger.debug('prctl PR_SET_NAME exited with status: %s', prctl_ret) prctl_ret = prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0, 0) - logger.debug("prctl PR_SET_CHILD_SUBREAPER exited with status: %s", prctl_ret) + logger.debug('prctl PR_SET_CHILD_SUBREAPER exited with status: %s', prctl_ret) - if os.environ.get("MANGOHUD") == "1": + if os.environ.get('MANGOHUD') == '1': if mangoshim := find_mangohud_shim(): - os.environ["LD_PRELOAD"] = f'{os.environ.get("LD_PRELOAD", "")}:{mangoshim}' + os.environ['LD_PRELOAD'] = f'{os.environ.get("LD_PRELOAD", "")}:{mangoshim}' elif mangobin := find_mangohud_bin(): command.insert(0, mangobin) pid = os.fork() # pylint: disable=E1101 if pid == -1: - logger.error("Fork failed") + logger.error('Fork failed') if pid == 0: sys.stdout.flush() @@ -123,7 +124,7 @@ def signal_handler(sig, frame): while True: try: child_pid, child_status = os.wait() # pylint: disable=E1101 - logger.info("Child %s exited with status: %s", child_pid, child_status) + logger.info('Child %s exited with status: %s', child_pid, child_status) except ChildProcessError as e: logger.info(e) break @@ -131,11 +132,11 @@ def signal_handler(sig, frame): return child_status -if __name__ == "__main__": - sep = sys.argv.index("--") +if __name__ == '__main__': + sep = sys.argv.index('--') argv = sys.argv[sep + 1 :] args = Namespace(command=argv.pop(0), workdir=os.getcwd(), debug=True) subreaper(args, argv) -__all__ = ["subreaper"] +__all__ = ['subreaper'] diff --git a/rare/commands/subreaper/util.py b/rare/commands/subreaper/util.py index b89d800e3b..90b702af35 100644 --- a/rare/commands/subreaper/util.py +++ b/rare/commands/subreaper/util.py @@ -1,39 +1,38 @@ import os import shutil -from typing import List -def find_all(name, path) -> List: +def find_all(name, path) -> list: result = [] - for root, dirs, files in os.walk(path): + for root, _, files in os.walk(path): if name in files: result.append(os.path.join(root, name)) return result def find_mangohud_shim() -> str: - libs = find_all("libMangoHud_shim.so", "/usr") + libs = find_all('libMangoHud_shim.so', '/usr') if 1 > len(libs) > 2: - return "" + return '' if len(libs) == 1: return libs[0] ret = [] - for left, right in zip(*(lib.split("/") for lib in libs)): + for left, right in zip(*(lib.split('/') for lib in libs), strict=False): if left == right: ret.append(left) - elif left.startswith("lib") and right.startswith("lib"): - ret.append("$LIB") + elif left.startswith('lib') and right.startswith('lib'): + ret.append('$LIB') else: - return "" - return "/".join(ret) + return '' + return '/'.join(ret) def find_mangohud_bin() -> str: - return shutil.which("mangohud") + return shutil.which('mangohud') -if __name__ == "__main__": +if __name__ == '__main__': print(find_mangohud_shim()) -__all__ = ["find_mangohud_shim", "find_mangohud_bin"] +__all__ = ['find_mangohud_shim', 'find_mangohud_bin'] diff --git a/rare/commands/webview.py b/rare/commands/webview.py index 3b38495f15..78323edc09 100644 --- a/rare/commands/webview.py +++ b/rare/commands/webview.py @@ -5,10 +5,10 @@ def webview(args: Namespace) -> int: - if webview_login.do_webview_login(callback_code=sys.stdout.write, user_agent=f"EpicGamesLauncher/{args.egl_version}"): + if webview_login.do_webview_login(callback_code=sys.stdout.write, user_agent=f'EpicGamesLauncher/{args.egl_version}'): return 0 else: return 1 -__all__ = ["webview"] +__all__ = ['webview'] diff --git a/rare/components/__init__.py b/rare/components/__init__.py index 9c460997c3..ac894e3669 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -2,7 +2,6 @@ import shutil from argparse import Namespace from datetime import datetime, timezone -from typing import Optional import requests.exceptions from PySide6.QtCore import Qt, QThreadPool, QTimer, Slot @@ -37,7 +36,7 @@ def _handler(self, exc_type, exc_value, exc_tb) -> bool: class Rare(RareApp): def __init__(self, args: Namespace): - super(Rare, self).__init__(args, f"{type(self).__name__}_{{0}}.log") + super(Rare, self).__init__(args, f'{type(self).__name__}_{{0}}.log') self._hook.deleteLater() self._hook = RareException(self) self.rcore = RareCore(self.settings, args=args) @@ -46,23 +45,23 @@ def __init__(self, args: Namespace): self.core = self.rcore.core() # set Application name for settings - self.main_window: Optional[RareWindow] = None - self.launch_dialog: Optional[LaunchDialog] = None - self.relogin_timer: Optional[QTimer] = None + self.main_window: RareWindow | None = None + self.launch_dialog: LaunchDialog | None = None + self.relogin_timer: QTimer | None = None # This launches the application after it has been instantiated. # The timer's signal will be serviced once we call `exec()` on the application QTimer.singleShot(0, self.launch_app) def poke_timer(self): - dt_exp = datetime.fromisoformat(self.core.lgd.userdata["expires_at"][:-1]).replace(tzinfo=timezone.utc) + dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc) dt_now = datetime.now(timezone.utc) td = abs(dt_exp - dt_now) self.relogin_timer.start(int(td.total_seconds() - 60) * 1000) - self.logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}") + self.logger.info(f'Renewed session expires at {self.core.lgd.userdata["expires_at"]}') def relogin(self): - self.logger.info("Session expires shortly. Renew session") + self.logger.info('Session expires shortly. Renew session') try: self.core.login(force_refresh=True) except requests.exceptions.ConnectionError: diff --git a/rare/components/dialogs/install/__init__.py b/rare/components/dialogs/install/__init__.py index 34ac9a864a..c0cb9854d9 100644 --- a/rare/components/dialogs/install/__init__.py +++ b/rare/components/dialogs/install/__init__.py @@ -1,3 +1,3 @@ from .dialog import InstallDialog -__all__ = ["InstallDialog"] +__all__ = ['InstallDialog'] diff --git a/rare/components/dialogs/install/advanced.py b/rare/components/dialogs/install/advanced.py index d2ba1267c0..9893d613c2 100644 --- a/rare/components/dialogs/install/advanced.py +++ b/rare/components/dialogs/install/advanced.py @@ -8,7 +8,7 @@ class InstallDialogAdvanced(CollapsibleFrame): def __init__(self, parent=None): super(InstallDialogAdvanced, self).__init__(parent=parent) - title = self.tr("Advanced options") + title = self.tr('Advanced options') self.setTitle(title) self.widget = QWidget(parent=self) @@ -17,4 +17,4 @@ def __init__(self, parent=None): self.setWidget(self.widget) -__all__ = ["InstallDialogAdvanced"] +__all__ = ['InstallDialogAdvanced'] diff --git a/rare/components/dialogs/install/dialog.py b/rare/components/dialogs/install/dialog.py index 7e69367f29..aa600af1b8 100644 --- a/rare/components/dialogs/install/dialog.py +++ b/rare/components/dialogs/install/dialog.py @@ -1,7 +1,6 @@ import os import platform as pf import shutil -from typing import Optional, Tuple, Union from PySide6.QtCore import Qt, QThreadPool, Signal, Slot from PySide6.QtGui import QShowEvent @@ -27,22 +26,22 @@ class InstallDialog(ActionDialog): result_ready = Signal(InstallQueueItemModel) - def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: InstallOptionsModel, parent=None): + def __init__(self, settings: RareAppSettings, rgame: 'RareGame', options: InstallOptionsModel, parent=None): super(InstallDialog, self).__init__(parent=parent) self.settings = settings - header = self.tr("Install") - bicon = qta_icon("ri.install-line") + header = self.tr('Install') + bicon = qta_icon('ri.install-line') if options.repair_mode: - header = self.tr("Repair") - bicon = qta_icon("fa.wrench", "mdi.progress-wrench") + header = self.tr('Repair') + bicon = qta_icon('fa.wrench', 'mdi.progress-wrench') if options.repair_and_update: - header = self.tr("Repair and update") + header = self.tr('Repair and update') elif options.update: - header = self.tr("Update") + header = self.tr('Update') elif options.reset_sdl: - header = self.tr("Modify") - bicon = qta_icon("fa.gear", "mdi.content-save-edit-outline") + header = self.tr('Modify') + bicon = qta_icon('fa.gear', 'mdi.content-save-edit-outline') self.setWindowTitle(game_title(header, rgame.app_title)) self.setSubtitle(game_title(header, rgame.app_title)) @@ -53,8 +52,8 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.core = rgame.core self.rgame = rgame self._options: InstallOptionsModel = options - self._download: Optional[InstallDownloadModel] = None - self._queue_item: Optional[InstallQueueItemModel] = None + self._download: InstallDownloadModel | None = None + self._queue_item: InstallQueueItemModel | None = None self.selectable = InstallDialogSelective(rgame, parent=self) self.selectable.stateChanged.connect(self._on_option_changed) @@ -122,10 +121,10 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal if options.repair_mode and not options.repair_and_update: self.selectable.click() - self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0)) + self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint('Legendary', 'max_workers', fallback=0)) self.advanced.ui.max_workers_spin.valueChanged.connect(self._on_option_changed) - self.advanced.ui.max_memory_spin.setValue(self.core.lgd.config.getint("Legendary", "max_memory", fallback=0)) + self.advanced.ui.max_memory_spin.setValue(self.core.lgd.config.getint('Legendary', 'max_memory', fallback=0)) self.advanced.ui.max_memory_spin.valueChanged.connect(self._on_option_changed) self.advanced.ui.read_files_check.setChecked(options.read_files) @@ -156,11 +155,11 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.ui.shortcut_check.setEnabled(False) self.selectable.setEnabled(False) - if pf.system() == "Darwin": + if pf.system() == 'Darwin': self.ui.shortcut_label.setDisabled(True) self.ui.shortcut_check.setDisabled(True) self.ui.shortcut_check.setChecked(False) - self.ui.shortcut_check.setToolTip(self.tr("Creating a shortcut is not supported on macOS")) + self.ui.shortcut_check.setToolTip(self.tr('Creating a shortcut is not supported on macOS')) self.advanced.ui.install_prereqs_label.setEnabled(False) self.advanced.ui.install_prereqs_check.setEnabled(False) @@ -170,10 +169,10 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal # lk: set object names for CSS properties self.accept_button.setText(header) self.accept_button.setIcon(bicon) - self.accept_button.setObjectName("InstallButton") + self.accept_button.setObjectName('InstallButton') - self.action_button.setText(self.tr("Validate")) - self.action_button.setIcon(qta_icon("fa.check", "fa5s.check")) + self.action_button.setText(self.tr('Validate')) + self.action_button.setIcon(qta_icon('fa.check', 'fa5s.check')) self.setCentralWidget(install_widget) @@ -204,19 +203,19 @@ def reset_install_dir(self, index: int): @Slot(int) def check_incompatible_platform(self, index: int): platform = self.ui.platform_combo.itemText(index) - if platform == "Mac" and pf.system() != "Darwin": + if platform == 'Mac' and pf.system() != 'Darwin': self.set_error_labels( - self.tr("Warning"), - self.tr("You will not be able to run the game if you select {} as platform").format(platform), + self.tr('Warning'), + self.tr('You will not be able to run the game if you select {} as platform').format(platform), ) else: self.set_error_labels() def get_options(self): - base_path = os.path.join(self.install_dir_edit.text(), ".overlay" if self._options.overlay else "") + base_path = os.path.join(self.install_dir_edit.text(), '.overlay' if self._options.overlay else '') # TODO: investigate if this check is needed if self.rgame.is_installed or self.rgame.is_dlc: - self._options.base_path = "" + self._options.base_path = '' else: self._options.base_path = base_path self._options.platform = self.ui.platform_combo.currentText() @@ -241,7 +240,7 @@ def get_download_info(self): def action_handler(self): self.set_error_labels() - message = self.tr("Updating...") + message = self.tr('Updating...') self.set_size_labels(message, message) self.setActive(True) self.options_changed = False @@ -265,12 +264,12 @@ def _on_option_changed_no_reload(self, state: Qt.CheckState): self._options.install_prereqs = state != Qt.CheckState.Unchecked @staticmethod - def _install_dir_edit_callback(path: str) -> Tuple[bool, str, int]: + def _install_dir_edit_callback(path: str) -> tuple[bool, str, int]: if not path: return False, path, IndicatorReasonsCommon.IS_EMPTY try: - perms_path = os.path.join(path, ".rare_perms") - open(perms_path, "w").close() + perms_path = os.path.join(path, '.rare_perms') + open(perms_path, 'w').close() os.unlink(perms_path) except PermissionError: return False, path, IndicatorReasonsCommon.PERM_NO_WRITE @@ -289,17 +288,17 @@ def _on_install_dir_validation(self, is_valid: bool, reason: str): self.accept_button.setEnabled(False) self.action_button.setEnabled(is_valid and not self.active()) if not is_valid: - self.set_error_labels(self.tr("Error"), reason) + self.set_error_labels(self.tr('Error'), reason) else: self.set_error_labels() @staticmethod def same_platform(download: InstallDownloadModel) -> bool: platform = download.igame.platform - if pf.system() == "Windows": - return platform in {"Windows", "Win32"} - elif pf.system() == "Darwin": - return platform == "Mac" + if pf.system() == 'Windows': + return platform in {'Windows', 'Win32'} + elif pf.system() == 'Darwin': + return platform == 'Mac' else: return False @@ -314,14 +313,14 @@ def _on_worker_result(self, download: InstallDownloadModel): if download_size or (not download_size and (download.game.is_dlc or download.repair)): self.accept_button.setEnabled(not self.options_changed) self.action_button.setEnabled(self.options_changed) - has_prereqs = bool(download.igame.prereq_info) and not download.igame.prereq_info.get("installed", False) + has_prereqs = bool(download.igame.prereq_info) and not download.igame.prereq_info.get('installed', False) if has_prereqs: - prereq_name = download.igame.prereq_info.get("name", "") - prereq_path = os.path.split(download.igame.prereq_info.get("path", ""))[-1] + prereq_name = download.igame.prereq_info.get('name', '') + prereq_path = os.path.split(download.igame.prereq_info.get('path', ''))[-1] prereq_desc = prereq_name if prereq_name else prereq_path - self.advanced.ui.install_prereqs_check.setText(self.tr("Also install: {}").format(prereq_desc)) + self.advanced.ui.install_prereqs_check.setText(self.tr('Also install: {}').format(prereq_desc)) else: - self.advanced.ui.install_prereqs_check.setText("") + self.advanced.ui.install_prereqs_check.setText('') # Offer to install prerequisites only on same platforms self.advanced.ui.install_prereqs_label.setEnabled(has_prereqs) self.advanced.ui.install_prereqs_check.setEnabled(has_prereqs) @@ -341,7 +340,7 @@ def _on_worker_result(self, download: InstallDownloadModel): @Slot(str) def _on_worker_failed(self, message: str): self.setActive(False) - error_text = self.tr("Error") + error_text = self.tr('Error') self.set_size_labels(error_text, error_text) self.set_error_labels(error_text, message) self.action_button.setEnabled(self.options_changed) @@ -350,7 +349,7 @@ def _on_worker_failed(self, message: str): self.open() @staticmethod - def _set_size_label(label: QLabel, value: Union[int, float, str]): + def _set_size_label(label: QLabel, value: int | float | str): is_numeric = isinstance(value, (int, float)) font = label.font() font.setBold(is_numeric) @@ -359,11 +358,11 @@ def _set_size_label(label: QLabel, value: Union[int, float, str]): text = format_size(value) if is_numeric else value label.setText(text) - def set_size_labels(self, download: Union[int, float, str], install: Union[int, float, str]): + def set_size_labels(self, download: int | float | str, install: int | float | str): self._set_size_label(self.ui.download_size_text, download) self._set_size_label(self.ui.install_size_text, install) - def set_error_labels(self, label: str = "", message: str = ""): + def set_error_labels(self, label: str = '', message: str = ''): self.ui.warning_label.setVisible(bool(label)) self.ui.warning_label.setText(label) self.ui.warning_text.setVisible(bool(message)) diff --git a/rare/components/dialogs/install/file_filters.py b/rare/components/dialogs/install/file_filters.py index 4ea756480f..e3104783ce 100644 --- a/rare/components/dialogs/install/file_filters.py +++ b/rare/components/dialogs/install/file_filters.py @@ -10,7 +10,7 @@ class InstallDialogFileFilters(CollapsibleFrame): def __init__(self, parent=None): super(InstallDialogFileFilters, self).__init__(parent=parent) - title = self.tr("File filters") + title = self.tr('File filters') self.setTitle(title) self.widget = QWidget(parent=self) @@ -27,9 +27,9 @@ def clear(self): def add_item(self, data: str): li = QListWidgetItem(data, self.ui.exclude_list) - li.setFont(QFont("monospace")) + li.setFont(QFont('monospace')) li.setCheckState(Qt.CheckState.Unchecked) self.ui.exclude_list.addItem(li) -__all__ = ["InstallDialogFileFilters"] +__all__ = ['InstallDialogFileFilters'] diff --git a/rare/components/dialogs/install/selective.py b/rare/components/dialogs/install/selective.py index 0a19162d69..51fe45c327 100644 --- a/rare/components/dialogs/install/selective.py +++ b/rare/components/dialogs/install/selective.py @@ -1,4 +1,4 @@ -from typing import List, Union +from logging import getLogger from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QFont @@ -9,14 +9,14 @@ class InstallTagCheckBox(QCheckBox): - def __init__(self, text, desc, tags: List[str], parent=None): + def __init__(self, text, desc, tags: list[str], parent=None): super(InstallTagCheckBox, self).__init__(parent) - self.setFont(QFont("monospace")) + self.setFont(QFont('monospace')) self.setText(text) self.setToolTip(desc) self.tags = tags - def isChecked(self) -> Union[bool, List[str]]: + def isChecked(self) -> bool | list[str]: return self.tags if super(InstallTagCheckBox, self).isChecked() else False @@ -25,6 +25,7 @@ class SelectiveWidget(QWidget): def __init__(self, rgame: RareGame, platform: str, parent=None): super().__init__(parent=parent) + self.logger = getLogger(type(self).__name__) self._has_tags = False main_layout = QVBoxLayout(self) @@ -32,18 +33,18 @@ def __init__(self, rgame: RareGame, platform: str, parent=None): core = rgame.core - config_tags = core.lgd.config.get(rgame.app_name, "install_tags", fallback=None) - config_disable_sdl = core.lgd.config.getboolean(rgame.app_name, "disable_sdl", fallback=False) + config_tags = core.lgd.config.get(rgame.app_name, 'install_tags', fallback=None) + config_disable_sdl = core.lgd.config.getboolean(rgame.app_name, 'disable_sdl', fallback=False) sdl_data = rgame.sdl_data(platform) if not config_disable_sdl and sdl_data: for group, info in sdl_data.items(): - cb = InstallTagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) - if group == "__required": + cb = InstallTagCheckBox(info['name'].strip(), info['description'].strip(), info['tags']) + if group == '__required': cb.setChecked(True) cb.setDisabled(True) if config_tags is not None: - if all(tag in config_tags for tag in info["tags"]): + if all(tag in config_tags for tag in info['tags']): cb.setChecked(True) cb.stateChanged.connect(self.stateChanged) main_layout.addWidget(cb) @@ -51,13 +52,13 @@ def __init__(self, rgame: RareGame, platform: str, parent=None): else: self._has_tags = False - def enabled_tags(self) -> List[str]: + def enabled_tags(self) -> list[str]: install_tags = set() for cb in self.findChildren(InstallTagCheckBox, options=Qt.FindChildOption.FindDirectChildrenOnly): if data := cb.isChecked(): # noinspection PyTypeChecker install_tags.update(data) - install_tags = ["", *install_tags] + install_tags = ['', *install_tags] return install_tags def supports_tags(self) -> bool: @@ -69,7 +70,7 @@ class InstallDialogSelective(CollapsibleFrame): def __init__(self, rgame: RareGame, parent=None): super(InstallDialogSelective, self).__init__(parent=parent) - title = self.tr("Optional downloads") + title = self.tr('Optional downloads') self.setTitle(title) self.setEnabled(False) @@ -85,8 +86,8 @@ def update_list(self, platform: str): self.widget.stateChanged.connect(self.stateChanged) self.setWidget(self.widget) - def enabled_tags(self) -> List[str]: + def enabled_tags(self) -> list[str]: return self.widget.enabled_tags() -__all__ = ["InstallDialogSelective", "SelectiveWidget"] +__all__ = ['InstallDialogSelective', 'SelectiveWidget'] diff --git a/rare/components/dialogs/launch.py b/rare/components/dialogs/launch.py index 30a3a2db14..63119f50ca 100644 --- a/rare/components/dialogs/launch.py +++ b/rare/components/dialogs/launch.py @@ -51,8 +51,8 @@ def login(self): # self.core.check_for_updates(force=True) # self.core.force_show_update = True if not self.core.login(force_refresh=True): - raise ValueError("You are not logged in. Opening login window.") - self.logger.info("You are logged in") + raise ValueError('You are not logged in. Opening login window.') + self.logger.info('You are logged in') self.login_dialog.close() except ValueError as e: self.logger.info(str(e)) @@ -74,7 +74,7 @@ def do_launch(self): self.launch() def launch(self): - self.progress_info.setText(self.tr("Preparing Rare")) + self.progress_info.setText(self.tr('Preparing Rare')) self.rcore.fetch() @Slot(int, str) @@ -83,5 +83,5 @@ def __on_progress(self, i: int, m: str): self.progress_info.setText(m) def __on_completed(self): - self.logger.info("Application starting") + self.logger.info('Application starting') self.start_app.emit() diff --git a/rare/components/dialogs/login/__init__.py b/rare/components/dialogs/login/__init__.py index 111efc5e3e..d75a5d2342 100644 --- a/rare/components/dialogs/login/__init__.py +++ b/rare/components/dialogs/login/__init__.py @@ -60,19 +60,19 @@ def __init__(self, args: Namespace, core: LegendaryCore, parent=None): self.import_page.validated.connect(self._on_page_validated) self.info_message = { - self.landing_index: self.tr("Select log-in method."), + self.landing_index: self.tr('Select log-in method.'), self.browser_index: self.tr( - "Click the Open Browser button to open the " - "login page in your web browser or copy the link and paste it " - "in any web browser. After logging in using the browser, copy " - "the text in the quotes after authorizationCode " - "in the same line into the empty input above." - "

DO NOT SHARE THE INFORMATION IN THE BROWSER PAGE WITH " - "ANYONE IN ANY FORM (TEXT OR SCREENSHOT)!
" + 'Click the Open Browser button to open the ' + 'login page in your web browser or copy the link and paste it ' + 'in any web browser. After logging in using the browser, copy ' + 'the text in the quotes after authorizationCode ' + 'in the same line into the empty input above.' + '

DO NOT SHARE THE INFORMATION IN THE BROWSER PAGE WITH ' + 'ANYONE IN ANY FORM (TEXT OR SCREENSHOT)!
' ), self.import_index: self.tr( - "Select the Wine prefix where Epic Games Launcher is installed. " - "You will get logged out from EGL in the process." + 'Select the Wine prefix where Epic Games Launcher is installed. ' + 'You will get logged out from EGL in the process.' ), } self.ui.info_label.setText(self.info_message[self.landing_index]) @@ -96,9 +96,9 @@ def __init__(self, args: Namespace, core: LegendaryCore, parent=None): self.login_stack.setCurrentWidget(self.landing_page) - self.ui.exit_button.setIcon(qta_icon("fa.remove", "fa5s.times")) - self.ui.back_button.setIcon(qta_icon("fa.chevron-left", "fa5s.chevron-left")) - self.ui.next_button.setIcon(qta_icon("fa.chevron-right", "fa5s.chevron-right")) + self.ui.exit_button.setIcon(qta_icon('fa.remove', 'fa5s.times')) + self.ui.back_button.setIcon(qta_icon('fa.chevron-left', 'fa5s.chevron-left')) + self.ui.next_button.setIcon(qta_icon('fa.chevron-right', 'fa5s.chevron-right')) # lk: Set next as the default button only to stop closing the dialog when pressing enter self.ui.exit_button.setAutoDefault(False) @@ -157,7 +157,7 @@ def login(self): def _on_login_successful(self): try: if not self.core.login(): - raise ValueError("Login failed.") + raise ValueError('Login failed.') self.logged_in = True self.accept() except Exception as e: @@ -165,4 +165,4 @@ def _on_login_successful(self): self.core.lgd.invalidate_userdata() self.ui.next_button.setEnabled(False) self.logged_in = False - QMessageBox.warning(None, self.tr("Login error"), str(e)) + QMessageBox.warning(None, self.tr('Login error'), str(e)) diff --git a/rare/components/dialogs/login/browser_login.py b/rare/components/dialogs/login/browser_login.py index 45861a370f..bc64a9f5d9 100644 --- a/rare/components/dialogs/login/browser_login.py +++ b/rare/components/dialogs/login/browser_login.py @@ -1,5 +1,4 @@ import json -from typing import Tuple from legendary.utils import webview_login from PySide6.QtCore import QProcess, QUrl, Slot @@ -26,11 +25,11 @@ def __init__(self, core: LegendaryCore, parent=None): self.login_url = self.core.egs.get_auth_url() self.auth_edit = IndicatorLineEdit( - placeholder=self.tr("Insert authorizationCode here"), edit_func=self.sid_edit_callback, parent=self + placeholder=self.tr('Insert authorizationCode here'), edit_func=self.sid_edit_callback, parent=self ) self.auth_edit.line_edit.setEchoMode(QLineEdit.EchoMode.Password) self.ui.link_text.setText(self.login_url) - self.ui.copy_button.setIcon(qta_icon("mdi.content-copy", "fa5.copy")) + self.ui.copy_button.setIcon(qta_icon('mdi.content-copy', 'fa5.copy')) self.ui.copy_button.clicked.connect(self._on_copy_link) self.ui.form_layout.setWidget( self.ui.form_layout.getWidgetPosition(self.ui.sid_label)[0], QFormLayout.ItemRole.FieldRole, self.auth_edit @@ -43,18 +42,18 @@ def __init__(self, core: LegendaryCore, parent=None): def _on_copy_link(self): clipboard = QApplication.instance().clipboard() clipboard.setText(self.login_url) - self.ui.status_field.setText(self.tr("Copied to clipboard")) + self.ui.status_field.setText(self.tr('Copied to clipboard')) def is_valid(self) -> bool: return self.auth_edit.is_valid @staticmethod - def sid_edit_callback(text) -> Tuple[bool, str, int]: + def sid_edit_callback(text) -> tuple[bool, str, int]: if text: text = text.strip() - if text.startswith("{") and text.endswith("}"): + if text.startswith('{') and text.endswith('}'): try: - text = json.loads(text).get("authorizationCode") + text = json.loads(text).get('authorizationCode') except json.JSONDecodeError: return False, text, IndicatorReasonsCommon.WRONG_FORMAT elif '"' in text: @@ -64,16 +63,16 @@ def sid_edit_callback(text) -> Tuple[bool, str, int]: return False, text, IndicatorReasonsCommon.VALID def do_login(self) -> None: - self.ui.status_field.setText(self.tr("Logging in...")) + self.ui.status_field.setText(self.tr('Logging in...')) auth_code = self.auth_edit.text() try: if self.core.auth_code(auth_code): - self.logger.info("Successfully logged in as %s", self.core.lgd.userdata["displayName"]) + self.logger.info('Successfully logged in as %s', self.core.lgd.userdata['displayName']) self.success.emit() except Exception as e: msg = e.message if isinstance(e, LgndrException) else str(e) - self.ui.status_field.setText(self.tr("Login failed: {}").format(msg)) - self.logger.error("Failed to login through browser") + self.ui.status_field.setText(self.tr('Login failed: {}').format(msg)) + self.logger.error('Failed to login through browser') self.logger.error(e) @Slot() @@ -82,25 +81,25 @@ def _on_open_browser(self): self.logger.warning("You don't have webengine installed, you will need to manually copy the authorizationCode.") QDesktopServices.openUrl(QUrl(self.login_url)) else: - cmd = get_rare_executable() + ["login", self.core.get_egl_version()] + cmd = get_rare_executable() + ['login', self.core.get_egl_version()] proc = QProcess(self) proc.start(cmd[0], cmd[1:]) proc.waitForFinished(-1) out, _ = ( - proc.readAllStandardOutput().data().decode("utf-8", "ignore"), - proc.readAllStandardError().data().decode("utf-8", "ignore"), + proc.readAllStandardOutput().data().decode('utf-8', 'ignore'), + proc.readAllStandardError().data().decode('utf-8', 'ignore'), ) proc.deleteLater() if out: try: self.core.auth_ex_token(out) - self.logger.info("Successfully logged in as %s", {self.core.lgd.userdata["displayName"]}) + self.logger.info('Successfully logged in as %s', {self.core.lgd.userdata['displayName']}) self.success.emit() except Exception as e: msg = e.message if isinstance(e, LgndrException) else str(e) - self.ui.status_field.setText(self.tr("Login failed: {}").format(msg)) - self.logger.error("Failed to login through browser") + self.ui.status_field.setText(self.tr('Login failed: {}').format(msg)) + self.logger.error('Failed to login through browser') self.logger.error(e) else: - self.logger.error("Failed to login through browser") + self.logger.error('Failed to login through browser') diff --git a/rare/components/dialogs/login/import_login.py b/rare/components/dialogs/login/import_login.py index 249932da8d..dc13abb848 100644 --- a/rare/components/dialogs/login/import_login.py +++ b/rare/components/dialogs/login/import_login.py @@ -15,11 +15,11 @@ class ImportLogin(LoginFrame): # FIXME: Use pathspec instead of duplicated code - if platform.system() == "Windows": - localappdata = os.path.expandvars("%LOCALAPPDATA%") + if platform.system() == 'Windows': + localappdata = os.path.expandvars('%LOCALAPPDATA%') else: - localappdata = os.path.join("drive_c/users", getuser(), "Local Settings/Application Data") - egl_appdata = os.path.join(localappdata, "EpicGamesLauncher", "Saved", "Config", "Windows") + localappdata = os.path.join('drive_c/users', getuser(), 'Local Settings/Application Data') + egl_appdata = os.path.join(localappdata, 'EpicGamesLauncher', 'Saved', 'Config', 'Windows') found = False def __init__(self, core: LegendaryCore, parent=None): @@ -29,9 +29,9 @@ def __init__(self, core: LegendaryCore, parent=None): self.ui.setupUi(self) self.text_egl_found = self.tr("Found EGL Program Data. Click 'Next' to import them.") - self.text_egl_notfound = self.tr("Could not find EGL Program Data. ") + self.text_egl_notfound = self.tr('Could not find EGL Program Data. ') - if platform.system() == "Windows": + if platform.system() == 'Windows': if not self.core.egl.appdata_path and os.path.exists(self.egl_appdata): self.core.egl.appdata_path = self.egl_appdata if not self.core.egl.appdata_path: @@ -42,12 +42,12 @@ def __init__(self, core: LegendaryCore, parent=None): self.ui.prefix_combo.setCurrentText(self.egl_appdata) else: if programdata_path := self.core.egl.programdata_path: - if wine_pfx := programdata_path.split("drive_c")[0]: + if wine_pfx := programdata_path.split('drive_c')[0]: self.ui.prefix_combo.addItem(wine_pfx) prefixes = self.get_wine_prefixes() if len(prefixes): self.ui.prefix_combo.addItems(prefixes) - self.ui.status_field.setText(self.tr("Select the Wine prefix you want to import.")) + self.ui.status_field.setText(self.tr('Select the Wine prefix you want to import.')) else: self.ui.status_field.setText(self.text_egl_notfound) @@ -56,8 +56,8 @@ def __init__(self, core: LegendaryCore, parent=None): def get_wine_prefixes(self): possible_prefixes = [ - os.path.expanduser("~/.wine"), - os.path.expanduser("~/Games/epic-games-store"), + os.path.expanduser('~/.wine'), + os.path.expanduser('~/Games/epic-games-store'), ] prefixes = [] for prefix in possible_prefixes: @@ -67,7 +67,7 @@ def get_wine_prefixes(self): @Slot() def _on_prefix_path(self): - prefix_dialog = QFileDialog(self, self.tr("Choose path"), os.path.expanduser("~/")) + prefix_dialog = QFileDialog(self, self.tr('Choose path'), os.path.expanduser('~/')) prefix_dialog.setFileMode(QFileDialog.FileMode.Directory) prefix_dialog.setOption(QFileDialog.Option.ShowDirsOnly) if prefix_dialog.exec_(): @@ -75,14 +75,14 @@ def _on_prefix_path(self): self.ui.prefix_combo.setCurrentText(names[0]) def is_valid(self) -> bool: - if platform.system() == "Windows": + if platform.system() == 'Windows': return self.found else: egl_wine_pfx = self.ui.prefix_combo.currentText() try: wine_folders = get_shell_folders(read_registry(egl_wine_pfx), egl_wine_pfx) self.egl_appdata = os.path.realpath( - os.path.join(wine_folders["Local AppData"], "EpicGamesLauncher", "Saved", "Config", "Windows") + os.path.join(wine_folders['Local AppData'], 'EpicGamesLauncher', 'Saved', 'Config', 'Windows') ) if path_exists := os.path.exists(self.egl_appdata): self.ui.status_field.setText(self.text_egl_found) @@ -91,16 +91,16 @@ def is_valid(self) -> bool: return False def do_login(self) -> None: - self.ui.status_field.setText(self.tr("Loading...")) - if os.name != "nt": - self.logger.info("Using EGL appdata path at %s", {self.egl_appdata}) + self.ui.status_field.setText(self.tr('Loading...')) + if os.name != 'nt': + self.logger.info('Using EGL appdata path at %s', {self.egl_appdata}) self.core.egl.appdata_path = self.egl_appdata try: if self.core.auth_import(): - self.logger.info("Logged in as %s", {self.core.lgd.userdata["displayName"]}) + self.logger.info('Logged in as %s', {self.core.lgd.userdata['displayName']}) self.success.emit() except Exception as e: msg = e.message if isinstance(e, LgndrException) else str(e) - self.ui.status_field.setText(self.tr("Login failed: {}").format(msg)) - self.logger.warning("Failed to import existing session") + self.ui.status_field.setText(self.tr('Login failed: {}').format(msg)) + self.logger.warning('Failed to import existing session') self.logger.error(e) diff --git a/rare/components/dialogs/move.py b/rare/components/dialogs/move.py index e1b216678f..32924fbee5 100644 --- a/rare/components/dialogs/move.py +++ b/rare/components/dialogs/move.py @@ -1,5 +1,4 @@ import os -from typing import Optional, Tuple, Union from PySide6.QtCore import QSignalBlocker, QThreadPool, Signal, Slot from PySide6.QtGui import QFont, QShowEvent, Qt @@ -21,7 +20,7 @@ class MoveDialog(ActionDialog): def __init__(self, rcore: RareCore, rgame: RareGame, parent=None): super(MoveDialog, self).__init__(parent=parent) - header = self.tr("Move") + header = self.tr('Move') self.setWindowTitle(game_title(header, rgame.app_title)) self.setSubtitle(game_title(header, rgame.app_title)) @@ -34,7 +33,7 @@ def __init__(self, rcore: RareCore, rgame: RareGame, parent=None): self.rcore = rcore self.core = rcore.core() - self.rgame: Optional[RareGame] = rgame + self.rgame: RareGame | None = rgame self.options: MoveGameModel = MoveGameModel(rgame.app_name, rgame.install_path, rgame.folder_name) self.target_path_edit = PathEdit( @@ -45,12 +44,12 @@ def __init__(self, rcore: RareCore, rgame: RareGame, parent=None): ) self.target_path_edit.setReadOnly(True) self.target_path_edit.reasons = { - MovePathEditReasons.MOVEDIALOG_DST_MISSING: self.tr("You need to provide the destination directory."), - MovePathEditReasons.MOVEDIALOG_NO_WRITE: self.tr("No write permission on destination."), - MovePathEditReasons.MOVEDIALOG_SAME_DIR: self.tr("Same directory or subdirectory selected."), - MovePathEditReasons.MOVEDIALOG_DST_IN_SRC: self.tr("Destination is inside source directory"), - MovePathEditReasons.MOVEDIALOG_NESTED_DIR: self.tr("Game install directories cannot be nested."), - MovePathEditReasons.MOVEDIALOG_NO_SPACE: self.tr("Not enough space available on drive."), + MovePathEditReasons.MOVEDIALOG_DST_MISSING: self.tr('You need to provide the destination directory.'), + MovePathEditReasons.MOVEDIALOG_NO_WRITE: self.tr('No write permission on destination.'), + MovePathEditReasons.MOVEDIALOG_SAME_DIR: self.tr('Same directory or subdirectory selected.'), + MovePathEditReasons.MOVEDIALOG_DST_IN_SRC: self.tr('Destination is inside source directory'), + MovePathEditReasons.MOVEDIALOG_NESTED_DIR: self.tr('Game install directories cannot be nested.'), + MovePathEditReasons.MOVEDIALOG_NO_SPACE: self.tr('Not enough space available on drive.'), } self.target_path_edit.validationFinished.connect(self.__on_target_path_validation) self.ui.main_layout.setWidget( @@ -60,7 +59,7 @@ def __init__(self, rcore: RareCore, rgame: RareGame, parent=None): ) self.full_path_info = ElideLabel(parent=self) - self.full_path_info.setFont(QFont("monospace")) + self.full_path_info.setFont(QFont('monospace')) self.ui.main_layout.setWidget( self.ui.main_layout.getWidgetPosition(self.ui.full_path_label)[0], QFormLayout.ItemRole.FieldRole, @@ -73,12 +72,12 @@ def __init__(self, rcore: RareCore, rgame: RareGame, parent=None): self.ui.reset_name_check.setChecked(self.options.reset_name) self.ui.reset_name_check.checkStateChanged.connect(self.__on_reset_name_changed) - self.accept_button.setText(self.tr("Move")) - self.accept_button.setIcon(qta_icon("mdi.folder-move-outline")) - self.accept_button.setObjectName("MoveButton") + self.accept_button.setText(self.tr('Move')) + self.accept_button.setIcon(qta_icon('mdi.folder-move-outline')) + self.accept_button.setObjectName('MoveButton') - self.action_button.setText(self.tr("Validate")) - self.action_button.setIcon(qta_icon("fa.check", "fa5s.check")) + self.action_button.setText(self.tr('Validate')) + self.action_button.setIcon(qta_icon('fa.check', 'fa5s.check')) self.setCentralWidget(move_widget) @@ -90,7 +89,7 @@ def showEvent(self, a0: QShowEvent) -> None: def action_handler(self): self.set_error_labels() - message = self.tr("Updating...") + message = self.tr('Updating...') self.set_size_labels(message, message) self.setActive(True, disable=False) self.options.target_path = self.target_path_edit.text() @@ -109,7 +108,7 @@ def accept_handler(self): def reject_handler(self): self.options.accepted = False - self.options.target_path = "" + self.options.target_path = '' @Slot(Qt.CheckState) def __on_rename_path_changed(self, state: Qt.CheckState): @@ -128,12 +127,12 @@ def __on_reset_name_changed(self, state: Qt.CheckState): self.action_button.setEnabled(True) @staticmethod - def __target_path_edit_callback(path: str) -> Tuple[bool, str, int]: + def __target_path_edit_callback(path: str) -> tuple[bool, str, int]: if not path: return False, path, IndicatorReasonsCommon.IS_EMPTY try: - perms_path = os.path.join(path, ".rare_perms") - open(perms_path, "w").close() + perms_path = os.path.join(path, '.rare_perms') + open(perms_path, 'w').close() os.unlink(perms_path) except PermissionError: return False, path, IndicatorReasonsCommon.PERM_NO_WRITE @@ -142,14 +141,12 @@ def __target_path_edit_callback(path: str) -> Tuple[bool, str, int]: return True, path, IndicatorReasonsCommon.VALID @Slot(bool, object, object, MovePathEditReasons) - def __on_worker_result( - self, is_valid: bool, src_size: Union[int, float], dst_size: Union[int, float], reason: MovePathEditReasons - ): + def __on_worker_result(self, is_valid: bool, src_size: int | float, dst_size: int | float, reason: MovePathEditReasons): self.setActive(False, disable=False) self.set_size_labels(src_size, dst_size) self.action_button.setEnabled(False) self.accept_button.setEnabled(is_valid) - error, reason = (self.tr("Error"), self.target_path_edit.reasons[reason]) if not is_valid else ("", "") + error, reason = (self.tr('Error'), self.target_path_edit.reasons[reason]) if not is_valid else ('', '') self.set_error_labels(error, reason) @Slot(bool, str) @@ -158,11 +155,11 @@ def __on_target_path_validation(self, is_valid: bool, reason: str): self.full_path_info.setText(self.options.full_path) self.action_button.setEnabled(is_valid and not self.active()) self.accept_button.setEnabled(False) - error, reason = (self.tr("Error"), reason) if not is_valid else ("", "") + error, reason = (self.tr('Error'), reason) if not is_valid else ('', '') self.set_error_labels(error, reason) @staticmethod - def __set_size_label(label: QLabel, value: Union[int, float, str]): + def __set_size_label(label: QLabel, value: int | float | str): is_numeric = isinstance(value, (int, float)) font = label.font() font.setBold(is_numeric) @@ -171,11 +168,11 @@ def __set_size_label(label: QLabel, value: Union[int, float, str]): text = format_size(value) if is_numeric else value label.setText(text) - def set_size_labels(self, required: Union[int, float, str], available: Union[int, float, str]): + def set_size_labels(self, required: int | float | str, available: int | float | str): self.__set_size_label(self.ui.required_space_text, required) self.__set_size_label(self.ui.available_space_text, available) - def set_error_labels(self, label: str = "", message: str = ""): + def set_error_labels(self, label: str = '', message: str = ''): self.ui.warning_label.setVisible(bool(label)) self.ui.warning_label.setText(label) self.ui.warning_text.setVisible(bool(message)) diff --git a/rare/components/dialogs/selective.py b/rare/components/dialogs/selective.py index 8581937398..bf6a3d439d 100644 --- a/rare/components/dialogs/selective.py +++ b/rare/components/dialogs/selective.py @@ -14,14 +14,14 @@ class SelectiveDialog(ButtonDialog): def __init__(self, rgame: RareGame, parent=None): super(SelectiveDialog, self).__init__(parent=parent) - header = self.tr("Optional downloads for") + header = self.tr('Optional downloads for') self.setWindowTitle(game_title(header, rgame.app_title)) self.setSubtitle(game_title(header, rgame.app_title)) self.rgame = rgame self.selective_widget = SelectiveWidget(rgame, rgame.igame.platform, self) - container = QGroupBox(self.tr("Optional downloads"), self) + container = QGroupBox(self.tr('Optional downloads'), self) container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) container_layout.addWidget(self.selective_widget) @@ -31,8 +31,8 @@ def __init__(self, rgame: RareGame, parent=None): self.setCentralLayout(layout) - self.accept_button.setText(self.tr("Verify")) - self.accept_button.setIcon(qta_icon("fa.check", "fa5s.check")) + self.accept_button.setText(self.tr('Verify')) + self.accept_button.setIcon(qta_icon('fa.check', 'fa5s.check')) self.options: SelectiveDownloadsModel = SelectiveDownloadsModel(rgame.app_name) diff --git a/rare/components/dialogs/uninstall.py b/rare/components/dialogs/uninstall.py index f2327f068a..468cfd9d12 100644 --- a/rare/components/dialogs/uninstall.py +++ b/rare/components/dialogs/uninstall.py @@ -1,5 +1,3 @@ -from typing import Union - from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtWidgets import ( QCheckBox, @@ -15,25 +13,25 @@ class UninstallDialog(ButtonDialog): result_ready = Signal(UninstallOptionsModel) - def __init__(self, rgame: Union[RareGame, RareEosOverlay], options: UninstallOptionsModel, parent=None): + def __init__(self, rgame: RareGame | RareEosOverlay, options: UninstallOptionsModel, parent=None): super(UninstallDialog, self).__init__(parent=parent) - header = self.tr("Uninstall") + header = self.tr('Uninstall') self.setWindowTitle(game_title(header, rgame.app_title)) self.setSubtitle(game_title(header, rgame.app_title)) - self.keep_files = QCheckBox(self.tr("Keep files")) + self.keep_files = QCheckBox(self.tr('Keep files')) self.keep_files.setChecked(bool(options.keep_files)) self.keep_files.setEnabled(not rgame.is_overlay) - self.keep_folder = QCheckBox(self.tr("Keep game folder")) + self.keep_folder = QCheckBox(self.tr('Keep game folder')) self.keep_folder.setChecked(bool(options.keep_folder)) self.keep_folder.setEnabled(not rgame.is_overlay and not rgame.is_dlc) - self.keep_config = QCheckBox(self.tr("Keep configuation")) + self.keep_config = QCheckBox(self.tr('Keep configuation')) self.keep_config.setChecked(bool(options.keep_config)) self.keep_config.setEnabled(not rgame.is_overlay and not rgame.is_dlc) - self.keep_overlay_keys = QCheckBox(self.tr("Keep EOS Overlay registry keys")) + self.keep_overlay_keys = QCheckBox(self.tr('Keep EOS Overlay registry keys')) self.keep_overlay_keys.setChecked(bool(options.keep_overlay_keys)) self.keep_overlay_keys.setEnabled(rgame.is_overlay) @@ -45,9 +43,9 @@ def __init__(self, rgame: Union[RareGame, RareEosOverlay], options: UninstallOpt self.setCentralLayout(layout) - self.accept_button.setText(self.tr("Uninstall")) - self.accept_button.setIcon(qta_icon("ri.uninstall-line")) - self.accept_button.setObjectName("UninstallButton") + self.accept_button.setText(self.tr('Uninstall')) + self.accept_button.setIcon(qta_icon('ri.uninstall-line')) + self.accept_button.setObjectName('UninstallButton') self.keep_files.checkStateChanged.connect(self.__on_keep_files_changed) diff --git a/rare/components/main_window.py b/rare/components/main_window.py index 2f9a85c5a5..fca75ad6a3 100644 --- a/rare/components/main_window.py +++ b/rare/components/main_window.py @@ -26,7 +26,7 @@ from .tabs import MainTabWidget from .tray_icon import TrayIcon -logger = getLogger("MainWindow") +logger = getLogger('MainWindow') class RareWindow(QMainWindow): @@ -55,7 +55,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.status_bar = QStatusBar(self) self.setStatusBar(self.status_bar) - self.active_label = QLabel(self.tr("Active:"), self.status_bar) + self.active_label = QLabel(self.tr('Active:'), self.status_bar) # lk: set top and botton margins to accommodate border for scroll area labels self.active_label.setContentsMargins(5, 1, 0, 1) self.status_bar.addWidget(self.active_label) @@ -66,7 +66,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): active_layout.setSizeConstraint(QHBoxLayout.SizeConstraint.SetFixedSize) self.status_bar.addWidget(self.active_container, stretch=0) - self.queued_label = QLabel(self.tr("Queued:"), self.status_bar) + self.queued_label = QLabel(self.tr('Queued:'), self.status_bar) # lk: set top and botton margins to accommodate border for scroll area labels self.queued_label.setContentsMargins(5, 1, 0, 1) self.status_bar.addPermanentWidget(self.queued_label) @@ -108,7 +108,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.discord_rpc = DiscordRPC(settings, rcore, parent=self) except ModuleNotFoundError: - logger.warning("Discord RPC module not found") + logger.warning('Discord RPC module not found') self.singleton_timer = QTimer(self) self.singleton_timer.setInterval(1000) @@ -122,7 +122,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): # enable kinetic scrolling for scroll_area in self.findChildren(QScrollArea): - if not scroll_area.property("no_kinetic_scroll"): + if not scroll_area.property('no_kinetic_scroll'): QScroller.grabGesture( scroll_area.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture, @@ -185,10 +185,10 @@ def update_statusbar(self): self.queued_container.layout().removeWidget(label) label.deleteLater() for info in self.rcore.queue_info(): - label = ElideLabel(f"{info.prefix}: {info.app_title}") - label.setObjectName("QueueWorkerLabel") - label.setToolTip(f"{info.prefix}: {info.app_title}") - label.setProperty("workertype", info.type) + label = ElideLabel(f'{info.prefix}: {info.app_title}') + label.setObjectName('QueueWorkerLabel') + label.setToolTip(f'{info.prefix}: {info.app_title}') + label.setProperty('workertype', info.type) label.setFixedWidth(160) label.setContentsMargins(3, 0, 3, 0) if info.state == QueueWorkerState.ACTIVE: @@ -203,9 +203,9 @@ def update_statusbar(self): def timer_finished(self): file_path = lock_file() if os.path.exists(file_path): - with open(file_path, "r") as file: + with open(file_path) as file: action = file.read() - if action.startswith("show"): + if action.startswith('show'): self.show() os.remove(file_path) self.singleton_timer.start() @@ -233,9 +233,10 @@ def closeEvent(self, e: QCloseEvent) -> None: if self.rcore.threadpool_disk.activeThreadCount() or self.rcore.threadpool_net.activeThreadCount(): reply = QMessageBox.question( self, - self.tr("Quit {}?").format(QApplication.applicationName()), + self.tr('Quit {}?').format(QApplication.applicationName()), self.tr( - "There are currently running operations. Rare cannot exit until they are completed.\n\nDo you want to clear the queue?" + 'There are currently running operations. Rare cannot exit until they are completed.\n\n' + 'Do you want to clear the queue?' ), buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No), defaultButton=QMessageBox.StandardButton.No, @@ -251,9 +252,9 @@ def closeEvent(self, e: QCloseEvent) -> None: elif self.tab_widget.downloads_tab.is_download_active: reply = QMessageBox.question( self, - self.tr("Quit {}?").format(QApplication.applicationName()), + self.tr('Quit {}?').format(QApplication.applicationName()), self.tr( - "There is an active download. Quitting Rare now will stop the download.\n\nAre you sure you want to quit?" + 'There is an active download. Quitting Rare now will stop the download.\n\nAre you sure you want to quit?' ), buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No), defaultButton=QMessageBox.StandardButton.No, diff --git a/rare/components/tabs/__init__.py b/rare/components/tabs/__init__.py index 73403e2fee..6c93545117 100644 --- a/rare/components/tabs/__init__.py +++ b/rare/components/tabs/__init__.py @@ -36,32 +36,32 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent): # Generate Tabs self.games_tab = GamesLibrary(self.settings, self.rcore, self) self.games_tab.import_clicked.connect(self.show_import) - self.games_index = self.addTab(self.games_tab, self.tr("Games")) + self.games_index = self.addTab(self.games_tab, self.tr('Games')) # Downloads Tab after Games Tab to use populated RareCore games list self.downloads_tab = DownloadsTab(self.settings, self.rcore, self) - self.downloads_index = self.addTab(self.downloads_tab, "") + self.downloads_index = self.addTab(self.downloads_tab, '') self.downloads_tab.update_title.connect(self.__on_downloads_update_title) self.downloads_tab.update_queues_count() self.setTabEnabled(self.downloads_index, not self.args.offline) if not self.args.offline: self.store_tab = StoreTab(self.core, parent=self) - self.store_index = self.addTab(self.store_tab, self.tr("Store (Beta)")) + self.store_index = self.addTab(self.store_tab, self.tr('Store (Under Construction)')) self.setTabEnabled(self.store_index, not self.args.offline) # Space Tab - space_index = self.addTab(QWidget(self), "Rare") + space_index = self.addTab(QWidget(self), 'Rare') self.setTabEnabled(space_index, False) self.main_bar.expanded_idx = space_index # Integrations Tab self.integrations_tab = IntegrationsTab(self.rcore, self) - self.integrations_index = self.addTab(self.integrations_tab, self.tr("Integrations")) + self.integrations_index = self.addTab(self.integrations_tab, self.tr('Integrations')) # Settings Tab self.settings_tab = SettingsTab(settings, rcore, self) - self.settings_index = self.addTab(self.settings_tab, qta_icon("fa.gear", "fa6s.gear"), self.tr("Settings")) + self.settings_index = self.addTab(self.settings_tab, qta_icon('fa.gear', 'fa6s.gear'), self.tr('Settings')) self.settings_tab.update_available.connect(self._on_update_available) # Account Tab @@ -73,19 +73,19 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent): self.account_menu.addAction(account_action) self.account_tab = QWidget(self) self.account_index = self.addTab( - self.account_tab, qta_icon("mdi.account-circle", "fa5s.user"), self.core.lgd.userdata.get("displayName") + self.account_tab, qta_icon('mdi.account-circle', 'fa5s.user'), self.core.lgd.userdata.get('displayName') ) # Open game list on click on Games tab button self.tabBarClicked.connect(self.mouse_clicked) # shortcuts - QShortcut("Alt+1", self).activated.connect(self._on_shortcut_activated_games) + QShortcut('Alt+1', self).activated.connect(self._on_shortcut_activated_games) if not self.args.offline: - QShortcut("Alt+2", self).activated.connect(self._on_shortcut_activated_downloads) - QShortcut("Alt+3", self).activated.connect(self._on_shortcut_activated_store) - QShortcut("Alt+4", self).activated.connect(self._on_shortcut_activated_integrations) - QShortcut("Alt+5", self).activated.connect(self._on_shortcut_activated_settings) + QShortcut('Alt+2', self).activated.connect(self._on_shortcut_activated_downloads) + QShortcut('Alt+3', self).activated.connect(self._on_shortcut_activated_store) + QShortcut('Alt+4', self).activated.connect(self._on_shortcut_activated_integrations) + QShortcut('Alt+5', self).activated.connect(self._on_shortcut_activated_settings) self.setCurrentIndex(self.games_index) @@ -126,7 +126,7 @@ def _on_shortcut_activated_settings(self): @Slot() def _on_update_available(self): - self.main_bar.setTabText(self.settings_index, self.tr("Settings (!)")) + self.main_bar.setTabText(self.settings_index, self.tr('Settings (!)')) @Slot() @Slot(str) @@ -151,10 +151,10 @@ def show_ubisoft(self): @Slot(int) def __on_downloads_update_title(self, num_downloads: int): - suffix = "" if not num_downloads else f" ({num_downloads})" + suffix = '' if not num_downloads else f' ({num_downloads})' self.setTabText( self.indexOf(self.downloads_tab), - self.tr("Downloads") + suffix, + self.tr('Downloads') + suffix, ) def mouse_clicked(self, index): @@ -173,16 +173,16 @@ def _on_exit_app(self, exit_code: int): if self.downloads_tab.is_download_active: QMessageBox.warning( self, - self.tr("Quit") if exit_code == ExitCodes.EXIT else self.tr("Logout"), - self.tr("There are active downloads. Stop them before trying to quit."), + self.tr('Quit') if exit_code == ExitCodes.EXIT else self.tr('Logout'), + self.tr('There are active downloads. Stop them before trying to quit.'), ) return # FIXME: End of FIXME if exit_code == ExitCodes.LOGOUT: reply = QMessageBox.question( self, - self.tr("Logout"), - self.tr("Do you really want to logout {}?").format(self.core.lgd.userdata.get("display_name")), + self.tr('Logout'), + self.tr('Do you really want to logout {}?').format(self.core.lgd.userdata.get('display_name')), buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No), defaultButton=QMessageBox.StandardButton.No, ) diff --git a/rare/components/tabs/account/__init__.py b/rare/components/tabs/account/__init__.py index 94326552b4..a3fe30a86a 100644 --- a/rare/components/tabs/account/__init__.py +++ b/rare/components/tabs/account/__init__.py @@ -18,23 +18,23 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore, parent): self.signals = signals self.core = core - username = self.core.lgd.userdata.get("displayName") + username = self.core.lgd.userdata.get('displayName') if not username: - username = "Offline" + username = 'Offline' self.open_browser = QPushButton( - qta_icon("fa.external-link", "fa5s.external-link-alt"), - self.tr("Account settings"), + qta_icon('fa.external-link', 'fa5s.external-link-alt'), + self.tr('Account settings'), ) self.open_browser.clicked.connect(self._on_browser_clicked) - self.logout_button = QPushButton(self.tr("Logout"), parent=self) + self.logout_button = QPushButton(self.tr('Logout'), parent=self) self.logout_button.clicked.connect(self._on_logout) - self.quit_button = QPushButton(self.tr("Quit"), parent=self) + self.quit_button = QPushButton(self.tr('Quit'), parent=self) self.quit_button.clicked.connect(self._on_quit) layout = QVBoxLayout(self) - layout.addWidget(QLabel(self.tr("Logged in as {}").format(username))) + layout.addWidget(QLabel(self.tr('Logged in as {}').format(username))) vspacer = QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) layout.addSpacerItem(vspacer) layout.addWidget(self.open_browser) @@ -43,7 +43,7 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore, parent): @Slot() def _on_browser_clicked(self): - webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames") + webbrowser.open('https://www.epicgames.com/account/personal?productName=epicgames') @Slot() def _on_quit(self): diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 42fb2ee7fc..b445599ea4 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -2,7 +2,6 @@ import platform from copy import deepcopy from logging import getLogger -from typing import Optional, Union from PySide6.QtCore import Qt, QThreadPool, Signal, Slot from PySide6.QtGui import QPixmap @@ -35,7 +34,7 @@ from .thread import DlResultCode, DlResultModel, DlThread -def get_time(seconds: Union[int, float]) -> str: +def get_time(seconds: int | float) -> str: return str(datetime.timedelta(seconds=seconds)) @@ -53,7 +52,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.signals = rcore.signals() self.args = rcore.args() - self.__thread: Optional[DlThread] = None + self.__thread: DlThread | None = None layout = QVBoxLayout(self) @@ -95,7 +94,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.signals.download.enqueue.connect(self.__add_update) self.signals.download.dequeue.connect(self.__remove_update) - self.__forced_item: Optional[InstallQueueItemModel] = None + self.__forced_item: InstallQueueItemModel | None = None self.__omit_requeue = False @Slot() @@ -115,7 +114,7 @@ def __check_updates(self): @Slot(str) @Slot(RareGame) @Slot(RareEosOverlay) - def __add_update(self, update: Union[str, RareGame, RareEosOverlay]): + def __add_update(self, update: str | RareGame | RareEosOverlay): if isinstance(update, str): update = self.rcore.get_game(update) @@ -186,19 +185,19 @@ def __refresh_download(self, item: InstallQueueItemModel): (lambda obj, d: obj.start_download(InstallQueueItemModel(options=item.options, download=d))).__get__(self) ) worker.signals.failed.connect( - (lambda obj, m: obj.logger.error(f"Failed to refresh download for {item.options.app_name} with error: {m}")).__get__( + (lambda obj, m: obj.logger.error(f'Failed to refresh download for {item.options.app_name} with error: {m}')).__get__( self ) ) worker.signals.finished.connect( - (lambda obj: obj.logger.info(f"Download refresh worker finished for {item.options.app_name}")).__get__(self) + (lambda obj: obj.logger.info(f'Download refresh worker finished for {item.options.app_name}')).__get__(self) ) QThreadPool.globalInstance().start(worker) def start_download(self, item: InstallQueueItemModel): rgame = self.rcore.get_game(item.options.app_name) - if not rgame.state == RareGame.State.DOWNLOADING: + if rgame.state != RareGame.State.DOWNLOADING: self.logger.error( f"Can't start download {item.options.app_name} due to incompatible state {RareGame.State(rgame.state).name}" ) @@ -218,25 +217,25 @@ def start_download(self, item: InstallQueueItemModel): self.download_widget.setPixmap(self.rcore.image_manager().get_pixmap(rgame.app_name, ImageSize.Wide, True)) self.signals.application.notify.emit( - self.tr("Downloads"), + self.tr('Downloads'), self.tr('Starting: "{}" is now downloading.').format(rgame.app_title), ) @Slot(UIUpdate, object) def __on_download_progress(self, ui_update: UIUpdate, dl_size: int): self.download_widget.ui.progress_bar.setValue(int(ui_update.progress)) - self.download_widget.ui.dl_speed.setText(f"{format_size(ui_update.download_compressed_speed)}/s") + self.download_widget.ui.dl_speed.setText(f'{format_size(ui_update.download_compressed_speed)}/s') self.download_widget.ui.cache_used.setText( - f"{format_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}" + f'{format_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else "0KB"}' ) - self.download_widget.ui.downloaded.setText(f"{format_size(ui_update.total_downloaded)} / {format_size(dl_size)}") + self.download_widget.ui.downloaded.setText(f'{format_size(ui_update.total_downloaded)} / {format_size(dl_size)}') self.download_widget.ui.time_left.setText(get_time(ui_update.estimated_time_left)) def __requeue_download(self, item: InstallQueueItemModel): rgame = self.rcore.get_game(item.options.app_name) rgame.state = RareGame.State.DOWNLOADING self.queue_group.push_front(item, rgame.igame) - self.logger.info(f"Re-queued download for {rgame.app_name} ({rgame.app_title})") + self.logger.info(f'Re-queued download for {rgame.app_name} ({rgame.app_title})') @Slot(DlResultModel) def __on_download_result(self, result: DlResultModel): @@ -245,21 +244,21 @@ def __on_download_result(self, result: DlResultModel): self.__thread.deleteLater() if result.code == DlResultCode.FINISHED: - self.logger.info(f"Download finished: {result.options.app_name}") + self.logger.info(f'Download finished: {result.options.app_name}') if result.shortcut and desktop_links_supported(): if not create_desktop_link( app_name=result.options.app_name, app_title=result.app_title, link_name=result.folder_name, - link_type="desktop", + link_type='desktop', ): # maybe add it to download summary, to show in finished downloads - self.logger.error(f"Failed to create desktop link on {platform.system()}") + self.logger.error(f'Failed to create desktop link on {platform.system()}') else: - self.logger.info(f"Created desktop link {result.folder_name} for {result.app_title}") + self.logger.info(f'Created desktop link {result.folder_name} for {result.app_title}') self.signals.application.notify.emit( - self.tr("Downloads"), + self.tr('Downloads'), self.tr('Finished: "{}" is now playable.').format(result.app_title), ) @@ -267,16 +266,16 @@ def __on_download_result(self, result: DlResultModel): self.updates_group.set_widget_enabled(result.options.app_name, True) elif result.code == DlResultCode.ERROR: - self.logger.error(f"Download error: {result.options.app_name} ({result.message})") + self.logger.error(f'Download error: {result.options.app_name} ({result.message})') QMessageBox.warning( self, - self.tr("Error - {}").format(result.app_title), - self.tr("Download error: {}").format(result.message), + self.tr('Error - {}').format(result.app_title), + self.tr('Download error: {}').format(result.message), QMessageBox.StandardButton.Close, ) elif result.code == DlResultCode.STOPPED: - self.logger.info(f"Download stopped: {result.options.app_name}") + self.logger.info(f'Download stopped: {result.options.app_name}') if not self.__omit_requeue: self.__requeue_download(InstallQueueItemModel(options=result.options)) else: @@ -298,12 +297,12 @@ def __reset_download(self): self.__thread = None self.download_widget.setPixmap(QPixmap()) self.download_widget.ui.kill_button.setDisabled(True) - self.download_widget.ui.dl_name.setText(self.tr("No active download")) + self.download_widget.ui.dl_name.setText(self.tr('No active download')) self.download_widget.ui.progress_bar.setValue(0) - self.download_widget.ui.dl_speed.setText("...") - self.download_widget.ui.time_left.setText("...") - self.download_widget.ui.cache_used.setText("...") - self.download_widget.ui.downloaded.setText("...") + self.download_widget.ui.dl_speed.setText('...') + self.download_widget.ui.time_left.setText('...') + self.download_widget.ui.cache_used.setText('...') + self.download_widget.ui.downloaded.setText('...') self.update_queues_count() @Slot(InstallOptionsModel) @@ -377,7 +376,7 @@ def __on_uninstall_worker_result(self, rgame: RareGame, success: bool, message: else: QMessageBox.warning( None, - self.tr("Uninstall - {}").format(rgame.app_title), + self.tr('Uninstall - {}').format(rgame.app_title), message, QMessageBox.StandardButton.Close, ) diff --git a/rare/components/tabs/downloads/groups.py b/rare/components/tabs/downloads/groups.py index 6fd185e993..247a361283 100644 --- a/rare/components/tabs/downloads/groups.py +++ b/rare/components/tabs/downloads/groups.py @@ -1,7 +1,6 @@ from collections import deque from enum import IntEnum from logging import getLogger -from typing import Deque, Optional from legendary.models.game import Game, InstalledGame from PySide6.QtCore import Qt, Signal, Slot @@ -19,7 +18,7 @@ from rare.shared.image_manager import ImageManager from rare.utils.misc import widget_object_name -logger = getLogger("QueueGroup") +logger = getLogger('QueueGroup') class UpdateGroup(QGroupBox): @@ -29,8 +28,8 @@ class UpdateGroup(QGroupBox): def __init__(self, imgmgr: ImageManager, parent=None): super(UpdateGroup, self).__init__(parent=parent) self.setObjectName(type(self).__name__) - self.setTitle(self.tr("Updates")) - self.__text = QLabel(self.tr("No updates available")) + self.setTitle(self.tr('Updates')) + self.__text = QLabel(self.tr('No updates available')) self.__text.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) # lk: For findChildren to work, the update's layout has to be in a widget @@ -45,7 +44,7 @@ def __init__(self, imgmgr: ImageManager, parent=None): self.imgmgr = imgmgr - def __find_widget(self, app_name: str) -> Optional[UpdateWidget]: + def __find_widget(self, app_name: str) -> UpdateWidget | None: return self.__container.findChild(UpdateWidget, name=widget_object_name(UpdateWidget, app_name)) def count(self) -> int: @@ -96,8 +95,8 @@ class QueueGroup(QGroupBox): def __init__(self, core: LegendaryCore, imgmgr: ImageManager, parent=None): super(QueueGroup, self).__init__(parent=parent) self.setObjectName(type(self).__name__) - self.setTitle(self.tr("Queue")) - self.__text = QLabel(self.tr("No downloads in queue"), self) + self.setTitle(self.tr('Queue')) + self.__text = QLabel(self.tr('No downloads in queue'), self) self.__text.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) # lk: For findChildren to work, the queue's layout has to be in a widget @@ -112,9 +111,9 @@ def __init__(self, core: LegendaryCore, imgmgr: ImageManager, parent=None): self.core = core self.imgmgr = imgmgr - self.__queue: Deque[str] = deque() + self.__queue: deque[str] = deque() - def __find_widget(self, app_name: str) -> Optional[QueueWidget]: + def __find_widget(self, app_name: str) -> QueueWidget | None: return self.__container.findChild(QueueWidget, name=widget_object_name(QueueWidget, app_name)) def count(self) -> int: diff --git a/rare/components/tabs/downloads/thread.py b/rare/components/tabs/downloads/thread.py index 85353de2c3..25a669f144 100644 --- a/rare/components/tabs/downloads/thread.py +++ b/rare/components/tabs/downloads/thread.py @@ -1,13 +1,11 @@ -import multiprocessing +import contextlib import os import platform import queue -import threading import time from dataclasses import dataclass from enum import IntEnum from logging import getLogger -from typing import Dict, List, Optional from PySide6.QtCore import QProcess, QThread, Signal @@ -29,13 +27,13 @@ class DlResultCode(IntEnum): class DlResultModel: options: InstallOptionsModel code: DlResultCode = DlResultCode.ERROR - message: str = "" - dlcs: Optional[List[Dict]] = None + message: str = '' + dlcs: list[dict] | None = None sync_saves: bool = False - tip_url: str = "" + tip_url: str = '' shortcut: bool = False - folder_name: str = "" - app_title: str = "" + folder_name: str = '' + app_title: str = '' class DlThread(QThread): @@ -85,33 +83,29 @@ def run(self): self.rgame.signals.progress.start.emit() time.sleep(1) while self.item.download.dlm.is_alive(): - try: + with contextlib.suppress(queue.Empty): self._status_callback(self.item.download.dlm.status_queue.get(timeout=1.0)) - except queue.Empty: - pass if self.dlm_signals.update: - try: + with contextlib.suppress(queue.Full): self.item.download.dlm.signals_queue.put(self.dlm_signals, block=False, timeout=1.0) - except queue.Full: - pass time.sleep(self.item.download.dlm.update_interval / 10) self.item.download.dlm.join() except Exception as e: self.kill() self.item.download.dlm.join() end_t = time.time() - self.logger.error(f"Installation failed after {end_t - start_t:.02f} seconds.") - self.logger.warning(f"The following exception occurred while waiting for the downloader to finish: {e!r}.") + self.logger.error(f'Installation failed after {end_t - start_t:.02f} seconds.') + self.logger.warning(f'The following exception occurred while waiting for the downloader to finish: {e!r}.') result.code = DlResultCode.ERROR - result.message = f"{e!r}" + result.message = f'{e!r}' return else: end_t = time.time() if self.dlm_signals.kill: - self.logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.") + self.logger.info(f'Download stopped after {end_t - start_t:.02f} seconds.') result.code = DlResultCode.STOPPED return - self.logger.info(f"Download finished in {end_t - start_t:.02f} seconds.") + self.logger.info(f'Download finished in {end_t - start_t:.02f} seconds.') result.code = DlResultCode.FINISHED @@ -135,9 +129,9 @@ def run(self): result.dlcs = [] result.dlcs.extend( { - "app_name": dlc.app_name, - "app_title": dlc.app_title, - "app_version": dlc.app_version(self.item.options.platform), + 'app_name': dlc.app_name, + 'app_title': dlc.app_title, + 'app_version': dlc.app_version(self.item.options.platform), } for dlc in dlcs ) @@ -165,27 +159,27 @@ def run(self): self._finish(result) def _handle_postinstall(self, postinstall, igame): - self.logger.info("This game lists the following prerequisites to be installed:") - self.logger.info(f"- {postinstall['name']}: {' '.join((postinstall['path'], postinstall['args']))}") - if platform.system() == "Windows": + self.logger.info('This game lists the following prerequisites to be installed:') + self.logger.info(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}') + if platform.system() == 'Windows': if self.item.options.install_prereqs: - self.logger.info("Launching prerequisite executable..") + self.logger.info('Launching prerequisite executable..') self.core.prereq_installed(igame.app_name) - req_path, req_exec = os.path.split(postinstall["path"]) + req_path, req_exec = os.path.split(postinstall['path']) work_dir = os.path.join(igame.install_path, req_path) fullpath = os.path.join(work_dir, req_exec) proc = QProcess(self) proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) proc.readyReadStandardOutput.connect( - (lambda obj: obj.logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))).__get__(self) + (lambda obj: obj.logger.debug(str(proc.readAllStandardOutput().data(), 'utf-8', 'ignore'))).__get__(self) ) proc.setProgram(fullpath) - proc.setArguments(postinstall.get("args", "").split(" ")) + proc.setArguments(postinstall.get('args', '').split(' ')) proc.setWorkingDirectory(work_dir) proc.start() proc.waitForFinished() # wait, because it is inside the thread else: - self.logger.info("Automatic installation not available on Linux.") + self.logger.info('Automatic installation not available on Linux.') def kill(self): self.dlm_signals.kill = True diff --git a/rare/components/tabs/downloads/widgets.py b/rare/components/tabs/downloads/widgets.py index afc7322394..2c042d9ee4 100644 --- a/rare/components/tabs/downloads/widgets.py +++ b/rare/components/tabs/downloads/widgets.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import Optional from legendary.models.downloading import AnalysisResult from legendary.models.game import Game, InstalledGame @@ -19,17 +18,17 @@ from rare.utils.misc import elide_text, format_size, qta_icon, widget_object_name from rare.widgets.image_widget import ImageSize, ImageWidget -logger = getLogger("DownloadWidgets") +logger = getLogger('DownloadWidgets') class QueueInfoWidget(QWidget): def __init__( self, imgmgr: ImageManager, - game: Optional[Game], - igame: Optional[InstalledGame], - analysis: Optional[AnalysisResult] = None, - old_igame: Optional[InstalledGame] = None, + game: Game | None, + igame: InstalledGame | None, + analysis: AnalysisResult | None = None, + old_igame: InstalledGame | None = None, parent=None, ): super(QueueInfoWidget, self).__init__(parent=parent) @@ -47,11 +46,11 @@ def __init__( if game and igame: self.update_information(game, igame, analysis, old_igame) else: - self.ui.title.setText("...") - self.ui.remote_version.setText("...") - self.ui.local_version.setText("...") - self.ui.dl_size.setText("...") - self.ui.install_size.setText("...") + self.ui.title.setText('...') + self.ui.remote_version.setText('...') + self.ui.local_version.setText('...') + self.ui.dl_size.setText('...') + self.ui.install_size.setText('...') if old_igame: self.ui.title.setText(old_igame.title) @@ -66,8 +65,8 @@ def update_information(self, game, igame, analysis, old_igame): ) ) self.ui.local_version.setText(elide_text(self.ui.local_version, igame.version)) - self.ui.dl_size.setText(format_size(analysis.dl_size) if analysis else "") - self.ui.install_size.setText(format_size(analysis.install_size) if analysis else "") + self.ui.dl_size.setText(format_size(analysis.dl_size) if analysis else '') + self.ui.install_size.setText(format_size(analysis.install_size) if analysis else '') self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, ImageSize.LibraryIcon)) @@ -148,12 +147,12 @@ def __init__( worker.signals.result.connect(self.__update_info) worker.signals.failed.connect( ( - lambda obj, m: obj.logger.error(f"Failed to requeue download for {item.options.app_name} with error: {m}") + lambda obj, m: obj.logger.error(f'Failed to requeue download for {item.options.app_name} with error: {m}') ).__get__(self) ) worker.signals.failed.connect((lambda obj, m: obj.remove.emit(item.options.app_name)).__get__(self)) worker.signals.finished.connect( - (lambda obj: obj.logger.error(f"Download requeue worker finished for {item.options.app_name}")).__get__(self) + (lambda obj: obj.logger.error(f'Download requeue worker finished for {item.options.app_name}')).__get__(self) ) QThreadPool.globalInstance().start(worker) self.info_widget = QueueInfoWidget(imgmgr, None, None, None, old_igame, parent=self) @@ -172,10 +171,10 @@ def __init__( self.old_igame = old_igame self.item = item - self.ui.move_up_button.setIcon(qta_icon("fa.arrow-up", "fa5s.arrow-up")) + self.ui.move_up_button.setIcon(qta_icon('fa.arrow-up', 'fa5s.arrow-up')) self.ui.move_up_button.clicked.connect(self._on_move_up) - self.ui.move_down_button.setIcon(qta_icon("fa.arrow-down", "fa5s.arrow-down")) + self.ui.move_down_button.setIcon(qta_icon('fa.arrow-down', 'fa5s.arrow-down')) self.ui.move_down_button.clicked.connect(self._on_move_down) self.ui.remove_button.clicked.connect(self._on_remove) diff --git a/rare/components/tabs/integrations/__init__.py b/rare/components/tabs/integrations/__init__.py index db3b020a81..45b7ad115d 100644 --- a/rare/components/tabs/integrations/__init__.py +++ b/rare/components/tabs/integrations/__init__.py @@ -1,5 +1,3 @@ -from typing import Optional - from PySide6.QtCore import Qt from PySide6.QtWidgets import QLabel, QSizePolicy, QVBoxLayout, QWidget @@ -18,34 +16,34 @@ def __init__(self, rcore: RareCore, parent=None): self.import_group = ImportGroup(rcore, self) self.import_widget = IntegrationsWidget( self.import_group, - self.tr("To import games from Epic Games Store, please enable EGL Sync."), + self.tr('To import games from Epic Games Store, please enable EGL Sync.'), self, ) - self.import_index = self.addTab(self.import_widget, self.tr("Import Games")) + self.import_index = self.addTab(self.import_widget, self.tr('Import Games')) self.egl_sync_group = EGLSyncGroup(rcore, self) self.egl_sync_widget = IntegrationsWidget( self.egl_sync_group, - self.tr("To import EGL games from directories, please use Import Game."), + self.tr('To import EGL games from directories, please use Import Game.'), self, ) - self.egl_sync_index = self.addTab(self.egl_sync_widget, self.tr("Sync with EGL")) + self.egl_sync_index = self.addTab(self.egl_sync_widget, self.tr('Sync with EGL')) self.eos_group = EosGroup(rcore, self) self.eos_widget = IntegrationsWidget( self.eos_group, - self.tr(""), + self.tr(''), self, ) - self.eos_index = self.addTab(self.eos_widget, self.tr("Epic Overlay")) + self.eos_index = self.addTab(self.eos_widget, self.tr('Epic Overlay')) self.ubisoft_group = UbisoftGroup(rcore, self) self.ubisoft_widget = IntegrationsWidget( self.ubisoft_group, - self.tr(""), + self.tr(''), self, ) - self.ubisoft_index = self.addTab(self.ubisoft_widget, self.tr("Ubisoft Link")) + self.ubisoft_index = self.addTab(self.ubisoft_widget, self.tr('Ubisoft Link')) self.setCurrentIndex(self.import_index) @@ -64,9 +62,9 @@ def show_ubisoft(self): class IntegrationsWidget(QWidget): - def __init__(self, widget: Optional[QWidget], info: str, parent=None): + def __init__(self, widget: QWidget | None, info: str, parent=None): super(IntegrationsWidget, self).__init__(parent=parent) - self.info = QLabel(f"{info}") + self.info = QLabel(f'{info}') self.__layout = QVBoxLayout(self) if widget is not None: diff --git a/rare/components/tabs/integrations/egl_sync_group.py b/rare/components/tabs/integrations/egl_sync_group.py index 341ab08cf5..516a4b71a7 100644 --- a/rare/components/tabs/integrations/egl_sync_group.py +++ b/rare/components/tabs/integrations/egl_sync_group.py @@ -1,8 +1,8 @@ import os import platform from abc import abstractmethod +from collections.abc import Iterable from logging import getLogger -from typing import Iterable, List, Tuple, Union from legendary.models.egl import EGLManifest from legendary.models.game import InstalledGame @@ -29,7 +29,7 @@ from rare.widgets.elide_label import ElideLabel from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit -logger = getLogger("EGLSync") +logger = getLogger('EGLSync') class EGLSyncGroup(QGroupBox): @@ -42,7 +42,7 @@ def __init__(self, rcore: RareCore, parent=None): 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"), + placeholder=self.tr('Path to the Wine prefix where EGL is installed, or the Manifests folder'), file_mode=QFileDialog.FileMode.Directory, edit_func=self.egl_path_edit_edit_cb, save_func=self.egl_path_edit_save_cb, @@ -62,7 +62,7 @@ def __init__(self, rcore: RareCore, parent=None): self.egl_path_info, ) - if platform.system() == "Windows": + if platform.system() == 'Windows': self.ui.egl_path_edit_label.setEnabled(False) self.egl_path_edit.setEnabled(False) self.ui.egl_path_info_label.setEnabled(False) @@ -97,8 +97,8 @@ def showEvent(self, a0: QShowEvent) -> None: super().showEvent(a0) def __run_wine_resolver(self): - self.egl_path_info.setText(self.tr("Updating...")) - wine_resolver = WinePathResolver(self.core, "default", str(PathSpec.egl_programdata())) + self.egl_path_info.setText(self.tr('Updating...')) + wine_resolver = WinePathResolver(self.core, 'default', str(PathSpec.egl_programdata())) wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result) QThreadPool.globalInstance().start(wine_resolver) @@ -106,26 +106,26 @@ def __on_wine_resolver_result(self, path): self.egl_path_info.setText(path) if not path: self.egl_path_info.setText( - self.tr("Default Wine prefix is unset, or path does not exist. Create it or configure it in Settings -> Linux.") + self.tr('Default Wine prefix is unset, or path does not exist. Create it or configure it in Settings -> Linux.') ) elif not os.path.exists(path): self.egl_path_info.setText( self.tr( - "Default Wine prefix is set but EGL manifests path does not exist. " - "Your configured default Wine prefix might not be where EGL is installed." + 'Default Wine prefix is set but EGL manifests path does not exist. ' + 'Your configured default Wine prefix might not be where EGL is installed.' ) ) else: self.egl_path_edit.setText(path) @staticmethod - def egl_path_edit_edit_cb(path) -> Tuple[bool, str, int]: + def egl_path_edit_edit_cb(path) -> tuple[bool, str, int]: if not path: return True, path, IndicatorReasonsCommon.VALID - if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists(os.path.join(path, "dosdevices/c:")): + if os.path.exists(os.path.join(path, 'system.reg')) and os.path.exists(os.path.join(path, 'dosdevices/c:')): # path is a wine prefix path = PathSpec.prefix_egl_programdata(path) - elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()): + elif not path.rstrip('/').endswith(PathSpec.wine_egl_programdata()): # lower() might or might not be needed in the check return False, path, IndicatorReasonsCommon.WRONG_FORMAT if os.path.exists(path): @@ -136,15 +136,15 @@ def egl_path_edit_save_cb(self, path): if not path or not os.path.exists(path): # This is the same as "--unlink" self.core.egl.programdata_path = None - self.core.lgd.config.remove_option("Legendary", "egl_programdata") - self.core.lgd.config.remove_option("Legendary", "egl_sync") + self.core.lgd.config.remove_option('Legendary', 'egl_programdata') + self.core.lgd.config.remove_option('Legendary', 'egl_sync') # remove EGL GUIDs from all games, DO NOT remove .egstore folders because that would fuck things up. for igame in self.core.get_installed_list(): - igame.egl_guid = "" + igame.egl_guid = '' self.core.install_game(igame) else: self.core.egl.programdata_path = path - self.core.lgd.config.set("Legendary", "egl_programdata", path) + self.core.lgd.config.set('Legendary', 'egl_programdata', path) self.core.lgd.save_config() @@ -160,9 +160,9 @@ def egl_sync_changed(self, state): if state == Qt.CheckState.Unchecked: self.import_list.setEnabled(bool(self.import_list.items)) self.export_list.setEnabled(bool(self.export_list.items)) - self.core.lgd.config.remove_option("Legendary", "egl_sync") + self.core.lgd.config.remove_option('Legendary', 'egl_sync') else: - self.core.lgd.config.set("Legendary", "egl_sync", str(True)) + self.core.lgd.config.set('Legendary', 'egl_sync', str(True)) # lk: do import/export here since automatic sync was selected self.import_list.mark(Qt.CheckState.Checked) self.export_list.mark(Qt.CheckState.Checked) @@ -194,7 +194,7 @@ def update_lists(self): class EGLSyncListItem(QListWidgetItem): - def __init__(self, core: LegendaryCore, game: Union[EGLManifest, InstalledGame]): + def __init__(self, core: LegendaryCore, game: EGLManifest | InstalledGame): super(EGLSyncListItem, self).__init__() self.core = core self.game = game @@ -207,7 +207,7 @@ def is_checked(self) -> bool: return self.checkState() == Qt.CheckState.Checked @abstractmethod - def action(self) -> Union[str, bool]: + def action(self) -> str | bool: pass @property @@ -224,7 +224,7 @@ class EGLSyncExportItem(EGLSyncListItem): def __init__(self, core: LegendaryCore, game: InstalledGame): super(EGLSyncExportItem, self).__init__(core, game=game) - def action(self) -> Union[str, bool]: + def action(self) -> str | bool: error = False try: self.core.egl_export(self.game.app_name) @@ -241,7 +241,7 @@ class EGLSyncImportItem(EGLSyncListItem): def __init__(self, core: LegendaryCore, game: EGLManifest): super(EGLSyncImportItem, self).__init__(core, game=game) - def action(self) -> Union[str, bool]: + def action(self) -> str | bool: error = False try: self.core.egl_import(self.game.app_name) @@ -313,7 +313,7 @@ def action(self): @Slot(list) @abstractmethod - def show_errors(self, errors: List): + def show_errors(self, errors: list): pass @property @@ -327,9 +327,9 @@ def items(self) -> Iterable[EGLSyncListItem]: class EGLSyncExportGroup(EGLSyncListGroup): def __init__(self, rcore: RareCore, parent=None): super(EGLSyncExportGroup, self).__init__(rcore, parent=parent) - self.setTitle(self.tr("Exportable games")) - self.ui.label.setText(self.tr("No games to export to EGL")) - self.ui.action_button.setText(self.tr("Export")) + self.setTitle(self.tr('Exportable games')) + self.ui.label.setText(self.tr('No games to export to EGL')) + self.ui.action_button.setText(self.tr('Export')) def populate(self, enabled: bool): if enabled: @@ -339,7 +339,7 @@ def populate(self, enabled: bool): i = EGLSyncExportItem(self.core, item) except AttributeError as e: logger.error( - "%s(%s) constructor failed with %s", + '%s(%s) constructor failed with %s', type(self).__name__, item.app_name, e, @@ -349,15 +349,15 @@ def populate(self, enabled: bool): super(EGLSyncExportGroup, self).populate(enabled) @Slot(list) - def show_errors(self, errors: List): + def show_errors(self, errors: list): QMessageBox.warning( self.parent(), - self.tr("The following errors occurred while exporting."), - "\n".join(errors), + self.tr('The following errors occurred while exporting.'), + '\n'.join(errors), ) def action(self): - errors: List = [] + errors: list = [] for item in self.items: if item.is_checked(): if e := item.action(): @@ -370,9 +370,9 @@ def action(self): class EGLSyncImportGroup(EGLSyncListGroup): def __init__(self, rcore: RareCore, parent=None): super(EGLSyncImportGroup, self).__init__(rcore, parent=parent) - self.setTitle(self.tr("Importable games")) - self.ui.label.setText(self.tr("No games to import from EGL")) - self.ui.action_button.setText(self.tr("Import")) + self.setTitle(self.tr('Importable games')) + self.ui.label.setText(self.tr('No games to import from EGL')) + self.ui.action_button.setText(self.tr('Import')) def populate(self, enabled: bool): if enabled: @@ -382,7 +382,7 @@ def populate(self, enabled: bool): i = EGLSyncImportItem(self.core, item) except AttributeError as e: logger.error( - "%s(%s) constructor failed with %s", + '%s(%s) constructor failed with %s', type(self).__name__, item.app_name, e, @@ -392,15 +392,15 @@ def populate(self, enabled: bool): super(EGLSyncImportGroup, self).populate(enabled) @Slot(list) - def show_errors(self, errors: List): + def show_errors(self, errors: list): QMessageBox.warning( self.parent(), - self.tr("The following errors occurred while importing."), - "\n".join(errors), + self.tr('The following errors occurred while importing.'), + '\n'.join(errors), ) def action(self): - errors: List = [] + errors: list = [] for item in self.items: if item.is_checked(): if e := item.action(): diff --git a/rare/components/tabs/integrations/eos_group.py b/rare/components/tabs/integrations/eos_group.py index e8c3b2993c..95e7e1814a 100644 --- a/rare/components/tabs/integrations/eos_group.py +++ b/rare/components/tabs/integrations/eos_group.py @@ -1,7 +1,6 @@ import os import platform from logging import getLogger -from typing import Optional from PySide6.QtCore import ( QObject, @@ -34,7 +33,7 @@ from rare.utils.misc import qta_icon, style_hyperlink from rare.widgets.elide_label import ElideLabel -logger = getLogger("EpicOverlay") +logger = getLogger('EpicOverlay') class CheckForUpdateWorkerSignals(QObject): @@ -56,7 +55,7 @@ def run(self) -> None: class EosPrefixWidget(QFrame): - def __init__(self, overlay: RareEosOverlay, prefix: Optional[str], parent=None): + def __init__(self, overlay: RareEosOverlay, prefix: str | None, parent=None): super(EosPrefixWidget, self).__init__(parent=parent) self.setFrameShape(QFrame.Shape.StyledPanel) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) @@ -65,7 +64,7 @@ def __init__(self, overlay: RareEosOverlay, prefix: Optional[str], parent=None): self.indicator.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) self.prefix_label = ElideLabel( - prefix.replace(os.path.expanduser("~"), "~") if prefix is not None else overlay.app_title, + prefix.replace(os.path.expanduser('~'), '~') if prefix is not None else overlay.app_title, parent=self, ) self.prefix_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) @@ -102,41 +101,41 @@ def __init__(self, overlay: RareEosOverlay, prefix: Optional[str], parent=None): @Slot(int) def path_changed(self, index: int) -> None: path = self.path_select.itemData(index, Qt.ItemDataRole.UserRole) - active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else '' if self.overlay.is_enabled(self.prefix) and (path == active_path): - self.button.setText(self.tr("Disable overlay")) + self.button.setText(self.tr('Disable overlay')) else: - self.button.setText(self.tr("Enable overlay")) + self.button.setText(self.tr('Enable overlay')) @Slot() def update_state(self) -> None: - active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else '' - self.overlay_label.setText(f"{active_path}") + self.overlay_label.setText(f'{active_path}') self.overlay_label.setVisible(bool(active_path)) self.path_select.clear() if not self.overlay.is_installed and not self.overlay.available_paths(self.prefix): self.setDisabled(True) - self.indicator.setPixmap(qta_icon("fa.circle-o", "fa5.circle", color="grey").pixmap(20, 20)) + self.indicator.setPixmap(qta_icon('fa.circle-o', 'fa5.circle', color='grey').pixmap(20, 20)) active_path = self.overlay.active_path(self.prefix) - self.overlay_label.setText(f"{active_path}") + self.overlay_label.setText(f'{active_path}') self.overlay_label.setVisible(bool(active_path)) - self.button.setText(self.tr("Unavailable")) + self.button.setText(self.tr('Unavailable')) return if self.overlay.is_enabled(self.prefix): - self.indicator.setPixmap(qta_icon("fa.check-circle-o", "fa5.check-circle", color="green").pixmap(QSize(20, 20))) + self.indicator.setPixmap(qta_icon('fa.check-circle-o', 'fa5.check-circle', color='green').pixmap(QSize(20, 20))) else: - self.indicator.setPixmap(qta_icon("fa.times-circle-o", "fa5.times-circle", color="red").pixmap(QSize(20, 20))) + self.indicator.setPixmap(qta_icon('fa.times-circle-o', 'fa5.times-circle', color='red').pixmap(QSize(20, 20))) - install_path = os.path.normpath(p) if (p := self.overlay.install_path) else "" + install_path = os.path.normpath(p) if (p := self.overlay.install_path) else '' - self.path_select.addItem("Auto-detect", "") - self.path_select.setItemData(0, "Auto-detect", Qt.ItemDataRole.ToolTipRole) + self.path_select.addItem('Auto-detect', '') + self.path_select.setItemData(0, 'Auto-detect', Qt.ItemDataRole.ToolTipRole) for path in self.overlay.available_paths(self.prefix): path = os.path.normpath(path) - self.path_select.addItem("Legendary-managed" if path == install_path else "EGL-managed", path) + self.path_select.addItem('Legendary-managed' if path == install_path else 'EGL-managed', path) self.path_select.setItemData(self.path_select.findData(path), path, Qt.ItemDataRole.ToolTipRole) self.path_select.setCurrentIndex(self.path_select.findData(active_path)) @@ -145,17 +144,17 @@ def update_state(self) -> None: @Slot() def action(self) -> None: path = self.path_select.currentData(Qt.ItemDataRole.UserRole) - active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" - install_path = os.path.normpath(p) if (p := self.overlay.install_path) else "" + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else '' + install_path = os.path.normpath(p) if (p := self.overlay.install_path) else '' if self.overlay.is_enabled(self.prefix) and (path == active_path): if not self.overlay.disable(prefix=self.prefix): QMessageBox.warning( self, - "Warning", - self.tr("Failed to completely disable the active EOS Overlay.{}").format( - self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") + 'Warning', + self.tr('Failed to completely disable the active EOS Overlay.{}').format( + self.tr(' Since the previous overlay was managed by EGL you can safely ignore this is.') if active_path != install_path - else "" + else '' ), ) else: @@ -163,11 +162,11 @@ def action(self) -> None: if not self.overlay.enable(prefix=self.prefix, path=path): QMessageBox.warning( self, - "Warning", - self.tr("Failed to completely enable EOS overlay.{}").format( - self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") + 'Warning', + self.tr('Failed to completely enable EOS overlay.{}').format( + self.tr(' Since the previous overlay was managed by EGL you can safely ignore this is.') if active_path != install_path - else "" + else '' ), ) self.update_state() @@ -184,19 +183,19 @@ def __init__(self, rcore: RareCore, parent=None): self.ui = Ui_EosWidget() self.ui.setupUi(self) # lk: set object names for CSS properties - self.ui.install_button.setObjectName("InstallButton") - self.ui.uninstall_button.setObjectName("UninstallButton") + self.ui.install_button.setObjectName('InstallButton') + self.ui.uninstall_button.setObjectName('UninstallButton') self.ui.install_page_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.ui.update_page_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - self.ui.install_button.setIcon(qta_icon("ri.install-line")) - self.ui.update_button.setIcon(qta_icon("ri.arrow-up-circle-line")) - self.ui.uninstall_button.setIcon(qta_icon("ri.uninstall-line")) + self.ui.install_button.setIcon(qta_icon('ri.install-line')) + self.ui.update_button.setIcon(qta_icon('ri.arrow-up-circle-line')) + self.ui.uninstall_button.setIcon(qta_icon('ri.uninstall-line')) self.version = ElideLabel(parent=self) self.install_path = QLabel(parent=self) - self.install_path.setObjectName("LinkLabel") + self.install_path.setObjectName('LinkLabel') self.install_path.setOpenExternalLinks(True) self.ui.info_layout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.version) @@ -211,7 +210,7 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.uninstall_button.clicked.connect(self.uninstall_overlay) self.threadpool = QThreadPool.globalInstance() - self.worker: Optional[CheckForUpdateWorker] = None + self.worker: CheckForUpdateWorker | None = None def showEvent(self, e: QShowEvent) -> None: if e.spontaneous(): @@ -232,10 +231,10 @@ def hideEvent(self, e: QHideEvent, /): @Slot() def update_state(self): if self.overlay.is_installed: # installed - self.version.setText(f"{self.overlay.version}") + self.version.setText(f'{self.overlay.version}') self.ui.button_stack.setCurrentWidget(self.ui.update_page) else: - self.version.setText(self.tr("Epic Online Services Overlay is not installed")) + self.version.setText(self.tr('Epic Online Services Overlay is not installed')) self.ui.button_stack.setCurrentWidget(self.ui.install_page) self.install_path.setEnabled(self.overlay.is_installed) self.install_path.setText( @@ -244,7 +243,7 @@ def update_state(self): self.overlay.install_path, ) if self.overlay.is_installed - else "N/A" + else 'N/A' ) self.ui.install_button.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE) @@ -256,16 +255,16 @@ def update_prefixes(self): widget.disconnect(widget) widget.deleteLater() - if platform.system() != "Windows": + if platform.system() != 'Windows': prefixes = config.get_prefixes() prefixes = sorted({prefix for prefix, _ in prefixes}) - if platform.system() == "Darwin": + if platform.system() == 'Darwin': # TODO: add crossover support pass for prefix in prefixes: widget = EosPrefixWidget(self.overlay, prefix) self.ui.eos_layout.addWidget(widget) - logger.debug("Updated prefixes") + logger.debug('Updated prefixes') else: widget = EosPrefixWidget(self.overlay, None) self.ui.eos_layout.addWidget(widget) @@ -290,11 +289,11 @@ def check_for_update(self): @Slot() def install_finished(self): if not self.overlay.is_installed: - logger.error("Something went wrong while installing EOS Overlay") + logger.error('Something went wrong while installing EOS Overlay') QMessageBox.warning( self, - "Error", - self.tr("Something went wrong while installing EOS Overlay"), + 'Error', + self.tr('Something went wrong while installing EOS Overlay'), QMessageBox.StandardButton.Close, ) return @@ -310,7 +309,7 @@ def install_overlay(self): def uninstall_overlay(self): if not self.overlay.is_installed: - logger.error("No Legendary-managed EOS Overlay installation found.") + logger.error('No Legendary-managed EOS Overlay installation found.') self.ui.button_stack.setCurrentWidget(self.ui.install_page) return self.overlay.uninstall() diff --git a/rare/components/tabs/integrations/import_group.py b/rare/components/tabs/integrations/import_group.py index e8cb9689e8..3e3b0301f2 100644 --- a/rare/components/tabs/integrations/import_group.py +++ b/rare/components/tabs/integrations/import_group.py @@ -4,7 +4,6 @@ from enum import IntEnum from logging import getLogger from pathlib import Path -from typing import Dict, List, Optional, Set, Tuple from PySide6.QtCore import QObject, QRunnable, Qt, QThreadPool, Signal, Slot from PySide6.QtGui import QShowEvent @@ -32,15 +31,15 @@ PathEdit, ) -logger = getLogger("Import") +logger = getLogger('Import') -def find_app_name(core, path: str) -> Optional[str]: - if os.path.exists(os.path.join(path, ".egstore")): - for i in os.listdir(os.path.join(path, ".egstore")): - if i.endswith(".mancpn"): - with open(os.path.join(path, ".egstore", i)) as file: - app_name = json.load(file).get("AppName") +def find_app_name(core, path: str) -> str | None: + if os.path.exists(os.path.join(path, '.egstore')): + for i in os.listdir(os.path.join(path, '.egstore')): + if i.endswith('.mancpn'): + with open(os.path.join(path, '.egstore', i)) as file: + app_name = json.load(file).get('AppName') return app_name elif app_name := LegendaryCLI(core)._resolve_aliases(os.path.basename(os.path.normpath(path))): # return None if game does not exist (Workaround for overlay) @@ -48,7 +47,7 @@ def find_app_name(core, path: str) -> Optional[str]: return None return app_name else: - logger.warning(f"Could not find AppName for {path}") + logger.warning(f'Could not find AppName for {path}') return None @@ -61,10 +60,10 @@ class ImportResult(IntEnum): @dataclass class ImportedGame: result: ImportResult - path: Optional[str] = None - app_name: Optional[str] = None - app_title: Optional[str] = None - message: Optional[str] = None + path: str | None = None + app_name: str | None = None + app_title: str | None = None + message: str | None = None class ImportWorkerSignals(QObject): @@ -78,7 +77,7 @@ def __init__( core: LegendaryCore, path: str, app_name: str = None, - platform: Optional[str] = None, + platform: str | None = None, import_folder: bool = False, import_dlcs: bool = False, import_force: bool = False, @@ -96,7 +95,7 @@ def __init__( self.import_force = import_force def run(self) -> None: - result_list: List = [] + result_list: list = [] if self.import_folder: folders = [i for i in self.path.iterdir() if i.is_dir()] number_of_folders = len(folders) @@ -123,7 +122,7 @@ def _try_import(self, path: Path, app_name: str = None) -> ImportedGame: result.app_title = game.app_title platform = self.platform if platform not in self.core.get_game(app_name, update_meta=False).asset_infos: - platform = "Windows" + platform = 'Windows' success, message = self._import_game(path, app_name, platform) if not success: result.result = ImportResult.FAILED @@ -158,12 +157,12 @@ def __init__(self, rcore: RareCore, parent=None): self.ui = Ui_ImportGroup() self.ui.setupUi(self) - self.worker: Optional[ImportWorker] = None + self.worker: ImportWorker | None = None self.threadpool = QThreadPool.globalInstance() - self._app_names: Dict[str, str] = {} - self._app_titles: Dict[str, str] = {} - self._install_dirs: Set[str] = set() + self._app_names: dict[str, str] = {} + self._app_titles: dict[str, str] = {} + self._install_dirs: set[str] = set() self.import_path_edit = PathEdit( path=self.core.get_default_install_dir(self.core.default_platform), @@ -180,7 +179,7 @@ def __init__(self, rcore: RareCore, parent=None): ) self.app_name_edit = IndicatorLineEdit( - placeholder=self.tr("Use in case the app name was not found automatically"), + placeholder=self.tr('Use in case the app name was not found automatically'), edit_func=self._app_name_edit_callback, save_func=self._app_name_save_callback, parent=self, @@ -196,14 +195,14 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.import_dlcs_check.setEnabled(False) self.ui.import_dlcs_check.checkStateChanged.connect(self._on_import_dlcs_changed) - self.ui.import_button_label.setText("") + self.ui.import_button_label.setText('') self.ui.import_button.setEnabled(False) self.ui.import_button.clicked.connect(self._on_import_clicked) self.button_info_stack = QStackedWidget(self) self.button_info_stack.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.button_info_stack.setFixedHeight(self.ui.import_button.sizeHint().height()) - self.info_label = ElideLabel(text="", parent=self.button_info_stack) + self.info_label = ElideLabel(text='', parent=self.button_info_stack) self.info_label.setFixedHeight(False) self.info_label.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.info_progress = QProgressBar(self.button_info_stack) @@ -234,32 +233,30 @@ def set_game(self, app_name: str): ) self.app_name_edit.setText(app_name) - def _path_edit_callback(self, path) -> Tuple[bool, str, int]: + def _path_edit_callback(self, path) -> tuple[bool, str, int]: if not os.path.exists(path): return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS - if os.path.exists(os.path.join(path, ".egstore")): - return True, path, IndicatorReasonsCommon.VALID - elif os.path.basename(path) in self._install_dirs: + if os.path.exists(os.path.join(path, '.egstore')) or os.path.basename(path) in self._install_dirs: return True, path, IndicatorReasonsCommon.VALID return False, path, IndicatorReasonsCommon.INVALID @Slot(str) def _path_changed(self, path: str): - self.info_label.setText("") + self.info_label.setText('') self.ui.import_folder_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) if self.import_path_edit.is_valid: self.app_name_edit.setText(find_app_name(self.core, path)) else: - self.app_name_edit.setText("") + self.app_name_edit.setText('') - def _app_name_edit_callback(self, text) -> Tuple[bool, str, int]: - self.app_name_edit.setInfo("") + def _app_name_edit_callback(self, text) -> tuple[bool, str, int]: + self.app_name_edit.setInfo('') if not text: return False, text, IndicatorReasonsCommon.UNDEFINED - if text in self._app_names.keys(): + if text in self._app_names: return True, self._app_names[text], IndicatorReasonsCommon.VALID - if text in self._app_titles.keys(): + if text in self._app_titles: return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS @@ -273,7 +270,7 @@ def _app_name_save_callback(self, text) -> None: @Slot(str) def _app_name_changed(self, app_name: str): - self.info_label.setText("") + self.info_label.setText('') self.ui.import_dlcs_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_dlcs_check.setEnabled(self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(app_name))) @@ -287,12 +284,12 @@ def _on_import_folder_changed(self, state: Qt.CheckState): self.ui.platform_combo.setEnabled(not state) self.ui.platform_combo.setToolTip( self.tr( - "When importing multiple games, the current OS will be used at the" - " platform for the games that support it, otherwise the Windows version" - " will be imported." + 'When importing multiple games, the current OS will be used at the' + ' platform for the games that support it, otherwise the Windows version' + ' will be imported.' ) if state != Qt.CheckState.Unchecked - else "" + else '' ) self.ui.import_dlcs_check.setCheckState(Qt.CheckState.Unchecked) self.ui.import_force_check.setCheckState(Qt.CheckState.Unchecked) @@ -316,9 +313,9 @@ def _on_import_clicked(self): self._import(self.import_path_edit.text()) @Slot(str) - def _import(self, path: Optional[str] = None): + def _import(self, path: str | None = None): self.ui.import_button.setDisabled(True) - self.info_label.setText(self.tr("Status: Importing games")) + self.info_label.setText(self.tr('Status: Importing games')) self.info_progress.setValue(0) self.button_info_stack.setCurrentWidget(self.info_progress) @@ -342,32 +339,33 @@ def _on_import_progress(self, imported: ImportedGame, progress: int): self.info_progress.setValue(progress) if imported.result == ImportResult.SUCCESS: self.rcore.get_game(imported.app_name).set_installed(True) - status = "error" if not imported.result else ("failed" if imported.result == ImportResult.FAILED else "successful") - logger.info(f"Import {status}: {imported.app_title}: {imported.path} ({imported.message})") + status = 'error' if not imported.result else ('failed' if imported.result == ImportResult.FAILED else 'successful') + logger.info(f'Import {status}: {imported.app_title}: {imported.path} ({imported.message})') @Slot(list) - def _on_import_result(self, result: List[ImportedGame]): + def _on_import_result(self, result: list[ImportedGame]): self.worker = None self.button_info_stack.setCurrentWidget(self.info_label) if len(result) == 1: res = result[0] if res.result == ImportResult.SUCCESS: - self.info_label.setText(self.tr("Success: {} imported").format(res.app_title)) + self.info_label.setText(self.tr('Success: {} imported').format(res.app_title)) elif res.result == ImportResult.FAILED: - self.info_label.setText(self.tr("Failed: {} - {}").format(res.app_title, res.message)) + self.info_label.setText(self.tr('Failed: {} - {}').format(res.app_title, res.message)) else: - self.info_label.setText(self.tr("Error: Could not find AppName for {}").format(res.path)) + self.info_label.setText(self.tr('Error: Could not find AppName for {}').format(res.path)) else: - self.info_label.setText(self.tr("Status: Finished importing games")) + self.info_label.setText(self.tr('Status: Finished importing games')) success = [r for r in result if r.result == ImportResult.SUCCESS] failure = [r for r in result if r.result == ImportResult.FAILED] errored = [r for r in result if r.result == ImportResult.ERROR] # pylint: disable=E1101 messagebox = QMessageBox( QMessageBox.Icon.Information, - self.tr("Import summary"), + self.tr('Import summary'), self.tr( - "Tried to import {} folders.\n\nSuccessfully imported {} games, failed to import {} games and {} errors occurred" + 'Tried to import {} folders.\n\n' + 'Successfully imported {} games, failed to import {} games and {} errors occurred' ).format( len(success) + len(failure) + len(errored), len(success), @@ -378,12 +376,12 @@ def _on_import_result(self, result: List[ImportedGame]): parent=self, ) messagebox.setWindowModality(Qt.WindowModality.NonModal) - details: List = [] + details: list = [] for res in success: - details.append(self.tr("Success: {} imported").format(res.app_title)) + details.append(self.tr('Success: {} imported').format(res.app_title)) for res in failure: - details.append(self.tr("Failed: {} - {}").format(res.app_title, res.message)) + details.append(self.tr('Failed: {} - {}').format(res.app_title, res.message)) for res in errored: - details.append(self.tr("Error: Could not find AppName for {}").format(res.path)) - messagebox.setDetailedText("\n".join(details)) + details.append(self.tr('Error: Could not find AppName for {}').format(res.path)) + messagebox.setDetailedText('\n'.join(details)) messagebox.show() diff --git a/rare/components/tabs/integrations/ubisoft_group.py b/rare/components/tabs/integrations/ubisoft_group.py index fd772dd26d..773e3b1f57 100644 --- a/rare/components/tabs/integrations/ubisoft_group.py +++ b/rare/components/tabs/integrations/ubisoft_group.py @@ -37,35 +37,35 @@ def __init__(self, core: LegendaryCore): def run_real(self) -> None: try: - with timelogger(self.logger, "Request external auths"): + with timelogger(self.logger, 'Request external auths'): external_auths = self.core.egs.get_external_auths() for ext_auth in external_auths: - if ext_auth["type"] != "ubisoft": + if ext_auth['type'] != 'ubisoft': continue - ubi_account_id = ext_auth["externalAuthId"] + ubi_account_id = ext_auth['externalAuthId'] break else: - self.signals.result.emit(set(), set(), "") + self.signals.result.emit(set(), set(), '') return - with timelogger(self.logger, "Request uplay codes"): + with timelogger(self.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"]} + key_list = uplay_keys['data']['PartnerIntegration']['accountUplayCodes'] + redeemed = {k['gameId'] for k in key_list if k['redeemedOnUplay']} if (entitlements := self.core.lgd.entitlements) is None: - with timelogger(self.logger, "Request entitlements"): + with timelogger(self.logger, 'Request entitlements'): try: entitlements = self.core.egs.get_user_entitlements_full() except Exception as e: self.logger.warning(e) self.core.lgd.entitlements = entitlements - entitlements = {i["entitlementName"] for i in entitlements} + entitlements = {i['entitlementName'] for i in entitlements} self.signals.result.emit(redeemed, entitlements, ubi_account_id) except Exception as e: self.logger.error(e) - self.signals.result.emit(set(), set(), "error") + self.signals.result.emit(set(), set(), 'error') class UbiConnectWorkerSignals(QObject): @@ -83,7 +83,7 @@ def __init__(self, core: LegendaryCore, ubi_account_id, partner_link_id): def run_real(self) -> None: if not self.ubi_account_id: # debug time.sleep(2) - self.signals.linked.emit("") + self.signals.linked.emit('') return try: self.core.egs.store_claim_uplay_code(self.ubi_account_id, self.partner_link_id) @@ -92,7 +92,7 @@ def run_real(self) -> None: self.signals.linked.emit(str(e)) return else: - self.signals.linked.emit("") + self.signals.linked.emit('') class UbiLinkWidget(QFrame): @@ -107,20 +107,20 @@ def __init__(self, rcore: RareCore, game: Game, ubi_account_id, activated: bool self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.redeem_indicator = QLabel(parent=self) - self.redeem_indicator.setPixmap(qta_icon("fa.circle-o", "fa5.circle", color="grey").pixmap(20, 20)) + self.redeem_indicator.setPixmap(qta_icon('fa.circle-o', 'fa5.circle', color='grey').pixmap(20, 20)) self.redeem_indicator.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) self.title_label = ElideLabel(game.app_title, parent=self) - self.redeem_button = QPushButton(self.tr("Redeem in Ubisoft"), parent=self) + self.redeem_button = QPushButton(self.tr('Redeem in Ubisoft'), parent=self) self.redeem_button.setMinimumWidth(150) self.redeem_button.clicked.connect(self.activate) if activated: - self.redeem_button.setText(self.tr("Already activated")) + self.redeem_button.setText(self.tr('Already activated')) self.redeem_button.setDisabled(True) self.redeem_indicator.setPixmap( - qta_icon("fa.check-circle-o", "fa5.check-circle", color="green").pixmap(QSize(20, 20)) + qta_icon('fa.check-circle-o', 'fa5.check-circle', color='green').pixmap(QSize(20, 20)) ) layout = QHBoxLayout(self) @@ -132,7 +132,7 @@ def __init__(self, rcore: RareCore, game: Game, ubi_account_id, activated: bool def activate(self): self.redeem_button.setDisabled(True) # self.ok_indicator.setPixmap(icon("mdi.loading", color="grey").pixmap(20, 20)) - self.redeem_indicator.setPixmap(qta_icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20)) + self.redeem_indicator.setPixmap(qta_icon('mdi.transit-connection-horizontal', color='grey').pixmap(20, 20)) if self.args.debug: worker = UbiConnectWorker(self.core, None, None) @@ -144,14 +144,14 @@ def activate(self): def _on_linked(self, error): if not error: self.redeem_indicator.setPixmap( - qta_icon("fa.check-circle-o", "fa5.check-circle", color="green").pixmap(QSize(20, 20)) + qta_icon('fa.check-circle-o', 'fa5.check-circle', color='green').pixmap(QSize(20, 20)) ) self.redeem_button.setDisabled(True) - self.redeem_button.setText(self.tr("Already activated")) + self.redeem_button.setText(self.tr('Already activated')) else: - self.redeem_indicator.setPixmap(qta_icon("fa.times-circle-o", "fa5.times-circle", color="red").pixmap(QSize(20, 20))) + self.redeem_indicator.setPixmap(qta_icon('fa.times-circle-o', 'fa5.times-circle', color='red').pixmap(QSize(20, 20))) self.redeem_indicator.setToolTip(error) - self.redeem_button.setText(self.tr("Try again")) + self.redeem_button.setText(self.tr('Try again')) self.redeem_button.setDisabled(False) @@ -164,14 +164,14 @@ def __init__(self, rcore: RareCore, parent=None): self.core = rcore.core() self.args = rcore.args() - self.setTitle(self.tr("Link Ubisoft Games")) + self.setTitle(self.tr('Link Ubisoft Games')) self.thread_pool = QThreadPool.globalInstance() self.worker: UbiGetInfoWorker = None self.info_label = QLabel(parent=self) - self.info_label.setText(self.tr("Getting information about your redeemable Ubisoft games.")) - self.link_button = QPushButton(self.tr("Link Ubisoft acccount"), parent=self) + self.info_label.setText(self.tr('Getting information about your redeemable Ubisoft games.')) + self.link_button = QPushButton(self.tr('Link Ubisoft acccount'), parent=self) self.link_button.setMinimumWidth(140) self.link_button.clicked.connect(self._on_link_clicked) self.link_button.setEnabled(False) @@ -189,7 +189,7 @@ def __init__(self, rcore: RareCore, parent=None): @Slot() def _on_link_clicked(self): - webbrowser.open("https://www.epicgames.com/id/link/ubisoft") + webbrowser.open('https://www.epicgames.com/id/link/ubisoft') def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): @@ -212,11 +212,11 @@ def showEvent(self, a0: QShowEvent) -> None: def _on_result(self, redeemed: set, entitlements: set, ubi_account_id: str): self.worker = None self.loading_widget.stop() - if not redeemed and ubi_account_id != "error": - self.logger.error("No linked ubisoft account found! Link your accounts via your browser and try again.") - self.info_label.setText(self.tr("Your account is not linked with Ubisoft. Please link your account and try again.")) + if not redeemed and ubi_account_id != 'error': + self.logger.error('No linked ubisoft account found! Link your accounts via your browser and try again.') + self.info_label.setText(self.tr('Your account is not linked with Ubisoft. Please link your account and try again.')) self.link_button.setEnabled(True) - elif ubi_account_id == "error": + elif ubi_account_id == 'error': self.info_label.setText(self.tr("An error has occurred while requesting your account's Ubisoft information.")) self.link_button.setEnabled(True) else: @@ -226,26 +226,26 @@ def _on_result(self, redeemed: set, entitlements: set, ubi_account_id: str): 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: + for dlc_data in game.metadata.get('dlcItemList', []): + if dlc_data['entitlementName'] not in entitlements: continue try: - app_name = dlc_data["releaseInfo"][0]["appId"] + app_name = dlc_data['releaseInfo'][0]['appId'] except (IndexError, KeyError): - app_name = "unknown" + app_name = 'unknown' dlc_game = Game( app_name=app_name, - app_title=dlc_data["title"], + app_title=dlc_data['title'], metadata=dlc_data, ) - if dlc_game.partner_link_type != "ubisoft": + 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": + if game.partner_link_type != 'ubisoft': continue if game.partner_link_id in redeemed: activated += 1 @@ -254,12 +254,12 @@ def _on_result(self, redeemed: set, entitlements: set, ubi_account_id: str): if not uplay_games: self.info_label.setText(self.tr("You don't own any Ubisoft games.")) elif activated == len(uplay_games): - self.info_label.setText(self.tr("All your Ubisoft games have already been activated.")) + self.info_label.setText(self.tr('All your Ubisoft games have already been activated.')) else: self.info_label.setText( - self.tr("You have {} games available to redeem.").format(len(uplay_games) - activated) + self.tr('You have {} games available to redeem.').format(len(uplay_games) - activated) ) - self.logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.") + self.logger.info(f'Found {len(uplay_games) - activated} game(s) to redeem.') for game in uplay_games: widget = UbiLinkWidget( @@ -275,8 +275,8 @@ def _on_result(self, redeemed: set, entitlements: set, ubi_account_id: str): widget = UbiLinkWidget( self.rcore, Game( - app_name="RareTestGame", - app_title="Super Fake Super Rare Super Test (Super?) Game", + app_name='RareTestGame', + app_title='Super Fake Super Rare Super Test (Super?) Game', ), ubi_account_id, activated=False, diff --git a/rare/components/tabs/library/__init__.py b/rare/components/tabs/library/__init__.py index 5d515d1a41..582e8f6778 100644 --- a/rare/components/tabs/library/__init__.py +++ b/rare/components/tabs/library/__init__.py @@ -19,7 +19,7 @@ from .head_bar import LibraryHeadBar from .widgets import LibraryFilter, LibraryOrder, LibraryView, LibraryWidgetController -logger = getLogger("GamesLibrary") +logger = getLogger('GamesLibrary') class GamesLibrary(QStackedWidget): @@ -105,7 +105,7 @@ def setup_game_list(self): for rgame in self.rcore.games: widget = self.add_library_widget(rgame) if not widget: - logger.warning("Excluding %s from the game list", rgame.app_title) + logger.warning('Excluding %s from the game list', rgame.app_title) continue self.filter_games(self.head_bar.current_filter()) self.order_games(self.head_bar.current_order()) @@ -115,18 +115,18 @@ def add_library_widget(self, rgame: RareGame): try: widget = self.library_controller.add_game(rgame) except Exception as e: - logger.error("Could not add widget for %s to library: %s", rgame.app_name, e) + logger.error('Could not add widget for %s to library: %s', rgame.app_name, e) return None widget.show_info.connect(self.show_game_info) return widget @Slot(str) - def search_games(self, search_text: str = ""): + def search_games(self, search_text: str = ''): self.filter_games(self.head_bar.current_filter(), search_text) @Slot(object) @Slot(object, str) - def filter_games(self, library_filter: LibraryFilter = None, search_text: str = ""): + def filter_games(self, library_filter: LibraryFilter = None, search_text: str = ''): if not search_text and (t := self.head_bar.search_bar.text()): search_text = t @@ -134,7 +134,7 @@ def filter_games(self, library_filter: LibraryFilter = None, search_text: str = @Slot(object) @Slot(object, str) - def order_games(self, library_order: LibraryOrder = None, search_text: str = ""): + def order_games(self, library_order: LibraryOrder = None, search_text: str = ''): if not search_text and (t := self.head_bar.search_bar.text()): search_text = t diff --git a/rare/components/tabs/library/details/__init__.py b/rare/components/tabs/library/details/__init__.py index f89ef8b54c..2cef232d1f 100644 --- a/rare/components/tabs/library/details/__init__.py +++ b/rare/components/tabs/library/details/__init__.py @@ -1,5 +1,5 @@ +import contextlib import platform as pf -from typing import Optional from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QKeyEvent, QShowEvent @@ -15,6 +15,7 @@ from .compat import LocalCompatSettings from .details import GameDetails from .dlcs import GameDlcs +from .environ import LocalEnvironSettings from .game import LocalGameSettings @@ -31,29 +32,32 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.details_tab = GameDetails(self.rcore, self) self.details_tab.import_clicked.connect(self.import_clicked) - self.details_index = self.addTab(self.details_tab, self.tr("Information")) + self.details_index = self.addTab(self.details_tab, self.tr('Information')) self.game_settings_tab = LocalGameSettings(settings, rcore, self) - self.game_settings_index = self.addTab(self.game_settings_tab, self.tr("Settings")) + self.game_settings_index = self.addTab(self.game_settings_tab, self.tr('Settings')) - if pf.system() != "Windows": + if pf.system() != 'Windows': self.compat_settings_tab = LocalCompatSettings(settings, rcore, self) - self.compat_settings_index = self.addTab(self.compat_settings_tab, self.tr("Compatibility")) + self.compat_settings_index = self.addTab(self.compat_settings_tab, self.tr('Compatibility')) + + self.environ_tab = LocalEnvironSettings(rcore, self) + self.environ_index = self.addTab(self.environ_tab, self.tr('Environment')) self.cloud_saves_tab = CloudSaves(settings, rcore, self) - self.cloud_saves_index = self.addTab(self.cloud_saves_tab, self.tr("Cloud Saves")) + self.cloud_saves_index = self.addTab(self.cloud_saves_tab, self.tr('Cloud Saves')) self.dlcs_tab = GameDlcs(rcore, self) - self.dlcs_index = self.addTab(self.dlcs_tab, self.tr("Downloadable Content")) + self.dlcs_index = self.addTab(self.dlcs_tab, self.tr('Downloadable Content')) # FIXME: Hiding didn't work, so don't add these tabs in normal mode. Fix this properly later if self.args.debug: self.game_meta_view = GameMetadataView(self) - self.game_meta_index = self.addTab(self.game_meta_view, self.tr("Game Metadata")) + self.game_meta_index = self.addTab(self.game_meta_view, self.tr('Game Metadata')) self.igame_meta_view = GameMetadataView(self) - self.igame_meta_index = self.addTab(self.igame_meta_view, self.tr("InstalledGame Metadata")) + self.igame_meta_index = self.addTab(self.igame_meta_view, self.tr('InstalledGame Metadata')) self.rgame_meta_view = GameMetadataView(self) - self.rgame_meta_index = self.addTab(self.rgame_meta_view, self.tr("RareGame Metadata")) + self.rgame_meta_index = self.addTab(self.rgame_meta_view, self.tr('RareGame Metadata')) self.setCurrentIndex(self.details_index) @@ -62,12 +66,14 @@ def update_game(self, rgame: RareGame): self.game_settings_tab.load_settings(rgame) self.game_settings_tab.launch.setEnabled(rgame.is_installed or rgame.is_origin) - self.game_settings_tab.env_vars.setEnabled(rgame.is_installed or rgame.is_origin) - if pf.system() != "Windows": + if pf.system() != 'Windows': self.compat_settings_tab.load_settings(rgame) self.compat_settings_tab.setEnabled(rgame.is_installed or rgame.is_origin) + self.environ_tab.load_settings(rgame) + self.environ_tab.setEnabled(rgame.is_installed or rgame.is_origin) + self.dlcs_tab.update_dlcs(rgame) self.dlcs_tab.setEnabled(rgame.is_installed and bool(rgame.owned_dlcs)) @@ -95,7 +101,7 @@ def __init__(self, parent=None): self.setEditTriggers(QTreeView.EditTrigger.NoEditTriggers) self.model = QJsonModel() self.setModel(self.model) - self.rgame: Optional[RareGame] = None + self.rgame: RareGame | None = None def showEvent(self, event: QShowEvent): if event.spontaneous(): @@ -106,8 +112,6 @@ def update_game(self, rgame: RareGame, view): self.rgame = rgame self.set_title.emit(self.rgame.app_title) self.model.clear() - try: + with contextlib.suppress(Exception): self.model.load(vars(view)) - except Exception: - pass self.resizeColumnToContents(0) diff --git a/rare/components/tabs/library/details/cloud_saves.py b/rare/components/tabs/library/details/cloud_saves.py index cc98e73c8f..340fe8d3a0 100644 --- a/rare/components/tabs/library/details/cloud_saves.py +++ b/rare/components/tabs/library/details/cloud_saves.py @@ -2,7 +2,6 @@ import platform from datetime import datetime from logging import getLogger -from typing import Tuple from legendary.models.game import SaveGameStatus from PySide6.QtCore import Qt, QThreadPool, Slot @@ -49,8 +48,8 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.rcore = rcore self.core = rcore.core() - self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa5s.desktop").pixmap(128, 128)) - self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "fa5s.cloud").pixmap(128, 128)) + self.sync_ui.icon_local.setPixmap(qta_icon('mdi.harddisk', 'fa5s.desktop').pixmap(128, 128)) + self.sync_ui.icon_remote.setPixmap(qta_icon('mdi.cloud-outline', 'fa5s.cloud').pixmap(128, 128)) self.sync_ui.upload_button.clicked.connect(self.upload) self.sync_ui.download_button.clicked.connect(self.download) @@ -66,7 +65,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.cloud_ui.setupUi(self.cloud_widget) self.cloud_save_path_edit = PathEdit( - path="", + path='', file_mode=QFileDialog.FileMode.Directory, placeholder=self.tr('Use "Resolve path" or "Browse" ...'), edit_func=self.edit_save_path, @@ -79,7 +78,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.cloud_save_path_edit, ) - self.compute_save_path_button = QPushButton(qta_icon("fa.magic", "fa5s.magic"), self.tr("Resolve path")) + self.compute_save_path_button = QPushButton(qta_icon('fa.magic', 'fa5s.magic'), self.tr('Resolve path')) self.compute_save_path_button.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) self.compute_save_path_button.clicked.connect(self.__compute_save_path) self.cloud_ui.main_layout.addRow(None, self.compute_save_path_button) @@ -94,7 +93,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): layout.addWidget(self.info_label) layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)) - def edit_save_path(self, text: str) -> Tuple[bool, str, int]: + def edit_save_path(self, text: str) -> tuple[bool, str, int]: # Validate against raw_save_path (usefull when the user types the path manually) # path = os.path.normpath(text.lower()).split("/") # spec = os.path.normpath(self.save_path_spec.lower()).split("/") @@ -102,7 +101,7 @@ def edit_save_path(self, text: str) -> Tuple[bool, str, int]: # depth = depth if depth > 0 else 1 # if path[-depth:] != spec[-depth:]: # return False, text, IndicatorReasonsCommon.INVALID - if platform.system() != "Windows": + if platform.system() != 'Windows': if os.path.exists(text): return True, text, IndicatorReasonsCommon.VALID else: @@ -128,9 +127,9 @@ def download(self): def __compute_save_path(self): if self.rgame.is_installed and self.rgame.game.supports_cloud_saves: try: - with timelogger(self.logger, "Detecting save path"): + with timelogger(self.logger, 'Detecting save path'): new_path = self.core.get_save_path(self.rgame.app_name) - if platform.system() != "Windows" and not os.path.exists(new_path): + if platform.system() != 'Windows' and not os.path.exists(new_path): raise ValueError(f'Path "{new_path}" does not exist.') except Exception as e: self.logger.warning(str(e)) @@ -140,7 +139,7 @@ def __compute_save_path(self): # self.cloud_save_path_edit.setText("") # QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings") # return - self.cloud_save_path_edit.setText(self.tr("Loading...")) + self.cloud_save_path_edit.setText(self.tr('Loading...')) self.cloud_save_path_edit.setDisabled(True) self.compute_save_path_button.setDisabled(True) @@ -156,8 +155,8 @@ def __sync_check_changed(self, state: Qt.CheckState): @Slot(str, str) def __on_wine_resolver_result(self, path, app_name): - self.logger.info("Wine resolver finished for %s", app_name) - self.logger.info("Computed save path: %s", path) + self.logger.info('Wine resolver finished for %s', app_name) + self.logger.info('Computed save path: %s', path) if app_name == self.rgame.app_name: self.cloud_save_path_edit.setDisabled(False) self.compute_save_path_button.setDisabled(False) @@ -165,17 +164,17 @@ def __on_wine_resolver_result(self, path, app_name): try: os.makedirs(path, exist_ok=True) except PermissionError: - self.cloud_save_path_edit.setText("") + self.cloud_save_path_edit.setText('') QMessageBox.warning( self, - self.tr("Error - {}").format(self.rgame.app_title), + self.tr('Error - {}').format(self.rgame.app_title), self.tr( - "Error while calculating path for {}. Insufficient permissions to create {}" + 'Error while calculating path for {}. Insufficient permissions to create {}' ).format(self.rgame.app_title, path), ) return if not path: - self.cloud_save_path_edit.setText("") + self.cloud_save_path_edit.setText('') return self.cloud_save_path_edit.setText(path) @@ -189,28 +188,28 @@ def __update_widget(self): info_text = ( self.tr("This game doesn't support cloud saves") if not supports_saves - else (self.tr("This game supports cloud saves, but it's not installed") if self.rgame.igame is None else "") + else (self.tr("This game supports cloud saves, but it's not installed") if self.rgame.igame is None else '') ) self.info_label.setText(info_text) self.info_label.setVisible(bool(info_text)) if not saves_ready: - self.sync_ui.date_info_local.setText("None") - self.sync_ui.age_label_local.setText("None") - self.sync_ui.date_info_remote.setText("None") - self.sync_ui.age_label_remote.setText("None") + self.sync_ui.date_info_local.setText('None') + self.sync_ui.age_label_local.setText('None') + self.sync_ui.date_info_remote.setText('None') + self.sync_ui.age_label_remote.setText('None') self.cloud_ui.sync_check.setChecked(False) - self.cloud_save_path_edit.setText("") + self.cloud_save_path_edit.setText('') return status, (dt_local, dt_remote) = self.rgame.save_game_state local_tz = datetime.now().astimezone().tzinfo - self.sync_ui.date_info_local.setText(dt_local.astimezone(local_tz).strftime("%A, %d %B %Y %X") if dt_local else "None") - self.sync_ui.date_info_remote.setText(dt_remote.astimezone(local_tz).strftime("%A, %d %B %Y %X") if dt_remote else "None") + self.sync_ui.date_info_local.setText(dt_local.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_local else 'None') + self.sync_ui.date_info_remote.setText(dt_remote.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_remote else 'None') - newer = self.tr("Newer") - self.sync_ui.age_label_local.setText(f"{newer}" if status == SaveGameStatus.LOCAL_NEWER else " ") - self.sync_ui.age_label_remote.setText(f"{newer}" if status == SaveGameStatus.REMOTE_NEWER else " ") + newer = self.tr('Newer') + self.sync_ui.age_label_local.setText(f'{newer}' if status == SaveGameStatus.LOCAL_NEWER else ' ') + self.sync_ui.age_label_remote.setText(f'{newer}' if status == SaveGameStatus.REMOTE_NEWER else ' ') button_disabled = self.rgame.state in [ RareGame.State.RUNNING, @@ -229,8 +228,8 @@ def __update_widget(self): self.cloud_ui.sync_check.setChecked(self.rgame.auto_sync_saves) self.cloud_ui.sync_check.blockSignals(False) - self.cloud_save_path_edit.setText(self.rgame.save_path if self.rgame.save_path else "") - if platform.system() == "Windows" and not self.rgame.save_path: + self.cloud_save_path_edit.setText(self.rgame.save_path if self.rgame.save_path else '') + if platform.system() == 'Windows' and not self.rgame.save_path: self.__compute_save_path() def update_game(self, rgame: RareGame): diff --git a/rare/components/tabs/library/details/compat.py b/rare/components/tabs/library/details/compat.py index b406cb20ad..9a6012ca94 100644 --- a/rare/components/tabs/library/details/compat.py +++ b/rare/components/tabs/library/details/compat.py @@ -1,6 +1,5 @@ import platform as pf from logging import getLogger -from typing import Tuple from PySide6.QtCore import QSignalBlocker, Qt, Slot from PySide6.QtGui import QHideEvent, QShowEvent @@ -18,18 +17,18 @@ from rare.shared import RareCore from rare.utils import config_helper as config from rare.utils.paths import compat_shaders_dir, proton_compat_dir, wine_prefix_dir -from rare.utils.steam_grades import SteamGrades +from rare.utils.steam_grades import steam_grades from rare.widgets.indicator_edit import ( ColumnCompleter, IndicatorLineEdit, IndicatorReasonsCommon, ) -if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() in {'Linux', 'FreeBSD'}: from rare.components.tabs.settings.widgets.overlay import MangoHudSettings from rare.components.tabs.settings.widgets.proton import ProtonSettings -logger = getLogger("LocalCompatSettings") +logger = getLogger('LocalCompatSettings') class LocalWineSettings(WineSettings): @@ -37,17 +36,17 @@ def load_settings(self, app_name): self.app_name = app_name -if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() in {'Linux', 'FreeBSD'}: class LocalProtonSettings(ProtonSettings): def load_settings(self, app_name: str): self.app_name = app_name def _get_compat_path(self, compat_location: ProtonSettings.CompatLocation): - folder_name = "default" + folder_name = 'default' local_folder_name = self.rcore.get_game(self.app_name).folder_name if compat_location == ProtonSettings.CompatLocation.NONE: - if wine_prefix_dir(local_folder_name).joinpath("system.reg").is_file(): + if wine_prefix_dir(local_folder_name).joinpath('system.reg').is_file(): compat_location = ProtonSettings.CompatLocation.ISOLATED if compat_location == ProtonSettings.CompatLocation.ISOLATED: folder_name = local_folder_name @@ -61,7 +60,7 @@ def load_settings(self, app_name: str): class LocalRunnerSettings(RunnerSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - if pf.system() in {"Linux", "FreeBSD"}: + if pf.system() in {'Linux', 'FreeBSD'}: super(LocalRunnerSettings, self).__init__(settings, rcore, LocalWineSettings, LocalProtonSettings, parent=parent) else: super(LocalRunnerSettings, self).__init__(settings, rcore, LocalWineSettings, parent=parent) @@ -69,28 +68,28 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.rgame: RareGame = None self.steam_appid_edit = IndicatorLineEdit( - placeholder=self.tr("Use in case the SteamAppID was not found automatically"), + placeholder=self.tr('Use in case the SteamAppID was not found automatically'), edit_func=self.__steam_appid_edit_callback, save_func=self.__steam_appid_save_callback, parent=self, ) - self.form_layout.addRow(self.tr("Steam AppID"), self.steam_appid_edit) + self.form_layout.addRow(self.tr('Steam AppID'), self.steam_appid_edit) - self.__grades = SteamGrades() + self.__grades = steam_grades def showEvent(self, e: QShowEvent): if e.spontaneous(): return super().showEvent(e) _ = QSignalBlocker(self.shader_cache_check) is_local_cache_enabled = self.settings.get_with_global(app_settings.local_shader_cache, self.rgame.app_name) - has_local_cache_path = bool(config.get_envvar(self.app_name, "STEAM_COMPAT_SHADER_PATH", False)) + has_local_cache_path = bool(config.get_envvar(self.app_name, 'STEAM_COMPAT_SHADER_PATH', False)) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) self.shader_cache_check.setChecked(is_local_cache_enabled or has_local_cache_path) _ = QSignalBlocker(self.steam_appid_edit) items = {k: v for k, v in self.__grades.steam_appids.items() if self.rgame.app_title.lower()[0:4] in k.lower()[0:4]} self.steam_appid_edit.setCompleter(ColumnCompleter(items=items)) - self.steam_appid_edit.setText(self.rgame.steam_appid if self.rgame.steam_appid else "") - self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(self.rgame.steam_appid, "")) + self.steam_appid_edit.setText(self.rgame.steam_appid if self.rgame.steam_appid else '') + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(self.rgame.steam_appid, '')) return super().showEvent(e) def hideEvent(self, e: QHideEvent): @@ -99,19 +98,19 @@ def hideEvent(self, e: QHideEvent): self.steam_appid_edit.setCompleter(None) return super().hideEvent(e) - def __steam_appid_edit_callback(self, text: str) -> Tuple[bool, str, int]: - self.steam_appid_edit.setInfo("") + def __steam_appid_edit_callback(self, text: str) -> tuple[bool, str, int]: + self.steam_appid_edit.setInfo('') if not text or len(text) < 3: return True, text, IndicatorReasonsCommon.UNDEFINED - if text in self.__grades.steam_appids.keys(): + if text in self.__grades.steam_appids: return True, self.__grades.steam_appids[text], IndicatorReasonsCommon.VALID - if text in self.__grades.steam_titles.keys(): + if text in self.__grades.steam_titles: return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.GAME_NOT_EXISTS def __steam_appid_save_callback(self, text: str) -> None: - self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(text, "")) + self.steam_appid_edit.setInfo(self.__grades.steam_titles.get(text, '')) if text == self.rgame.steam_appid: return self.rgame.steam_appid = text @@ -122,13 +121,13 @@ def _shader_cache_check_changed(self, state: Qt.CheckState): if checked := (state != Qt.CheckState.Unchecked): config.set_envvar( self.rgame.app_name, - "STEAM_COMPAT_SHADER_PATH", + 'STEAM_COMPAT_SHADER_PATH', compat_shaders_dir(self.rgame.folder_name).as_posix(), ) else: - config.remove_envvar(self.rgame.app_name, "STEAM_COMPAT_SHADER_PATH") + config.remove_envvar(self.rgame.app_name, 'STEAM_COMPAT_SHADER_PATH') self.settings.set_with_global(app_settings.local_shader_cache, checked, self.rgame.app_name) - self.environ_changed.emit("STEAM_COMPAT_SHADER_PATH") + self.environ_changed.emit('STEAM_COMPAT_SHADER_PATH') def load_settings(self, rgame: RareGame): self.rgame = rgame @@ -155,7 +154,7 @@ def load_settings(self, app_name: str): class LocalCompatSettings(CompatSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - if pf.system() in {"Linux", "FreeBSD"}: + if pf.system() in {'Linux', 'FreeBSD'}: super(LocalCompatSettings, self).__init__( settings, rcore, diff --git a/rare/components/tabs/library/details/details.py b/rare/components/tabs/library/details/details.py index a15b7bd8a1..ef63856bb7 100644 --- a/rare/components/tabs/library/details/details.py +++ b/rare/components/tabs/library/details/details.py @@ -2,7 +2,6 @@ import platform from hashlib import sha1 from logging import getLogger -from typing import Dict, Optional, Tuple from PySide6.QtCore import ( QCoreApplication, @@ -33,12 +32,12 @@ from rare.ui.components.tabs.library.details.details import Ui_GameDetails from rare.utils.misc import format_size, qta_icon, relative_date, style_hyperlink from rare.utils.paths import cache_dir -from rare.utils.qt_requests import QtRequests +from rare.utils.qrequests import QRequests from rare.widgets.dialogs import ButtonDialog, game_title from rare.widgets.image_widget import ImageSize, ImageWidget, LoadingImageWidget from rare.widgets.side_tab import SideTabContents -logger = getLogger("GameInfo") +logger = getLogger('GameInfo') class GameDetails(QWidget, SideTabContents): @@ -51,21 +50,21 @@ def __init__(self, rcore: RareCore, parent=None): self.ui = Ui_GameDetails() self.ui.setupUi(self) # lk: set object names for CSS properties - self.ui.install_path.setObjectName("LinkLabel") - self.ui.install_button.setObjectName("InstallButton") - self.ui.modify_button.setObjectName("InstallButton") - self.ui.verify_button.setObjectName("VerifyButton") - self.ui.move_button.setObjectName("MoveButton") - self.ui.uninstall_button.setObjectName("UninstallButton") - - self.ui.install_button.setIcon(qta_icon("ri.install-line")) - self.ui.import_button.setIcon(qta_icon("mdi.application-import")) - - self.ui.modify_button.setIcon(qta_icon("mdi.content-save-edit-outline")) - self.ui.verify_button.setIcon(qta_icon("mdi.check-underline")) - self.ui.repair_button.setIcon(qta_icon("mdi.progress-wrench")) - self.ui.move_button.setIcon(qta_icon("mdi.folder-move-outline")) - self.ui.uninstall_button.setIcon(qta_icon("ri.uninstall-line")) + self.ui.install_path.setObjectName('LinkLabel') + self.ui.install_button.setObjectName('InstallButton') + self.ui.modify_button.setObjectName('InstallButton') + self.ui.verify_button.setObjectName('VerifyButton') + self.ui.move_button.setObjectName('MoveButton') + self.ui.uninstall_button.setObjectName('UninstallButton') + + self.ui.install_button.setIcon(qta_icon('ri.install-line')) + self.ui.import_button.setIcon(qta_icon('mdi.application-import')) + + self.ui.modify_button.setIcon(qta_icon('mdi.content-save-edit-outline')) + self.ui.verify_button.setIcon(qta_icon('mdi.check-underline')) + self.ui.repair_button.setIcon(qta_icon('mdi.progress-wrench')) + self.ui.move_button.setIcon(qta_icon('mdi.folder-move-outline')) + self.ui.uninstall_button.setIcon(qta_icon('ri.uninstall-line')) self.ui.grade.setOpenExternalLinks(True) self.ui.install_path.setOpenExternalLinks(True) @@ -73,9 +72,9 @@ def __init__(self, rcore: RareCore, parent=None): self.rcore = rcore self.core = rcore.core() self.args = rcore.args() - self.net_manager = QtRequests(cache=str(cache_dir().joinpath("achievements")), parent=self) + self.net_manager = QRequests(cache=str(cache_dir().joinpath('achievements')), parent=self) - self.rgame: Optional[RareGame] = None + self.rgame: RareGame | None = None self.image = ImageWidget(self) self.image.setFixedSize(ImageSize.DisplayTall) @@ -91,17 +90,17 @@ def __init__(self, rcore: RareCore, parent=None): self.ui.uninstall_button.clicked.connect(self.__on_uninstall) self.steam_grade_ratings = { - "platinum": self.tr("Platinum"), - "gold": self.tr("Gold"), - "silver": self.tr("Silver"), - "bronze": self.tr("Bronze"), - "borked": self.tr("Borked"), - "fail": self.tr("Failed to get rating"), - "pending": self.tr("Loading..."), - "na": self.tr("Not applicable"), + 'platinum': self.tr('Platinum'), + 'gold': self.tr('Gold'), + 'silver': self.tr('Silver'), + 'bronze': self.tr('Bronze'), + 'borked': self.tr('Borked'), + 'fail': self.tr('Failed to get rating'), + 'pending': self.tr('Loading...'), + 'na': self.tr('Not applicable'), } - self.ui.add_tag_button.setIcon(qta_icon("mdi.plus")) + self.ui.add_tag_button.setIcon(qta_icon('mdi.plus')) self.ui.add_tag_button.clicked.connect(self.__on_tag_add) ach_progress_layout = QVBoxLayout(self.ui.ach_progress_page) @@ -145,12 +144,12 @@ def __on_modify(self): @Slot() def __on_repair(self): """This method is to be called from the button only""" - repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.repair") + repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.rgame.app_name}.repair') if not os.path.exists(repair_file): QMessageBox.warning( self, - self.tr("Error - {}").format(self.rgame.app_title), - self.tr("Repair file does not exist or game does not need a repair. Please verify game first"), + self.tr('Error - {}').format(self.rgame.app_title), + self.tr('Repair file does not exist or game does not need a repair. Please verify game first'), ) return self.repair_game(self.rgame) @@ -161,9 +160,10 @@ def repair_game(self, rgame: RareGame): if rgame.has_update: mbox = QMessageBox.question( self, - self.tr("Repair and update? - {}").format(self.rgame.app_title), + self.tr('Repair and update? - {}').format(self.rgame.app_title), self.tr( - "There is an update for {} from {} to {}. Do you want to update the game while repairing it?" + 'There is an update for {} from {} to {}. ' + 'Do you want to update the game while repairing it?' ).format(rgame.app_title, rgame.version, rgame.remote_version), ) ans = mbox == QMessageBox.StandardButton.Yes @@ -171,17 +171,17 @@ def repair_game(self, rgame: RareGame): @Slot(RareGame, str) def __on_worker_error(self, rgame: RareGame, message: str): - QMessageBox.warning(self, self.tr("Error - {}").format(rgame.app_title), message) + QMessageBox.warning(self, self.tr('Error - {}').format(rgame.app_title), message) @Slot() def __on_verify(self): """This method is to be called from the button only""" if not os.path.exists(self.rgame.igame.install_path): - logger.error(f"Installation path {self.rgame.igame.install_path} for {self.rgame.app_title} does not exist") + logger.error(f'Installation path {self.rgame.igame.install_path} for {self.rgame.app_title} does not exist') QMessageBox.warning( self, - self.tr("Error - {}").format(self.rgame.app_title), - self.tr("Installation path for {} does not exist. Cannot continue.").format(self.rgame.app_title), + self.tr('Error - {}').format(self.rgame.app_title), + self.tr('Installation path for {} does not exist. Cannot continue.').format(self.rgame.app_title), ) return if self.rgame.sdl_available: @@ -196,7 +196,7 @@ def verify_game(self, rgame: RareGame, sdl_model: SelectiveDownloadsModel = None if sdl_model is not None: if not sdl_model.accepted or sdl_model.install_tag is None: return - self.core.lgd.config.set(rgame.app_name, "install_tags", ",".join(sdl_model.install_tag)) + self.core.lgd.config.set(rgame.app_name, 'install_tags', ','.join(sdl_model.install_tag)) self.core.lgd.save_config() worker = VerifyWorker(self.core, self.args, rgame) worker.signals.progress.connect(self.__on_verify_progress) @@ -217,15 +217,16 @@ def __on_verify_result(self, rgame: RareGame, success, failed, missing): if success: QMessageBox.information( self, - self.tr("Summary - {}").format(rgame.app_title), - self.tr("{} has been verified successfully. No missing or corrupt files found").format(rgame.app_title), + self.tr('Summary - {}').format(rgame.app_title), + self.tr('{} has been verified successfully. No missing or corrupt files found').format(rgame.app_title), ) else: ans = QMessageBox.question( self, - self.tr("Summary - {}").format(rgame.app_title), + self.tr('Summary - {}').format(rgame.app_title), self.tr( - "{} failed verification, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?" + '{} failed verification, {} file(s) corrupted, {} file(s) are missing. ' + 'Do you want to repair them?' ).format(rgame.app_title, failed, missing), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes, @@ -251,8 +252,8 @@ def move_game(self, rgame: RareGame, options: MoveGameModel): if not options.dst_exists and options.target_name in os.listdir(options.target_path): ans = QMessageBox.question( self, - self.tr("Move game? - {}").format(self.rgame.app_title), - self.tr("Destination {} already exists. Are you sure you want to overwrite it?").format(new_install_path), + self.tr('Move game? - {}').format(self.rgame.app_title), + self.tr('Destination {} already exists. Are you sure you want to overwrite it?').format(new_install_path), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes, ) @@ -279,8 +280,8 @@ def __on_move_progress(self, rgame: RareGame, progress: int, total_size: int, co def __on_move_result(self, rgame: RareGame, dst_path: str): QMessageBox.information( self, - self.tr("Summary - {}").format(rgame.app_title), - self.tr("{} successfully moved to {}.").format(rgame.app_title, dst_path), + self.tr('Summary - {}').format(rgame.app_title), + self.tr('{} successfully moved to {}.').format(rgame.app_title, dst_path), ) @Slot(Qt.CheckState, str) @@ -325,11 +326,11 @@ def __update_widget(self): self.ui.version_label.setDisabled(self.rgame.is_non_asset) self.ui.version.setDisabled(self.rgame.is_non_asset) - self.ui.version.setText(self.rgame.version if not self.rgame.is_non_asset else "N/A") + self.ui.version.setText(self.rgame.version if not self.rgame.is_non_asset else 'N/A') self.ui.install_size_label.setEnabled(bool(self.rgame.install_size)) self.ui.install_size.setEnabled(bool(self.rgame.install_size)) - self.ui.install_size.setText(format_size(self.rgame.install_size) if self.rgame.install_size else "N/A") + self.ui.install_size.setText(format_size(self.rgame.install_size) if self.rgame.install_size else 'N/A') self.ui.install_path_label.setEnabled(bool(self.rgame.install_path)) self.ui.install_path.setEnabled(bool(self.rgame.install_path)) @@ -339,19 +340,19 @@ def __update_widget(self): self.rgame.install_path, ) if self.rgame.install_path - else "N/A" + else 'N/A' ) self.ui.platform.setText( self.rgame.igame.platform if self.rgame.is_installed and not self.rgame.is_non_asset else self.rgame.default_platform ) - self.ui.grade_label.setDisabled(self.rgame.is_unreal or platform.system() == "Windows") - self.ui.grade.setDisabled(self.rgame.is_unreal or platform.system() == "Windows") + self.ui.grade_label.setDisabled(self.rgame.is_unreal or platform.system() == 'Windows') + self.ui.grade.setDisabled(self.rgame.is_unreal or platform.system() == 'Windows') self.ui.grade.setText( style_hyperlink( - f"https://www.protondb.com/app/{self.rgame.steam_appid}", - f"{self.steam_grade_ratings[self.rgame.get_steam_grade()]} ({self.rgame.steam_appid})", + f'https://www.protondb.com/app/{self.rgame.steam_appid}', + f'{self.steam_grade_ratings[self.rgame.get_steam_grade()]} ({self.rgame.steam_appid})', ) ) @@ -410,12 +411,12 @@ def update_game(self, rgame: RareGame): try: worker.signals.progress.disconnect(self.__on_verify_progress) except TypeError as e: - logger.warning(f"{self.rgame.app_name} verify worker: {e}") + logger.warning(f'{self.rgame.app_name} verify worker: {e}') if isinstance(worker, MoveWorker): try: worker.signals.progress.disconnect(self.__on_move_progress) except TypeError as e: - logger.warning(f"{self.rgame.app_name} move worker: {e}") + logger.warning(f'{self.rgame.app_name} move worker: {e}') self.rgame.signals.widget.refresh.disconnect(self.__update_widget) self.rgame = None @@ -432,12 +433,12 @@ def update_game(self, rgame: RareGame): self.ui.dev.setText(rgame.developer) if rgame.is_non_asset: - self.ui.install_button.setText(self.tr("Link/Launch")) + self.ui.install_button.setText(self.tr('Link/Launch')) self.ui.actions_stack.setCurrentWidget(self.ui.uninstalled_page) else: - self.ui.install_button.setText(self.tr("Install")) + self.ui.install_button.setText(self.tr('Install')) - self.ui.description_field.setText(rgame.game.metadata["description"]) + self.ui.description_field.setText(rgame.game.metadata['description']) for page in ( self.ui.ach_progress_page, @@ -451,15 +452,15 @@ def update_game(self, rgame: RareGame): w.deleteLater() if ach := rgame.achievements: - self.ui.progress_field.setText(f"{ach.user_unlocked}/{ach.total_achievements}") - self.ui.exp_field.setText(f"{ach.user_xp}/{ach.total_product_xp}") + self.ui.progress_field.setText(f'{ach.user_unlocked}/{ach.total_achievements}') + self.ui.exp_field.setText(f'{ach.user_xp}/{ach.total_product_xp}') for group, page in zip( ( - sorted(ach.hidden, key=lambda a: a["xp"], reverse=False), - sorted(ach.uninitiated, key=lambda a: a["xp"], reverse=False), - sorted(ach.completed, key=lambda a: a["unlock_date"], reverse=True), - sorted(ach.in_progress, key=lambda a: a["progress"], reverse=True), + sorted(ach.hidden, key=lambda a: a['xp'], reverse=False), + sorted(ach.uninitiated, key=lambda a: a['xp'], reverse=False), + sorted(ach.completed, key=lambda a: a['unlock_date'], reverse=True), + sorted(ach.in_progress, key=lambda a: a['progress'], reverse=True), ), ( self.ui.ach_hidden_page, @@ -467,6 +468,7 @@ def update_game(self, rgame: RareGame): self.ui.ach_completed_page, self.ui.ach_progress_page, ), + strict=False, ): self.ui.achievements_toolbox.setItemEnabled(self.ui.achievements_toolbox.indexOf(page), bool(group)) if bool(group): @@ -474,33 +476,33 @@ def update_game(self, rgame: RareGame): for item in group: page.layout().addWidget(AchievementWidget(self.net_manager, item), alignment=Qt.AlignmentFlag.AlignTop) else: - self.ui.progress_field.setText(self.tr("No data")) - self.ui.exp_field.setText(self.tr("No data")) + self.ui.progress_field.setText(self.tr('No data')) + self.ui.exp_field.setText(self.tr('No data')) self.ui.achievements_group.setVisible(bool(ach)) self.rgame = rgame class AchievementWidget(QFrame): - def __init__(self, manager: QtRequests, achievement: Dict, parent=None): + def __init__(self, manager: QRequests, achievement: dict, parent=None): super().__init__(parent=parent) self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShadow(QFrame.Shadow.Sunken) image = LoadingImageWidget(manager, parent=self) image.setFixedSize(ImageSize.LibraryIcon) - image.fetchPixmap(achievement["icon_link"]) + image.fetchPixmap(achievement['icon_link']) title = QLabel( - f"{achievement['display_name']} ({achievement['xp']} XP)", + f'{achievement["display_name"]} ({achievement["xp"]} XP)', parent=self, ) title.setWordWrap(True) - description = QLabel(achievement["description"], parent=self) + description = QLabel(achievement['description'], parent=self) description.setWordWrap(True) - unlock_date = achievement["unlock_date"].astimezone() if achievement["unlock_date"] else None - unlock_date_str = f" ( On: {relative_date(unlock_date)} )" if unlock_date else "" - progress = QLabel(f"Progress: {achievement['progress'] * 100:,.2f}% {unlock_date_str}", parent=self) + unlock_date = achievement['unlock_date'].astimezone() if achievement['unlock_date'] else None + unlock_date_str = f' ( On: {relative_date(unlock_date)} )' if unlock_date else '' + progress = QLabel(f'Progress: {achievement["progress"] * 100:,.2f}% {unlock_date_str}', parent=self) if unlock_date: progress.setToolTip(str(unlock_date)) @@ -519,10 +521,10 @@ class GameTagCheckBox(QCheckBox): checkStateChangedData = Signal(Qt.CheckState, str) tag_translations = { - "backlog": QCoreApplication.translate("GameTagCheckBox", "Backlog", None), - "completed": QCoreApplication.translate("GameTagCheckBox", "Completed", None), - "favorite": QCoreApplication.translate("GameTagCheckBox", "Favorite", None), - "hidden": QCoreApplication.translate("GameTagCheckBox", "Hidden", None), + 'backlog': QCoreApplication.translate('GameTagCheckBox', 'Backlog', None), + 'completed': QCoreApplication.translate('GameTagCheckBox', 'Completed', None), + 'favorite': QCoreApplication.translate('GameTagCheckBox', 'Favorite', None), + 'hidden': QCoreApplication.translate('GameTagCheckBox', 'Hidden', None), } def __init__(self, tag: str, parent=None): @@ -531,14 +533,15 @@ def __init__(self, tag: str, parent=None): self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setText(self.tag_translations.get(tag, tag)) self.tag = tag - base_color = (int(sha1(tag.encode("utf-8")).hexdigest()[0:6], base=16) & 0x707070) | 0x0C0C0C + base_color = (int(sha1(tag.encode('utf-8')).hexdigest()[0:6], base=16) & 0x707070) | 0x0C0C0C border_color = base_color | 0x3F3F3F luminance = ( ((base_color & 0xFF0000) >> 16) * 0.2126 + ((base_color & 0x00FF00) >> 8) * 0.7152 + (base_color & 0x0000FF) * 0.0722 ) - font_color = "white" if luminance < 140 else "black" - style = "QCheckBox#{0}{{color: {1};border-color: #{2:x};background-color: #{3:x};}}".format( - self.objectName(), font_color, border_color, base_color + font_color = 'white' if luminance < 140 else 'black' + style = ( + f'QCheckBox#{self.objectName()}' + f'{{color: {font_color};border-color: #{border_color:x};background-color: #{base_color:x};}}' ) self.setStyleSheet(style) self.checkStateChanged.connect(self._on_state_changed) @@ -561,9 +564,9 @@ def setText(self, text, /): class GameTagAddDialog(ButtonDialog): result_ready = Signal(bool, str) - def __init__(self, rgame: RareGame, tags: Tuple[str, ...], parent=None): + def __init__(self, rgame: RareGame, tags: tuple[str, ...], parent=None): super(GameTagAddDialog, self).__init__(parent=parent) - header = self.tr("Add tag") + header = self.tr('Add tag') self.setWindowTitle(header) self.setSubtitle(game_title(header, rgame.app_title)) @@ -575,12 +578,12 @@ def __init__(self, rgame: RareGame, tags: Tuple[str, ...], parent=None): self.setCentralLayout(self.widget_layout) - self.accept_button.setText(self.tr("Save")) - self.accept_button.setIcon(qta_icon("fa.edit", "fa5s.edit")) + self.accept_button.setText(self.tr('Save')) + self.accept_button.setIcon(qta_icon('fa.edit', 'fa5s.edit')) self.accept_button.setEnabled(False) self.tags = tags - self.result: Tuple = (False, "") + self.result: tuple = (False, '') @Slot(str) def __on_text_changed(self, text: str): diff --git a/rare/components/tabs/library/details/dlcs.py b/rare/components/tabs/library/details/dlcs.py index 12a6f1bb40..e02abd858e 100644 --- a/rare/components/tabs/library/details/dlcs.py +++ b/rare/components/tabs/library/details/dlcs.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import QFrame, QMessageBox, QToolBox @@ -54,10 +52,10 @@ class InstalledGameDlcWidget(GameDlcWidget): def __init__(self, rgame: RareGame, rdlc: RareGame, parent=None): super(InstalledGameDlcWidget, self).__init__(rgame=rgame, rdlc=rdlc, parent=parent) # lk: set object names for CSS properties - self.ui.action_button.setObjectName("UninstallButton") + 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(qta_icon("ri.uninstall-line")) + self.ui.action_button.setText(self.tr('Uninstall DLC')) + self.ui.action_button.setIcon(qta_icon('ri.uninstall-line')) # lk: don't reference `self.rdlc` here because the object has been deleted rdlc.signals.game.uninstalled.connect(self.__uninstalled) @@ -75,10 +73,10 @@ class AvailableGameDlcWidget(GameDlcWidget): def __init__(self, rgame: RareGame, rdlc: RareGame, parent=None): super(AvailableGameDlcWidget, self).__init__(rgame=rgame, rdlc=rdlc, parent=parent) # lk: set object names for CSS properties - self.ui.action_button.setObjectName("InstallButton") + 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(qta_icon("ri.install-line")) + self.ui.action_button.setText(self.tr('Install DLC')) + self.ui.action_button.setIcon(qta_icon('ri.install-line')) # lk: don't reference `self.rdlc` here because the object has been deleted rdlc.signals.game.installed.connect(self.__installed) @@ -91,8 +89,8 @@ def install_dlc(self): if not self.rgame.is_installed: QMessageBox.warning( self, - self.tr("Error"), - self.tr("Base Game is not installed. Please install {} first").format(self.rgame.app_title), + self.tr('Error'), + self.tr('Base Game is not installed. Please install {} first').format(self.rgame.app_title), ) return self.rdlc.install() @@ -107,26 +105,26 @@ def __init__(self, rcore: RareCore, parent=None): self.core = rcore.core() self.signals = rcore.signals() - self.rgame: Optional[RareGame] = None + self.rgame: RareGame | None = None - def list_installed(self) -> List[InstalledGameDlcWidget]: + def list_installed(self) -> list[InstalledGameDlcWidget]: return self.ui.installed_dlc_container.findChildren( InstalledGameDlcWidget, options=Qt.FindChildOption.FindDirectChildrenOnly ) - def list_available(self) -> List[AvailableGameDlcWidget]: + def list_available(self) -> list[AvailableGameDlcWidget]: return self.ui.available_dlc_container.findChildren( AvailableGameDlcWidget, options=Qt.FindChildOption.FindDirectChildrenOnly ) - def get_installed(self, app_name: str) -> Optional[InstalledGameDlcWidget]: + def get_installed(self, app_name: str) -> InstalledGameDlcWidget | None: return self.ui.installed_dlc_container.findChild( InstalledGameDlcWidget, name=widget_object_name(InstalledGameDlcWidget, app_name), options=Qt.FindChildOption.FindDirectChildrenOnly, ) - def get_available(self, app_name: str) -> Optional[AvailableGameDlcWidget]: + def get_available(self, app_name: str) -> AvailableGameDlcWidget | None: return self.ui.available_dlc_container.findChild( AvailableGameDlcWidget, name=widget_object_name(AvailableGameDlcWidget, app_name), diff --git a/rare/components/tabs/library/details/environ.py b/rare/components/tabs/library/details/environ.py new file mode 100644 index 0000000000..362d6d21f3 --- /dev/null +++ b/rare/components/tabs/library/details/environ.py @@ -0,0 +1,12 @@ +from rare.components.tabs.settings.environ import EnvironSettingsBase +from rare.models.game import RareGame +from rare.shared import RareCore + + +class LocalEnvironSettings(EnvironSettingsBase): + def __init__(self, rcore: RareCore, parent=None): + super(LocalEnvironSettings, self).__init__(rcore, parent=parent) + + def load_settings(self, rgame: RareGame): + self.set_title.emit(rgame.app_title) + self.app_name = rgame.app_name diff --git a/rare/components/tabs/library/details/game.py b/rare/components/tabs/library/details/game.py index 09ada8b422..f4f8a57ca5 100644 --- a/rare/components/tabs/library/details/game.py +++ b/rare/components/tabs/library/details/game.py @@ -1,6 +1,5 @@ import os from logging import getLogger -from typing import Tuple from legendary.models.game import Game, InstalledGame from PySide6.QtCore import Qt, Slot @@ -8,7 +7,6 @@ from PySide6.QtWidgets import QComboBox, QFileDialog, QLineEdit from rare.components.tabs.settings.game import GameSettingsBase -from rare.components.tabs.settings.widgets.env_vars import EnvVars from rare.components.tabs.settings.widgets.launch import LaunchSettingsBase from rare.components.tabs.settings.widgets.wrappers import WrapperSettings from rare.models.game import RareGame @@ -17,7 +15,7 @@ from rare.utils import config_helper as config from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit -logger = getLogger("LocalGameSettings") +logger = getLogger('LocalGameSettings') class LocalWrapperSettings(WrapperSettings): @@ -33,51 +31,51 @@ def __init__(self, rcore: RareCore, parent=None): self.igame: InstalledGame = None self.skip_update_combo = QComboBox(self) - self.skip_update_combo.addItem(self.tr("Default"), None) - self.skip_update_combo.addItem(self.tr("No"), "false") - self.skip_update_combo.addItem(self.tr("Yes"), "true") + self.skip_update_combo.addItem(self.tr('Default'), None) + self.skip_update_combo.addItem(self.tr('No'), 'false') + self.skip_update_combo.addItem(self.tr('Yes'), 'true') self.skip_update_combo.currentIndexChanged.connect(self.__skip_update_changed) self.offline_combo = QComboBox(self) - self.offline_combo.addItem(self.tr("Default"), None) - self.offline_combo.addItem(self.tr("No"), "false") - self.offline_combo.addItem(self.tr("Yes"), "true") + self.offline_combo.addItem(self.tr('Default'), None) + self.offline_combo.addItem(self.tr('No'), 'false') + self.offline_combo.addItem(self.tr('Yes'), 'true') self.offline_combo.currentIndexChanged.connect(self.__offline_changed) - self.override_exe_name_filters: Tuple[str, ...] = ( - "*.exe", - "*.app", - "*.bat", - "*.ps1", - "*.sh", + self.override_exe_name_filters: tuple[str, ...] = ( + '*.exe', + '*.app', + '*.bat', + '*.ps1', + '*.sh', ) self.override_exe_edit = PathEdit( file_mode=QFileDialog.FileMode.ExistingFile, name_filters=self.override_exe_name_filters, - placeholder=self.tr("Relative path to the replacement executable"), + placeholder=self.tr('Relative path to the replacement executable'), edit_func=self.__override_exe_edit_callback, save_func=self.__override_exe_save_callback, parent=self, ) self.launch_params_edit = QLineEdit(self) - self.launch_params_edit.setPlaceholderText(self.tr("Game specific command line arguments")) + self.launch_params_edit.setPlaceholderText(self.tr('Game specific command line arguments')) self.launch_params_edit.textChanged.connect(self.__launch_params_changed) - self.main_layout.insertRow(0, self.tr("Skip update check"), self.skip_update_combo) - self.main_layout.insertRow(1, self.tr("Offline mode"), self.offline_combo) - self.main_layout.insertRow(2, self.tr("Launch parameters"), self.launch_params_edit) - self.main_layout.insertRow(3, self.tr("Override executable"), self.override_exe_edit) + self.main_layout.insertRow(0, self.tr('Skip update check'), self.skip_update_combo) + self.main_layout.insertRow(1, self.tr('Offline mode'), self.offline_combo) + self.main_layout.insertRow(2, self.tr('Launch parameters'), self.launch_params_edit) + self.main_layout.insertRow(3, self.tr('Override executable'), self.override_exe_edit) def showEvent(self, a0: QShowEvent): if a0.spontaneous(): return super().showEvent(a0) - skip_update = config.get_option(self.app_name, "skip_update_check", fallback=None) + skip_update = config.get_option(self.app_name, 'skip_update_check', fallback=None) self.skip_update_combo.setCurrentIndex(self.offline_combo.findData(skip_update, Qt.ItemDataRole.UserRole)) - offline = config.get_option(self.app_name, "offline", fallback=None) + offline = config.get_option(self.app_name, 'offline', fallback=None) self.offline_combo.setCurrentIndex(self.offline_combo.findData(offline, Qt.ItemDataRole.UserRole)) if self.igame: @@ -85,12 +83,12 @@ def showEvent(self, a0: QShowEvent): self.override_exe_edit.setRootPath(self.igame.install_path) else: self.offline_combo.setEnabled(False) - self.override_exe_edit.setRootPath(os.path.expanduser("~/")) + self.override_exe_edit.setRootPath(os.path.expanduser('~/')) - launch_params = config.get_option(self.app_name, "start_params", "") + launch_params = config.get_option(self.app_name, 'start_params', '') self.launch_params_edit.setText(launch_params) - override_exe = config.get_option(self.app_name, "override_exe", fallback="") + override_exe = config.get_option(self.app_name, 'override_exe', fallback='') self.override_exe_edit.setText(override_exe) return super().showEvent(a0) @@ -98,9 +96,9 @@ def showEvent(self, a0: QShowEvent): @Slot(int) def __skip_update_changed(self, index): data = self.skip_update_combo.itemData(index, Qt.ItemDataRole.UserRole) - config.adjust_option(self.app_name, "skip_update_check", data) + config.adjust_option(self.app_name, 'skip_update_check', data) - def __override_exe_edit_callback(self, path: str) -> Tuple[bool, str, int]: + def __override_exe_edit_callback(self, path: str) -> tuple[bool, str, int]: if not path or self.igame is None: return True, path, IndicatorReasonsCommon.VALID if not os.path.isabs(path): @@ -113,21 +111,21 @@ def __override_exe_edit_callback(self, path: str) -> Tuple[bool, str, int]: if not os.path.exists(path): return False, path, IndicatorReasonsCommon.WRONG_PATH - if not path.endswith(tuple(map(lambda s: s.replace("*", ""), self.override_exe_name_filters))): + if not path.endswith(tuple(map(lambda s: s.replace('*', ''), self.override_exe_name_filters))): return False, path, IndicatorReasonsCommon.WRONG_PATH path = os.path.relpath(path, self.igame.install_path) return True, path, IndicatorReasonsCommon.VALID def __override_exe_save_callback(self, path: str): - config.adjust_option(self.app_name, "override_exe", path) + config.adjust_option(self.app_name, 'override_exe', path) @Slot(int) def __offline_changed(self, index): data = self.skip_update_combo.itemData(index, Qt.ItemDataRole.UserRole) - config.adjust_option(self.app_name, "offline", data) + config.adjust_option(self.app_name, 'offline', data) def __launch_params_changed(self, value) -> None: - config.adjust_option(self.app_name, "start_params", value) + config.adjust_option(self.app_name, 'start_params', value) def load_settings(self, rgame: RareGame): self.game = rgame.game @@ -136,19 +134,11 @@ def load_settings(self, rgame: RareGame): self.wrappers_widget.load_settings(rgame.app_name) -class LocalEnvVars(EnvVars): - def load_settings(self, app_name): - self.app_name = app_name - - class LocalGameSettings(GameSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - super(LocalGameSettings, self).__init__( - settings, rcore, launch_widget=LocalLaunchSettings, envvar_widget=LocalEnvVars, parent=parent - ) + super(LocalGameSettings, self).__init__(settings, rcore, launch_widget=LocalLaunchSettings, parent=parent) def load_settings(self, rgame: RareGame): self.set_title.emit(rgame.app_title) self.app_name = rgame.app_name self.launch.load_settings(rgame) - self.env_vars.load_settings(rgame.app_name) diff --git a/rare/components/tabs/library/head_bar.py b/rare/components/tabs/library/head_bar.py index 9618d12b40..6482f47dda 100644 --- a/rare/components/tabs/library/head_bar.py +++ b/rare/components/tabs/library/head_bar.py @@ -35,23 +35,23 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.filter = QComboBox(self) filters = { - LibraryFilter.ALL: self.tr("All games"), - LibraryFilter.INSTALLED: self.tr("Installed"), - LibraryFilter.OFFLINE: self.tr("Offline"), - LibraryFilter.HIDDEN: self.tr("Hidden"), - LibraryFilter.FAVORITES: self.tr("Favorites"), + LibraryFilter.ALL: self.tr('All games'), + LibraryFilter.INSTALLED: self.tr('Installed'), + LibraryFilter.OFFLINE: self.tr('Offline'), + LibraryFilter.HIDDEN: self.tr('Hidden'), + LibraryFilter.FAVORITES: self.tr('Favorites'), } for data, text in filters.items(): self.filter.addItem(text, data) if self.rcore.bit32_games: - self.filter.addItem(self.tr("Only 32bit"), LibraryFilter.WIN32) + self.filter.addItem(self.tr('Only 32bit'), LibraryFilter.WIN32) if self.rcore.mac_games: - self.filter.addItem(self.tr("Only macOS"), LibraryFilter.MAC) + self.filter.addItem(self.tr('Only macOS'), LibraryFilter.MAC) if self.rcore.non_asset_games: - self.filter.addItem(self.tr("Exclude non-asset"), LibraryFilter.INSTALLABLE) - self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE) - self.filter.addItem(self.tr("Android"), LibraryFilter.ANDROID) + self.filter.addItem(self.tr('Exclude non-asset'), LibraryFilter.INSTALLABLE) + self.filter.addItem(self.tr('Include Unreal'), LibraryFilter.INCLUDE_UE) + self.filter.addItem(self.tr('Android'), LibraryFilter.ANDROID) try: _filter = LibraryFilter(self.settings.get_value(app_settings.library_filter)) @@ -60,7 +60,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): else: self.filter.setCurrentIndex(index) except (TypeError, ValueError) as e: - self.logger.error("Error while loading library: %s", e) + self.logger.error('Error while loading library: %s', e) self.settings.set_value(app_settings.library_filter, app_settings.library_filter.default) _filter = LibraryFilter(app_settings.library_filter.default) self.filter.setCurrentIndex(self.filter.findData(_filter, Qt.ItemDataRole.UserRole)) @@ -68,10 +68,10 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.order = QComboBox(parent=self) sortings = { - LibraryOrder.TITLE: self.tr("Title"), - LibraryOrder.RECENT: self.tr("Recently played"), - LibraryOrder.NEWEST: self.tr("Newest"), - LibraryOrder.OLDEST: self.tr("Oldest"), + LibraryOrder.TITLE: self.tr('Title'), + LibraryOrder.RECENT: self.tr('Recently played'), + LibraryOrder.NEWEST: self.tr('Newest'), + LibraryOrder.OLDEST: self.tr('Oldest'), } for data, text in sortings.items(): self.order.addItem(text, data) @@ -83,35 +83,35 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): else: self.order.setCurrentIndex(index) except (TypeError, ValueError) as e: - self.logger.error("Error while loading library: %s", e) + self.logger.error('Error while loading library: %s', e) self.settings.set_value(app_settings.library_order, app_settings.library_order.default) _order = LibraryOrder(app_settings.library_order.default) self.order.setCurrentIndex(self.order.findData(_order, Qt.ItemDataRole.UserRole)) self.order.currentIndexChanged.connect(self.__order_changed) - self.search_bar = ButtonLineEdit("fa5s.search", placeholder_text=self.tr("Search (use :: to filter by tag)")) + self.search_bar = ButtonLineEdit('fa5s.search', placeholder_text=self.tr('Search (use :: to filter by tag)')) self.search_bar.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) - self.search_bar.setObjectName("SearchBar") + self.search_bar.setObjectName('SearchBar') self.search_bar.setMinimumWidth(250) - installed_tooltip = self.tr("Installed games") + installed_tooltip = self.tr('Installed games') self.installed_icon = QLabel(parent=self) - self.installed_icon.setPixmap(qta_icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16))) + self.installed_icon.setPixmap(qta_icon('ph.floppy-disk-back-fill').pixmap(QSize(16, 16))) self.installed_icon.setToolTip(installed_tooltip) self.installed_label = QLabel(parent=self) font = self.installed_label.font() font.setBold(True) self.installed_label.setFont(font) self.installed_label.setToolTip(installed_tooltip) - available_tooltip = self.tr("Available games") + available_tooltip = self.tr('Available games') self.available_icon = QLabel(parent=self) - self.available_icon.setPixmap(qta_icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16))) + self.available_icon.setPixmap(qta_icon('ph.floppy-disk-back-light').pixmap(QSize(16, 16))) self.available_icon.setToolTip(available_tooltip) self.available_label = QLabel(parent=self) self.available_label.setToolTip(available_tooltip) self.refresh_list = QPushButton(parent=self) - self.refresh_list.setIcon(qta_icon("fa.refresh", "fa5s.sync")) # Reload icon + self.refresh_list.setIcon(qta_icon('fa.refresh', 'fa5s.sync')) # Reload icon self.refresh_list.clicked.connect(self.__refresh_clicked) left_layout = QHBoxLayout() @@ -141,7 +141,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): def __game_tags_updated(self): if self.search_bar.completer(): self.search_bar.completer().deleteLater() - wordlist = tuple(map(lambda x: "::" + x, self.rcore.game_tags)) + wordlist = tuple(map(lambda x: '::' + x, self.rcore.game_tags)) completer = QCompleter(wordlist, self.search_bar) completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self.search_bar.setCompleter(completer) @@ -179,16 +179,16 @@ class SelectViewWidget(QWidget): def __init__(self, icon_view: bool, parent=None): super(SelectViewWidget, self).__init__(parent=parent) self.icon_button = QPushButton(self) - self.icon_button.setObjectName(f"{type(self).__name__}Button") + self.icon_button.setObjectName(f'{type(self).__name__}Button') self.list_button = QPushButton(self) - self.list_button.setObjectName(f"{type(self).__name__}Button") + self.list_button.setObjectName(f'{type(self).__name__}Button') if icon_view: - self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange")) - self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee")) + self.icon_button.setIcon(qta_icon('mdi.view-grid-outline', 'ei.th-large', color='orange')) + self.list_button.setIcon(qta_icon('fa5s.list', 'ei.th-list', color='#eee')) else: - self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee")) - self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange")) + self.icon_button.setIcon(qta_icon('mdi.view-grid-outline', 'ei.th-large', color='#eee')) + self.list_button.setIcon(qta_icon('fa5s.list', 'ei.th-list', color='orange')) self.icon_button.clicked.connect(self.icon) self.list_button.clicked.connect(self.list) @@ -201,11 +201,11 @@ def __init__(self, icon_view: bool, parent=None): self.setLayout(layout) def icon(self): - self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="orange")) - self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="#eee")) + self.icon_button.setIcon(qta_icon('mdi.view-grid-outline', 'ei.th-large', color='orange')) + self.list_button.setIcon(qta_icon('fa5s.list', 'ei.th-list', color='#eee')) self.toggled.emit(True) def list(self): - self.icon_button.setIcon(qta_icon("mdi.view-grid-outline", "ei.th-large", color="#eee")) - self.list_button.setIcon(qta_icon("fa5s.list", "ei.th-list", color="orange")) + self.icon_button.setIcon(qta_icon('mdi.view-grid-outline', 'ei.th-large', color='#eee')) + self.list_button.setIcon(qta_icon('fa5s.list', 'ei.th-list', color='orange')) self.toggled.emit(False) diff --git a/rare/components/tabs/library/widgets/__init__.py b/rare/components/tabs/library/widgets/__init__.py index e9d37cffa5..0af343c954 100644 --- a/rare/components/tabs/library/widgets/__init__.py +++ b/rare/components/tabs/library/widgets/__init__.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Tuple, Type, TypeVar, Union +from typing import TypeVar from PySide6.QtCore import QObject, Qt, Slot from PySide6.QtWidgets import QScrollArea, QVBoxLayout, QWidget @@ -12,7 +12,7 @@ from .icon_game_widget import IconGameWidget from .list_game_widget import ListGameWidget -ViewWidget = TypeVar("ViewWidget", IconGameWidget, ListGameWidget) +ViewWidget = TypeVar('ViewWidget', IconGameWidget, ListGameWidget) class ViewContainer(QWidget): @@ -20,29 +20,29 @@ def __init__(self, rcore: RareCore, parent=None): super().__init__(parent=parent) self.rcore: RareCore = rcore - def _add_widget(self, widget_type: Type[ViewWidget], rgame: RareGame) -> ViewWidget: + def _add_widget(self, widget_type: type[ViewWidget], rgame: RareGame) -> ViewWidget: widget = widget_type(rgame, self) self.layout().addWidget(widget) return widget __is_visible = { - LibraryFilter.HIDDEN: lambda x: "hidden" in x.metadata.tags, - LibraryFilter.FAVORITES: lambda x: "favorite" in x.metadata.tags, - LibraryFilter.INSTALLED: lambda x: x.is_installed and not x.is_unreal and "hidden" not in x.metadata.tags, - LibraryFilter.OFFLINE: lambda x: x.can_run_offline and not x.is_unreal and "hidden" not in x.metadata.tags, - LibraryFilter.WIN32: lambda x: x.is_win32 and not x.is_unreal and "hidden" not in x.metadata.tags, - LibraryFilter.MAC: lambda x: x.is_mac and not x.is_unreal and "hidden" not in x.metadata.tags, - LibraryFilter.INSTALLABLE: lambda x: not x.is_non_asset and not x.is_unreal and "hidden" not in x.metadata.tags, - LibraryFilter.INCLUDE_UE: lambda x: not x.is_android_only and "hidden" not in x.metadata.tags, + LibraryFilter.HIDDEN: lambda x: 'hidden' in x.metadata.tags, + LibraryFilter.FAVORITES: lambda x: 'favorite' in x.metadata.tags, + LibraryFilter.INSTALLED: lambda x: x.is_installed and not x.is_unreal and 'hidden' not in x.metadata.tags, + LibraryFilter.OFFLINE: lambda x: x.can_run_offline and not x.is_unreal and 'hidden' not in x.metadata.tags, + LibraryFilter.WIN32: lambda x: x.is_win32 and not x.is_unreal and 'hidden' not in x.metadata.tags, + LibraryFilter.MAC: lambda x: x.is_mac and not x.is_unreal and 'hidden' not in x.metadata.tags, + LibraryFilter.INSTALLABLE: lambda x: not x.is_non_asset and not x.is_unreal and 'hidden' not in x.metadata.tags, + LibraryFilter.INCLUDE_UE: lambda x: not x.is_android_only and 'hidden' not in x.metadata.tags, LibraryFilter.ANDROID: lambda x: x.is_android_only, - LibraryFilter.ALL: lambda x: not x.is_unreal and not x.is_android_only and "hidden" not in x.metadata.tags, + LibraryFilter.ALL: lambda x: not x.is_unreal and not x.is_android_only and 'hidden' not in x.metadata.tags, } - def __visibility(self, widget: ViewWidget, library_filter, search_text) -> Tuple[bool, float]: + def __visibility(self, widget: ViewWidget, library_filter, search_text) -> tuple[bool, float]: name_search = True tag_search = False - if search_text.startswith("::"): - search_text = search_text.removeprefix("::") + if search_text.startswith('::'): + search_text = search_text.removeprefix('::') tag_search = True name_search = False visible = True @@ -64,9 +64,9 @@ def __visibility(self, widget: ViewWidget, library_filter, search_text) -> Tuple def _filter_view( self, - widget_type: Type[ViewWidget], + widget_type: type[ViewWidget], filter_by: LibraryFilter = LibraryFilter.ALL, - search_text: str = "", + search_text: str = '', ): widgets = self.findChildren(widget_type) for iw in widgets: @@ -74,7 +74,7 @@ def _filter_view( iw.setOpacity(opacity) iw.setVisible(visibility) - def _update_view(self, widget_type: Type[ViewWidget]): + def _update_view(self, widget_type: type[ViewWidget]): widgets = self.findChildren(widget_type) app_names = {iw.rgame.app_name for iw in widgets} games = list(self.rcore.games) @@ -85,7 +85,7 @@ def _update_view(self, widget_type: Type[ViewWidget]): w = widget_type(game, self) self.layout().addWidget(w) - def _find_widget(self, widget_type: Type[ViewWidget], app_name: str) -> ViewWidget: + def _find_widget(self, widget_type: type[ViewWidget], app_name: str) -> ViewWidget: w = self.findChild(widget_type, app_name) return w @@ -116,13 +116,13 @@ def update_view(self): def find_widget(self, app_name: str) -> ViewWidget: return self._find_widget(IconGameWidget, app_name) - def filter_view(self, filter_by: LibraryFilter, search_text: str = ""): + def filter_view(self, filter_by: LibraryFilter, search_text: str = ''): self._filter_view(IconGameWidget, filter_by, search_text) - def order_view(self, order_by: LibraryOrder, search_text: str = ""): + def order_view(self, order_by: LibraryOrder, search_text: str = ''): if search_text: - if search_text.startswith("::"): - self.layout().sort(lambda x: search_text.removeprefix("::") not in x.widget().rgame.metadata.tags) + if search_text.startswith('::'): + self.layout().sort(lambda x: search_text.removeprefix('::') not in x.widget().rgame.metadata.tags) else: self.layout().sort( lambda x: search_text not in x.widget().rgame.app_title.lower() @@ -177,14 +177,14 @@ def update_view(self): def find_widget(self, app_name: str) -> ViewWidget: return self._find_widget(ListGameWidget, app_name) - def filter_view(self, filter_by: LibraryFilter, search_text: str = ""): + def filter_view(self, filter_by: LibraryFilter, search_text: str = ''): self._filter_view(ListGameWidget, filter_by, search_text) - def order_view(self, order_by: LibraryOrder, search_text: str = ""): + def order_view(self, order_by: LibraryOrder, search_text: str = ''): list_widgets = self.findChildren(ListGameWidget) if search_text: - if search_text.startswith("::"): - list_widgets.sort(key=lambda x: search_text.removeprefix("::") not in x.rgame.metadata.tags) + if search_text.startswith('::'): + list_widgets.sort(key=lambda x: search_text.removeprefix('::') not in x.rgame.metadata.tags) else: list_widgets.sort( key=lambda x: search_text not in x.rgame.app_title.lower() and search_text not in x.rgame.app_name.lower() @@ -246,13 +246,13 @@ def add_game(self, rgame: RareGame): def add_widget(self, rgame: RareGame) -> ViewWidget: return self._container.add_widget(rgame) - def filter_game_view(self, filter_by: LibraryFilter = None, search_text: str = ""): + def filter_game_view(self, filter_by: LibraryFilter = None, search_text: str = ''): self._current_filter = filter_by if filter_by is not None else self._current_filter self._container.filter_view(self._current_filter, search_text) self.order_game_view(self._current_order, search_text=search_text) @Slot() - def order_game_view(self, order_by: LibraryOrder = None, search_text: str = ""): + def order_game_view(self, order_by: LibraryOrder = None, search_text: str = ''): self._current_order = order_by if order_by is not None else self._current_order self._container.order_view(self._current_order, search_text) @@ -265,5 +265,5 @@ def refresh_game_view(self): self._container.update_view() self.order_game_view(self._current_order) - def __find_widget(self, app_name: str) -> Union[ViewWidget, None]: + def __find_widget(self, app_name: str) -> ViewWidget | None: return self._container.find_widget(app_name) diff --git a/rare/components/tabs/library/widgets/game_widget.py b/rare/components/tabs/library/widgets/game_widget.py index 36e23e5d2e..995831470a 100644 --- a/rare/components/tabs/library/widgets/game_widget.py +++ b/rare/components/tabs/library/widgets/game_widget.py @@ -25,7 +25,7 @@ from .library_widget import LibraryWidget -logger = getLogger("GameWidget") +logger = getLogger('GameWidget') class GameWidget(LibraryWidget): @@ -38,10 +38,10 @@ def __init__(self, rgame: RareGame, parent=None): self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) - self.launch_action = QAction(self.tr("Launch"), self) + self.launch_action = QAction(self.tr('Launch'), self) self.launch_action.triggered.connect(self._launch) - self.install_action = QAction(self.tr("Install"), self) + self.install_action = QAction(self.tr('Install'), self) self.install_action.triggered.connect(self._install) self.desktop_link_action = QAction(self) @@ -53,10 +53,10 @@ def __init__(self, rgame: RareGame, parent=None): self.steam_shortcut_action = QAction(self) self.steam_shortcut_action.triggered.connect(self._create_steam_shortcut) - self.reload_action = QAction(self.tr("Reload Image"), self) + self.reload_action = QAction(self.tr('Reload Image'), self) self.reload_action.triggered.connect(self._on_reload_image) - self.uninstall_action = QAction(self.tr("Uninstall"), self) + self.uninstall_action = QAction(self.tr('Uninstall'), self) self.uninstall_action.triggered.connect(self._uninstall) self.update_actions() @@ -73,27 +73,27 @@ def __init__(self, rgame: RareGame, parent=None): self.rgame.signals.progress.finish.connect(self.hideProgress) self.state_strings = { - RareGame.State.IDLE: "", - RareGame.State.RUNNING: self.tr("Running..."), - RareGame.State.DOWNLOADING: self.tr("Downloading..."), - RareGame.State.VERIFYING: self.tr("Verifying..."), - RareGame.State.MOVING: self.tr("Moving..."), - RareGame.State.UNINSTALLING: self.tr("Uninstalling..."), - RareGame.State.SYNCING: self.tr("Syncing saves..."), - "has_update": self.tr("Update available"), - "needs_verification": self.tr("Needs verification"), - "not_can_launch": self.tr("Can't launch"), - "save_not_up_to_date": self.tr("Save is not up-to-date"), + RareGame.State.IDLE: '', + RareGame.State.RUNNING: self.tr('Running...'), + RareGame.State.DOWNLOADING: self.tr('Downloading...'), + RareGame.State.VERIFYING: self.tr('Verifying...'), + RareGame.State.MOVING: self.tr('Moving...'), + RareGame.State.UNINSTALLING: self.tr('Uninstalling...'), + RareGame.State.SYNCING: self.tr('Syncing saves...'), + 'has_update': self.tr('Update available'), + 'needs_verification': self.tr('Needs verification'), + 'not_can_launch': self.tr("Can't launch"), + 'save_not_up_to_date': self.tr('Save is not up-to-date'), } self.hover_strings = { - "info": self.tr("Show information"), - "install": self.tr("Install game"), - "can_launch": self.tr("Launch game"), - "is_foreign": self.tr("Launch offline"), - "has_update": self.tr("Launch without version check"), - "is_origin": self.tr("Launch/Link"), - "not_can_launch": self.tr("Can't launch"), + 'info': self.tr('Show information'), + 'install': self.tr('Install game'), + 'can_launch': self.tr('Launch game'), + 'is_foreign': self.tr('Launch offline'), + 'has_update': self.tr('Launch without version check'), + 'is_origin': self.tr('Launch/Link'), + 'not_can_launch': self.tr("Can't launch"), } self._ui = None @@ -138,17 +138,17 @@ def showEvent(self, a0: QShowEvent) -> None: def update_state(self): if self.rgame.is_idle: if self.rgame.has_update: - self.ui.status_label.setText(self.state_strings["has_update"]) + self.ui.status_label.setText(self.state_strings['has_update']) elif self.rgame.needs_verification: - self.ui.status_label.setText(self.state_strings["needs_verification"]) + self.ui.status_label.setText(self.state_strings['needs_verification']) elif not self.rgame.can_launch and self.rgame.is_installed: - self.ui.status_label.setText(self.state_strings["not_can_launch"]) + self.ui.status_label.setText(self.state_strings['not_can_launch']) elif ( self.rgame.igame and (self.rgame.game.supports_cloud_saves or self.rgame.game.supports_mac_cloud_saves) and not self.rgame.is_save_up_to_date ): - self.ui.status_label.setText(self.state_strings["save_not_up_to_date"]) + self.ui.status_label.setText(self.state_strings['save_not_up_to_date']) else: self.ui.status_label.setText(self.state_strings[self.rgame.state]) else: @@ -175,22 +175,22 @@ def update_actions(self): self.addAction(self.install_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")) + if desktop_link_path(self.rgame.folder_name, 'desktop').exists(): + self.desktop_link_action.setText(self.tr('Remove Desktop link')) else: - self.desktop_link_action.setText(self.tr("Create Desktop link")) + self.desktop_link_action.setText(self.tr('Create Desktop link')) self.addAction(self.desktop_link_action) - if desktop_link_path(self.rgame.folder_name, "start_menu").exists(): - self.menu_link_action.setText(self.tr("Remove Start Menu link")) + if desktop_link_path(self.rgame.folder_name, 'start_menu').exists(): + self.menu_link_action.setText(self.tr('Remove Start Menu link')) else: - self.menu_link_action.setText(self.tr("Create Start Menu link")) + self.menu_link_action.setText(self.tr('Create Start Menu link')) self.addAction(self.menu_link_action) if steam_shortcuts_supported() and self.rgame.is_installed: if steam_shortcut_exists(self.rgame.app_name): - self.steam_shortcut_action.setText(self.tr("Remove Steam shortcut")) + self.steam_shortcut_action.setText(self.tr('Remove Steam shortcut')) else: - self.steam_shortcut_action.setText(self.tr("Create Steam shortcut")) + self.steam_shortcut_action.setText(self.tr('Create Steam shortcut')) self.addAction(self.steam_shortcut_action) self.addAction(self.reload_action) @@ -205,7 +205,7 @@ def eventFilter(self, a0: QObject, a1: QEvent) -> bool: # \ # is not a QEvent object logger.error( - "Supplied arg1 %s with target %s is not a QEvent object", + 'Supplied arg1 %s with target %s is not a QEvent object', type(a1), type(a0), ) @@ -213,29 +213,29 @@ def eventFilter(self, a0: QObject, a1: QEvent) -> bool: if a0 is self.ui.launch_btn: if a1.type() == QEvent.Type.Enter: if not self.rgame.can_launch: - self.ui.tooltip_label.setText(self.hover_strings["not_can_launch"]) + self.ui.tooltip_label.setText(self.hover_strings['not_can_launch']) elif self.rgame.is_origin: - self.ui.tooltip_label.setText(self.hover_strings["is_origin"]) + self.ui.tooltip_label.setText(self.hover_strings['is_origin']) elif self.rgame.has_update: - self.ui.tooltip_label.setText(self.hover_strings["has_update"]) + self.ui.tooltip_label.setText(self.hover_strings['has_update']) elif self.rgame.is_foreign and self.rgame.can_run_offline: - self.ui.tooltip_label.setText(self.hover_strings["is_foreign"]) + self.ui.tooltip_label.setText(self.hover_strings['is_foreign']) elif self.rgame.can_launch: - self.ui.tooltip_label.setText(self.hover_strings["can_launch"]) + self.ui.tooltip_label.setText(self.hover_strings['can_launch']) return True if a1.type() == QEvent.Type.Leave: - self.ui.tooltip_label.setText(self.hover_strings["info"]) + self.ui.tooltip_label.setText(self.hover_strings['info']) # return True if a0 is self.ui.install_btn: if a1.type() == QEvent.Type.Enter: - self.ui.tooltip_label.setText(self.hover_strings["install"]) + self.ui.tooltip_label.setText(self.hover_strings['install']) return True if a1.type() == QEvent.Type.Leave: - self.ui.tooltip_label.setText(self.hover_strings["info"]) + self.ui.tooltip_label.setText(self.hover_strings['info']) # return True if a0 is self: if a1.type() == QEvent.Type.Enter: - self.ui.tooltip_label.setText(self.hover_strings["info"]) + self.ui.tooltip_label.setText(self.hover_strings['info']) return super(GameWidget, self).eventFilter(a0, a1) def mousePressEvent(self, e: QMouseEvent) -> None: @@ -270,19 +270,19 @@ def _uninstall(self): @Slot() def _create_link_desktop(self): - self._create_link(self.rgame.folder_name, "desktop") + self._create_link(self.rgame.folder_name, 'desktop') @Slot() def _create_link_start_menu(self): - self._create_link(self.rgame.folder_name, "start_menu") + self._create_link(self.rgame.folder_name, 'start_menu') @Slot(str, str) def _create_link(self, name: str, link_type: str): if not desktop_links_supported(): QMessageBox.warning( self, - self.tr("Warning"), - self.tr("Creating shortcuts is currently unsupported on {}").format(platform.system()), + self.tr('Warning'), + self.tr('Creating shortcuts is currently unsupported on {}').format(platform.system()), ) return @@ -298,7 +298,7 @@ def _create_link(self, name: str, link_type: str): ): raise PermissionError except PermissionError: - QMessageBox.warning(self, "Error", "Could not create shortcut.") + QMessageBox.warning(self, 'Error', 'Could not create shortcut.') return else: if shortcut_path.exists(): diff --git a/rare/components/tabs/library/widgets/icon_game_widget.py b/rare/components/tabs/library/widgets/icon_game_widget.py index 8330f34b84..a93c2a7355 100644 --- a/rare/components/tabs/library/widgets/icon_game_widget.py +++ b/rare/components/tabs/library/widgets/icon_game_widget.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import Optional from PySide6.QtCore import QEvent, Slot @@ -9,13 +8,13 @@ from .game_widget import GameWidget from .icon_widget import IconWidget -logger = getLogger("IconGameWidget") +logger = getLogger('IconGameWidget') class IconGameWidget(GameWidget): def __init__(self, rgame: RareGame, parent=None): super().__init__(rgame, parent) - self.setObjectName(f"{rgame.app_name}") + self.setObjectName(f'{rgame.app_name}') self.setFixedSize(ImageSize.LibraryTall) self.ui = IconWidget() self.ui.setupUi(self) @@ -46,12 +45,12 @@ def start_progress(self): self.rgame.get_pixmap(ImageSize.LibraryTall, False), ) - def enterEvent(self, a0: Optional[QEvent] = None) -> None: + def enterEvent(self, a0: QEvent | None = None) -> None: if a0 is not None: a0.accept() self.ui.enterAnimation(self) - def leaveEvent(self, a0: Optional[QEvent] = None) -> None: + def leaveEvent(self, a0: QEvent | None = None) -> None: if a0 is not None: a0.accept() self.ui.leaveAnimation(self) diff --git a/rare/components/tabs/library/widgets/icon_widget.py b/rare/components/tabs/library/widgets/icon_widget.py index 103c3a92af..19c5766149 100644 --- a/rare/components/tabs/library/widgets/icon_widget.py +++ b/rare/components/tabs/library/widgets/icon_widget.py @@ -14,7 +14,7 @@ from rare.widgets.elide_label import ElideLabel -class IconWidget(object): +class IconWidget: def __init__(self): self._effect = None self._animation: QPropertyAnimation = None @@ -30,14 +30,14 @@ def __init__(self): def setupUi(self, widget: QWidget): # information at top self.status_label = ElideLabel(parent=widget) - self.status_label.setObjectName(f"{type(self).__name__}StatusLabel") + self.status_label.setObjectName(f'{type(self).__name__}StatusLabel') self.status_label.setFixedHeight(False) self.status_label.setContentsMargins(6, 6, 6, 6) self.status_label.setAutoFillBackground(False) # on-hover popup self.mini_widget = QWidget(parent=widget) - self.mini_widget.setObjectName(f"{type(self).__name__}MiniWidget") + self.mini_widget.setObjectName(f'{type(self).__name__}MiniWidget') self.mini_widget.setFixedHeight(widget.height() // 3) self.mini_effect = QGraphicsOpacityEffect(self.mini_widget) @@ -45,7 +45,7 @@ def setupUi(self, widget: QWidget): # game title self.title_label = QLabel(parent=self.mini_widget) - self.title_label.setObjectName(f"{type(self).__name__}TitleLabel") + self.title_label.setObjectName(f'{type(self).__name__}TitleLabel') self.title_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.title_label.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.title_label.setAutoFillBackground(False) @@ -53,19 +53,19 @@ def setupUi(self, widget: QWidget): # information below title self.tooltip_label = ElideLabel(parent=self.mini_widget) - self.tooltip_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.tooltip_label.setObjectName(f'{type(self).__name__}TooltipLabel') self.tooltip_label.setAutoFillBackground(False) # play button self.launch_btn = QPushButton(parent=self.mini_widget) - self.launch_btn.setObjectName(f"{type(self).__name__}Button") - self.launch_btn.setIcon(qta_icon("ei.play-alt", color="white")) + self.launch_btn.setObjectName(f'{type(self).__name__}Button') + self.launch_btn.setIcon(qta_icon('ei.play-alt', color='white')) self.launch_btn.setIconSize(QSize(20, 20)) self.launch_btn.setFixedSize(QSize(widget.width() // 4, widget.width() // 4)) self.install_btn = QPushButton(parent=self.mini_widget) - self.install_btn.setObjectName(f"{type(self).__name__}Button") - self.install_btn.setIcon(qta_icon("ri.install-fill", color="white")) + self.install_btn.setObjectName(f'{type(self).__name__}Button') + self.install_btn.setIcon(qta_icon('ri.install-fill', color='white')) self.install_btn.setIconSize(QSize(20, 20)) self.install_btn.setFixedSize(QSize(widget.width() // 4, widget.width() // 4)) @@ -113,7 +113,7 @@ def translateUi(self, widget: QWidget): pass def enterAnimation(self, widget: QWidget): - self._animation = QPropertyAnimation(self.mini_effect, b"opacity") + self._animation = QPropertyAnimation(self.mini_effect, b'opacity') self._animation.setDuration(250) self._animation.setStartValue(0) self._animation.setEndValue(1) @@ -121,7 +121,7 @@ def enterAnimation(self, widget: QWidget): self._animation.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped) def leaveAnimation(self, widget: QWidget): - self._animation = QPropertyAnimation(self.mini_effect, b"opacity") + self._animation = QPropertyAnimation(self.mini_effect, b'opacity') self._animation.setDuration(150) self._animation.setStartValue(1) self._animation.setEndValue(0) diff --git a/rare/components/tabs/library/widgets/library_widget.py b/rare/components/tabs/library/widgets/library_widget.py index f1a43ff34a..2229ba8015 100644 --- a/rare/components/tabs/library/widgets/library_widget.py +++ b/rare/components/tabs/library/widgets/library_widget.py @@ -1,5 +1,3 @@ -from typing import List, Optional, Tuple - from PySide6.QtCore import QEvent, QObject, Qt, Slot from PySide6.QtGui import ( QBrush, @@ -26,7 +24,7 @@ def __init__(self, parent=None): def __center_on_parent(self): fm = QFontMetrics(self.font()) - rect = fm.boundingRect(" 100% ") + rect = fm.boundingRect(' 100% ') rect.moveCenter(self.parent().contentsRect().center()) self.setGeometry(rect) @@ -52,15 +50,15 @@ def eventFilter(self, a0: QObject, a1: QEvent) -> bool: return False @staticmethod - def calculateColors(image: QImage) -> Tuple[QColor, QColor]: - color: List[int] = [0, 0, 0] + def calculateColors(image: QImage) -> tuple[QColor, QColor]: + color: list[int] = [0, 0, 0] # take the two diagonals of the center square section min_d = min(image.width(), image.height()) origin_w = (image.width() - min_d) // 2 origin_h = (image.height() - min_d) // 2 - for x, y in zip(range(origin_w, min_d), range(origin_h, min_d)): + for x, y in zip(range(origin_w, min_d), range(origin_h, min_d), strict=False): pixel = image.pixelColor(x, y).getRgb() - color = list(map(lambda t: sum(t) // 2, zip(pixel[:3], color))) + color = list(map(lambda t: sum(t) // 2, zip(pixel[:3], color, strict=False))) # take the V component of the HSV color fg_color = QColor(0, 0, 0) if QColor(*color).value() < 127 else QColor(255, 255, 255) bg_color = QColor(*map(lambda c: 255 - c, color)) @@ -68,11 +66,11 @@ def calculateColors(image: QImage) -> Tuple[QColor, QColor]: def setStyleSheetColors(self, bg: QColor, fg: QColor, brd: QColor): sheet = ( - f"QLabel#{type(self).__name__} {{" - f"background-color: rgba({bg.red()}, {bg.green()}, {bg.blue()}, 65%);" - f"color: rgb({fg.red()}, {fg.green()}, {fg.blue()});" - f"border-color: rgb({brd.red()}, {brd.green()}, {brd.blue()});" - f"}}" + f'QLabel#{type(self).__name__} {{' + f'background-color: rgba({bg.red()}, {bg.green()}, {bg.blue()}, 65%);' + f'color: rgb({fg.red()}, {fg.green()}, {fg.blue()});' + f'border-color: rgb({brd.red()}, {brd.green()}, {brd.blue()});' + f'}}' ) self.setStyleSheet(sheet) @@ -84,8 +82,8 @@ def __init__(self, parent=None) -> None: self.progress_label.setVisible(False) self.progressPixmap = self.horizontalProgressPixmap - self._color_pixmap: Optional[QPixmap] = None - self._gray_pixmap: Optional[QPixmap] = None + self._color_pixmap: QPixmap | None = None + self._gray_pixmap: QPixmap | None = None # lk: keep percentage to not over-generate the image self._progress: int = -1 @@ -151,7 +149,7 @@ def showProgress(self, color_pm: QPixmap, gray_pm: QPixmap) -> None: @Slot(int) def updateProgress(self, progress: int): - self.progress_label.setText(f"{progress:02}%") + self.progress_label.setText(f'{progress:02}%') if progress > self._progress: self._progress = progress self.setPixmap(self.progressPixmap(self._color_pixmap, self._gray_pixmap, progress)) diff --git a/rare/components/tabs/library/widgets/list_game_widget.py b/rare/components/tabs/library/widgets/list_game_widget.py index 39d08dcbcc..8a197d76b9 100644 --- a/rare/components/tabs/library/widgets/list_game_widget.py +++ b/rare/components/tabs/library/widgets/list_game_widget.py @@ -18,7 +18,7 @@ from .game_widget import GameWidget from .list_widget import ListWidget -logger = getLogger("ListGameWidget") +logger = getLogger('ListGameWidget') class ListGameWidget(GameWidget): @@ -26,7 +26,7 @@ def __init__(self, rgame: RareGame, parent=None): super().__init__(rgame, parent) self.progressPixmap = self.verticalProgressPixmap - self.setObjectName(f"{rgame.app_name}") + self.setObjectName(f'{rgame.app_name}') self.ui = ListWidget() self.ui.setupUi(self) @@ -38,12 +38,12 @@ def __init__(self, rgame: RareGame, parent=None): self.ui.launch_btn.setEnabled(self.rgame.can_launch) - self.ui.launch_btn.setText(self.tr("Launch") if not self.rgame.is_origin else self.tr("Link/Play")) + self.ui.launch_btn.setText(self.tr('Launch') if not self.rgame.is_origin else self.tr('Link/Play')) self.ui.developer_label.setText(self.rgame.developer) # self.version_label.setVisible(self.is_installed) if self.rgame.igame: self.ui.version_label.setText(self.rgame.version) - self.ui.size_label.setText(format_size(self.rgame.install_size) if self.rgame.install_size else "") + self.ui.size_label.setText(format_size(self.rgame.install_size) if self.rgame.install_size else '') self.update_state() diff --git a/rare/components/tabs/library/widgets/list_widget.py b/rare/components/tabs/library/widgets/list_widget.py index 337f964607..4034b3331b 100644 --- a/rare/components/tabs/library/widgets/list_widget.py +++ b/rare/components/tabs/library/widgets/list_widget.py @@ -13,7 +13,7 @@ from rare.widgets.elide_label import ElideLabel -class ListWidget(object): +class ListWidget: def __init__(self): self.title_label = None self.status_label = None @@ -27,26 +27,26 @@ def __init__(self): def setupUi(self, widget: QWidget): self.title_label = ElideLabel(parent=widget) - self.title_label.setObjectName(f"{type(self).__name__}TitleLabel") + self.title_label.setObjectName(f'{type(self).__name__}TitleLabel') self.title_label.setWordWrap(False) self.status_label = QLabel(parent=widget) - self.status_label.setObjectName(f"{type(self).__name__}StatusLabel") + self.status_label.setObjectName(f'{type(self).__name__}StatusLabel') self.status_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.tooltip_label = QLabel(parent=widget) - self.tooltip_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.tooltip_label.setObjectName(f'{type(self).__name__}TooltipLabel') self.tooltip_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.install_btn = QPushButton(parent=widget) - self.install_btn.setObjectName(f"{type(self).__name__}Button") - self.install_btn.setIcon(qta_icon("ri.install-line")) + self.install_btn.setObjectName(f'{type(self).__name__}Button') + self.install_btn.setIcon(qta_icon('ri.install-line')) self.install_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.install_btn.setFixedWidth(120) self.launch_btn = QPushButton(parent=widget) - self.launch_btn.setObjectName(f"{type(self).__name__}Button") - self.launch_btn.setIcon(qta_icon("ei.play-alt")) + self.launch_btn.setObjectName(f'{type(self).__name__}Button') + self.launch_btn.setIcon(qta_icon('ei.play-alt')) self.launch_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.launch_btn.setFixedWidth(120) @@ -58,15 +58,15 @@ def setupUi(self, widget: QWidget): self.install_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.developer_label = ElideLabel(parent=widget) - self.developer_label.setObjectName(f"{type(self).__name__}InfoLabel") + self.developer_label.setObjectName(f'{type(self).__name__}InfoLabel') self.developer_label.setFixedWidth(120) self.version_label = ElideLabel(parent=widget) - self.version_label.setObjectName(f"{type(self).__name__}InfoLabel") + self.version_label.setObjectName(f'{type(self).__name__}InfoLabel') self.version_label.setFixedWidth(120) self.size_label = ElideLabel(parent=widget) - self.size_label.setObjectName(f"{type(self).__name__}InfoLabel") + self.size_label.setObjectName(f'{type(self).__name__}InfoLabel') self.size_label.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) self.size_label.setFixedWidth(60) @@ -110,4 +110,4 @@ def setupUi(self, widget: QWidget): self.translateUi(widget) def translateUi(self, widget: QWidget): - self.install_btn.setText(widget.tr("Install")) + self.install_btn.setText(widget.tr('Install')) diff --git a/rare/components/tabs/settings/__init__.py b/rare/components/tabs/settings/__init__.py index 3bb248221f..299764c5c6 100644 --- a/rare/components/tabs/settings/__init__.py +++ b/rare/components/tabs/settings/__init__.py @@ -9,6 +9,7 @@ from .about import About from .compat import GlobalCompatSettings from .debug import DebugSettings +from .environ import GlobalEnvironSettings from .game import GlobalGameSettings from .legendary import LegendarySettings from .rare import RareSettings @@ -21,30 +22,33 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): super(SettingsTab, self).__init__(parent=parent) rare_settings = RareSettings(settings, rcore, self) - self.rare_index = self.addTab(rare_settings, "Rare") + self.rare_index = self.addTab(rare_settings, 'Rare') legendary_settings = LegendarySettings(settings, rcore, self) - self.legendary_index = self.addTab(legendary_settings, "Legendary") + self.legendary_index = self.addTab(legendary_settings, 'Legendary') game_settings = GlobalGameSettings(settings, rcore, self) - self.game_index = self.addTab(game_settings, self.tr("Defaults")) + self.game_index = self.addTab(game_settings, self.tr('Defaults')) - if pf.system() != "Windows": + if pf.system() != 'Windows': compat_settings = GlobalCompatSettings(settings, rcore, self) - self.compat_index = self.addTab(compat_settings, self.tr("Compatibility")) + self.compat_index = self.addTab(compat_settings, self.tr('Compatibility')) + + environ_settings = GlobalEnvironSettings(rcore, self) + self.environ_index = self.addTab(environ_settings, self.tr('Environment')) self.about = About(self) - title = self.tr("About") + title = self.tr('About') self.about_index = self.addTab(self.about, title, title) self.about.update_available.connect(self._on_update_available) self.about.update_available.connect(self.update_available) if rcore.args().debug: - title = self.tr("Debug") + title = self.tr('Debug') self.debug_index = self.addTab(DebugSettings(rcore.signals(), self), title, title) self.setCurrentIndex(self.rare_index) @Slot() def _on_update_available(self): - self.tabBar().setTabText(self.about_index, "About (!)") + self.tabBar().setTabText(self.about_index, 'About (!)') diff --git a/rare/components/tabs/settings/about.py b/rare/components/tabs/settings/about.py index 6dc16fe18a..5c316ded32 100644 --- a/rare/components/tabs/settings/about.py +++ b/rare/components/tabs/settings/about.py @@ -1,6 +1,5 @@ import webbrowser from logging import getLogger -from typing import Tuple from PySide6.QtCore import Signal, Slot from PySide6.QtGui import QShowEvent @@ -8,16 +7,16 @@ from rare import __codename__, __version__ from rare.ui.components.tabs.settings.about import Ui_About -from rare.utils.qt_requests import QtRequests +from rare.utils.qrequests import QRequests -logger = getLogger("About") +logger = getLogger('About') -def versiontuple(v) -> Tuple[int, ...]: +def versiontuple(v) -> tuple[int, ...]: try: - return tuple(map(int, (v.split(".")))) + return tuple(map(int, (v.split('.')))) except Exception as e: - logger.error("Error while parsing version %s", v) + logger.error('Error while parsing version %s', v) logger.error(e) return 99, 99, 99, 999 @@ -27,19 +26,20 @@ class About(QWidget): def __init__(self, parent=None): super(About, self).__init__(parent=parent) + self.ui = Ui_About() self.ui.setupUi(self) - self.ui.version.setText(f"{__version__} {__codename__}") + self.ui.version.setText(f'{__version__} {__codename__}') self.ui.update_label.setEnabled(False) self.ui.update_field.setEnabled(False) self.ui.open_browser.setVisible(False) self.ui.open_browser.setEnabled(False) - self.releases_url = "https://api.github.com/repos/RareDevs/Rare/releases/latest" + self.releases_url = 'https://api.github.com/repos/RareDevs/Rare/releases/latest' - self.manager = QtRequests(parent=self) + self.manager = QRequests(parent=self) self.manager.get(self.releases_url, self._on_update_check_finished) self.ui.open_browser.clicked.connect(self._on_browser_clicked) @@ -54,21 +54,21 @@ def showEvent(self, a0: QShowEvent) -> None: @Slot() def _on_browser_clicked(self): - webbrowser.open("https://github.com/RareDevs/Rare/releases/latest") + webbrowser.open('https://github.com/RareDevs/Rare/releases/latest') @Slot(dict) def _on_update_check_finished(self, data: dict): - if latest_tag := data.get("tag_name"): + if latest_tag := data.get('tag_name'): self._update_available = versiontuple(latest_tag) > versiontuple(__version__) else: self._update_available = False if self._update_available: - logger.info(f"Update available: {__version__} -> {latest_tag}") - self.ui.update_field.setText(f"{__version__} -> {latest_tag}") + logger.info(f'Update available: {__version__} -> {latest_tag}') + self.ui.update_field.setText(f'{__version__} -> {latest_tag}') self.update_available.emit() else: - self.ui.update_field.setText(self.tr("You have the latest version")) + self.ui.update_field.setText(self.tr('You have the latest version')) self.ui.update_label.setEnabled(self._update_available) self.ui.update_field.setEnabled(self._update_available) self.ui.open_browser.setVisible(self._update_available) diff --git a/rare/components/tabs/settings/compat.py b/rare/components/tabs/settings/compat.py index 3566fd8a95..f2e69a3515 100644 --- a/rare/components/tabs/settings/compat.py +++ b/rare/components/tabs/settings/compat.py @@ -1,6 +1,4 @@ import platform as pf -from logging import getLogger -from typing import Type from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QHideEvent @@ -15,12 +13,10 @@ from .widgets.runner import RunnerSettingsBase, RunnerSettingsType from .widgets.wine import WineSettings -if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() in {'Linux', 'FreeBSD'}: from .widgets.overlay import MangoHudSettings from .widgets.proton import ProtonSettings -logger = getLogger("GlobalCompatSettings") - class CompatSettingsBase(QWidget, SideTabContents): # str: option key @@ -32,18 +28,18 @@ def __init__( self, settings: RareAppSettings, rcore: RareCore, - dxvk_hud_widget: Type[DxvkHudSettings], - dxvk_config_widget: Type[DxvkConfigSettings], - dxvk_nvapi_drs_widget: Type[DxvkNvapiDrsSettings], - runner_widget: Type["RunnerSettingsType"], - mangohud_widget: Type["MangoHudSettings"] = None, + dxvk_hud_widget: type[DxvkHudSettings], + dxvk_config_widget: type[DxvkConfigSettings], + dxvk_nvapi_drs_widget: type[DxvkNvapiDrsSettings], + runner_widget: type['RunnerSettingsType'], + mangohud_widget: type['MangoHudSettings'] = None, parent=None, ): super(CompatSettingsBase, self).__init__(parent=parent) self.settings = settings self.core = rcore.core() - self.app_name: str = "default" + self.app_name: str = 'default' self.runner = runner_widget(settings, rcore, self) self.runner.environ_changed.connect(self.environ_changed) @@ -81,7 +77,7 @@ def hideEvent(self, a0: QHideEvent): class GlobalRunnerSettings(RunnerSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - if pf.system() in {"Linux", "FreeBSD"}: + if pf.system() in {'Linux', 'FreeBSD'}: super(GlobalRunnerSettings, self).__init__(settings, rcore, WineSettings, ProtonSettings, parent=parent) else: super(GlobalRunnerSettings, self).__init__(settings, rcore, WineSettings, parent=parent) @@ -89,7 +85,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): class GlobalCompatSettings(CompatSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - if pf.system() in {"Linux", "FreeBSD"}: + if pf.system() in {'Linux', 'FreeBSD'}: super(GlobalCompatSettings, self).__init__( settings, rcore, diff --git a/rare/components/tabs/settings/debug.py b/rare/components/tabs/settings/debug.py index 08b02856e0..1c5f508dcb 100644 --- a/rare/components/tabs/settings/debug.py +++ b/rare/components/tabs/settings/debug.py @@ -10,11 +10,11 @@ def __init__(self, signals: GlobalSignals, parent=None): super(DebugSettings, self).__init__(parent=parent) self.signals = signals - self.raise_runtime_exception_button = QPushButton("Raise Exception", self) + self.raise_runtime_exception_button = QPushButton('Raise Exception', self) self.raise_runtime_exception_button.clicked.connect(self.raise_exception) - self.restart_button = QPushButton("Restart", self) + self.restart_button = QPushButton('Restart', self) self.restart_button.clicked.connect(self._on_restart_clicked) - self.send_notification_button = QPushButton("Notify", self) + self.send_notification_button = QPushButton('Notify', self) self.send_notification_button.clicked.connect(self.send_notification) layout = QVBoxLayout(self) @@ -28,7 +28,7 @@ def _on_restart_clicked(self): self.signals.application.quit.emit(ExitCodes.LOGOUT) def raise_exception(self): - raise RuntimeError("Debug Crash") + raise RuntimeError('Debug Crash') def send_notification(self): - self.signals.application.notify.emit("Debug", "Test notification") + self.signals.application.notify.emit('Debug', 'Test notification') diff --git a/rare/components/tabs/settings/environ.py b/rare/components/tabs/settings/environ.py new file mode 100644 index 0000000000..32cbac24d7 --- /dev/null +++ b/rare/components/tabs/settings/environ.py @@ -0,0 +1,19 @@ +from rare.shared import RareCore +from rare.widgets.side_tab import SideTabContents + +from .widgets.envvars import EnvVars + + +class EnvironSettingsBase(EnvVars, SideTabContents): + def __init__( + self, + rcore: RareCore, + parent=None, + ): + super(EnvironSettingsBase, self).__init__(rcore.core(), parent=parent) + self.implements_scrollarea = True + + +class GlobalEnvironSettings(EnvironSettingsBase): + def __init__(self, rcore: RareCore, parent=None): + super(GlobalEnvironSettings, self).__init__(rcore, parent=parent) diff --git a/rare/components/tabs/settings/game.py b/rare/components/tabs/settings/game.py index 538353140f..93d55a59cc 100644 --- a/rare/components/tabs/settings/game.py +++ b/rare/components/tabs/settings/game.py @@ -1,6 +1,3 @@ -from logging import getLogger -from typing import Type - from PySide6.QtCore import Qt from PySide6.QtGui import QHideEvent from PySide6.QtWidgets import QVBoxLayout, QWidget @@ -10,35 +7,28 @@ from rare.utils import config_helper as config from rare.widgets.side_tab import SideTabContents -from .widgets.env_vars import EnvVars from .widgets.launch import LaunchSettingsBase, LaunchSettingsType from .widgets.wrappers import WrapperSettings -logger = getLogger("GlobalGameSettings") - class GameSettingsBase(QWidget, SideTabContents): def __init__( self, settings: RareAppSettings, rcore: RareCore, - launch_widget: Type[LaunchSettingsType], - envvar_widget: Type[EnvVars], + launch_widget: type[LaunchSettingsType], parent=None, ): super(GameSettingsBase, self).__init__(parent=parent) self.implements_scrollarea = True self.settings = settings - self.core = rcore.core() - self.app_name: str = "default" + self.app_name: str = 'default' self.launch = launch_widget(rcore, self) - self.env_vars = envvar_widget(self.core, self) self.main_layout = QVBoxLayout(self) self.main_layout.addWidget(self.launch, stretch=0) - self.main_layout.addWidget(self.env_vars, stretch=2) self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) def hideEvent(self, a0: QHideEvent): @@ -55,6 +45,4 @@ def __init__(self, rcore: RareCore, parent=None): class GlobalGameSettings(GameSettingsBase): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): - super(GlobalGameSettings, self).__init__( - settings, rcore, launch_widget=GlobalLaunchSettings, envvar_widget=EnvVars, parent=parent - ) + super(GlobalGameSettings, self).__init__(settings, rcore, launch_widget=GlobalLaunchSettings, parent=parent) diff --git a/rare/components/tabs/settings/legendary.py b/rare/components/tabs/settings/legendary.py index c7a5e274ac..6c01ab9420 100644 --- a/rare/components/tabs/settings/legendary.py +++ b/rare/components/tabs/settings/legendary.py @@ -2,7 +2,6 @@ import platform as pf import re from logging import getLogger -from typing import Set, Tuple from PySide6.QtCore import QObject, Qt, QThreadPool, Signal, Slot from PySide6.QtGui import QHideEvent, QShowEvent @@ -20,19 +19,17 @@ PathEdit, ) -logger = getLogger("LegendarySettings") - class RefreshGameMetaWorkerSignals(QObject): finished = Signal() class RefreshGameMetaWorker(Worker): - def __init__(self, core: LegendaryCore, platforms: Set[str], include_unreal: bool): + def __init__(self, core: LegendaryCore, platforms: set[str], include_unreal: bool): super(RefreshGameMetaWorker, self).__init__() self.core = core self.signals = RefreshGameMetaWorkerSignals() - self.platforms = platforms if platforms else {"Windows"} + self.platforms = platforms if platforms else {'Windows'} self.skip_ue = not include_unreal def run_real(self) -> None: @@ -44,6 +41,8 @@ def run_real(self) -> None: class LegendarySettings(QWidget): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): super(LegendarySettings, self).__init__(parent=parent) + self.logger = getLogger(type(self).__name__) + self.ui = Ui_LegendarySettings() self.ui.setupUi(self) @@ -52,10 +51,10 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.core = rcore.core() # Platform specific installation directory for macOS games - if pf.system() == "Darwin": + if pf.system() == 'Darwin': self.mac_install_dir_edit = PathEdit( - path=self.core.get_default_install_dir("Mac"), - placeholder=self.tr("Default installation folder for macOS games"), + path=self.core.get_default_install_dir('Mac'), + placeholder=self.tr('Default installation folder for macOS games'), file_mode=QFileDialog.FileMode.Directory, edit_func=self.__path_edit_callback, save_func=self._path_save_callback_mac, @@ -66,7 +65,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): # Platform-independent installation directory self.install_dir_edit = PathEdit( path=self.core.get_default_install_dir(), - placeholder=self.tr("Default installation folder for Windows games"), + placeholder=self.tr('Default installation folder for Windows games'), file_mode=QFileDialog.FileMode.Directory, edit_func=self.__path_edit_callback, save_func=self._path_save_callback_win, @@ -75,19 +74,19 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.install_dir_layout.addWidget(self.install_dir_edit) # Max Workers - self.ui.max_worker_spin.setValue(self.core.lgd.config["Legendary"].getint("max_workers", fallback=0)) + self.ui.max_worker_spin.setValue(self.core.lgd.config['Legendary'].getint('max_workers', fallback=0)) self.ui.max_worker_spin.valueChanged.connect(self.max_worker_save) # Max memory - self.ui.max_memory_spin.setValue(self.core.lgd.config["Legendary"].getint("max_memory", fallback=0)) + self.ui.max_memory_spin.setValue(self.core.lgd.config['Legendary'].getint('max_memory', fallback=0)) self.ui.max_memory_spin.valueChanged.connect(self.max_memory_save) # Preferred CDN - self.ui.preferred_cdn_line.setText(self.core.lgd.config["Legendary"].get("preferred_cdn", fallback="")) + self.ui.preferred_cdn_line.setText(self.core.lgd.config['Legendary'].get('preferred_cdn', fallback='')) self.ui.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save) # Disable HTTPS - self.ui.disable_https_check.setChecked(self.core.lgd.config["Legendary"].getboolean("disable_https", fallback=False)) + self.ui.disable_https_check.setChecked(self.core.lgd.config['Legendary'].getboolean('disable_https', fallback=False)) self.ui.disable_https_check.checkStateChanged.connect(self.disable_https_save) # Clean metadata @@ -95,7 +94,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.clean_keep_manifests_button.clicked.connect(self._on_clean_keep_manifests_clicked) self.locale_edit = IndicatorLineEdit( - f"{self.core.language_code}-{self.core.country_code}", + f'{self.core.language_code}-{self.core.country_code}', edit_func=self.__locale_edit_callback, save_func=self.__locale_save_callback, horiz_policy=QSizePolicy.Policy.Minimum, @@ -108,7 +107,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.fetch_macos_check.setChecked(self.settings.get_value(app_settings.macos_meta)) self.ui.fetch_macos_check.checkStateChanged.connect(self._on_fetch_macos_changed) - self.ui.fetch_macos_check.setDisabled(pf.system() == "Darwin") + self.ui.fetch_macos_check.setDisabled(pf.system() == 'Darwin') self.ui.fetch_unreal_check.setChecked(self.settings.get_value(app_settings.unreal_meta)) self.ui.fetch_unreal_check.checkStateChanged.connect(self._on_fetch_unreal_changed) @@ -127,7 +126,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): def showEvent(self, a0: QShowEvent): if a0.spontaneous(): return super().showEvent(a0) - if pf.system() == "Darwin": + if pf.system() == 'Darwin': self.mac_install_dir_edit.refresh() self.install_dir_edit.refresh() return super().showEvent(a0) @@ -146,20 +145,20 @@ def _refresh_metadata(self): self.ui.refresh_metadata_button.setDisabled(True) platforms = set() if self.ui.fetch_win32_check.isChecked(): - platforms.add("Win32") + platforms.add('Win32') if self.ui.fetch_macos_check.isChecked(): - platforms.add("Mac") + platforms.add('Mac') worker = RefreshGameMetaWorker(self.core, platforms, self.ui.fetch_unreal_check.isChecked()) worker.signals.finished.connect(self._on_refresh_worker_finished) QThreadPool.globalInstance().start(worker) @staticmethod - def __locale_edit_callback(text: str) -> Tuple[bool, str, int]: + def __locale_edit_callback(text: str) -> tuple[bool, str, int]: if text: - if re.match("^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$", text): - language, country = text.split("-" if "-" in text else "_") - text = "-".join([language.lower(), country.upper()]) - if bool(re.match("^[a-z]{2,3}-[A-Z]{2,3}$", text)): + if re.match('^[a-zA-Z]{2,3}[-_][a-zA-Z]{2,3}$', text): + language, country = text.split('-' if '-' in text else '_') + text = '-'.join([language.lower(), country.upper()]) + if bool(re.match('^[a-z]{2,3}-[A-Z]{2,3}$', text)): return True, text, IndicatorReasonsCommon.VALID else: return False, text, IndicatorReasonsCommon.WRONG_FORMAT @@ -168,18 +167,18 @@ def __locale_edit_callback(text: str) -> Tuple[bool, str, int]: def __locale_save_callback(self, text: str): if text: - self.core.egs.language_code, self.core.egs.country_code = text.split("-") - self.core.lgd.config.set("Legendary", "locale", text) + self.core.egs.language_code, self.core.egs.country_code = text.split('-') + self.core.lgd.config.set('Legendary', 'locale', text) else: - self.core.lgd.config.remove_option("Legendary", "locale") + self.core.lgd.config.remove_option('Legendary', 'locale') @staticmethod - def __path_edit_callback(path: str) -> Tuple[bool, str, int]: + def __path_edit_callback(path: str) -> tuple[bool, str, int]: if not path: return False, path, IndicatorReasonsCommon.IS_EMPTY try: - perms_path = os.path.join(path, ".rare_perms") - open(perms_path, "w").close() + perms_path = os.path.join(path, '.rare_perms') + open(perms_path, 'w').close() os.unlink(perms_path) except PermissionError: return False, path, IndicatorReasonsCommon.PERM_NO_WRITE @@ -189,70 +188,70 @@ def __path_edit_callback(path: str) -> Tuple[bool, str, int]: @Slot(str) def _path_save_callback_mac(self, text: str) -> None: - self._path_save(text, "mac_install_dir") + self._path_save(text, 'mac_install_dir') @Slot(str) def _path_save_callback_win(self, text: str) -> None: - self._path_save(text, "install_dir") - if pf.system() != "Darwin": + self._path_save(text, 'install_dir') + if pf.system() != 'Darwin': self._path_save_callback_mac(text) def _path_save(self, text: str, option: str): if text: - self.core.lgd.config.set("Legendary", option, text) + self.core.lgd.config.set('Legendary', option, text) else: - self.core.lgd.config.remove_option("Legendary", option) + self.core.lgd.config.remove_option('Legendary', option) @Slot(int) def max_worker_save(self, workers: int): if workers: - self.core.lgd.config.set("Legendary", "max_workers", str(workers)) + self.core.lgd.config.set('Legendary', 'max_workers', str(workers)) else: - self.core.lgd.config.remove_option("Legendary", "max_workers") + self.core.lgd.config.remove_option('Legendary', 'max_workers') @Slot(int) def max_memory_save(self, memory: int): if memory: - self.core.lgd.config.set("Legendary", "max_memory", str(memory)) + self.core.lgd.config.set('Legendary', 'max_memory', str(memory)) else: - self.core.lgd.config.remove_option("Legendary", "max_memory") + self.core.lgd.config.remove_option('Legendary', 'max_memory') @Slot(str) def preferred_cdn_save(self, cdn: str): if cdn: - self.core.lgd.config.set("Legendary", "preferred_cdn", cdn.strip()) + self.core.lgd.config.set('Legendary', 'preferred_cdn', cdn.strip()) else: - self.core.lgd.config.remove_option("Legendary", "preferred_cdn") + self.core.lgd.config.remove_option('Legendary', 'preferred_cdn') @Slot(Qt.CheckState) def disable_https_save(self, state: Qt.CheckState): - self.core.lgd.config.set("Legendary", "disable_https", str(state != Qt.CheckState.Unchecked).lower()) + self.core.lgd.config.set('Legendary', 'disable_https', str(state != Qt.CheckState.Unchecked).lower()) def clean_metadata(self, keep_manifests: bool): before = self.core.lgd.get_dir_size() - logger.debug("Removing app metadata...") + self.logger.debug('Removing app metadata...') app_names = {g.app_name for g in self.core.get_assets(update_assets=False)} self.core.lgd.clean_metadata(app_names) if not keep_manifests: - logger.debug("Removing manifests...") + self.logger.debug('Removing manifests...') installed = [(ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_list()] installed.extend((ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_dlc_list()) self.core.lgd.clean_manifests(installed) - logger.debug("Removing tmp data") + self.logger.debug('Removing tmp data') self.core.lgd.clean_tmp_data() after = self.core.lgd.get_dir_size() - logger.info(f"Cleanup complete! Removed {(before - after) / 1024 / 1024:.02f} MiB.") + self.logger.info(f'Cleanup complete! Removed {(before - after) / 1024 / 1024:.02f} MiB.') if (before - after) > 0: QMessageBox.information( self, - self.tr("Cleanup"), - self.tr("Cleanup complete! Successfully removed {}").format(format_size(before - after)), + self.tr('Cleanup'), + self.tr('Cleanup complete! Successfully removed {}').format(format_size(before - after)), ) else: - QMessageBox.information(self, self.tr("Cleanup"), self.tr("Nothing to clean")) + QMessageBox.information(self, self.tr('Cleanup'), self.tr('Nothing to clean')) @Slot() def _on_clean_clicked(self): diff --git a/rare/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py index c5923c998f..fd75774324 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -25,12 +25,12 @@ log_dir, ) -logger = getLogger("RareSettings") - class RareSettings(QWidget): def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): super(RareSettings, self).__init__(parent=parent) + self.logger = getLogger(type(self).__name__) + self.settings = settings self.rcore = rcore self.core = rcore.core() @@ -39,7 +39,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.setupUi(self) # Select lang - self.ui.lang_select.addItem(self.tr("System default"), app_settings.language.default) + self.ui.lang_select.addItem(self.tr('System default'), app_settings.language.default) for lang_code, title in get_translations(): self.ui.lang_select.addItem(title, lang_code) language = self.settings.get_value(app_settings.language) @@ -49,7 +49,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.lang_select.setCurrentIndex(0) self.ui.lang_select.currentIndexChanged.connect(self._on_lang_changed) - self.ui.color_select.addItem(self.tr("None"), "") + self.ui.color_select.addItem(self.tr('None'), '') for item in get_color_schemes(): self.ui.color_select.addItem(item, item) color = self.settings.get_value(app_settings.color_scheme) @@ -61,7 +61,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.color_select.setCurrentIndex(0) self.ui.color_select.currentIndexChanged.connect(self._on_color_select_changed) - self.ui.style_select.addItem(self.tr("None"), "") + self.ui.style_select.addItem(self.tr('None'), '') for item in get_style_sheets(): self.ui.style_select.addItem(item, item) style = self.settings.get_value(app_settings.style_sheet) @@ -73,8 +73,8 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.style_select.setCurrentIndex(0) self.ui.style_select.currentIndexChanged.connect(self._on_style_select_changed) - self.ui.view_combo.addItem(self.tr("Game covers"), LibraryView.COVER) - self.ui.view_combo.addItem(self.tr("Vertical list"), LibraryView.VLIST) + self.ui.view_combo.addItem(self.tr('Game covers'), LibraryView.COVER) + self.ui.view_combo.addItem(self.tr('Vertical list'), LibraryView.VLIST) view = LibraryView(self.settings.get_value(app_settings.library_view)) if (index := self.ui.view_combo.findData(view)) > -1: self.ui.view_combo.setCurrentIndex(index) @@ -112,21 +112,21 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.ui.log_games.checkStateChanged.connect(self._on_log_games_changed) if desktop_links_supported(): - self.desktop_link = desktop_link_path("Rare", "desktop") - self.start_menu_link = desktop_link_path("Rare", "start_menu") + self.desktop_link = desktop_link_path('Rare', 'desktop') + self.start_menu_link = desktop_link_path('Rare', 'start_menu') else: - self.ui.desktop_link_btn.setToolTip(self.tr("Not supported")) + self.ui.desktop_link_btn.setToolTip(self.tr('Not supported')) self.ui.desktop_link_btn.setDisabled(True) - self.ui.startmenu_link_btn.setToolTip(self.tr("Not supported")) + self.ui.startmenu_link_btn.setToolTip(self.tr('Not supported')) self.ui.startmenu_link_btn.setDisabled(True) - self.desktop_link = "" - self.start_menu_link = "" + self.desktop_link = '' + self.start_menu_link = '' if self.desktop_link and self.desktop_link.exists(): - self.ui.desktop_link_btn.setText(self.tr("Remove desktop link")) + self.ui.desktop_link_btn.setText(self.tr('Remove desktop link')) if self.start_menu_link and self.start_menu_link.exists(): - self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link")) + self.ui.startmenu_link_btn.setText(self.tr('Remove start menu link')) self.ui.desktop_link_btn.clicked.connect(self._create_desktop_link) self.ui.startmenu_link_btn.clicked.connect(self._create_start_menu_link) @@ -175,7 +175,7 @@ def _clean_logdir(self): if log_dir().joinpath(f).is_file(): log_dir().joinpath(f).unlink() except PermissionError as e: - logger.error(e) + self.logger.error(e) size = sum(log_dir().joinpath(f).stat().st_size for f in log_dir().iterdir() if log_dir().joinpath(f).is_file()) self.ui.log_dir_size_label.setText(format_size(size)) @@ -183,36 +183,36 @@ def _clean_logdir(self): def _create_start_menu_link(self): try: if not os.path.exists(self.start_menu_link): - if not create_desktop_link(app_name="rare_shortcut", link_type="start_menu"): + if not create_desktop_link(app_name='rare_shortcut', link_type='start_menu'): return - self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link")) + self.ui.startmenu_link_btn.setText(self.tr('Remove start menu link')) else: os.remove(self.start_menu_link) - self.ui.startmenu_link_btn.setText(self.tr("Create start menu link")) + self.ui.startmenu_link_btn.setText(self.tr('Create start menu link')) except PermissionError as e: - logger.error(str(e)) + self.logger.error(str(e)) QMessageBox.warning( self, - self.tr("Error"), - self.tr("Permission error, cannot remove {}").format(self.start_menu_link), + self.tr('Error'), + self.tr('Permission error, cannot remove {}').format(self.start_menu_link), ) @Slot() def _create_desktop_link(self): try: if not os.path.exists(self.desktop_link): - if not create_desktop_link(app_name="rare_shortcut", link_type="desktop"): + if not create_desktop_link(app_name='rare_shortcut', link_type='desktop'): return - self.ui.desktop_link_btn.setText(self.tr("Remove Desktop link")) + self.ui.desktop_link_btn.setText(self.tr('Remove Desktop link')) else: os.remove(self.desktop_link) - self.ui.desktop_link_btn.setText(self.tr("Create desktop link")) + self.ui.desktop_link_btn.setText(self.tr('Create desktop link')) except PermissionError as e: - logger.error(str(e)) - logger.warning( + self.logger.error(str(e)) + QMessageBox.warning( self, - self.tr("Error"), - self.tr("Permission error, cannot remove {}").format(self.start_menu_link), + self.tr('Error'), + self.tr('Permission error, cannot remove {}').format(self.start_menu_link), ) @Slot(int) diff --git a/rare/components/tabs/settings/widgets/discord_rpc.py b/rare/components/tabs/settings/widgets/discord_rpc.py index 088a19c183..b3e41b09bb 100644 --- a/rare/components/tabs/settings/widgets/discord_rpc.py +++ b/rare/components/tabs/settings/widgets/discord_rpc.py @@ -17,9 +17,9 @@ def __init__(self, settings: RareAppSettings, signals: GlobalSignals, parent): self.ui = Ui_DiscordRPCSettings() self.ui.setupUi(self) - self.ui.mode_combo.addItem(self.tr("When playing"), DiscordRPCMode.GAME_ONLY) - self.ui.mode_combo.addItem(self.tr("Always"), DiscordRPCMode.ALWAYS) - self.ui.mode_combo.addItem(self.tr("Never"), DiscordRPCMode.NEVER) + self.ui.mode_combo.addItem(self.tr('When playing'), DiscordRPCMode.GAME_ONLY) + self.ui.mode_combo.addItem(self.tr('Always'), DiscordRPCMode.ALWAYS) + self.ui.mode_combo.addItem(self.tr('Never'), DiscordRPCMode.NEVER) rpc_mode = DiscordRPCMode(self.settings.get_value(app_settings.discord_rpc_mode)) if (index := self.ui.mode_combo.findData(rpc_mode, Qt.ItemDataRole.UserRole)) < 0: @@ -39,9 +39,9 @@ def __init__(self, settings: RareAppSettings, signals: GlobalSignals, parent): self.ui.time_check.setChecked(self.settings.get_value(app_settings.discord_rpc_time)) self.ui.time_check.checkStateChanged.connect(self._on_time_changed) - if not importlib.util.find_spec("pypresence"): + if not importlib.util.find_spec('pypresence'): self.setDisabled(True) - self.setToolTip(self.tr("Pypresence is not installed")) + self.setToolTip(self.tr('Pypresence is not installed')) @Slot(Qt.CheckState) def _on_game_changed(self, state: Qt.CheckState): diff --git a/rare/components/tabs/settings/widgets/env_vars.py b/rare/components/tabs/settings/widgets/env_vars.py deleted file mode 100644 index 1e48aea4a0..0000000000 --- a/rare/components/tabs/settings/widgets/env_vars.py +++ /dev/null @@ -1,70 +0,0 @@ -from logging import getLogger - -from PySide6.QtCore import Qt -from PySide6.QtGui import QShowEvent -from PySide6.QtWidgets import ( - QGroupBox, - QHeaderView, - QTableView, - QVBoxLayout, -) - -from rare.lgndr.core import LegendaryCore - -from .env_vars_model import EnvVarsTableModel - -logger = getLogger("EnvVars") - - -class EnvVars(QGroupBox): - def __init__(self, core: LegendaryCore, parent): - super(EnvVars, self).__init__(parent=parent) - self.setTitle(self.tr("Environment")) - - self.core = core - self.app_name: str = "default" - - self.table_model = EnvVarsTableModel(self.core) - self.table_view = QTableView(self) - self.table_view.setModel(self.table_model) - self.table_view.verticalHeader().sectionPressed.disconnect() - self.table_view.horizontalHeader().sectionPressed.disconnect() - self.table_view.verticalHeader().sectionClicked.connect(self.table_model.removeRow) - self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) - self.table_view.horizontalHeader().setStretchLastSection(True) - self.table_view.setCornerButtonEnabled(False) - - # FIXME: investigate signaling between widgets - # We use this function to keep an eye on the config. - # When the user uses for example the wineprefix settings, we need to update the table. - # With this function, when the config file changes, we update the table. - # self.config_file_watcher = QFileSystemWatcher([str(self.core.lgd.config_path)], self) - # self.config_file_watcher.fileChanged.connect(self.table_model.reset) - - row_height = self.table_view.rowHeight(0) - self.table_view.setMinimumHeight(row_height * 7) - - layout = QVBoxLayout(self) - layout.addWidget(self.table_view) - - def showEvent(self, a0: QShowEvent): - if a0.spontaneous(): - return super().showEvent(a0) - self.table_model.load(self.app_name) - return super().showEvent(a0) - - def keyPressEvent(self, a0): - if a0.key() in {Qt.Key.Key_Delete, Qt.Key.Key_Backspace}: - indexes = self.table_view.selectedIndexes() - if not len(indexes): - return - for idx in indexes: - if idx.column() == 0: - self.table_view.model().removeRow(idx.row()) - elif idx.column() == 1: - self.table_view.model().setData(idx, "", Qt.ItemDataRole.EditRole) - elif a0.key() == Qt.Key.Key_Escape: - a0.ignore() - - def reset_model(self): - self.table_model.reset() diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/envvars.py similarity index 67% rename from rare/components/tabs/settings/widgets/env_vars_model.py rename to rare/components/tabs/settings/widgets/envvars.py index 3a8a07c1f3..2f3a1f3101 100644 --- a/rare/components/tabs/settings/widgets/env_vars_model.py +++ b/rare/components/tabs/settings/widgets/envvars.py @@ -1,19 +1,24 @@ import platform import re -import sys from collections import ChainMap -from typing import Any, Union +from typing import Any from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Slot -from PySide6.QtGui import QFont +from PySide6.QtGui import QFont, QShowEvent +from PySide6.QtWidgets import ( + QGroupBox, + QHeaderView, + QTableView, + QVBoxLayout, +) from rare.lgndr.core import LegendaryCore from rare.utils.misc import qta_icon -if platform.system() != "Windows": +if platform.system() != 'Windows': from rare.utils.compat.wine import get_wine_environment -if platform.system() in {"Linux", "FreeBSD"}: +if platform.system() in {'Linux', 'FreeBSD'}: from rare.utils.compat.steam import get_steam_environment @@ -25,26 +30,27 @@ def __init__(self, core: LegendaryCore, parent=None): # lk: validator matches anything starting with a letter or underscore # lk: and containing letters, numbers or underscores. # lk: Empty strings are considered invalid. - self.__validator = re.compile(r"(^[A-Za-z_][A-Za-z0-9_]*)") + self.__validator = re.compile(r'(^[A-Za-z_][A-Za-z0-9_]*)') self.__data_map: ChainMap = ChainMap() self.__readonly = set() - if platform.system() != "Windows": + if platform.system() != 'Windows': self.__readonly.update( { - "DXVK_HUD", - "DXVK_CONFIG", - "DXVK_NVAPI_DRS_SETTINGS", - "MANGOHUD", - "MANGOHUD_CONFIG", + 'DXVK_HUD', + 'DXVK_CONFIG', + 'DXVK_NVAPI_DRS_SETTINGS', + 'MANGOHUD', + 'MANGOHUD_CONFIG', + 'LEGENDARY_WRAPPER_EXE', } ) self.__readonly.update(get_wine_environment().keys()) - if platform.system() in {"Linux", "FreeBSD"}: + if platform.system() in {'Linux', 'FreeBSD'}: self.__readonly.update(get_steam_environment().keys()) - self.__readonly.add("STEAM_COMPAT_SHADER_PATH") - self.__default: str = "default" - self.__appname: str = None + self.__readonly.add('STEAM_COMPAT_SHADER_PATH') + self.__default: str = 'default' + self.__appname: str | None = None @Slot(str) def reset(self): @@ -55,44 +61,44 @@ def reset(self): def load(self, app_name: str): self.__appname = app_name self.beginResetModel() - if not self.core.lgd.config.has_section(f"{self.__appname}.env"): - self.core.lgd.config[f"{self.__appname}.env"] = {} + if not self.core.lgd.config.has_section(f'{self.__appname}.env'): + self.core.lgd.config[f'{self.__appname}.env'] = {} self.__data_map = ChainMap( - self.core.lgd.config[f"{self.__appname}.env"], - self.core.lgd.config[f"{self.__default}.env"] if self.__appname != self.__default else {}, + self.core.lgd.config[f'{self.__appname}.env'], + self.core.lgd.config[f'{self.__default}.env'] if self.__appname != self.__default else {}, ) self.endResetModel() - def __key(self, index: Union[QModelIndex, int]): + def __key(self, index: QModelIndex | int): if isinstance(index, QModelIndex): index = index.row() try: return list(self.__data_map)[index] except Exception: - return "" + return '' - def __is_local(self, index: Union[QModelIndex, int]): + def __is_local(self, index: QModelIndex | int): key = self.__key(index) - return key in self.__data_map.maps[0].keys() + return key in self.__data_map.maps[0] - def __is_global(self, index: Union[QModelIndex, int]): + def __is_global(self, index: QModelIndex | int): key = self.__key(index) - return key in self.__data_map.maps[1].keys() + return key in self.__data_map.maps[1] - def __is_readonly(self, index: Union[QModelIndex, int]): + def __is_readonly(self, index: QModelIndex | int): key = self.__key(index) return key in self.__readonly - def __value(self, index: Union[QModelIndex, int]): + def __value(self, index: QModelIndex | int): if isinstance(index, QModelIndex): index = index.row() return self.__data_map[self.__key(index)] def __title(self, section: int): if section == 0: - return self.tr("Key") + return self.tr('Key') elif section == 1: - return self.tr("Value") + return self.tr('Value') def __data_length(self): return len(self.__data_map) @@ -106,7 +112,7 @@ def __is_key_valid(self, value: str): def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if role in {Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole}: if index.row() == self.__data_length(): - return "" + return '' if index.column() == 0: return self.__key(index) else: @@ -119,7 +125,7 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> A return Qt.AlignmentFlag.AlignVCenter + Qt.AlignmentFlag.AlignLeft if role == Qt.ItemDataRole.FontRole: - font = QFont("Monospace") + font = QFont('Monospace') font.setStyleHint(QFont.StyleHint.Monospace) if index.row() < self.__data_length() and not self.__is_local(index): font.setWeight(QFont.Weight.Bold) @@ -130,12 +136,12 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> A if role == Qt.ItemDataRole.ToolTipRole: if index.row() == self.__data_length(): if index.column() == 1: - return self.tr("Disabled, please set the variable name first.") + return self.tr('Disabled, please set the variable name first.') return None if self.__key(index) in self.__readonly: if index.column() == 1: - return self.tr("Value: {}").format(self.__value(index)) - return self.tr("Readonly, please edit this via setting the appropriate setting.") + return self.tr('Value: {}').format(self.__value(index)) + return self.tr('Readonly, please edit this via setting the appropriate setting.') def headerData( self, @@ -150,12 +156,12 @@ def headerData( if orientation == Qt.Orientation.Vertical: if section < self.__data_length(): if self.__is_readonly(section) or not self.__is_local(section): - return qta_icon("mdi.lock", "ei.lock") + return qta_icon('mdi.lock', 'ei.lock') if self.__is_global(section) and self.__is_local(section): - return qta_icon("mdi.refresh", "ei.refresh") + return qta_icon('mdi.refresh', 'ei.refresh') if self.__is_local(section): - return qta_icon("mdi.delete", "ei.remove-sign") - return qta_icon("mdi.plus", "ei.plus-sign") + return qta_icon('mdi.delete', 'ei.remove-sign') + return qta_icon('mdi.plus', 'ei.plus-sign') if role == Qt.ItemDataRole.TextAlignmentRole: return Qt.AlignmentFlag.AlignVCenter + Qt.AlignmentFlag.AlignHCenter return None @@ -192,13 +198,13 @@ def setData(self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.Di if (not self.__is_key_valid(value)) or value in self.__readonly: return False # Do not accept existing variable names (this also protects against unchanged contents) - if value in self.__data_map.keys(): + if value in self.__data_map: return False if index.row() == self.__data_length(): self.beginInsertRows(QModelIndex(), self.rowCount(index), self.rowCount(index)) self.endInsertRows() - self.__data_map[value] = "" + self.__data_map[value] = '' self.core.lgd.save_config() self.dataChanged.emit(index, index, []) self.headerDataChanged.emit(Qt.Orientation.Vertical, index.row(), index.row()) @@ -216,11 +222,11 @@ def setData(self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.Di # old key remains, new key added -> insert row for new key, update from index to end # new key masking global key: # can't happen because we do not accept existing keys - if old_key in self.__data_map.maps[0].keys(): + if old_key in self.__data_map.maps[0]: # delete the old key if it is a local one, replacing a local key del self.__data_map[old_key] self.core.lgd.save_config() - if old_key in self.__data_map.maps[1].keys(): + if old_key in self.__data_map.maps[1]: self.beginInsertRows(QModelIndex(), self.__data_length(), self.__data_length()) self.endInsertRows() self.dataChanged.emit(index, self.index(index.row(), 1), []) @@ -274,42 +280,58 @@ def removeRow(self, row: int, parent: QModelIndex = None) -> bool: return True -if __name__ == "__main__": - from PySide6.QtWidgets import ( - QApplication, - QDialog, - QHeaderView, - QTableView, - QVBoxLayout, - ) +class EnvVars(QGroupBox): + def __init__(self, core: LegendaryCore, parent): + super(EnvVars, self).__init__(parent=parent) + self.setTitle(self.tr('Environment')) - from rare.lgndr.core import LegendaryCore - from rare.utils.misc import set_style_sheet - - class MainDialog(QDialog): - def __init__(self): - super().__init__() - - self.table = QTableView() - self.model = EnvVarsTableModel(LegendaryCore()) - self.model.load("Tamarind") - - self.table.setModel(self.model) - self.table.verticalHeader().sectionPressed.disconnect() - self.table.horizontalHeader().sectionPressed.disconnect() - self.table.verticalHeader().sectionClicked.connect(self.model.removeRow) - self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) - self.table.horizontalHeader().setStretchLastSection(True) - self.table.setCornerButtonEnabled(False) - - self.setLayout(QVBoxLayout(self)) - self.layout().addWidget(self.table) - - app = QApplication(sys.argv) - - set_style_sheet("RareStyle") - - window = MainDialog() - window.setFixedSize(800, 600) - window.show() - app.exec() + self.core = core + self.app_name: str = 'default' + + self.table_model = EnvVarsTableModel(self.core) + self.table_view = QTableView(self) + self.table_view.setModel(self.table_model) + self.table_view.verticalHeader().sectionPressed.disconnect() + self.table_view.horizontalHeader().sectionPressed.disconnect() + self.table_view.verticalHeader().sectionClicked.connect(self.table_model.removeRow) + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.table_view.horizontalHeader().setStretchLastSection(True) + self.table_view.setCornerButtonEnabled(False) + + # FIXME: investigate signaling between widgets + # We use this function to keep an eye on the config. + # When the user uses for example the wineprefix settings, we need to update the table. + # With this function, when the config file changes, we update the table. + # self.config_file_watcher = QFileSystemWatcher([str(self.core.lgd.config_path)], self) + # self.config_file_watcher.fileChanged.connect(self.table_model.reset) + + row_height = self.table_view.rowHeight(0) + self.table_view.setMinimumHeight(row_height * 7) + + layout = QVBoxLayout(self) + layout.addWidget(self.table_view) + + def showEvent(self, a0: QShowEvent): + if a0.spontaneous(): + return super().showEvent(a0) + self.table_model.load(self.app_name) + return super().showEvent(a0) + + def keyPressEvent(self, a0): + if a0.key() in {Qt.Key.Key_Delete, Qt.Key.Key_Backspace}: + indexes = self.table_view.selectedIndexes() + if not len(indexes): + return + for idx in indexes: + if idx.column() == 0: + self.table_view.model().removeRow(idx.row()) + elif idx.column() == 1: + self.table_view.model().setData(idx, '', Qt.ItemDataRole.EditRole) + elif a0.key() == Qt.Key.Key_Escape: + a0.ignore() + + def reset_model(self): + self.table_model.reset() + + +__all__ = ['EnvVars'] diff --git a/rare/components/tabs/settings/widgets/launch.py b/rare/components/tabs/settings/widgets/launch.py index 7d971545b9..6141dbd135 100644 --- a/rare/components/tabs/settings/widgets/launch.py +++ b/rare/components/tabs/settings/widgets/launch.py @@ -1,7 +1,7 @@ import os import shlex import shutil -from typing import Tuple, Type, TypeVar +from typing import TypeVar from PySide6.QtCore import Qt, Slot from PySide6.QtGui import QShowEvent @@ -22,30 +22,30 @@ class LaunchSettingsBase(QGroupBox): - def __init__(self, rcore: RareCore, wrapper_widget: Type[WrapperSettings], parent=None): + def __init__(self, rcore: RareCore, wrapper_widget: type[WrapperSettings], parent=None): super(LaunchSettingsBase, self).__init__(parent=parent) - self.setTitle(self.tr("Launch")) + self.setTitle(self.tr('Launch')) self.core = rcore.core() - self.app_name: str = "default" + self.app_name: str = 'default' self.prelaunch_cmd = PathEdit( - path="", - placeholder=self.tr("Path to a script or program to run before the game"), + path='', + placeholder=self.tr('Path to a script or program to run before the game'), file_mode=QFileDialog.FileMode.ExistingFile, edit_func=self.__prelaunch_cmd_edit_callback, save_func=self.__prelaunch_cmd_save_callback, ) - self.prelaunch_args = QLineEdit("") - self.prelaunch_args.setPlaceholderText(self.tr("Arguments to the script or program to run before the game")) + self.prelaunch_args = QLineEdit('') + self.prelaunch_args.setPlaceholderText(self.tr('Arguments to the script or program to run before the game')) self.prelaunch_args.setToolTip(self.prelaunch_args.placeholderText()) self.prelaunch_args.textChanged.connect(self.__prelaunch_changed) font = self.font() font.setItalic(True) - self.prelaunch_check = QCheckBox(self.tr("Wait for the pre-launch command to finish before launching the game")) + self.prelaunch_check = QCheckBox(self.tr('Wait for the pre-launch command to finish before launching the game')) self.prelaunch_check.setFont(font) self.prelaunch_check.checkStateChanged.connect(self.__prelauch_check_changed) @@ -61,16 +61,16 @@ def __init__(self, rcore: RareCore, wrapper_widget: Type[WrapperSettings], paren self.wrappers_widget = wrapper_widget(rcore, self) - self.main_layout.addRow(self.tr("Wrappers"), self.wrappers_widget) - self.main_layout.addRow(self.tr("Pre-launch"), prelaunch_layout) + self.main_layout.addRow(self.tr('Wrappers'), self.wrappers_widget) + self.main_layout.addRow(self.tr('Pre-launch'), prelaunch_layout) def showEvent(self, a0: QShowEvent): if a0.spontaneous(): return super().showEvent(a0) - prelaunch = shlex.split(config.get_option(self.app_name, "pre_launch_command", fallback="")) - command = prelaunch.pop(0) if len(prelaunch) else "" + prelaunch = shlex.split(config.get_option(self.app_name, 'pre_launch_command', fallback='')) + command = prelaunch.pop(0) if len(prelaunch) else '' arguments = prelaunch if len(prelaunch) else [] - wait = config.get_boolean(self.app_name, "pre_launch_wait", fallback=False) + wait = config.get_boolean(self.app_name, 'pre_launch_wait', fallback=False) self.prelaunch_cmd.setText(command) self.prelaunch_args.setText(shlex.join(arguments)) @@ -84,7 +84,7 @@ def tool_enabled(self): self.wrappers_widget.update_state() @staticmethod - def __prelaunch_cmd_edit_callback(text: str) -> Tuple[bool, str, int]: + def __prelaunch_cmd_edit_callback(text: str) -> tuple[bool, str, int]: if not text.strip(): return True, text, IndicatorReasonsCommon.UNDEFINED if not os.path.isfile(text) and not shutil.which(text): @@ -98,18 +98,18 @@ def __prelaunch_cmd_save_callback(self, text): @Slot(Qt.CheckState) def __prelauch_check_changed(self, state: Qt.CheckState): - config.set_boolean(self.app_name, "pre_launch_wait", state != Qt.CheckState.Unchecked) + config.set_boolean(self.app_name, 'pre_launch_wait', state != Qt.CheckState.Unchecked) @Slot() def __prelaunch_changed(self): command = self.prelaunch_cmd.text().strip() if not command: - config.adjust_option(self.app_name, "pre_launch_command", command) - config.remove_option(self.app_name, "pre_launch_wait") + config.adjust_option(self.app_name, 'pre_launch_command', command) + config.remove_option(self.app_name, 'pre_launch_wait') return command = shlex.quote(command) arguments = self.prelaunch_args.text().strip() - config.adjust_option(self.app_name, "pre_launch_command", " ".join([command, arguments])) + config.adjust_option(self.app_name, 'pre_launch_command', ' '.join([command, arguments])) -LaunchSettingsType = TypeVar("LaunchSettingsType", bound=LaunchSettingsBase) +LaunchSettingsType = TypeVar('LaunchSettingsType', bound=LaunchSettingsBase) diff --git a/rare/components/tabs/settings/widgets/overlay.py b/rare/components/tabs/settings/widgets/overlay.py index 127d9c1dc1..a12b22e4eb 100644 --- a/rare/components/tabs/settings/widgets/overlay.py +++ b/rare/components/tabs/settings/widgets/overlay.py @@ -1,7 +1,6 @@ from abc import abstractmethod from enum import IntEnum from logging import getLogger -from typing import Dict, List, Optional, Tuple, Union from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QDoubleValidator, QIntValidator, QShowEvent @@ -10,7 +9,7 @@ from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings from rare.utils import config_helper as config -logger = getLogger("GameOverlays") +logger = getLogger('GameOverlays') class OverlayLineEdit(QLineEdit): @@ -22,12 +21,12 @@ def __init__(self, option: str, placeholder: str, parent=None): self.setPlaceholderText(placeholder) def setDefault(self): - self.setText("") + self.setText('') - def getValue(self) -> Optional[str]: - return f"{self.option}={text}" if (text := self.text()) else None + def getValue(self) -> str | None: + return f'{self.option}={text}' if (text := self.text()) else None - def setValue(self, options: Dict[str, str]): + def setValue(self, options: dict[str, str]): if (value := options.get(self.option, None)) is not None: self.setText(value) options.pop(self.option) @@ -44,10 +43,10 @@ def __init__(self, option: str, parent=None): def setDefault(self): self.setCurrentIndex(0) - def getValue(self) -> Optional[str]: - return f"{self.option}={self.currentData(Qt.ItemDataRole.UserRole)}" if self.currentIndex() > 0 else None + def getValue(self) -> str | None: + return f'{self.option}={self.currentData(Qt.ItemDataRole.UserRole)}' if self.currentIndex() > 0 else None - def setValue(self, options: Dict[str, str]): + def setValue(self, options: dict[str, str]): if (value := options.get(self.option, None)) is not None: self.setCurrentIndex(self.findData(value, Qt.ItemDataRole.UserRole)) options.pop(self.option) @@ -60,9 +59,9 @@ def __init__( self, option: str, title: str, - desc: str = "", + desc: str = '', default_enabled: bool = False, - values: Tuple = None, + values: tuple = None, parent=None, ): self.option = option @@ -75,17 +74,17 @@ def __init__( def setDefault(self): self.setChecked(self.default_enabled) - def getValue(self) -> Optional[str]: + def getValue(self) -> str | None: # lk: return the check state in case of non-default, otherwise None checked = self.isChecked() value = ( - f"{self.option}={self.values[int(checked)] if self.values else int(checked)}" + f'{self.option}={self.values[int(checked)] if self.values else int(checked)}' if self.default_enabled or self.values else self.option ) return value if checked ^ self.default_enabled else None - def setValue(self, options: Dict[str, str]): + def setValue(self, options: dict[str, str]): if options.get(self.option, None) is not None: if self.values: self.setChecked(bool(self.values.index(options[self.option]))) @@ -102,14 +101,14 @@ def __init__(self, option: str, placeholder: str, parent=None): class OverlayNumberInput(OverlayLineEdit): - def __init__(self, option: str, placeholder: Union[int, float], parent=None): + def __init__(self, option: str, placeholder: int | float, parent=None): super().__init__(option, str(placeholder), parent=parent) validator = QDoubleValidator(self) if isinstance(placeholder, float) else QIntValidator(self) self.setValidator(validator) class OverlaySelectInput(OverlayComboBox): - def __init__(self, option: str, values: Tuple, parent=None): + def __init__(self, option: str, values: tuple, parent=None): super().__init__(option, parent=parent) for item in values: text, data = item @@ -134,33 +133,33 @@ def __init__(self, parent=None): self.ui = Ui_OverlaySettings() self.ui.setupUi(self) - self.ui.overlay_state_combo.addItem(self.tr("Global"), ActivationStates.GLOBAL) - self.ui.overlay_state_combo.addItem(self.tr("Disabled"), ActivationStates.DISABLED) - self.ui.overlay_state_combo.addItem(self.tr("Enabled (defaults)"), ActivationStates.DEFAULTS) - self.ui.overlay_state_combo.addItem(self.tr("Enabled (custom)"), ActivationStates.CUSTOM) + self.ui.overlay_state_combo.addItem(self.tr('Global'), ActivationStates.GLOBAL) + self.ui.overlay_state_combo.addItem(self.tr('Disabled'), ActivationStates.DISABLED) + self.ui.overlay_state_combo.addItem(self.tr('Enabled (defaults)'), ActivationStates.DEFAULTS) + self.ui.overlay_state_combo.addItem(self.tr('Enabled (custom)'), ActivationStates.CUSTOM) - self.control_key: Union[str, None] = None - self.config_key: Union[str, None] = None - self.force_disabled: Union[str, None] = None - self.force_defaults: Union[str, None] = None - self.separator: Union[str, None] = None - self.grid_row_items: Union[int, None] = None - self.app_name: str = "default" + self.control_key: str | None = None + self.config_key: str | None = None + self.force_disabled: str | None = None + self.force_defaults: str | None = None + self.separator: str | None = None + self.grid_row_items: int | None = None + self.app_name: str = 'default' - self.option_widgets: List[Union[OverlayCheckBox, OverlayLineEdit, OverlayComboBox]] = [] + self.option_widgets: list[OverlayCheckBox | OverlayLineEdit | OverlayComboBox] = [] # self.checkboxes: Dict[str, OverlayCheckBox] = {} # self.values: Dict[str, Union[OverlayLineEdit, OverlayComboBox]] = {} - self.ui.options_group.setTitle(self.tr("Custom options")) + self.ui.options_group.setTitle(self.tr('Custom options')) self.ui.overlay_state_combo.currentIndexChanged.connect(self._update_settings) self.environ_changed.connect(self._update_current_value) def setupWidget( self, - grid_map: List[OverlayCheckBox], - left_form_map: List[Tuple[Union[OverlayLineEdit, OverlayComboBox], str]], - right_form_map: List[Tuple[Union[OverlayLineEdit, OverlayComboBox], str]], + grid_map: list[OverlayCheckBox], + left_form_map: list[tuple[OverlayLineEdit | OverlayComboBox, str]], + right_form_map: list[tuple[OverlayLineEdit | OverlayComboBox, str]], label: str, control_key: str, config_key: str, @@ -235,7 +234,7 @@ def _update_settings(self): @Slot() def _update_current_value(self): - self.ui.current_value_info.setText(config.get_envvar_with_global(self.app_name, self.config_key, "")) + self.ui.current_value_info.setText(config.get_envvar_with_global(self.app_name, self.config_key, '')) def setCurrentState(self, state: ActivationStates): self.ui.overlay_state_combo.setCurrentIndex(self.ui.overlay_state_combo.findData(state, Qt.ItemDataRole.UserRole)) @@ -248,7 +247,7 @@ def showEvent(self, a0: QShowEvent): config_options = config.get_envvar(self.app_name, self.config_key, fallback=None) if config_options is None: - logger.debug("Setting %s is not present", self.config_key) + logger.debug('Setting %s is not present', self.config_key) self.setCurrentState(ActivationStates.GLOBAL) elif config_options == self.force_disabled: @@ -261,18 +260,18 @@ def showEvent(self, a0: QShowEvent): self.setCurrentState(ActivationStates.CUSTOM) opts = {} for o in config_options.split(self.separator): - if "=" in o: - k, v = o.split("=") + if '=' in o: + k, v = o.split('=') opts[k] = v else: # lk: The value doesn't matter other than not being None - opts[o] = "enable" + opts[o] = 'enable' for widget in self.option_widgets: widget.setValue(opts) if opts: logger.info( - "Remaining options without a gui switch: %s", + 'Remaining options without a gui switch: %s', self.separator.join(opts.keys()), ) @@ -284,36 +283,36 @@ def showEvent(self, a0: QShowEvent): class DxvkHudSettings(OverlaySettings): def __init__(self, parent=None): super(DxvkHudSettings, self).__init__(parent=parent) - self.setTitle(self.tr("DXVK HUD")) + self.setTitle(self.tr('DXVK HUD')) grid = [ - OverlayCheckBox("fps", self.tr("FPS")), - OverlayCheckBox("frametimes", self.tr("Frame time graph")), - OverlayCheckBox("memory", self.tr("Memory usage")), - OverlayCheckBox("allocations", self.tr("Memory chunk suballocation")), - OverlayCheckBox("gpuload", self.tr("GPU usage")), - OverlayCheckBox("devinfo", self.tr("Device info")), - OverlayCheckBox("version", self.tr("DXVK version")), - OverlayCheckBox("api", self.tr("D3D feature level")), - OverlayCheckBox("compiler", self.tr("Compiler activity")), - OverlayCheckBox("devinfo", self.tr("GPU driver and version")), - OverlayCheckBox("drawcalls", self.tr("Draw calls per frame")), - OverlayCheckBox("full", self.tr("All HUD elements")), + OverlayCheckBox('fps', self.tr('FPS')), + OverlayCheckBox('frametimes', self.tr('Frame time graph')), + OverlayCheckBox('memory', self.tr('Memory usage')), + OverlayCheckBox('allocations', self.tr('Memory chunk suballocation')), + OverlayCheckBox('gpuload', self.tr('GPU usage')), + OverlayCheckBox('devinfo', self.tr('Device info')), + OverlayCheckBox('version', self.tr('DXVK version')), + OverlayCheckBox('api', self.tr('D3D feature level')), + OverlayCheckBox('compiler', self.tr('Compiler activity')), + OverlayCheckBox('devinfo', self.tr('GPU driver and version')), + OverlayCheckBox('drawcalls', self.tr('Draw calls per frame')), + OverlayCheckBox('full', self.tr('All HUD elements')), ] left_form = [ - (OverlayNumberInput("scale", 1.0), self.tr("Scale")), - (OverlayNumberInput("opacity", 1.0), self.tr("Opacity")), + (OverlayNumberInput('scale', 1.0), self.tr('Scale')), + (OverlayNumberInput('opacity', 1.0), self.tr('Opacity')), ] right_form = [] self.setupWidget( grid, left_form, right_form, - label=self.tr("Show HUD"), - control_key="DXVK_HUD", - config_key="DXVK_HUD", - force_disabled="0", - force_defaults="1", - separator=",", + label=self.tr('Show HUD'), + control_key='DXVK_HUD', + config_key='DXVK_HUD', + force_disabled='0', + force_defaults='1', + separator=',', grid_row_items=4, ) @@ -324,9 +323,9 @@ def update_settings_override(self, state: ActivationStates): class DxvkConfigSettings(OverlaySettings): def __init__(self, parent=None): super(DxvkConfigSettings, self).__init__(parent=parent) - self.setTitle(self.tr("DXVK Config")) - dxvk_config_boolean = (("Default", ""), ("True", "True"), ("False", "False")) - dxvk_config_tristate = (("Default", ""), ("Auto", "Auto"), ("True", "True"), ("False", "False")) + self.setTitle(self.tr('DXVK Config')) + dxvk_config_boolean = (('Default', ''), ('True', 'True'), ('False', 'False')) + dxvk_config_tristate = (('Default', ''), ('Auto', 'Auto'), ('True', 'True'), ('False', 'False')) # fmt: off grid = [] left_form = [ @@ -350,12 +349,12 @@ def __init__(self, parent=None): grid, left_form, right_form, - label=self.tr("Mode"), - control_key="DXVK_CONFIG", - config_key="DXVK_CONFIG", - force_disabled="0", - force_defaults="", - separator=";", + label=self.tr('Mode'), + control_key='DXVK_CONFIG', + config_key='DXVK_CONFIG', + force_disabled='0', + force_defaults='', + separator=';', grid_row_items=4, ) @@ -366,48 +365,48 @@ def update_settings_override(self, state: ActivationStates): class DxvkNvapiDrsSettings(OverlaySettings): def __init__(self, parent=None): super(DxvkNvapiDrsSettings, self).__init__(parent=parent) - self.setTitle(self.tr("DXVK NVAPI Driver Settings")) + self.setTitle(self.tr('DXVK NVAPI Driver Settings')) - def preset_range(start: str, end: str) -> Tuple[Tuple, ...]: - return tuple(tuple(f"{p}preset_{chr(c)}" for p in ("", "render_")) for c in range(ord(start), ord(end) + 1)) + def preset_range(start: str, end: str) -> tuple[tuple, ...]: + return tuple(tuple(f'{p}preset_{chr(c)}' for p in ('', 'render_')) for c in range(ord(start), ord(end) + 1)) ngx_rr_presets = ( - ("off", "off"), - *preset_range("a", "o"), - ("latest", "render_preset_latest"), - ("default", "default"), + ('off', 'off'), + *preset_range('a', 'o'), + ('latest', 'render_preset_latest'), + ('default', 'default'), ) ngx_sr_presets = ( - ("off", "off"), - *preset_range("a", "o"), - ("latest", "render_preset_latest"), - ("default", "default"), + ('off', 'off'), + *preset_range('a', 'o'), + ('latest', 'render_preset_latest'), + ('default', 'default'), ) grid = [ OverlayCheckBox( - "ngx_dlss_sr_override", - self.tr("Super Resolution override"), - values=("off", "on"), + 'ngx_dlss_sr_override', + self.tr('Super Resolution override'), + values=('off', 'on'), ), OverlayCheckBox( - "ngx_dlss_rr_override", - self.tr("Ray Reconstruction override"), - values=("off", "on"), + 'ngx_dlss_rr_override', + self.tr('Ray Reconstruction override'), + values=('off', 'on'), ), OverlayCheckBox( - "ngx_dlss_fg_override", - self.tr("Frame Generation override"), - values=("off", "on"), + 'ngx_dlss_fg_override', + self.tr('Frame Generation override'), + values=('off', 'on'), ), ] left_form = [ ( - OverlaySelectInput("ngx_dlss_sr_override_render_preset_selection", ngx_sr_presets), - "Super Resolution preset", + OverlaySelectInput('ngx_dlss_sr_override_render_preset_selection', ngx_sr_presets), + 'Super Resolution preset', ), ( - OverlaySelectInput("ngx_dlss_rr_override_render_preset_selection", ngx_rr_presets), - "Ray Reconstruction preset", + OverlaySelectInput('ngx_dlss_rr_override_render_preset_selection', ngx_rr_presets), + 'Ray Reconstruction preset', ), ] right_form = [] @@ -415,12 +414,12 @@ def preset_range(start: str, end: str) -> Tuple[Tuple, ...]: grid, left_form, right_form, - label=self.tr("Mode"), - control_key="DXVK_NVAPI_DRS_SETTINGS", - config_key="DXVK_NVAPI_DRS_SETTINGS", - force_disabled="0", - force_defaults="", - separator=",", + label=self.tr('Mode'), + control_key='DXVK_NVAPI_DRS_SETTINGS', + config_key='DXVK_NVAPI_DRS_SETTINGS', + force_disabled='0', + force_defaults='', + separator=',', grid_row_items=4, ) @@ -431,71 +430,71 @@ def update_settings_override(self, state: ActivationStates): class MangoHudSettings(OverlaySettings): def __init__(self, parent=None): super(MangoHudSettings, self).__init__(parent=parent) - self.setTitle(self.tr("MangoHud")) + self.setTitle(self.tr('MangoHud')) mangohud_position = ( - ("default", "default"), - ("top-left", "top-left"), - ("top-right", "top-right"), - ("middle-left", "middle-left"), - ("middle-right", "middle-right"), - ("bottom-left", "bottom-left"), - ("bottom-right", "bottom-right"), - ("top-center", "top-center"), + ('default', 'default'), + ('top-left', 'top-left'), + ('top-right', 'top-right'), + ('middle-left', 'middle-left'), + ('middle-right', 'middle-right'), + ('bottom-left', 'bottom-left'), + ('bottom-right', 'bottom-right'), + ('top-center', 'top-center'), ) mangohud_vsync = ( - ("config", None), - ("adaptive", "0"), - ("off", "1"), - ("mailbox", "2"), - ("on", "3"), + ('config', None), + ('adaptive', '0'), + ('off', '1'), + ('mailbox', '2'), + ('on', '3'), ) mangohud_gl_vsync = ( - ("config", None), - ("off", "0"), - ("on", "1"), - ("half", "2"), - ("third", "3"), - ("quarter", "4"), + ('config', None), + ('off', '0'), + ('on', '1'), + ('half', '2'), + ('third', '3'), + ('quarter', '4'), ) grid = [ - OverlayCheckBox("read_cfg", self.tr("Read config")), - OverlayCheckBox("fps", self.tr("FPS"), default_enabled=True), - OverlayCheckBox("frame_timing", self.tr("Frame time"), default_enabled=True), - OverlayCheckBox("cpu_stats", self.tr("CPU load"), default_enabled=True), - OverlayCheckBox("gpu_stats", self.tr("GPU load"), default_enabled=True), - OverlayCheckBox("cpu_temp", self.tr("CPU temperature")), - OverlayCheckBox("gpu_temp", self.tr("GPU temperature")), - OverlayCheckBox("ram", self.tr("Memory usage")), - OverlayCheckBox("vram", self.tr("VRAM usage")), - OverlayCheckBox("time", self.tr("Local time")), - OverlayCheckBox("version", self.tr("MangoHud version")), - OverlayCheckBox("arch", self.tr("System architecture")), - OverlayCheckBox("histogram", self.tr("FPS graph")), - OverlayCheckBox("gpu_name", self.tr("GPU name")), - OverlayCheckBox("cpu_power", self.tr("CPU power consumption")), - OverlayCheckBox("gpu_power", self.tr("GPU power consumption")), + OverlayCheckBox('read_cfg', self.tr('Read config')), + OverlayCheckBox('fps', self.tr('FPS'), default_enabled=True), + OverlayCheckBox('frame_timing', self.tr('Frame time'), default_enabled=True), + OverlayCheckBox('cpu_stats', self.tr('CPU load'), default_enabled=True), + OverlayCheckBox('gpu_stats', self.tr('GPU load'), default_enabled=True), + OverlayCheckBox('cpu_temp', self.tr('CPU temperature')), + OverlayCheckBox('gpu_temp', self.tr('GPU temperature')), + OverlayCheckBox('ram', self.tr('Memory usage')), + OverlayCheckBox('vram', self.tr('VRAM usage')), + OverlayCheckBox('time', self.tr('Local time')), + OverlayCheckBox('version', self.tr('MangoHud version')), + OverlayCheckBox('arch', self.tr('System architecture')), + OverlayCheckBox('histogram', self.tr('FPS graph')), + OverlayCheckBox('gpu_name', self.tr('GPU name')), + OverlayCheckBox('cpu_power', self.tr('CPU power consumption')), + OverlayCheckBox('gpu_power', self.tr('GPU power consumption')), ] left_form = [ - (OverlayNumberInput("font_size", 24), self.tr("Font size")), - (OverlaySelectInput("position", mangohud_position), self.tr("Position")), + (OverlayNumberInput('font_size', 24), self.tr('Font size')), + (OverlaySelectInput('position', mangohud_position), self.tr('Position')), ] right_form = [ - (OverlayNumberInput("fps_limit", 0), self.tr("FPS limit")), - (OverlaySelectInput("vsync", mangohud_vsync), self.tr("Vulkan vsync")), - (OverlaySelectInput("gl_vsync", mangohud_gl_vsync), self.tr("OpenGL vsync")), + (OverlayNumberInput('fps_limit', 0), self.tr('FPS limit')), + (OverlaySelectInput('vsync', mangohud_vsync), self.tr('Vulkan vsync')), + (OverlaySelectInput('gl_vsync', mangohud_gl_vsync), self.tr('OpenGL vsync')), ] self.setupWidget( grid, left_form, right_form, - label=self.tr("Show HUD"), - control_key="MANGOHUD", - config_key="MANGOHUD_CONFIG", - force_disabled="no_display", - force_defaults="read_cfg", - separator=",", + label=self.tr('Show HUD'), + control_key='MANGOHUD', + config_key='MANGOHUD_CONFIG', + force_disabled='no_display', + force_defaults='read_cfg', + separator=',', grid_row_items=4, ) @@ -510,15 +509,13 @@ def update_settings_override(self, state: ActivationStates): # pylint: disable= if state == ActivationStates.GLOBAL: config.remove_envvar(self.app_name, self.control_key) elif state == ActivationStates.DISABLED: - config.set_envvar(self.app_name, self.control_key, "0") - elif state == ActivationStates.DEFAULTS: - config.set_envvar(self.app_name, self.control_key, "1") - elif state == ActivationStates.CUSTOM: - config.set_envvar(self.app_name, self.control_key, "1") + config.set_envvar(self.app_name, self.control_key, '0') + elif state == ActivationStates.DEFAULTS or state == ActivationStates.CUSTOM: + config.set_envvar(self.app_name, self.control_key, '1') self.environ_changed.emit(self.control_key) -if __name__ == "__main__": +if __name__ == '__main__': import sys from argparse import Namespace @@ -528,10 +525,10 @@ def update_settings_override(self, state: ActivationStates): # pylint: disable= config = Namespace() def get_envvar(x, y, fallback): - if y == "DXVK_NVAPI_DRS_SETTINGS": - return "ngx_dlss_sr_override=off,ngx_dlss_rr_override_render_preset_selection=render_preset_d" + if y == 'DXVK_NVAPI_DRS_SETTINGS': + return 'ngx_dlss_sr_override=off,ngx_dlss_rr_override_render_preset_selection=render_preset_d' else: - return "" + return '' config.get_envvar = get_envvar config.set_option = lambda x, y, z: print(x, y, z) diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py index 8b70304611..917af1239f 100644 --- a/rare/components/tabs/settings/widgets/proton.py +++ b/rare/components/tabs/settings/widgets/proton.py @@ -1,7 +1,6 @@ import os from enum import IntEnum from logging import getLogger -from typing import Optional, Tuple, Union from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QShowEvent @@ -21,7 +20,7 @@ from rare.utils.paths import proton_compat_dir from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit -logger = getLogger("ProtonSettings") +logger = getLogger('ProtonSettings') class ProtonSettings(QGroupBox): @@ -43,25 +42,25 @@ def __init__(self, rcore: RareCore, parent=None): self.rcore = rcore self.core = rcore.core() self.wrappers: Wrappers = rcore.wrappers() - self.tool_wrapper: Optional[Wrapper] = None - self.app_name: str = "default" + self.tool_wrapper: Wrapper | None = None + self.app_name: str = 'default' - self.setTitle(self.tr("Proton")) + self.setTitle(self.tr('Proton')) self.tool_combo = QComboBox(self) self.tool_combo.currentIndexChanged.connect(self._on_tool_changed) self.compat_combo = QComboBox(self) - self.compat_combo.addItem(self.tr("Shared"), ProtonSettings.CompatLocation.SHARED) - self.compat_combo.addItem(self.tr("Isolated"), ProtonSettings.CompatLocation.ISOLATED) - self.compat_combo.addItem(self.tr("Custom"), ProtonSettings.CompatLocation.CUSTOM) + self.compat_combo.addItem(self.tr('Shared'), ProtonSettings.CompatLocation.SHARED) + self.compat_combo.addItem(self.tr('Isolated'), ProtonSettings.CompatLocation.ISOLATED) + self.compat_combo.addItem(self.tr('Custom'), ProtonSettings.CompatLocation.CUSTOM) self.compat_combo.currentIndexChanged.connect(self._on_compat_changed) self.compat_edit = PathEdit( file_mode=QFileDialog.FileMode.Directory, edit_func=self._proton_prefix_edit, save_func=self._proton_prefix_save, - placeholder=self.tr("Please select path for proton prefix"), + placeholder=self.tr('Please select path for proton prefix'), parent=self, ) self.compat_edit.setReadOnly(True) @@ -75,18 +74,18 @@ def __init__(self, rcore: RareCore, parent=None): # button_layout.setAlignment(Qt.AlignmentFlag.AlignRight) layout = QFormLayout(self) - layout.addRow(self.tr("Proton tool"), self.tool_combo) + layout.addRow(self.tr('Proton tool'), self.tool_combo) folder_layout = QHBoxLayout() folder_layout.addWidget(self.compat_combo) folder_layout.addWidget(self.compat_edit) - layout.addRow(self.tr("Compat folder"), folder_layout) + layout.addRow(self.tr('Compat folder'), folder_layout) layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) layout.setFormAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) # layout.addRow(button_layout) def _get_compat_path(self, compat_location: CompatLocation): - folder_name = "default" + folder_name = 'default' compat_path = proton_compat_dir(folder_name) return compat_path @@ -130,7 +129,7 @@ def showEvent(self, a0: QShowEvent) -> None: self.tool_combo.blockSignals(False) enabled = bool(self.tool_combo.currentData(Qt.ItemDataRole.UserRole)) - compat_path = lgd_config.get_compat_data_path(self.app_name, fallback="") + compat_path = lgd_config.get_compat_data_path(self.app_name, fallback='') self.compat_combo.blockSignals(True) compat_location = self._update_compat_folder(compat_path) @@ -146,20 +145,18 @@ def showEvent(self, a0: QShowEvent) -> None: @Slot(int) def _on_tool_changed(self, index: int): - steam_tool: Union[steam.ProtonTool, steam.CompatibilityTool, None] = self.tool_combo.itemData( - index, Qt.ItemDataRole.UserRole - ) + steam_tool: steam.ProtonTool | steam.CompatibilityTool | None = self.tool_combo.itemData(index, Qt.ItemDataRole.UserRole) steam_environ = steam.get_steam_environment(steam_tool, self.compat_edit.text()) - library_paths = steam_environ["STEAM_COMPAT_LIBRARY_PATHS"] if "STEAM_COMPAT_LIBRARY_PATHS" in steam_environ else "" - if self.app_name != "default": + library_paths = steam_environ.get('STEAM_COMPAT_LIBRARY_PATHS', '') + if self.app_name != 'default': install_path = self.rcore.get_game(self.app_name).install_path library_paths = ( - ":".join([library_paths, os.path.dirname(install_path)]) if library_paths else os.path.dirname(install_path) + ':'.join([library_paths, os.path.dirname(install_path)]) if library_paths else os.path.dirname(install_path) ) # https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/steam-compat-tool-interface.md#non-steam-games - steam_environ["STEAM_COMPAT_INSTALL_PATH"] = install_path - steam_environ["STEAM_COMPAT_LIBRARY_PATHS"] = library_paths + steam_environ['STEAM_COMPAT_INSTALL_PATH'] = install_path + steam_environ['STEAM_COMPAT_LIBRARY_PATHS'] = library_paths for key, value in steam_environ.items(): lgd_config.adjust_envvar(self.app_name, key, value) self.environ_changed.emit(key) @@ -181,9 +178,9 @@ def _on_tool_changed(self, index: int): self.compat_combo.setEnabled(steam_tool is not None) self.compat_edit.setEnabled(steam_tool is not None) - compat_path = "" + compat_path = '' if steam_tool: - compat_path = lgd_config.get_compat_data_path(self.app_name, fallback="") + compat_path = lgd_config.get_compat_data_path(self.app_name, fallback='') if not compat_path: compat_path = str(self._get_compat_path(ProtonSettings.CompatLocation.NONE)) self._update_compat_folder(compat_path) @@ -205,7 +202,7 @@ def _on_compat_changed(self, index): ) @staticmethod - def _proton_prefix_edit(text: str) -> Tuple[bool, str, int]: + def _proton_prefix_edit(text: str) -> tuple[bool, str, int]: if not text: return False, text, IndicatorReasonsCommon.IS_EMPTY if os.path.isdir(text): @@ -213,7 +210,7 @@ def _proton_prefix_edit(text: str) -> Tuple[bool, str, int]: if not dir_list: return True, text, IndicatorReasonsCommon.VALID if any( - (x in dir_list) for x in ("pfx", "shadercache", "dosdevices", "drive_c", "system.reg", "user.reg", "userdef.reg") + (x in dir_list) for x in ('pfx', 'shadercache', 'dosdevices', 'drive_c', 'system.reg', 'user.reg', 'userdef.reg') ): return True, text, IndicatorReasonsCommon.VALID return False, text, IndicatorReasonsCommon.DIR_NOT_EMPTY @@ -222,5 +219,5 @@ def _proton_prefix_edit(text: str) -> Tuple[bool, str, int]: def _proton_prefix_save(self, text: str): lgd_config.adjust_compat_data_path(self.app_name, text) - self.environ_changed.emit("STEAM_COMPAT_DATA_PATH") + self.environ_changed.emit('STEAM_COMPAT_DATA_PATH') self.compat_path_changed.emit(text) diff --git a/rare/components/tabs/settings/widgets/runner.py b/rare/components/tabs/settings/widgets/runner.py index f9a3ef9c55..571388fdb1 100644 --- a/rare/components/tabs/settings/widgets/runner.py +++ b/rare/components/tabs/settings/widgets/runner.py @@ -1,7 +1,7 @@ import os.path import platform as pf from getpass import getuser -from typing import Type, TypeVar +from typing import TypeVar from PySide6.QtCore import Qt, QUrl, Signal, Slot from PySide6.QtGui import QDesktopServices @@ -13,7 +13,7 @@ from .wine import WineSettings -if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() in {'Linux', 'FreeBSD'}: from .proton import ProtonSettings @@ -27,15 +27,15 @@ def __init__( self, settings: RareAppSettings, rcore: RareCore, - wine_widget: Type["WineSettings"], - proton_widget: Type["ProtonSettings"] = None, + wine_widget: type['WineSettings'], + proton_widget: type['ProtonSettings'] = None, parent=None, ): super().__init__(parent=parent) self.settings = settings - self.app_name: str = "default" + self.app_name: str = 'default' - self.setTitle(self.tr("Compatibility")) + self.setTitle(self.tr('Compatibility')) self.main_layout = QVBoxLayout(self) @@ -51,11 +51,11 @@ def __init__( self.ctool.compat_path_changed.connect(self.wine.compat_path_changed) self.main_layout.addWidget(self.ctool) - self.pfx_folder_button = QPushButton(self.tr("Open prefix folder"), self) + self.pfx_folder_button = QPushButton(self.tr('Open prefix folder'), self) self.pfx_folder_button.clicked.connect(self._open_pfx_folder) - self.usr_folder_button = QPushButton(self.tr("Open user folder"), self) + self.usr_folder_button = QPushButton(self.tr('Open user folder'), self) self.usr_folder_button.clicked.connect(self._open_usr_folder) - self.winetricks_button = QPushButton(self.tr("Run winetricks"), self) + self.winetricks_button = QPushButton(self.tr('Run winetricks'), self) self.button_layout = QHBoxLayout() self.button_layout.addWidget(self.pfx_folder_button) @@ -65,14 +65,14 @@ def __init__( font = self.font() font.setItalic(True) - self.shader_cache_check = QCheckBox(self.tr("Use game-specific shader cache directory"), self) + self.shader_cache_check = QCheckBox(self.tr('Use game-specific shader cache directory'), self) self.shader_cache_check.setFont(font) self.shader_cache_check.setChecked(self.settings.get_value(app_settings.local_shader_cache)) self.shader_cache_check.checkStateChanged.connect(self._shader_cache_check_changed) self.form_layout = QFormLayout() - self.form_layout.addRow(self.tr(""), self.button_layout) - self.form_layout.addRow(self.tr("Shader cache"), self.shader_cache_check) + self.form_layout.addRow(self.tr(''), self.button_layout) + self.form_layout.addRow(self.tr('Shader cache'), self.shader_cache_check) self.form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.form_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) @@ -88,9 +88,9 @@ def _open_pfx_folder(self): @Slot() def _open_usr_folder(self): - path = os.path.join(lgd_config.get_wine_prefix_with_global(self.app_name), "drive_c", "users", getuser()) + path = os.path.join(lgd_config.get_wine_prefix_with_global(self.app_name), 'drive_c', 'users', getuser()) if not os.path.exists(path): - path = os.path.join(lgd_config.get_wine_prefix_with_global(self.app_name), "drive_c", "users", "steamuser") + path = os.path.join(lgd_config.get_wine_prefix_with_global(self.app_name), 'drive_c', 'users', 'steamuser') QDesktopServices.openUrl(path) @Slot() @@ -98,4 +98,4 @@ def _run_winetricks(self): pass -RunnerSettingsType = TypeVar("RunnerSettingsType", bound=RunnerSettingsBase) +RunnerSettingsType = TypeVar('RunnerSettingsType', bound=RunnerSettingsBase) diff --git a/rare/components/tabs/settings/widgets/wine.py b/rare/components/tabs/settings/widgets/wine.py index 13592c1306..a140e7741f 100644 --- a/rare/components/tabs/settings/widgets/wine.py +++ b/rare/components/tabs/settings/widgets/wine.py @@ -1,6 +1,5 @@ import os from logging import getLogger -from typing import Optional, Tuple from PySide6.QtCore import QSignalBlocker, Qt, Signal, Slot from PySide6.QtGui import QShowEvent @@ -11,7 +10,7 @@ from rare.utils import config_helper as lgd_config from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit -logger = getLogger("WineSettings") +logger = getLogger('WineSettings') class WineSettings(QGroupBox): @@ -23,13 +22,13 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.settings = settings self.signals = rcore.signals() self.lgd_core = rcore.core() - self.app_name: Optional[str] = "default" + self.app_name: str | None = 'default' - self.setTitle(self.tr("Wine")) + self.setTitle(self.tr('Wine')) # Wine prefix self.wine_prefix_edit = PathEdit( - path="", + path='', file_mode=QFileDialog.FileMode.Directory, edit_func=self._wine_prefix_edit, save_func=self.save_prefix, @@ -37,9 +36,9 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): # Wine executable self.wine_execut_edit = PathEdit( - path="", + path='', file_mode=QFileDialog.FileMode.ExistingFile, - name_filters=("wine", "wine64"), + name_filters=('wine', 'wine64'), edit_func=lambda text: ( os.path.isfile(text) or not text, text, @@ -49,8 +48,8 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): ) layout = QFormLayout(self) - layout.addRow(self.tr("Executable"), self.wine_execut_edit) - layout.addRow(self.tr("Prefix folder"), self.wine_prefix_edit) + layout.addRow(self.tr('Executable'), self.wine_execut_edit) + layout.addRow(self.tr('Prefix folder'), self.wine_prefix_edit) layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) layout.setFormAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) @@ -60,7 +59,7 @@ def __update_widget(self): self.wine_prefix_edit.setText(self.load_prefix()) _ = QSignalBlocker(self.wine_execut_edit) self.wine_execut_edit.setText(self.load_execut()) - self.setDisabled(lgd_config.get_boolean(self.app_name, "no_wine", fallback=False)) + self.setDisabled(lgd_config.get_boolean(self.app_name, 'no_wine', fallback=False)) def showEvent(self, a0: QShowEvent): if a0.spontaneous(): @@ -70,47 +69,47 @@ def showEvent(self, a0: QShowEvent): @Slot(str) def compat_path_changed(self, text: str): - path = os.path.join(text, "pfx") if text else text + path = os.path.join(text, 'pfx') if text else text self.wine_prefix_edit.setText(path) @Slot(bool) def compat_tool_enabled(self, enabled: bool, path: str): - old_wine_execut = self.settings.value(f"{self.app_name}/wine_execut", defaultValue=None) - old_wine_prefix = self.settings.value(f"{self.app_name}/wine_prefix", defaultValue=None) + old_wine_execut = self.settings.value(f'{self.app_name}/wine_execut', defaultValue=None) + old_wine_prefix = self.settings.value(f'{self.app_name}/wine_prefix', defaultValue=None) if enabled: - wine_execut = lgd_config.get_option(self.app_name, "wine_executable", "") - wine_prefix = lgd_config.get_wine_prefix(self.app_name, "") + wine_execut = lgd_config.get_option(self.app_name, 'wine_executable', '') + wine_prefix = lgd_config.get_wine_prefix(self.app_name, '') if old_wine_execut is None: - self.settings.setValue(f"{self.app_name}/wine_execut", wine_execut) + self.settings.setValue(f'{self.app_name}/wine_execut', wine_execut) if old_wine_prefix is None: - self.settings.setValue(f"{self.app_name}/wine_prefix", wine_prefix) - self.wine_execut_edit.setText("") - self.wine_prefix_edit.setText(os.path.join(path, "pfx")) - lgd_config.set_boolean(self.app_name, "no_wine", True) + self.settings.setValue(f'{self.app_name}/wine_prefix', wine_prefix) + self.wine_execut_edit.setText('') + self.wine_prefix_edit.setText(os.path.join(path, 'pfx')) + lgd_config.set_boolean(self.app_name, 'no_wine', True) else: - self.settings.remove(f"{self.app_name}/wine_execut") - self.settings.remove(f"{self.app_name}/wine_prefix") + self.settings.remove(f'{self.app_name}/wine_execut') + self.settings.remove(f'{self.app_name}/wine_prefix') if old_wine_execut is not None: self.wine_execut_edit.setText(old_wine_execut) if old_wine_prefix is not None: self.wine_prefix_edit.setText(old_wine_prefix) - lgd_config.remove_option(self.app_name, "no_wine") + lgd_config.remove_option(self.app_name, 'no_wine') self.setDisabled(enabled) def load_prefix(self) -> str: if self.app_name is None: raise RuntimeError - return lgd_config.get_wine_prefix(self.app_name, "") + return lgd_config.get_wine_prefix(self.app_name, '') @staticmethod - def _wine_prefix_edit(text: str) -> Tuple[bool, str, int]: + def _wine_prefix_edit(text: str) -> tuple[bool, str, int]: if not text: return True, text, IndicatorReasonsCommon.VALID if os.path.isdir(text): dir_list = os.listdir(text) if not dir_list: return True, text, IndicatorReasonsCommon.VALID - if any((x in dir_list) for x in ("dosdevices", "drive_c", "system.reg", "user.reg", "userdef.reg")): + if any((x in dir_list) for x in ('dosdevices', 'drive_c', 'system.reg', 'user.reg', 'userdef.reg')): return True, text, IndicatorReasonsCommon.VALID return False, text, IndicatorReasonsCommon.DIR_NOT_EMPTY else: @@ -120,14 +119,14 @@ def save_prefix(self, path: str) -> None: if self.app_name is None: raise RuntimeError lgd_config.adjust_wine_prefix(self.app_name, path) - self.environ_changed.emit("WINEPREFIX") + self.environ_changed.emit('WINEPREFIX') def load_execut(self) -> str: if self.app_name is None: raise RuntimeError - return lgd_config.get_option(self.app_name, "wine_executable", "") + return lgd_config.get_option(self.app_name, 'wine_executable', '') def save_execut(self, text: str) -> None: if self.app_name is None: raise RuntimeError - lgd_config.adjust_option(self.app_name, "wine_executable", text) + lgd_config.adjust_option(self.app_name, 'wine_executable', text) diff --git a/rare/components/tabs/settings/widgets/wrappers.py b/rare/components/tabs/settings/widgets/wrappers.py index cfa7b58838..e69100608a 100644 --- a/rare/components/tabs/settings/widgets/wrappers.py +++ b/rare/components/tabs/settings/widgets/wrappers.py @@ -1,8 +1,8 @@ import platform as pf import shlex import shutil +from collections.abc import Iterable from logging import getLogger -from typing import Iterable, Optional, Tuple from PySide6.QtCore import QEvent, QMimeData, QObject, QSize, Qt, Signal, Slot from PySide6.QtGui import ( @@ -37,10 +37,10 @@ from rare.utils.misc import qta_icon from rare.widgets.dialogs import ButtonDialog, game_title -if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() in {'Linux', 'FreeBSD'}: from rare.utils.compat import steam -logger = getLogger("WrapperSettings") +logger = getLogger('WrapperSettings') class WrapperEditDialog(ButtonDialog): @@ -57,14 +57,14 @@ def __init__(self, parent=None): self.setCentralLayout(self.widget_layout) - self.accept_button.setText(self.tr("Save")) - self.accept_button.setIcon(qta_icon("fa.edit", "fa5s.edit")) + self.accept_button.setText(self.tr('Save')) + self.accept_button.setIcon(qta_icon('fa.edit', 'fa5s.edit')) self.accept_button.setEnabled(False) - self.result: Tuple = (False, "") + self.result: tuple = (False, '') def setup(self, wrapper: Wrapper): - header = self.tr("Edit wrapper") + header = self.tr('Edit wrapper') self.setWindowTitle(header) self.setSubtitle(game_title(header, wrapper.name)) self.line_edit.setText(wrapper.as_str) @@ -87,16 +87,16 @@ class WrapperAddDialog(WrapperEditDialog): def __init__(self, parent=None): super(WrapperAddDialog, self).__init__(parent=parent) self.combo_box = QComboBox(self) - self.combo_box.addItem("None", "") + self.combo_box.addItem('None', '') self.combo_box.currentIndexChanged.connect(self.__on_index_changed) self.widget_layout.insertWidget(0, self.combo_box) def setup(self, wrappers: Iterable[Wrapper]): - header = self.tr("Add wrapper") + header = self.tr('Add wrapper') self.setWindowTitle(header) self.setSubtitle(header) for wrapper in wrappers: - self.combo_box.addItem(f"{wrapper.name} ({wrapper.as_str})", wrapper.as_str) + self.combo_box.addItem(f'{wrapper.name} ({wrapper.as_str})', wrapper.as_str) @Slot(int) def __on_index_changed(self, index: int): @@ -120,29 +120,29 @@ def __init__(self, wrapper: Wrapper, parent=None): self.text_lbl = QCheckBox(wrapper.name, parent=self) self.text_lbl.setChecked(wrapper.is_enabled) - self.text_lbl.setFont(QFont("monospace")) + self.text_lbl.setFont(QFont('monospace')) self.text_lbl.setEnabled(wrapper.is_editable) self.text_lbl.checkStateChanged.connect(self.__on_state_changed) image_lbl = QLabel(parent=self) - image_lbl.setPixmap(qta_icon("mdi.drag-vertical").pixmap(QSize(20, 20))) + image_lbl.setPixmap(qta_icon('mdi.drag-vertical').pixmap(QSize(20, 20))) - edit_action = QAction("Edit", parent=self) + edit_action = QAction('Edit', parent=self) edit_action.triggered.connect(self.__on_edit) - delete_action = QAction("Delete", parent=self) + delete_action = QAction('Delete', parent=self) delete_action.triggered.connect(self.__on_delete) manage_menu = QMenu(parent=self) manage_menu.addActions([edit_action, delete_action]) manage_button = QPushButton(parent=self) - manage_button.setIcon(qta_icon("mdi.menu", "fa5s.align-justify")) + manage_button.setIcon(qta_icon('mdi.menu', 'fa5s.align-justify')) manage_button.setMenu(manage_menu) manage_button.setEnabled(wrapper.is_editable) if not wrapper.is_editable: - manage_button.setToolTip(self.tr("Manage through settings")) + manage_button.setToolTip(self.tr('Manage through settings')) else: - manage_button.setToolTip(self.tr("Manage")) + manage_button.setToolTip(self.tr('Manage')) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -155,7 +155,7 @@ def __init__(self, wrapper: Wrapper, parent=None): # lk: set object names for the stylesheet self.setObjectName(type(self).__name__) - manage_button.setObjectName(f"{self.objectName()}Button") + manage_button.setObjectName(f'{self.objectName()}Button') def data(self) -> Wrapper: return self.wrapper @@ -207,11 +207,11 @@ def __init__(self, parent=None): self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.setWidgetResizable(True) - self.setProperty("no_kinetic_scroll", True) + self.setProperty('no_kinetic_scroll', True) self.setObjectName(type(self).__name__) - self.horizontalScrollBar().setObjectName(f"{self.objectName()}Bar") - self.verticalScrollBar().setObjectName(f"{self.objectName()}Bar") + self.horizontalScrollBar().setObjectName(f'{self.objectName()}Bar') + self.verticalScrollBar().setObjectName(f'{self.objectName()}Bar') def setWidget(self, w): super().setWidget(w) @@ -245,9 +245,9 @@ def __init__(self, rcore: RareCore, parent=None): super(WrapperSettings, self).__init__(parent=parent) self.core = rcore.core() self.wrappers = rcore.wrappers() - self.app_name: str = "default" + self.app_name: str = 'default' - self.add_button = QPushButton(self.tr("Add wrapper"), self) + self.add_button = QPushButton(self.tr('Add wrapper'), self) self.add_button.clicked.connect(self.__on_add) self.wrapper_scroll = WrapperSettingsScroll(self) @@ -257,14 +257,14 @@ def __init__(self, rcore: RareCore, parent=None): self.wrapper_container.orderChanged.connect(self.__on_order_changed) self.wrapper_scroll.setWidget(self.wrapper_container) - self.wrapper_label = QLabel(self.tr("No wrappers defined"), self) + self.wrapper_label = QLabel(self.tr('No wrappers defined'), self) self.wrapper_label.setFrameStyle(QLabel.Shape.StyledPanel | QLabel.Shadow.Plain) self.wrapper_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.wrapper_container.main_layout.addWidget(self.wrapper_label) # lk: set object names for the stylesheet - self.setObjectName("WrapperSettings") - self.wrapper_label.setObjectName(f"{self.objectName()}Label") + self.setObjectName('WrapperSettings') + self.wrapper_label.setObjectName(f'{self.objectName()}Label') main_layout = QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -324,29 +324,29 @@ def add_user_wrapper(self, wrapper: Wrapper, position: int = -1): if not wrapper: return - if pf.system() in {"Linux", "FreeBSD"}: + if pf.system() in {'Linux', 'FreeBSD'}: compat_cmds = [tool.command() for tool in steam.find_tools()] if wrapper.as_str in compat_cmds: QMessageBox.warning( self, - self.tr("Warning"), - self.tr("Do not insert compatibility tools manually. Add them through Proton settings"), + self.tr('Warning'), + self.tr('Do not insert compatibility tools manually. Add them through Proton settings'), ) return if wrapper.checksum in self.wrappers.get_checksums(self.app_name): QMessageBox.warning( self, - self.tr("Warning"), - self.tr("Wrapper {0} is already in the list").format(wrapper.as_str), + self.tr('Warning'), + self.tr('Wrapper {0} is already in the list').format(wrapper.as_str), ) return if not shutil.which(wrapper.executable): ans = QMessageBox.question( self, - self.tr("Warning"), - self.tr("Wrapper {0} is not in $PATH. Add it anyway?").format(wrapper.executable), + self.tr('Warning'), + self.tr('Wrapper {0} is not in $PATH. Add it anyway?').format(wrapper.executable), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) @@ -401,7 +401,7 @@ def __init__(self, parent=None): super(WrapperContainer, self).__init__(parent=parent) self.setAcceptDrops(True) self.__layout = QHBoxLayout() - self.__drag_widget: Optional[QWidget] = None + self.__drag_widget: QWidget | None = None self.main_layout = QHBoxLayout(self) self.main_layout.addLayout(self.__layout) diff --git a/rare/components/tabs/store/__init__.py b/rare/components/tabs/store/__init__.py index 5958ea6440..c6693b68b3 100644 --- a/rare/components/tabs/store/__init__.py +++ b/rare/components/tabs/store/__init__.py @@ -17,20 +17,20 @@ def __init__(self, core: LegendaryCore, parent=None): self.core = core # self.rcore = rcore self.api = StoreAPI( - self.core.egs.session.headers["Authorization"], + self.core.egs.session.headers['Authorization'], self.core.language_code, self.core.country_code, [], # [i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)] ) self.landing = LandingPage(self.api, parent=self) - self.landing_index = self.addTab(self.landing, self.tr("Store")) + self.landing_index = self.addTab(self.landing, self.tr('Store')) self.search = SearchPage(self.api, parent=self) - self.search_index = self.addTab(self.search, self.tr("Search")) + self.search_index = self.addTab(self.search, self.tr('Search')) self.wishlist = WishlistPage(self.api, parent=self) - self.wishlist_index = self.addTab(self.wishlist, self.tr("Wishlist")) + self.wishlist_index = self.addTab(self.wishlist, self.tr('Wishlist')) def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous() or self.init: diff --git a/rare/components/tabs/store/__main__.py b/rare/components/tabs/store/__main__.py index cf4747acc8..949c7ab339 100644 --- a/rare/components/tabs/store/__main__.py +++ b/rare/components/tabs/store/__main__.py @@ -22,7 +22,7 @@ def __init__(self): self.store_tab.show() -if __name__ == "__main__": +if __name__ == '__main__': import logging # import rare.resources.stylesheets.RareStyle @@ -31,13 +31,13 @@ def __init__(self): logging.getLogger().setLevel(logging.DEBUG) app = QApplication(sys.argv) - app.setApplicationName("Rare") - app.setOrganizationName("Rare") + app.setApplicationName('Rare') + app.setOrganizationName('Rare') - set_style_sheet("") - set_style_sheet("RareStyle") + set_style_sheet('') + set_style_sheet('RareStyle') window = StoreWindow() - window.setWindowTitle(f"{app.applicationName()} - Store") + window.setWindowTitle(f'{app.applicationName()} - Store') window.resize(QSize(1280, 800)) window.show() app.exec() diff --git a/rare/components/tabs/store/api/debug.py b/rare/components/tabs/store/api/debug.py index 27bb92bcdb..170404c9d3 100644 --- a/rare/components/tabs/store/api/debug.py +++ b/rare/components/tabs/store/api/debug.py @@ -1,3 +1,5 @@ +import contextlib + from PySide6.QtCore import Qt from PySide6.QtWidgets import QDialog, QTreeView, QVBoxLayout @@ -12,10 +14,8 @@ def __init__(self, data, parent=None): self.model = QJsonModel(self) self.setModel(self.model) self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) - try: + with contextlib.suppress(Exception): self.model.load(data) - except Exception: - pass self.resizeColumnToContents(0) diff --git a/rare/components/tabs/store/api/models/diesel.py b/rare/components/tabs/store/api/models/diesel.py index 9d50ca255a..589c58f17e 100644 --- a/rare/components/tabs/store/api/models/diesel.py +++ b/rare/components/tabs/store/api/models/diesel.py @@ -1,11 +1,11 @@ from dataclasses import dataclass, field from logging import getLogger -from typing import Any, Dict, List, Tuple, Type +from typing import Any -logger = getLogger("DieselModels") +logger = getLogger('DieselModels') # lk: Typing overloads for unimplemented types -DieselSocialLinks = Dict +DieselSocialLinks = dict @dataclass @@ -14,16 +14,16 @@ class DieselSystemDetailItem: minimum: str = None recommended: str = None title: str = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem": + def from_dict(cls: type['DieselSystemDetailItem'], src: dict[str, Any]) -> 'DieselSystemDetailItem': d = src.copy() return cls( - _type=d.pop("_type", ""), - minimum=d.pop("minimum", ""), - recommended=d.pop("recommended", ""), - title=d.pop("title", ""), + _type=d.pop('_type', ''), + minimum=d.pop('minimum', ''), + recommended=d.pop('recommended', ''), + title=d.pop('title', ''), unmapped=d, ) @@ -31,18 +31,18 @@ def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "Dies @dataclass class DieselSystemDetail: _type: str = None - details: Tuple[DieselSystemDetailItem, ...] = None + details: tuple[DieselSystemDetailItem, ...] = None systemType: str = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail": + def from_dict(cls: type['DieselSystemDetail'], src: dict[str, Any]) -> 'DieselSystemDetail': d = src.copy() - details = tuple(map(DieselSystemDetailItem.from_dict, d.pop("details", []))) + details = tuple(map(DieselSystemDetailItem.from_dict, d.pop('details', []))) return cls( - _type=d.pop("_type", ""), + _type=d.pop('_type', ''), details=details, - systemType=d.pop("systemType", ""), + systemType=d.pop('systemType', ''), unmapped=d, ) @@ -50,19 +50,19 @@ def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSy @dataclass class DieselSystemDetails: _type: str = None - languages: List[str] = None - rating: Dict = None - systems: Tuple[DieselSystemDetail, ...] = None - unmapped: Dict[str, Any] = field(default_factory=dict) + languages: list[str] = None + rating: dict = None + systems: tuple[DieselSystemDetail, ...] = None + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails": + def from_dict(cls: type['DieselSystemDetails'], src: dict[str, Any]) -> 'DieselSystemDetails': d = src.copy() - systems = tuple(map(DieselSystemDetail.from_dict, d.pop("systems", []))) + systems = tuple(map(DieselSystemDetail.from_dict, d.pop('systems', []))) return cls( - _type=d.pop("_type", ""), - languages=d.pop("languages", []), - rating=d.pop("rating", {}), + _type=d.pop('_type', ''), + languages=d.pop('languages', []), + rating=d.pop('rating', {}), systems=systems, unmapped=d, ) @@ -75,17 +75,17 @@ class DieselProductAbout: developerAttribution: str = None publisherAttribution: str = None shortDescription: str = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout": + def from_dict(cls: type['DieselProductAbout'], src: dict[str, Any]) -> 'DieselProductAbout': d = src.copy() return cls( - _type=d.pop("_type", ""), - description=d.pop("description", ""), - developerAttribution=d.pop("developerAttribution", ""), - publisherAttribution=d.pop("publisherAttribution", ""), - shortDescription=d.pop("shortDescription", ""), + _type=d.pop('_type', ''), + description=d.pop('description', ''), + developerAttribution=d.pop('developerAttribution', ''), + publisherAttribution=d.pop('publisherAttribution', ''), + shortDescription=d.pop('shortDescription', ''), unmapped=d, ) @@ -96,18 +96,18 @@ class DieselProductDetail: about: DieselProductAbout = None requirements: DieselSystemDetails = None socialLinks: DieselSocialLinks = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail": + def from_dict(cls: type['DieselProductDetail'], src: dict[str, Any]) -> 'DieselProductDetail': d = src.copy() - about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None - requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None + about = DieselProductAbout.from_dict(x) if (x := d.pop('about'), {}) else None + requirements = DieselSystemDetails.from_dict(x) if (x := d.pop('requirements', {})) else None return cls( - _type=d.pop("_type", ""), + _type=d.pop('_type', ''), about=about, requirements=requirements, - socialLinks=d.pop("socialLinks", {}), + socialLinks=d.pop('socialLinks', {}), unmapped=d, ) @@ -115,32 +115,32 @@ def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselP @dataclass class DieselProduct: _id: str = None - _images_: List[str] = None + _images_: list[str] = None _locale: str = None _slug: str = None _title: str = None _urlPattern: str = None namespace: str = None - pages: Tuple["DieselProduct", ...] = None + pages: tuple['DieselProduct', ...] = None data: DieselProductDetail = None productName: str = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct": + def from_dict(cls: type['DieselProduct'], src: dict[str, Any]) -> 'DieselProduct': d = src.copy() - pages = tuple(map(DieselProduct.from_dict, d.pop("pages", []))) - data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None + pages = tuple(map(DieselProduct.from_dict, d.pop('pages', []))) + data = DieselProductDetail.from_dict(x) if (x := d.pop('data', {})) else None return cls( - _id=d.pop("_id", ""), - _images_=d.pop("_images_", []), - _locale=d.pop("_locale", ""), - _slug=d.pop("_slug", ""), - _title=d.pop("_title", ""), - _urlPattern=d.pop("_urlPattern", ""), - namespace=d.pop("namespace", ""), + _id=d.pop('_id', ''), + _images_=d.pop('_images_', []), + _locale=d.pop('_locale', ''), + _slug=d.pop('_slug', ''), + _title=d.pop('_title', ''), + _urlPattern=d.pop('_urlPattern', ''), + namespace=d.pop('namespace', ''), pages=pages, data=data, - productName=d.pop("productName", ""), + productName=d.pop('productName', ''), unmapped=d, ) diff --git a/rare/components/tabs/store/api/models/query.py b/rare/components/tabs/store/api/models/query.py index d41ff6f6d9..90b5f63783 100644 --- a/rare/components/tabs/store/api/models/query.py +++ b/rare/components/tabs/store/api/models/query.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import List @dataclass @@ -11,71 +10,71 @@ class SearchDateRange: def __str__(self): def fmt_date(date: datetime) -> str: # lk: The formatting accepted by the GraphQL API is either '%Y-%m-%dT%H:%M:%S.000Z' or '%Y-%m-%d' - return datetime.strftime(date, "%Y-%m-%dT%H:%M:%S.000Z") + return datetime.strftime(date, '%Y-%m-%dT%H:%M:%S.000Z') - return f"[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]" + return f'[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]' @dataclass class SearchStoreQuery: - country: str = "US" - category: str = "games/edition/base|bundles/games|editors|software/edition/base" + country: str = 'US' + category: str = 'games/edition/base|bundles/games|editors|software/edition/base' count: int = 30 - keywords: str = "" - language: str = "en" - namespace: str = "" + keywords: str = '' + language: str = 'en' + namespace: str = '' with_mapping: bool = True - item_ns: str = "" - sort_by: str = "releaseDate" - sort_dir: str = "DESC" + item_ns: str = '' + sort_by: str = 'releaseDate' + sort_dir: str = 'DESC' start: int = 0 - tag: List[str] = "" + tag: list[str] = '' release_date: SearchDateRange = field(default_factory=SearchDateRange) with_price: bool = True with_promotions: bool = True - price_range: str = "" + price_range: str = '' free_game: bool = None on_sale: bool = None effective_date: SearchDateRange = field(default_factory=SearchDateRange) def __post_init__(self): - self.locale = f"{self.language}-{self.country}" + self.locale = f'{self.language}-{self.country}' def to_dict(self): payload = { - "allowCountries": self.country, - "category": self.category, - "count": self.count, - "country": self.country, - "keywords": self.keywords, - "locale": self.locale, - "namespace": self.namespace, - "withMapping": self.with_mapping, - "itemNs": self.item_ns, - "sortBy": self.sort_by, - "sortDir": self.sort_dir, - "start": self.start, - "tag": self.tag, - "releaseDate": str(self.release_date), - "withPrice": self.with_price, - "withPromotions": self.with_promotions, - "priceRange": self.price_range, - "freeGame": self.free_game, - "onSale": self.on_sale, - "effectiveDate": str(self.effective_date), + 'allowCountries': self.country, + 'category': self.category, + 'count': self.count, + 'country': self.country, + 'keywords': self.keywords, + 'locale': self.locale, + 'namespace': self.namespace, + 'withMapping': self.with_mapping, + 'itemNs': self.item_ns, + 'sortBy': self.sort_by, + 'sortDir': self.sort_dir, + 'start': self.start, + 'tag': self.tag, + 'releaseDate': str(self.release_date), + 'withPrice': self.with_price, + 'withPromotions': self.with_promotions, + 'priceRange': self.price_range, + 'freeGame': self.free_game, + 'onSale': self.on_sale, + 'effectiveDate': str(self.effective_date), } # payload.pop("withPromotions") - payload.pop("onSale") - if self.price_range == "free": - payload["freeGame"] = True - payload.pop("priceRange") - elif self.price_range.startswith(""): - payload["priceRange"] = self.price_range.replace("", "") + payload.pop('onSale') + if self.price_range == 'free': + payload['freeGame'] = True + payload.pop('priceRange') + elif self.price_range.startswith(''): + payload['priceRange'] = self.price_range.replace('', '') if self.on_sale: - payload["onSale"] = True + payload['onSale'] = True if self.price_range: - payload["effectiveDate"] = self.effective_date + payload['effectiveDate'] = self.effective_date else: - payload.pop("priceRange") + payload.pop('priceRange') return payload diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py index 540e2ac09b..13ae7e039c 100644 --- a/rare/components/tabs/store/api/models/response.py +++ b/rare/components/tabs/store/api/models/response.py @@ -1,22 +1,22 @@ from dataclasses import dataclass, field from datetime import datetime from logging import getLogger -from typing import Any, Dict, List, Tuple, Type +from typing import Any from .utils import parse_date -logger = getLogger("StoreApiModels") +logger = getLogger('StoreApiModels') # lk: Typing overloads for unimplemented types -DieselSocialLinks = Dict +DieselSocialLinks = dict -CatalogNamespaceModel = Dict -CategoryModel = Dict -CustomAttributeModel = Dict -ItemModel = Dict -SellerModel = Dict -PageSandboxModel = Dict -TagModel = Dict +CatalogNamespaceModel = dict +CategoryModel = dict +CustomAttributeModel = dict +ItemModel = dict +SellerModel = dict +PageSandboxModel = dict +TagModel = dict @dataclass @@ -24,39 +24,39 @@ class ImageUrlModel: type: str = None url: str = None - def as_dict(self) -> Dict[str, Any]: - tmp: Dict[str, Any] = {} + def as_dict(self) -> dict[str, Any]: + tmp: dict[str, Any] = {} tmp.update({}) if self.type is not None: - tmp["type"] = self.type + tmp['type'] = self.type if self.url is not None: - tmp["url"] = self.url + tmp['url'] = self.url return tmp @classmethod - def from_dict(cls: Type["ImageUrlModel"], src: Dict[str, Any]) -> "ImageUrlModel": + def from_dict(cls: type['ImageUrlModel'], src: dict[str, Any]) -> 'ImageUrlModel': d = src.copy() - return cls(type=d.pop("type", ""), url=d.pop("url", "")) + return cls(type=d.pop('type', ''), url=d.pop('url', '')) @dataclass class KeyImagesModel: - key_images: Tuple[ImageUrlModel, ...] = None + key_images: tuple[ImageUrlModel, ...] = None tall_types = ( - "DieselGameBoxTall", - "DieselStoreFrontTall", - "OfferImageTall", - "DieselGameBoxLogo", - "Thumbnail", - "ProductLogo", + 'DieselGameBoxTall', + 'DieselStoreFrontTall', + 'OfferImageTall', + 'DieselGameBoxLogo', + 'Thumbnail', + 'ProductLogo', ) wide_types = ( - "DieselGameBoxwide", - "DieselStoreFrontWide", - "OfferImageWide", - "DieselGameBox", - "VaultClosed", - "ProductLogo", + 'DieselGameBoxwide', + 'DieselStoreFrontWide', + 'OfferImageWide', + 'DieselGameBox', + 'VaultClosed', + 'ProductLogo', ) def __getitem__(self, item): @@ -66,17 +66,17 @@ def __bool__(self): return bool(self.key_images) @classmethod - def from_list(cls: Type["KeyImagesModel"], src: List[Dict]): + def from_list(cls: type['KeyImagesModel'], src: list[dict]): d = src.copy() key_images = tuple(map(ImageUrlModel.from_dict, d)) return cls(key_images=key_images) - def available_tall(self) -> List[ImageUrlModel]: + def available_tall(self) -> list[ImageUrlModel]: tall_images = filter(lambda img: img.type in KeyImagesModel.tall_types, self.key_images) tall_images = sorted(tall_images, key=lambda x: KeyImagesModel.tall_types.index(x.type)) return tall_images - def available_wide(self) -> List[ImageUrlModel]: + def available_wide(self) -> list[ImageUrlModel]: wide_images = filter(lambda img: img.type in KeyImagesModel.wide_types, self.key_images) wide_images = sorted(wide_images, key=lambda x: KeyImagesModel.wide_types.index(x.type)) return wide_images @@ -95,9 +95,9 @@ def for_dimensions(self, w: int, h: int) -> ImageUrlModel: return model -CurrencyModel = Dict -FormattedPriceModel = Dict -LineOffersModel = Dict +CurrencyModel = dict +FormattedPriceModel = dict +LineOffersModel = dict @dataclass @@ -109,19 +109,19 @@ class TotalPriceModel: currencyCode: str = None currencyInfo: CurrencyModel = None fmtPrice: FormattedPriceModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["TotalPriceModel"], src: Dict[str, Any]) -> "TotalPriceModel": + def from_dict(cls: type['TotalPriceModel'], src: dict[str, Any]) -> 'TotalPriceModel': d = src.copy() return cls( - discountPrice=d.pop("discountPrice", 0), - originalPrice=d.pop("originalPrice", 0), - voucherDiscount=d.pop("voucherDiscount", 0), - discount=d.pop("discount", 0), - currencyCode=d.pop("currencyCode", ""), - currencyInfo=d.pop("currrencyInfo", {}), - fmtPrice=d.pop("fmtPrice", {}), + discountPrice=d.pop('discountPrice', 0), + originalPrice=d.pop('originalPrice', 0), + voucherDiscount=d.pop('voucherDiscount', 0), + discount=d.pop('discount', 0), + currencyCode=d.pop('currencyCode', ''), + currencyInfo=d.pop('currrencyInfo', {}), + fmtPrice=d.pop('fmtPrice', {}), unmapped=d, ) @@ -130,16 +130,16 @@ def from_dict(cls: Type["TotalPriceModel"], src: Dict[str, Any]) -> "TotalPriceM class GetPriceResModel: totalPrice: TotalPriceModel = None lineOffers: LineOffersModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel": + def from_dict(cls: type['GetPriceResModel'], src: dict[str, Any]) -> 'GetPriceResModel': d = src.copy() - total_price = TotalPriceModel.from_dict(x) if (x := d.pop("totalPrice", {})) else None - return cls(totalPrice=total_price, lineOffers=d.pop("lineOffers", {}), unmapped=d) + total_price = TotalPriceModel.from_dict(x) if (x := d.pop('totalPrice', {})) else None + return cls(totalPrice=total_price, lineOffers=d.pop('lineOffers', {}), unmapped=d) -DiscountSettingModel = Dict +DiscountSettingModel = dict @dataclass @@ -147,44 +147,44 @@ class PromotionalOfferModel: startDate: datetime = None endDate: datetime = None discountSetting: DiscountSettingModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["PromotionalOfferModel"], src: Dict[str, Any]) -> "PromotionalOfferModel": + def from_dict(cls: type['PromotionalOfferModel'], src: dict[str, Any]) -> 'PromotionalOfferModel': d = src.copy() - start_date = parse_date(x) if (x := d.pop("startDate", "")) else None - end_date = parse_date(x) if (x := d.pop("endDate", "")) else None + start_date = parse_date(x) if (x := d.pop('startDate', '')) else None + end_date = parse_date(x) if (x := d.pop('endDate', '')) else None return cls( startDate=start_date, endDate=end_date, - discountSetting=d.pop("discountSetting", {}), + discountSetting=d.pop('discountSetting', {}), unmapped=d, ) @dataclass class PromotionalOffersModel: - promotionalOffers: Tuple[PromotionalOfferModel, ...] = None - unmapped: Dict[str, Any] = field(default_factory=dict) + promotionalOffers: tuple[PromotionalOfferModel, ...] = None + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_list(cls: Type["PromotionalOffersModel"], src: Dict[str, List]) -> "PromotionalOffersModel": + def from_list(cls: type['PromotionalOffersModel'], src: dict[str, list]) -> 'PromotionalOffersModel': d = src.copy() - promotional_offers = tuple(map(PromotionalOfferModel.from_dict, d.pop("promotionalOffers", []))) + promotional_offers = tuple(map(PromotionalOfferModel.from_dict, d.pop('promotionalOffers', []))) return cls(promotionalOffers=promotional_offers, unmapped=d) @dataclass class PromotionsModel: - promotionalOffers: Tuple[PromotionalOffersModel, ...] = None - upcomingPromotionalOffers: Tuple[PromotionalOffersModel, ...] = None - unmapped: Dict[str, Any] = field(default_factory=dict) + promotionalOffers: tuple[PromotionalOffersModel, ...] = None + upcomingPromotionalOffers: tuple[PromotionalOffersModel, ...] = None + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["PromotionsModel"], src: Dict[str, Any]) -> "PromotionsModel": + def from_dict(cls: type['PromotionsModel'], src: dict[str, Any]) -> 'PromotionsModel': d = src.copy() - promotional_offers = tuple(map(PromotionalOffersModel.from_list, d.pop("promotionalOffers", []))) - upcoming_promotional_offers = tuple(map(PromotionalOffersModel.from_list, d.pop("upcomingPromotionalOffers", []))) + promotional_offers = tuple(map(PromotionalOffersModel.from_list, d.pop('promotionalOffers', []))) + upcoming_promotional_offers = tuple(map(PromotionalOffersModel.from_list, d.pop('upcomingPromotionalOffers', []))) return cls( promotionalOffers=promotional_offers, upcomingPromotionalOffers=upcoming_promotional_offers, @@ -195,62 +195,62 @@ def from_dict(cls: Type["PromotionsModel"], src: Dict[str, Any]) -> "PromotionsM @dataclass class CatalogOfferModel: catalogNs: CatalogNamespaceModel = None - categories: List[CategoryModel] = None - customAttributes: List[CustomAttributeModel] = None + categories: list[CategoryModel] = None + customAttributes: list[CustomAttributeModel] = None description: str = None effectiveDate: datetime = None expiryDate: datetime = None id: str = None isCodeRedemptionOnly: bool = None - items: List[ItemModel] = None + items: list[ItemModel] = None keyImages: KeyImagesModel = None namespace: str = None - offerMappings: List[PageSandboxModel] = None + offerMappings: list[PageSandboxModel] = None offerType: str = None price: GetPriceResModel = None productSlug: str = None promotions: PromotionsModel = None seller: SellerModel = None status: str = None - tags: List[TagModel] = None + tags: list[TagModel] = None title: str = None url: str = None urlSlug: str = None viewableDate: datetime = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["CatalogOfferModel"], src: Dict[str, Any]) -> "CatalogOfferModel": + def from_dict(cls: type['CatalogOfferModel'], src: dict[str, Any]) -> 'CatalogOfferModel': d = src.copy() - effective_date = parse_date(x) if (x := d.pop("effectiveDate", "")) else None - expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None - key_images = KeyImagesModel.from_list(d.pop("keyImages", [])) - price = GetPriceResModel.from_dict(x) if (x := d.pop("price", {})) else None - promotions = PromotionsModel.from_dict(x) if (x := d.pop("promotions", {})) else None - viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None + effective_date = parse_date(x) if (x := d.pop('effectiveDate', '')) else None + expiry_date = parse_date(x) if (x := d.pop('expiryDate', '')) else None + key_images = KeyImagesModel.from_list(d.pop('keyImages', [])) + price = GetPriceResModel.from_dict(x) if (x := d.pop('price', {})) else None + promotions = PromotionsModel.from_dict(x) if (x := d.pop('promotions', {})) else None + viewable_date = parse_date(x) if (x := d.pop('viewableDate', '')) else None return cls( - catalogNs=d.pop("catalogNs", {}), - categories=d.pop("categories", []), - customAttributes=d.pop("customAttributes", []), - description=d.pop("description", ""), + catalogNs=d.pop('catalogNs', {}), + categories=d.pop('categories', []), + customAttributes=d.pop('customAttributes', []), + description=d.pop('description', ''), effectiveDate=effective_date, expiryDate=expiry_date, - id=d.pop("id", ""), - isCodeRedemptionOnly=d.pop("isCodeRedemptionOnly", False), - items=d.pop("items", []), + id=d.pop('id', ''), + isCodeRedemptionOnly=d.pop('isCodeRedemptionOnly', False), + items=d.pop('items', []), keyImages=key_images, - namespace=d.pop("namespace", ""), - offerMappings=d.pop("offerMappings", []), - offerType=d.pop("offerType", ""), + namespace=d.pop('namespace', ''), + offerMappings=d.pop('offerMappings', []), + offerType=d.pop('offerType', ''), price=price, - productSlug=d.pop("productSlug", ""), + productSlug=d.pop('productSlug', ''), promotions=promotions, - seller=d.pop("seller", {}), - status=d.pop("status", ""), - tags=d.pop("tags", []), - title=d.pop("title", ""), - url=d.pop("url", ""), - urlSlug=d.pop("urlSlug", ""), + seller=d.pop('seller', {}), + status=d.pop('status', ''), + tags=d.pop('tags', []), + title=d.pop('title', ''), + url=d.pop('url', ''), + urlSlug=d.pop('urlSlug', ''), viewableDate=viewable_date, unmapped=d, ) @@ -266,21 +266,21 @@ class WishlistItemModel: order: Any = None updated: datetime = None offer: CatalogOfferModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["WishlistItemModel"], src: Dict[str, Any]) -> "WishlistItemModel": + def from_dict(cls: type['WishlistItemModel'], src: dict[str, Any]) -> 'WishlistItemModel': d = src.copy() - created = parse_date(x) if (x := d.pop("created", "")) else None - offer = CatalogOfferModel.from_dict(x) if (x := d.pop("offer", {})) else None - updated = parse_date(x) if (x := d.pop("updated", "")) else None + created = parse_date(x) if (x := d.pop('created', '')) else None + offer = CatalogOfferModel.from_dict(x) if (x := d.pop('offer', {})) else None + updated = parse_date(x) if (x := d.pop('updated', '')) else None return cls( created=created, - id=d.pop("id", ""), - namespace=d.pop("namespace", ""), - isFirstTime=d.pop("isFirstTime", False), - offerId=d.pop("offerId", ""), - order=d.pop("order", ""), + id=d.pop('id', ''), + namespace=d.pop('namespace', ''), + isFirstTime=d.pop('isFirstTime', False), + offerId=d.pop('offerId', ''), + order=d.pop('order', ''), updated=updated, offer=offer, unmapped=d, @@ -291,80 +291,80 @@ def from_dict(cls: Type["WishlistItemModel"], src: Dict[str, Any]) -> "WishlistI class PagingModel: count: int = None total: int = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["PagingModel"], src: Dict[str, Any]) -> "PagingModel": + def from_dict(cls: type['PagingModel'], src: dict[str, Any]) -> 'PagingModel': d = src.copy() - count = d.pop("count", 0) - total = d.pop("total", 0) + count = d.pop('count', 0) + total = d.pop('total', 0) return cls(count=count, total=total, unmapped=d) @dataclass class SearchStoreModel: - elements: Tuple[CatalogOfferModel, ...] = None + elements: tuple[CatalogOfferModel, ...] = None paging: PagingModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["SearchStoreModel"], src: Dict[str, Any]) -> "SearchStoreModel": + def from_dict(cls: type['SearchStoreModel'], src: dict[str, Any]) -> 'SearchStoreModel': d = src.copy() - _elements = d.pop("elements", []) + _elements = d.pop('elements', []) elements = tuple(map(CatalogOfferModel.from_dict, _elements)) - paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None + paging = PagingModel.from_dict(x) if (x := d.pop('paging', {})) else None return cls(elements=elements, paging=paging, unmapped=d) @dataclass class CatalogModel: searchStore: SearchStoreModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["CatalogModel"], src: Dict[str, Any]) -> "CatalogModel": + def from_dict(cls: type['CatalogModel'], src: dict[str, Any]) -> 'CatalogModel': d = src.copy() - search_store = SearchStoreModel.from_dict(x) if (x := d.pop("searchStore", {})) else None + search_store = SearchStoreModel.from_dict(x) if (x := d.pop('searchStore', {})) else None return cls(searchStore=search_store, unmapped=d) @dataclass class WishlistItemsModel: - elements: Tuple[WishlistItemModel, ...] = None + elements: tuple[WishlistItemModel, ...] = None paging: PagingModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["WishlistItemsModel"], src: Dict[str, Any]) -> "WishlistItemsModel": + def from_dict(cls: type['WishlistItemsModel'], src: dict[str, Any]) -> 'WishlistItemsModel': d = src.copy() - _elements = d.pop("elements", []) + _elements = d.pop('elements', []) elements = tuple(map(WishlistItemModel.from_dict, _elements)) - paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None + paging = PagingModel.from_dict(x) if (x := d.pop('paging', {})) else None return cls(elements=elements, paging=paging, unmapped=d) @dataclass class RemoveFromWishlistModel: success: bool = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["RemoveFromWishlistModel"], src: Dict[str, Any]) -> "RemoveFromWishlistModel": + def from_dict(cls: type['RemoveFromWishlistModel'], src: dict[str, Any]) -> 'RemoveFromWishlistModel': d = src.copy() - return cls(success=d.pop("success", False), unmapped=d) + return cls(success=d.pop('success', False), unmapped=d) @dataclass class AddToWishlistModel: wishlistItem: WishlistItemModel = None success: bool = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["AddToWishlistModel"], src: Dict[str, Any]) -> "AddToWishlistModel": + def from_dict(cls: type['AddToWishlistModel'], src: dict[str, Any]) -> 'AddToWishlistModel': d = src.copy() - wishlist_item = WishlistItemModel.from_dict(x) if (x := d.pop("wishlistItem", {})) else None - return cls(wishlistItem=wishlist_item, success=d.pop("success", False), unmapped=d) + wishlist_item = WishlistItemModel.from_dict(x) if (x := d.pop('wishlistItem', {})) else None + return cls(wishlistItem=wishlist_item, success=d.pop('success', False), unmapped=d) @dataclass @@ -372,14 +372,14 @@ class WishlistModel: wishlistItems: WishlistItemsModel = None removeFromWishlist: RemoveFromWishlistModel = None addToWishlist: AddToWishlistModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel": + def from_dict(cls: type['WishlistModel'], src: dict[str, Any]) -> 'WishlistModel': d = src.copy() - wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop("wishlistItems", {})) else None - remove_from_wishlist = RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None - add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop("addToWishlist", {})) else None + wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop('wishlistItems', {})) else None + remove_from_wishlist = RemoveFromWishlistModel.from_dict(x) if (x := d.pop('removeFromWishlist', {})) else None + add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop('addToWishlist', {})) else None return cls( wishlistItems=wishlist_items, removeFromWishlist=remove_from_wishlist, @@ -388,7 +388,7 @@ def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel ) -ProductModel = Dict +ProductModel = dict @dataclass @@ -396,14 +396,14 @@ class DataModel: product: ProductModel = None catalog: CatalogModel = None wishlist: WishlistModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel": + def from_dict(cls: type['DataModel'], src: dict[str, Any]) -> 'DataModel': d = src.copy() - catalog = CatalogModel.from_dict(x) if (x := d.pop("Catalog", {})) else None - wishlist = WishlistModel.from_dict(x) if (x := d.pop("Wishlist", {})) else None - return cls(product=d.pop("Product", {}), catalog=catalog, wishlist=wishlist, unmapped=d) + catalog = CatalogModel.from_dict(x) if (x := d.pop('Catalog', {})) else None + wishlist = WishlistModel.from_dict(x) if (x := d.pop('Wishlist', {})) else None + return cls(product=d.pop('Product', {}), catalog=catalog, wishlist=wishlist, unmapped=d) @dataclass @@ -411,28 +411,28 @@ class ErrorModel: message: str = None correlationId: str = None serviceResponse: str = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) def __str__(self): - return f"{self.correlationId} - {self.message}" + return f'{self.correlationId} - {self.message}' @classmethod - def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel": + def from_dict(cls: type['ErrorModel'], src: dict[str, Any]) -> 'ErrorModel': d = src.copy() return cls( - message=d.pop("message", ""), - correlationId=d.pop("correlationId", ""), - serviceResponse=d.pop("serviceResponse", ""), + message=d.pop('message', ''), + correlationId=d.pop('correlationId', ''), + serviceResponse=d.pop('serviceResponse', ''), unmapped=d, ) @dataclass class ExtensionsModel: - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsModel": + def from_dict(cls: type['ExtensionsModel'], src: dict[str, Any]) -> 'ExtensionsModel': d = src.copy() return cls(unmapped=d) @@ -440,15 +440,15 @@ def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsM @dataclass class ResponseModel: data: DataModel = None - errors: Tuple[ErrorModel, ...] = None + errors: tuple[ErrorModel, ...] = None extensions: ExtensionsModel = None - unmapped: Dict[str, Any] = field(default_factory=dict) + unmapped: dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["ResponseModel"], src: Dict[str, Any]) -> "ResponseModel": + def from_dict(cls: type['ResponseModel'], src: dict[str, Any]) -> 'ResponseModel': d = src.copy() - data = DataModel.from_dict(x) if (x := d.pop("data", {})) else None - _errors = d.pop("errors", []) + data = DataModel.from_dict(x) if (x := d.pop('data', {})) else None + _errors = d.pop('errors', []) errors = tuple(map(ErrorModel.from_dict, _errors)) - extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None + extensions = ExtensionsModel.from_dict(x) if (x := d.pop('extensions', {})) else None return cls(data=data, errors=errors, extensions=extensions, unmapped=d) diff --git a/rare/components/tabs/store/constants.py b/rare/components/tabs/store/constants.py index 5a81668236..8adb6da61c 100644 --- a/rare/components/tabs/store/constants.py +++ b/rare/components/tabs/store/constants.py @@ -7,40 +7,40 @@ def __init__(self): super(Constants, self).__init__() self.categories = sorted( [ - (self.tr("Action"), "1216"), - (self.tr("Adventure"), "1117"), - (self.tr("Puzzle"), "1298"), - (self.tr("Open world"), "1307"), - (self.tr("Racing"), "1212"), - (self.tr("RPG"), "1367"), - (self.tr("Shooter"), "1210"), - (self.tr("Strategy"), "1115"), - (self.tr("Survival"), "1080"), - (self.tr("First Person"), "1294"), - (self.tr("Indie"), "1263"), - (self.tr("Simulation"), "1393"), - (self.tr("Sport"), "1283"), + (self.tr('Action'), '1216'), + (self.tr('Adventure'), '1117'), + (self.tr('Puzzle'), '1298'), + (self.tr('Open world'), '1307'), + (self.tr('Racing'), '1212'), + (self.tr('RPG'), '1367'), + (self.tr('Shooter'), '1210'), + (self.tr('Strategy'), '1115'), + (self.tr('Survival'), '1080'), + (self.tr('First Person'), '1294'), + (self.tr('Indie'), '1263'), + (self.tr('Simulation'), '1393'), + (self.tr('Sport'), '1283'), ], key=lambda x: x[0], ) self.platforms = [ - ("MacOS", "9548"), - ("Windows", "9547"), + ('MacOS', '9548'), + ('Windows', '9547'), ] self.others = [ - (self.tr("Single player"), "1370"), - (self.tr("Multiplayer"), "1203"), - (self.tr("Controller"), "9549"), - (self.tr("Co-op"), "1264"), + (self.tr('Single player'), '1370'), + (self.tr('Multiplayer'), '1203'), + (self.tr('Controller'), '9549'), + (self.tr('Co-op'), '1264'), ] self.types = [ - (self.tr("Editor"), "editors"), - (self.tr("Game"), "games/edition/base"), - (self.tr("Bundle"), "bundles/games"), - (self.tr("Add-on"), "addons"), - (self.tr("Apps"), "software/edition/base"), + (self.tr('Editor'), 'editors'), + (self.tr('Game'), 'games/edition/base'), + (self.tr('Bundle'), 'bundles/games'), + (self.tr('Add-on'), 'addons'), + (self.tr('Apps'), 'software/edition/base'), ] @@ -56,7 +56,7 @@ def __init__(self): prePurchaseOfferId """ -__PageSandboxModel = """ +__PageSandboxModel = f""" pageSlug pageType productId @@ -64,25 +64,25 @@ def __init__(self): createdDate updatedDate deletedDate -mappings { - %s -} -""" % (__StorePageMapping) +mappings {{ + {__StorePageMapping} +}} +""" -__CatalogNamespace = """ +__CatalogNamespace = f""" parent displayName store -home: mappings(pageType: "productHome") { - %s -} -addons: mappings(pageType: "addon--cms-hybrid") { - %s -} -offers: mappings(pageType: "offer") { - %s -} -""" % (__PageSandboxModel, __PageSandboxModel, __PageSandboxModel) +home: mappings(pageType: "productHome") {{ + {__PageSandboxModel} +}} +addons: mappings(pageType: "addon--cms-hybrid") {{ + {__PageSandboxModel} +}} +offers: mappings(pageType: "offer") {{ + {__PageSandboxModel} +}} +""" __CatalogItem = """ id @@ -140,7 +140,7 @@ def __init__(self): } """ -__CatalogOffer = """ +__CatalogOffer = f""" title id namespace @@ -150,59 +150,52 @@ def __init__(self): isCodeRedemptionOnly description effectiveDate -keyImages { - %(image)s -} +keyImages {{ + {__Image} +}} currentPrice -seller { +seller {{ id name -} +}} productSlug urlSlug url -tags { +tags {{ id name groupName -} -items { - %(catalog_item)s -} -customAttributes { +}} +items {{ + {__CatalogItem} +}} +customAttributes {{ key value -} -categories { +}} +categories {{ path -} -catalogNs @include(if: $withMapping) { - %(catalog_namespace)s -} -offerMappings @include(if: $withMapping) { - %(page_sandbox_model)s -} -price(country: $country) @include(if: $withPrice) { - %(get_price_res)s -} -promotions(category: $category) @include(if: $withPromotions) { - %(promotions)s -} -""" % { - "image": __Image, - "catalog_item": __CatalogItem, - "catalog_namespace": __CatalogNamespace, - "page_sandbox_model": __PageSandboxModel, - "get_price_res": __GetPriceRes, - "promotions": __Promotions, -} +}} +catalogNs @include(if: $withMapping) {{ + {__CatalogNamespace} +}} +offerMappings @include(if: $withMapping) {{ + {__PageSandboxModel} +}} +price(country: $country) @include(if: $withPrice) {{ + {__GetPriceRes} +}} +promotions(category: $category) @include(if: $withPromotions) {{ + {__Promotions} +}} +""" __Pagination = """ count total """ -SEARCH_STORE_QUERY = """ +SEARCH_STORE_QUERY = f""" query searchStoreQuery( $allowCountries: String $category: String @@ -224,8 +217,8 @@ def __init__(self): $freeGame: Boolean $onSale: Boolean $effectiveDate: String -) { - Catalog { +) {{ + Catalog {{ searchStore( allowCountries: $allowCountries category: $category @@ -244,20 +237,19 @@ def __init__(self): freeGame: $freeGame onSale: $onSale effectiveDate: $effectiveDate - ) { - elements { - %s - } - paging { - %s - } - } - } -} -""" % (__CatalogOffer, __Pagination) + ) {{ + elements {{ + {__CatalogOffer} + }} + paging {{ + {__Pagination} + }} + }} + }} +}} +""" -__WISHLIST_ITEM = ( - """ +__WISHLIST_ITEM = f""" id order created @@ -265,15 +257,12 @@ def __init__(self): updated namespace isFirstTime -offer(locale: $locale) { - %s -} +offer(locale: $locale) {{ + {__CatalogOffer} +}} """ - % __CatalogOffer -) -WISHLIST_QUERY = ( - """ +WISHLIST_QUERY = f""" query wishlistQuery( $country: String! $locale: String @@ -281,21 +270,18 @@ def __init__(self): $withMapping: Boolean = false $withPrice: Boolean = false $withPromotions: Boolean = false -) { - Wishlist { - wishlistItems { - elements { - %s - } - } - } -} +) {{ + Wishlist {{ + wishlistItems {{ + elements {{ + {__WISHLIST_ITEM} + }} + }} + }} +}} """ - % __WISHLIST_ITEM -) -WISHLIST_ADD_QUERY = ( - """ +WISHLIST_ADD_QUERY = f""" mutation addWishlistMutation( $namespace: String! $offerId: String! @@ -305,22 +291,20 @@ def __init__(self): $withMapping: Boolean = false $withPrice: Boolean = false $withPromotions: Boolean = false -) { - Wishlist { +) {{ + Wishlist {{ addToWishlist( namespace: $namespace offerId: $offerId - ) { - wishlistItem { - %s - } + ) {{ + wishlistItem {{ + {__WISHLIST_ITEM} + }} success - } - } -} + }} + }} +}} """ - % __WISHLIST_ITEM -) WISHLIST_REMOVE_QUERY = """ mutation removeFromWishlistMutation( @@ -447,7 +431,7 @@ def __init__(self): def compress_query(query: str) -> str: - return query.replace(" ", "").replace("\n", " ") + return query.replace(' ', '').replace('\n', ' ') game_query = compress_query(SEARCH_STORE_QUERY) @@ -459,5 +443,5 @@ def compress_query(query: str) -> str: store_config_query = compress_query(STORE_CONFIG_QUERY) -if __name__ == "__main__": +if __name__ == '__main__': print(SEARCH_STORE_QUERY) diff --git a/rare/components/tabs/store/landing.py b/rare/components/tabs/store/landing.py index 820c54d42f..c1e72a26a5 100644 --- a/rare/components/tabs/store/landing.py +++ b/rare/components/tabs/store/landing.py @@ -1,6 +1,5 @@ from datetime import datetime, timezone from logging import getLogger -from typing import List from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QHideEvent, QShowEvent @@ -26,7 +25,7 @@ from .widgets.groups import StoreGroup from .widgets.items import StoreItemWidget -logger = getLogger("StoreLanding") +logger = getLogger('StoreLanding') class LandingPage(SlidingStackedWidget, SideTabContents): @@ -77,18 +76,18 @@ def __init__(self, api: StoreAPI, parent=None): self.setLayout(layout) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - self.free_games_now = StoreGroup(self.tr("Free now"), layout=FlowLayout, parent=self) + self.free_games_now = StoreGroup(self.tr('Free now'), layout=FlowLayout, parent=self) self.free_games_now.main_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.free_games_now.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.free_games_next = StoreGroup(self.tr("Free next week"), layout=FlowLayout, parent=self) + self.free_games_next = StoreGroup(self.tr('Free next week'), layout=FlowLayout, parent=self) self.free_games_next.main_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.free_games_next.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.discounts_group = StoreGroup(self.tr("Wishlist discounts"), layout=FlowLayout, parent=self) + self.discounts_group = StoreGroup(self.tr('Wishlist discounts'), layout=FlowLayout, parent=self) self.discounts_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.games_group = StoreGroup(self.tr("Free to play"), FlowLayout, self) + self.games_group = StoreGroup(self.tr('Free to play'), FlowLayout, self) self.games_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.games_group.loading(False) self.games_group.setVisible(False) @@ -112,7 +111,7 @@ def hideEvent(self, a0: QHideEvent) -> None: # TODO: Implement tab unloading return super().hideEvent(a0) - def _update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): + def _update_wishlist_discounts(self, wishlist: list[WishlistItemModel]): for w in self.discounts_group.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.discounts_group.layout().removeWidget(w) w.disconnect(w) @@ -126,7 +125,7 @@ def _update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): self.discounts_group.setVisible(have_discounts) self.discounts_group.loading(False) - def _update_free_games(self, free_games: List[CatalogOfferModel]): + def _update_free_games(self, free_games: list[CatalogOfferModel]): for w in self.free_games_now.findChildren(StoreItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly): self.free_games_now.layout().removeWidget(w) w.disconnect(w) @@ -145,7 +144,7 @@ def _update_free_games(self, free_games: List[CatalogOfferModel]): if item.price.totalPrice.discountPrice == 0: free_now.append(item) continue - if item.title == "Mystery Game": + if item.title == 'Mystery Game': free_next.append(item) continue except KeyError as e: @@ -174,7 +173,7 @@ def _update_free_games(self, free_games: List[CatalogOfferModel]): self.free_games_next.setVisible(bool(free_next)) for item in free_next: w = StoreItemWidget(self.api.cached_manager, item) - if item.title != "Mystery Game": + if item.title != 'Mystery Game': w.show_details.connect(self.show_details) self.free_games_next.layout().addWidget(w) self.free_games_next.loading(False) diff --git a/rare/components/tabs/store/search.py b/rare/components/tabs/store/search.py index aa8e9aedfc..626c601351 100644 --- a/rare/components/tabs/store/search.py +++ b/rare/components/tabs/store/search.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import List from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtWidgets import ( @@ -23,7 +22,7 @@ from .widgets.details import StoreDetailsWidget from .widgets.items import SearchItemWidget -logger = getLogger("Shop") +logger = getLogger('Shop') class SearchPage(SlidingStackedWidget, SideTabContents): @@ -70,16 +69,16 @@ def __init__(self, store_api: StoreAPI, parent=None): self.ui.filter_scrollarea.viewport().setAutoFillBackground(False) self.store_api = store_api - self.price = "" + self.price = '' self.tags = [] self.types = [] self.update_games_allowed = True self.active_search_request = False - self.next_search = "" - self.wishlist: List = [] + self.next_search = '' + self.wishlist: list = [] - self.search_bar = ButtonLineEdit("fa5s.search", placeholder_text=self.tr("Search")) + self.search_bar = ButtonLineEdit('fa5s.search', placeholder_text=self.tr('Search')) self.results_scrollarea = ResultsWidget(self.store_api, self) self.results_scrollarea.show_details.connect(self.show_details) @@ -103,27 +102,27 @@ def show_search_results(self): def init_filter(self): # self.ui.none_price.toggled.connect(lambda: self.prepare_request("") if self.ui.none_price.isChecked() else None) self.ui.none_price.toggled.connect( - (lambda obj: obj.prepare_request("") if self.ui.none_price.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('') if self.ui.none_price.isChecked() else None).__get__(self) ) # self.ui.free_button.toggled.connect(lambda: self.prepare_request("free") if self.ui.free_button.isChecked() else None) self.ui.free_button.toggled.connect( - (lambda obj: obj.prepare_request("free") if self.ui.free_button.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('free') if self.ui.free_button.isChecked() else None).__get__(self) ) # self.ui.under10.toggled.connect(lambda: self.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None) self.ui.under10.toggled.connect( - (lambda obj: obj.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('[0, 1000)') if self.ui.under10.isChecked() else None).__get__(self) ) # self.ui.under20.toggled.connect(lambda: self.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None) self.ui.under20.toggled.connect( - (lambda obj: obj.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('[0, 2000)') if self.ui.under20.isChecked() else None).__get__(self) ) # self.ui.under30.toggled.connect(lambda: self.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None) self.ui.under30.toggled.connect( - (lambda obj: obj.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('[0, 3000)') if self.ui.under30.isChecked() else None).__get__(self) ) # self.ui.above.toggled.connect(lambda: self.prepare_request("[1499,]") if self.ui.above.isChecked() else None) self.ui.above.toggled.connect( - (lambda obj: obj.prepare_request("[1499,]") if self.ui.above.isChecked() else None).__get__(self) + (lambda obj: obj.prepare_request('[1499,]') if self.ui.above.isChecked() else None).__get__(self) ) # self.on_discount.toggled.connect( # lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None @@ -173,8 +172,8 @@ def prepare_request( price: str = None, added_tag: int = 0, removed_tag: int = 0, - added_type: str = "", - removed_type: str = "", + added_type: str = '', + removed_type: str = '', ): if not self.update_games_allowed: return @@ -207,10 +206,10 @@ def prepare_request( price_range=self.price, on_sale=self.ui.on_discount.isChecked(), ) - browse_model.tag = "|".join(self.tags) + browse_model.tag = '|'.join(self.tags) if self.types: - browse_model.category = "|".join(self.types) + browse_model.category = '|'.join(self.types) self.store_api.browse_games(browse_model, self.show_games) @@ -252,7 +251,7 @@ def __init__(self, store_api, parent=None): def load_results(self, text: str): self.setEnabled(False) - if text != "": + if text != '': self.store_api.search_game(text, self.show_results) def show_results(self, results: dict): @@ -266,7 +265,7 @@ def show_results(self, results: dict): w.deleteLater() if not results: - self.results_layout.addWidget(QLabel(self.tr("No results found"))) + self.results_layout.addWidget(QLabel(self.tr('No results found'))) else: for res in results: w = SearchItemWidget(self.store_api.cached_manager, res, parent=self.results_container) diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py index 1d513b74aa..f7f8cb02ec 100644 --- a/rare/components/tabs/store/store_api.py +++ b/rare/components/tabs/store/store_api.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from logging import getLogger -from typing import Callable, Tuple from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QApplication @@ -11,7 +11,7 @@ wishlist_remove_query, ) from rare.utils.paths import cache_dir -from rare.utils.qt_requests import QtRequests +from rare.utils.qrequests import QRequests from .api.models.diesel import DieselProduct from .api.models.query import SearchStoreQuery @@ -19,12 +19,12 @@ ResponseModel, ) -graphql_url = "https://store.epicgames.com/graphql" +graphql_url = 'https://store.epicgames.com/graphql' # graphql_url = "https://launcher.store.epicgames.com/graphql" def DEBUG() -> bool: - return "--debug" in QApplication.arguments() + return '--debug' in QApplication.arguments() class StoreAPI(QObject): @@ -36,10 +36,10 @@ def __init__(self, token, language: str, country: str, installed): self.token = token self.language_code: str = language self.country_code: str = country - self.locale = f"{self.language_code}-{self.country_code}" - self.manager = QtRequests(parent=self) - self.authed_manager = QtRequests(token=token, parent=self) - self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self) + self.locale = f'{self.language_code}-{self.country_code}' + self.manager = QRequests(parent=self) + self.authed_manager = QRequests(token=token, parent=self) + self.cached_manager = QRequests(cache=str(cache_dir().joinpath('store')), parent=self) self.installed = installed @@ -47,11 +47,11 @@ def __init__(self, token, language: str, country: str, installed): self.next_browse_request = tuple(()) def get_free(self, callback: Callable): - url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions" + url = 'https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions' params = { - "locale": self.locale, - "country": self.country_code, - "allowCountries": self.country_code, + 'locale': self.locale, + 'country': self.country_code, + 'allowCountries': self.country_code, } self.manager.get(url, lambda data: self.__handle_free_games(data, callback), params=params) @@ -66,7 +66,7 @@ def __handle_free_games(self, data, callback: Callable): if DEBUG(): raise e elements = False - self.logger.error("Free games request failed with: %s", e) + self.logger.error('Free games request failed with: %s', e) callback(elements) def get_wishlist(self, callback: Callable): @@ -74,16 +74,16 @@ def get_wishlist(self, callback: Callable): graphql_url, lambda data: self.__handle_wishlist(data, callback), { - "query": wishlist_query, - "variables": { - "country": self.country_code, - "locale": self.locale, - "withPrice": True, + 'query': wishlist_query, + 'variables': { + 'country': self.country_code, + 'locale': self.locale, + 'withPrice': True, }, }, ) - def __handle_wishlist(self, data, callback: Callable[[Tuple], None]): + def __handle_wishlist(self, data, callback: Callable[[tuple], None]): try: response = ResponseModel.from_dict(data) if response.errors: @@ -94,30 +94,30 @@ def __handle_wishlist(self, data, callback: Callable[[Tuple], None]): if DEBUG(): raise e elements = False - self.logger.error("Wishlist request failed with: %s", e) + self.logger.error('Wishlist request failed with: %s', e) callback(elements) def search_game(self, name, callback: Callable): payload = { - "query": search_query, - "variables": { - "category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 20, - "country": self.country_code, - "keywords": name, - "locale": self.locale, - "sortDir": "DESC", - "allowCountries": self.country_code, - "start": 0, - "tag": "", - "withMapping": False, - "withPrice": True, + 'query': search_query, + 'variables': { + 'category': 'games/edition/base|bundles/games|editors|software/edition/base', + 'count': 20, + 'country': self.country_code, + 'keywords': name, + 'locale': self.locale, + 'sortDir': 'DESC', + 'allowCountries': self.country_code, + 'start': 0, + 'tag': '', + 'withMapping': False, + 'withPrice': True, }, } self.manager.post(graphql_url, lambda data: self.__handle_search(data, callback), payload) - def __handle_search(self, data, callback: Callable[[Tuple], None]): + def __handle_search(self, data, callback: Callable[[tuple], None]): try: response = ResponseModel.from_dict(data) if response.errors: @@ -128,7 +128,7 @@ def __handle_search(self, data, callback: Callable[[Tuple], None]): if DEBUG(): raise e elements = False - self.logger.error("Search request failed with: %s", e) + self.logger.error('Search request failed with: %s', e) callback(elements) def browse_games(self, browse_model: SearchStoreQuery, callback): @@ -136,7 +136,7 @@ def browse_games(self, browse_model: SearchStoreQuery, callback): self.next_browse_request = (browse_model, callback) return self.browse_active = True - payload = {"query": search_query, "variables": browse_model.to_dict()} + payload = {'query': search_query, 'variables': browse_model.to_dict()} self.manager.post( graphql_url, lambda data: self.__handle_browse_games(data, callback), @@ -158,7 +158,7 @@ def __handle_browse_games(self, data, callback): if DEBUG(): raise e elements = False - self.logger.error("Browse request failed with: %s", e) + self.logger.error('Browse request failed with: %s', e) callback(elements) else: self.browse_games(*self.next_browse_request) # pylint: disable=E1120 @@ -179,9 +179,9 @@ def __make_api_query(self): pass def get_game_config_cms(self, slug: str, is_bundle: bool, callback: Callable): - url = "https://store-content.ak.epicgames.com/api" - url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" - self.logger.debug("Quering game config: %s", url) + url = 'https://store-content.ak.epicgames.com/api' + url += f'/{self.locale}/content/{"products" if not is_bundle else "bundles"}/{slug}' + self.logger.debug('Quering game config: %s', url) self.manager.get(url, lambda data: self.__handle_get_game(data, callback)) def __handle_get_game(self, data, callback): @@ -197,12 +197,12 @@ def __handle_get_game(self, data, callback): # needs a captcha def add_to_wishlist(self, namespace, offer_id, callback: Callable): payload = { - "query": wishlist_add_query, - "variables": { - "offerId": offer_id, - "namespace": namespace, - "country": self.country_code, - "locale": self.locale, + 'query': wishlist_add_query, + 'variables': { + 'offerId': offer_id, + 'namespace': namespace, + 'country': self.country_code, + 'locale': self.locale, }, } self.authed_manager.post( @@ -221,18 +221,18 @@ def _handle_add_to_wishlist(self, data, callback): except Exception as e: if DEBUG(): raise e - self.logger.error("Add to wishlist request failed with: %s", e) + self.logger.error('Add to wishlist request failed with: %s', e) success = False callback(success) self.update_wishlist.emit() def remove_from_wishlist(self, namespace, offer_id, callback: Callable): payload = { - "query": wishlist_remove_query, - "variables": { - "offerId": offer_id, - "namespace": namespace, - "operation": "REMOVE", + 'query': wishlist_remove_query, + 'variables': { + 'offerId': offer_id, + 'namespace': namespace, + 'operation': 'REMOVE', }, } self.authed_manager.post( @@ -251,7 +251,7 @@ def _handle_remove_from_wishlist(self, data, callback): except Exception as e: if DEBUG(): raise e - self.logger.error("Remove from wishlist request failed with: %s", e) + self.logger.error('Remove from wishlist request failed with: %s', e) success = False callback(success) self.update_wishlist.emit() diff --git a/rare/components/tabs/store/widgets/details.py b/rare/components/tabs/store/widgets/details.py index 6221055775..36f4efe3b0 100644 --- a/rare/components/tabs/store/widgets/details.py +++ b/rare/components/tabs/store/widgets/details.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import List from PySide6.QtCore import Qt, QUrl, Signal, Slot from PySide6.QtGui import QDesktopServices, QKeyEvent @@ -25,14 +24,14 @@ from rare.widgets.image_widget import LoadingSpinnerImageWidget from rare.widgets.side_tab import SideTabContents, SideTabWidget -logger = getLogger("StoreDetails") +logger = getLogger('StoreDetails') class StoreDetailsWidget(QWidget, SideTabContents): back_clicked: Signal = Signal() # TODO Design - def __init__(self, installed: List, store_api: StoreAPI, parent=None): + def __init__(self, installed: list, store_api: StoreAPI, parent=None): super(StoreDetailsWidget, self).__init__(parent=parent) self.implements_scrollarea = True @@ -60,18 +59,18 @@ def __init__(self, installed: List, store_api: StoreAPI, parent=None): self.ui.requirements_layout.addWidget(self.requirements_tabs) self.ui.requirements_layout.setAlignment(Qt.AlignmentFlag.AlignBottom) - self.ui.back_button.setIcon(qta_icon("fa.chevron-left", "fa5s.chevron-left")) + self.ui.back_button.setIcon(qta_icon('fa.chevron-left', 'fa5s.chevron-left')) self.ui.back_button.clicked.connect(self.back_clicked) self.setDisabled(False) - def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]): - if wishlist and wishlist[0] == "error": + def handle_wishlist_update(self, wishlist: list[CatalogOfferModel]): + if wishlist and wishlist[0] == 'error': return self.wishlist = [game.id for game in wishlist] if self.id_str in self.wishlist: self.in_wishlist = True - self.ui.wishlist_button.setText(self.tr("Remove from Wishlist")) + self.ui.wishlist_button.setText(self.tr('Remove from Wishlist')) else: self.in_wishlist = False @@ -91,29 +90,29 @@ def update_game(self, offer: CatalogOfferModel): slug = offer.productSlug if not slug: for mapping in offer.offerMappings: - if mapping["pageType"] == "productHome": - slug = mapping["pageSlug"] + if mapping['pageType'] == 'productHome': + slug = mapping['pageSlug'] break else: - logger.error("Could not get page information") - slug = "" - if "/home" in slug: - slug = slug.replace("/home", "") + logger.error('Could not get page information') + slug = '' + if '/home' in slug: + slug = slug.replace('/home', '') self.slug = slug if offer.namespace in self.installed: - self.ui.store_button.setText(self.tr("Show Game on Epic Page")) + self.ui.store_button.setText(self.tr('Show Game on Epic Page')) self.ui.status.setVisible(True) else: - self.ui.store_button.setText(self.tr("Buy Game in Epic Games Store")) + self.ui.store_button.setText(self.tr('Buy Game in Epic Games Store')) self.ui.status.setVisible(False) - self.ui.original_price.setText(self.tr("Loading")) + self.ui.original_price.setText(self.tr('Loading')) # self.title.setText(self.tr("Loading")) # self.image.setPixmap(QPixmap()) is_bundle = False for i in offer.categories: - if "bundles" in i.get("path", ""): + if 'bundles' in i.get('path', ''): is_bundle = True # init API request @@ -128,17 +127,17 @@ def add_to_wishlist(self): self.store_api.add_to_wishlist( self.catalog_offer.namespace, self.catalog_offer.id, - lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist")) + lambda success: self.ui.wishlist_button.setText(self.tr('Remove from wishlist')) if success - else self.ui.wishlist_button.setText("Something went wrong"), + else self.ui.wishlist_button.setText('Something went wrong'), ) else: self.store_api.remove_from_wishlist( self.catalog_offer.namespace, self.catalog_offer.id, - lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist")) + lambda success: self.ui.wishlist_button.setText(self.tr('Add to wishlist')) if success - else self.ui.wishlist_button.setText("Something went wrong"), + else self.ui.wishlist_button.setText('Something went wrong'), ) def data_received(self, product: DieselProduct): @@ -152,17 +151,17 @@ def data_received(self, product: DieselProduct): logger.error(str(e)) self.ui.original_price.setFont(self.font()) - price = self.catalog_offer.price.totalPrice.fmtPrice["originalPrice"] - discount_price = self.catalog_offer.price.totalPrice.fmtPrice["discountPrice"] - if price == "0" or price == 0: - self.ui.original_price.setText(self.tr("Free")) + price = self.catalog_offer.price.totalPrice.fmtPrice['originalPrice'] + discount_price = self.catalog_offer.price.totalPrice.fmtPrice['discountPrice'] + if price == '0' or price == 0: + self.ui.original_price.setText(self.tr('Free')) else: self.ui.original_price.setText(price) if price != discount_price: font = self.font() font.setStrikeOut(True) self.ui.original_price.setFont(font) - self.ui.discount_price.setText(discount_price if discount_price != "0" else self.tr("Free")) + self.ui.discount_price.setText(discount_price if discount_price != '0' else self.tr('Free')) self.ui.discount_price.setVisible(True) else: self.ui.discount_price.setVisible(False) @@ -185,9 +184,9 @@ def data_received(self, product: DieselProduct): # self.image_stack.setCurrentIndex(0) about = product_data.about description = about.description - description = description.replace("### ", "##### ") - description = description.replace("## ", "#### ") - description = description.replace("# ", "### ") + description = description.replace('### ', '##### ') + description = description.replace('## ', '#### ') + description = description.replace('# ', '### ') self.ui.description_label.setMarkdown(description) self.ui.developer.setText(about.developerAttribution) # try: @@ -197,8 +196,8 @@ def data_received(self, product: DieselProduct): # self.ui.dev.setText(self.game.developer) # except KeyError: # pass - tags = product_data.unmapped["meta"].get("tags", []) - self.ui.tags.setText(", ".join(tags)) + tags = product_data.unmapped['meta'].get('tags', []) + self.ui.tags.setText(', '.join(tags)) # clear Layout for b in self.ui.social_links.findChildren(SocialButton, options=Qt.FindChildOption.FindDirectChildrenOnly): @@ -209,16 +208,16 @@ def data_received(self, product: DieselProduct): links = product_data.socialLinks link_count = 0 for name, url in links.items(): - if name == "_type": + if name == '_type': continue - name = name.replace("link", "").lower() - if name == "homepage": - icn = qta_icon("mdi.web", "fa5s.globe", scale_factor=1.2) - elif name == "title": - icn = qta_icon("mdi.home-circle", "fa5s.home", scale_factor=1.2) + name = name.replace('link', '').lower() + if name == 'homepage': + icn = qta_icon('mdi.web', 'fa5s.globe', scale_factor=1.2) + elif name == 'title': + icn = qta_icon('mdi.home-circle', 'fa5s.home', scale_factor=1.2) else: try: - icn = qta_icon(f"mdi.{name}", f"fa5b.{name}", scale_factor=1.2) + icn = qta_icon(f'mdi.{name}', f'fa5b.{name}', scale_factor=1.2) except Exception as e: logger.error(str(e)) continue @@ -237,7 +236,7 @@ def data_received(self, product: DieselProduct): # self.wishlist.append(game["offer"]["title"]) def button_clicked(self): - QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.store_api.language_code}/p/{self.slug}")) + QDesktopServices.openUrl(QUrl(f'https://www.epicgames.com/store/{self.store_api.language_code}/p/{self.slug}')) def keyPressEvent(self, a0: QKeyEvent): if a0.key() == Qt.Key.Key_Escape: @@ -246,7 +245,7 @@ def keyPressEvent(self, a0: QKeyEvent): class SocialButton(QPushButton): def __init__(self, icn, url, parent=None): - super(SocialButton, self).__init__(icn, "", parent=parent) + super(SocialButton, self).__init__(icn, '', parent=parent) self.setFixedSize(36, 36) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.url = url @@ -267,9 +266,9 @@ def __init__(self, system: DieselSystemDetail, parent=None): bold_font.setBold(True) req_layout = QGridLayout(self) - min_label = QLabel(self.tr("Minimum"), parent=self) + min_label = QLabel(self.tr('Minimum'), parent=self) min_label.setFont(bold_font) - rec_label = QLabel(self.tr("Recommend"), parent=self) + rec_label = QLabel(self.tr('Recommend'), parent=self) rec_label.setFont(bold_font) req_layout.addWidget(min_label, 0, 1) req_layout.addWidget(rec_label, 0, 2) diff --git a/rare/components/tabs/store/widgets/icon_widget.py b/rare/components/tabs/store/widgets/icon_widget.py index c7905ae7dd..30513deb1b 100644 --- a/rare/components/tabs/store/widgets/icon_widget.py +++ b/rare/components/tabs/store/widgets/icon_widget.py @@ -9,7 +9,7 @@ ) -class IconWidget(object): +class IconWidget: def __init__(self): self.mini_widget: QWidget = None self.title_label: QLabel = None @@ -20,12 +20,12 @@ def __init__(self): def setupUi(self, widget: QWidget): # on-hover popup self.mini_widget = QWidget(parent=widget) - self.mini_widget.setObjectName(f"{type(self).__name__}MiniWidget") + self.mini_widget.setObjectName(f'{type(self).__name__}MiniWidget') self.mini_widget.setFixedHeight(int(widget.height() // 3)) # game title self.title_label = QLabel(parent=self.mini_widget) - self.title_label.setObjectName(f"{type(self).__name__}TitleLabel") + self.title_label.setObjectName(f'{type(self).__name__}TitleLabel') self.title_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.title_label.setAlignment(Qt.AlignmentFlag.AlignTop) self.title_label.setAutoFillBackground(False) @@ -33,17 +33,17 @@ def setupUi(self, widget: QWidget): # information below title self.developer_label = QLabel(parent=self.mini_widget) - self.developer_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.developer_label.setObjectName(f'{type(self).__name__}TooltipLabel') self.developer_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.developer_label.setAutoFillBackground(False) self.price_label = QLabel(parent=self.mini_widget) - self.price_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.price_label.setObjectName(f'{type(self).__name__}TooltipLabel') self.price_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.price_label.setAutoFillBackground(False) self.discount_label = QLabel(parent=self.mini_widget) - self.discount_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.discount_label.setObjectName(f'{type(self).__name__}TooltipLabel') self.discount_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.discount_label.setAutoFillBackground(False) diff --git a/rare/components/tabs/store/widgets/items.py b/rare/components/tabs/store/widgets/items.py index f496f8d755..69233c068f 100644 --- a/rare/components/tabs/store/widgets/items.py +++ b/rare/components/tabs/store/widgets/items.py @@ -7,18 +7,18 @@ from rare.components.tabs.store.api.models.response import CatalogOfferModel from rare.models.image import ImageSize from rare.utils.misc import qta_icon -from rare.utils.qt_requests import QtRequests +from rare.utils.qrequests import QRequests from rare.widgets.image_widget import LoadingSpinnerImageWidget from .icon_widget import IconWidget -logger = getLogger("StoreWidgets") +logger = getLogger('StoreWidgets') class ItemWidgetSpinner(LoadingSpinnerImageWidget): show_details = Signal(CatalogOfferModel) - def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None): + def __init__(self, manager: QRequests, catalog_game: CatalogOfferModel = None, parent=None): super(ItemWidgetSpinner, self).__init__(manager, parent=parent) self.ui = IconWidget() self.catalog_game = catalog_game @@ -32,7 +32,7 @@ def mousePressEvent(self, a0: QMouseEvent) -> None: class StoreItemWidget(ItemWidgetSpinner): - def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None): + def __init__(self, manager: QRequests, catalog_game: CatalogOfferModel = None, parent=None): super(StoreItemWidget, self).__init__(manager, catalog_game, parent=parent) self.setFixedSize(ImageSize.DisplayWide) self.ui.setupUi(self) @@ -41,25 +41,25 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, def init_ui(self, game: CatalogOfferModel): if not game: - self.ui.title_label.setText(self.tr("An error occurred")) + self.ui.title_label.setText(self.tr('An error occurred')) return self.ui.title_label.setText(game.title) for attr in game.customAttributes: - if attr["key"] == "developerName": - developer = attr["value"] + if attr['key'] == 'developerName': + developer = attr['value'] break else: - developer = game.seller["name"] + developer = game.seller['name'] self.ui.developer_label.setText(developer) - price = game.price.totalPrice.fmtPrice["originalPrice"] - discount_price = game.price.totalPrice.fmtPrice["discountPrice"] - self.ui.price_label.setText(f"{price if price != '0' else self.tr('Free')}") + price = game.price.totalPrice.fmtPrice['originalPrice'] + discount_price = game.price.totalPrice.fmtPrice['discountPrice'] + self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') if price != discount_price: font = self.ui.price_label.font() font.setStrikeOut(True) self.ui.price_label.setFont(font) - self.ui.discount_label.setText(f"{discount_price if discount_price != '0' else self.tr('Free')}") + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') else: self.ui.discount_label.setVisible(False) @@ -77,7 +77,7 @@ def init_ui(self, game: CatalogOfferModel): class SearchItemWidget(ItemWidgetSpinner): - def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None): + def __init__(self, manager: QRequests, catalog_game: CatalogOfferModel, parent=None): super(SearchItemWidget, self).__init__(manager, catalog_game, parent=parent) self.setFixedSize(ImageSize.LibraryTall) self.ui.setupUi(self) @@ -87,14 +87,14 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent= self.ui.title_label.setText(catalog_game.title) - price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] - discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] - self.ui.price_label.setText(f"{price if price != '0' else self.tr('Free')}") + price = catalog_game.price.totalPrice.fmtPrice['originalPrice'] + discount_price = catalog_game.price.totalPrice.fmtPrice['discountPrice'] + self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') if price != discount_price: font = self.ui.price_label.font() font.setStrikeOut(True) self.ui.price_label.setFont(font) - self.ui.discount_label.setText(f"{discount_price if discount_price != '0' else self.tr('Free')}") + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') else: self.ui.discount_label.setVisible(False) @@ -102,35 +102,35 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent= class WishlistItemWidget(ItemWidgetSpinner): delete_from_wishlist = Signal(CatalogOfferModel) - def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None): + def __init__(self, manager: QRequests, catalog_game: CatalogOfferModel, parent=None): super(WishlistItemWidget, self).__init__(manager, catalog_game, parent=parent) self.setFixedSize(ImageSize.DisplayWide) self.ui.setupUi(self) for attr in catalog_game.customAttributes: - if attr["key"] == "developerName": - developer = attr["value"] + if attr['key'] == 'developerName': + developer = attr['value'] break else: - developer = catalog_game.seller["name"] - original_price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] - discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] + developer = catalog_game.seller['name'] + original_price = catalog_game.price.totalPrice.fmtPrice['originalPrice'] + discount_price = catalog_game.price.totalPrice.fmtPrice['discountPrice'] self.ui.title_label.setText(catalog_game.title) self.ui.developer_label.setText(developer) - self.ui.price_label.setText(f"{original_price if original_price != '0' else self.tr('Free')}") + self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}') if original_price != discount_price: font = self.ui.price_label.font() font.setStrikeOut(True) self.ui.price_label.setFont(font) - self.ui.discount_label.setText(f"{discount_price if discount_price != '0' else self.tr('Free')}") + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') else: self.ui.discount_label.setVisible(False) key_images = catalog_game.keyImages self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) self.delete_button = QPushButton(self) - self.delete_button.setIcon(qta_icon("mdi.delete", color="white")) + self.delete_button.setIcon(qta_icon('mdi.delete', color='white')) self.delete_button.clicked.connect(self._on_delete_clicked) self.layout().insertWidget(0, self.delete_button, alignment=Qt.AlignmentFlag.AlignRight) diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index 8bf2af4274..2f7632336a 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -1,5 +1,4 @@ from enum import IntEnum -from typing import List from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtGui import QShowEvent @@ -74,24 +73,24 @@ def __init__(self, api: StoreAPI, parent=None): self.ui.container_layout.addLayout(self.wishlist_layout, stretch=1) filters = { - WishlistFilter.NONE: self.tr("All items"), - WishlistFilter.DISCOUNT: self.tr("Discount"), + WishlistFilter.NONE: self.tr('All items'), + WishlistFilter.DISCOUNT: self.tr('Discount'), } for data, text in filters.items(): self.ui.filter_combo.addItem(text, data) self.ui.filter_combo.currentIndexChanged.connect(self.filter_wishlist) sortings = { - WishlistOrder.NAME: self.tr("Name"), - WishlistOrder.PRICE: self.tr("Price"), - WishlistOrder.DISCOUNT: self.tr("Discount"), - WishlistOrder.DEVELOPER: self.tr("Developer"), + WishlistOrder.NAME: self.tr('Name'), + WishlistOrder.PRICE: self.tr('Price'), + WishlistOrder.DISCOUNT: self.tr('Discount'), + WishlistOrder.DEVELOPER: self.tr('Developer'), } for data, text in sortings.items(): self.ui.order_combo.addItem(text, data) self.ui.order_combo.currentIndexChanged.connect(self.order_wishlist) - self.ui.reload_button.setIcon(qta_icon("fa.refresh", "fa5s.sync", color="white")) + self.ui.reload_button.setIcon(qta_icon('fa.refresh', 'fa5s.sync', color='white')) self.ui.reload_button.clicked.connect(self._update_widget) self.ui.reverse_check.stateChanged.connect(self._on_reverse_changed) @@ -112,7 +111,7 @@ def delete_from_wishlist(self, game: CatalogOfferModel): game.id, lambda success: self._update_widget() if success - else QMessageBox.warning(self, "Error", self.tr("Could not remove game from wishlist")), + else QMessageBox.warning(self, 'Error', self.tr('Could not remove game from wishlist')), ) self.update_wishlist.emit() @@ -133,7 +132,7 @@ def filter_wishlist(self, index: int = int(WishlistFilter.NONE)): __ordering = { WishlistOrder.NAME: lambda x: x.catalog_game.title, WishlistOrder.PRICE: lambda x: x.catalog_game.price.totalPrice.discountPrice, - WishlistOrder.DEVELOPER: lambda x: x.catalog_game.seller["name"], + WishlistOrder.DEVELOPER: lambda x: x.catalog_game.seller['name'], WishlistOrder.DISCOUNT: lambda x: 1 - (x.catalog_game.price.totalPrice.discountPrice / x.catalog_game.price.totalPrice.originalPrice), } @@ -155,8 +154,8 @@ def _on_reverse_changed(self, state: Qt.CheckState): self.order_wishlist(self.ui.order_combo.currentIndex()) @Slot(object) - def set_wishlist(self, wishlist: List[WishlistItemModel] = None): - if wishlist and wishlist[0] == "error": + def set_wishlist(self, wishlist: list[WishlistItemModel] = None): + if wishlist and wishlist[0] == 'error': return widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindChildOption.FindDirectChildrenOnly) diff --git a/rare/components/tabs/tab_widgets.py b/rare/components/tabs/tab_widgets.py index 26a950141a..4ac53973e9 100644 --- a/rare/components/tabs/tab_widgets.py +++ b/rare/components/tabs/tab_widgets.py @@ -7,7 +7,7 @@ class MainTabBar(QTabBar): def __init__(self, parent=None): super(MainTabBar, self).__init__(parent=parent) self.setObjectName(type(self).__name__) - self.setProperty("drawBase", False) + self.setProperty('drawBase', False) font = self.font() font.setPointSize(font.pointSize() + 1) diff --git a/rare/components/tray_icon.py b/rare/components/tray_icon.py index a17dfe851a..a751425d32 100644 --- a/rare/components/tray_icon.py +++ b/rare/components/tray_icon.py @@ -1,5 +1,4 @@ from logging import getLogger -from typing import List from PySide6.QtCore import Signal, Slot from PySide6.QtGui import QAction, QIcon @@ -8,7 +7,7 @@ from rare.models.settings import RareAppSettings, app_settings from rare.shared import RareCore -logger = getLogger("TrayIcon") +logger = getLogger('TrayIcon') class TrayIcon(QSystemTrayIcon): @@ -25,7 +24,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.signals = rcore.signals() self.core = rcore.core() - self.setIcon(QIcon(":/images/icon.png")) + self.setIcon(QIcon(':/images/icon.png')) self.setVisible(True) self.setToolTip(QApplication.applicationName()) @@ -36,17 +35,17 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.menu.addAction(self.show_action) self.menu.addSeparator() - self.text_action = QAction("Quick launch") + self.text_action = QAction('Quick launch') self.text_action.setEnabled(False) self.menu.addAction(self.text_action) # We need to reference this separator to add game actions before it self.separator = self.menu.addSeparator() - self.exit_action = QAction(self.tr("Quit")) + self.exit_action = QAction(self.tr('Quit')) self.exit_action.triggered.connect(self._on_exit_triggered) self.menu.addAction(self.exit_action) - self.game_actions: List[QAction] = [] + self.game_actions: list[QAction] = [] self.update_actions() self.setContextMenu(self.menu) @@ -55,7 +54,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.signals.application.notify.connect(self.notify) self.signals.application.update_tray.connect(self.update_actions) - def last_played(self) -> List: + def last_played(self) -> list: last_played = [game for game in self.rcore.games if (game.metadata and game.is_installed)] last_played.sort(key=lambda g: g.metadata.last_played, reverse=True) return last_played[:5] @@ -68,7 +67,7 @@ def _on_exit_triggered(self): def notify(self, title: str, body: str): if self.settings.get_value(app_settings.notification): self.showMessage( - f"{title} - {QApplication.applicationName()}", + f'{title} - {QApplication.applicationName()}', body, QSystemTrayIcon.MessageIcon.Information, 4000, @@ -86,7 +85,7 @@ def update_actions(self): @Slot(str) def remove_button(self, app_name: str): - if action := next((i for i in self.game_actions if i.property("app_name") == app_name), None): + if action := next((i for i in self.game_actions if i.property('app_name') == app_name), None): self.game_actions.remove(action) action.deleteLater() diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py index e58bbbec40..34555218df 100644 --- a/rare/lgndr/cli.py +++ b/rare/lgndr/cli.py @@ -218,7 +218,6 @@ def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, if not analysis.dl_size and not game.is_dlc: logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...') self.install_game_cleanup(game, igame, args.repair_mode, repair_file) - # return # Rare: Return what we know about the download to queue a 0 size DLC res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game, updating=self.core.is_installed(args.app_name), @@ -526,7 +525,8 @@ def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], pr logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"') repair_file = [] - for result, path, result_hash, bytes_read in validate_files(igame.install_path, file_list): + for result, path, result_hash, bytes_read in validate_files(igame.install_path, file_list, + case_insensitive=igame.platform.startswith('Win')): processed += bytes_read percentage = (processed / total_size) * 100.0 num += 1 diff --git a/rare/lgndr/downloader/mp/manager.py b/rare/lgndr/downloader/mp/manager.py index cbf09c3735..a3b827be64 100644 --- a/rare/lgndr/downloader/mp/manager.py +++ b/rare/lgndr/downloader/mp/manager.py @@ -59,7 +59,8 @@ def run_real(self): self.log.info('Starting file writing worker...') writer_p = FileWorker(self.writer_queue, self.writer_result_q, self.dl_dir, - self.shared_memory.name, self.cache_dir, self.logging_queue) + self.shared_memory.name, self.cache_dir, self.logging_queue, + self.case_insensitive) self.children.append(writer_p) writer_p.start() diff --git a/rare/main.py b/rare/main.py index cb29d984c3..b713d2f7bd 100755 --- a/rare/main.py +++ b/rare/main.py @@ -7,112 +7,112 @@ def main() -> int: - if getattr(sys, "frozen", False) or ("__compiled__" in globals()): + if getattr(sys, 'frozen', False) or ('__compiled__' in globals()): # If we are on Windows, and we are in a "compiled" GUI application form # stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'` # errors, redirect both of them to devnull - if os.name == "nt": + if os.name == 'nt': # Check if stdout and stderr are None before redirecting # This is useful in the case of test builds that enable console if sys.stdout is None: - sys.stdout = open(os.devnull, "w") + sys.stdout = open(os.devnull, 'w') # noqa: SIM115 if sys.stderr is None: - sys.stderr = open(os.devnull, "w") + sys.stderr = open(os.devnull, 'w') # noqa: SIM115 # Nuitka and possibly cx_freeze have issues with launching multiprocessing's resource_tracker due # to the way it is invoked. To accomodate for that, check here if the "-c" argument was used, and # execute the incoming command instead of Rare itself. - if "MULTIPROCESSING_LAUNCH_TOKEN" in os.environ: - if len(sys.argv) == 5 and sys.argv[1:4] == ["-S", "-s", "-c"]: + if 'MULTIPROCESSING_LAUNCH_TOKEN' in os.environ: + if len(sys.argv) == 5 and sys.argv[1:4] == ['-S', '-s', '-c']: exec(sys.argv[4]) return 0 - os.environ["MULTIPROCESSING_LAUNCH_TOKEN"] = "1" + os.environ['MULTIPROCESSING_LAUNCH_TOKEN'] = '1' # remove QT_QPA_PLATFORMTHEME to avoid broken theming - if "QT_QPA_PLATFORMTHEME" in os.environ: - del os.environ["QT_QPA_PLATFORMTHEME"] - os.environ["QT_STYLE_OVERRIDE"] = "fusion" + if 'QT_QPA_PLATFORMTHEME' in os.environ: + del os.environ['QT_QPA_PLATFORMTHEME'] + os.environ['QT_STYLE_OVERRIDE'] = 'fusion' # remove XDG_CONFIG_HOME if running under Wine to stop legendary from using the Linux client config - if platform.system() == "Windows" and "XDG_CONFIG_HOME" in os.environ: - del os.environ["XDG_CONFIG_HOME"] - if "LEGENDARY_CONFIG_PATH" in os.environ: - os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"]) + if platform.system() == 'Windows' and 'XDG_CONFIG_HOME' in os.environ: + del os.environ['XDG_CONFIG_HOME'] + if 'LEGENDARY_CONFIG_PATH' in os.environ: + os.environ['LEGENDARY_CONFIG_PATH'] = os.path.expanduser(os.environ['LEGENDARY_CONFIG_PATH']) # fix cx_freeze multiprocessing.freeze_support() # CLI Options parser = ArgumentParser() - parser.add_argument("-V", "--version", action="store_true", help="Shows version and exits") + parser.add_argument('-V', '--version', action='store_true', help='Shows version and exits') parser.add_argument( - "-S", - "--silent", - action="store_true", - help="Launch Rare in background. Open it from System Tray Icon", + '-S', + '--silent', + action='store_true', + help='Launch Rare in background. Open it from System Tray Icon', ) - parser.add_argument("--debug", action="store_true", help="Launch in debug mode") - parser.add_argument("--offline", action="store_true", help="Launch Rare in offline mode") - parser.add_argument("--test-start", action="store_true", help="Quit immediately after launch") + parser.add_argument('--debug', action='store_true', help='Launch in debug mode') + parser.add_argument('--offline', action='store_true', help='Launch Rare in offline mode') + parser.add_argument('--test-start', action='store_true', help='Quit immediately after launch') parser.add_argument( - "--desktop-shortcut", - action="store_true", - dest="desktop_shortcut", - help="Use this, if there is no link on desktop to start Rare", + '--desktop-shortcut', + action='store_true', + dest='desktop_shortcut', + help='Use this, if there is no link on desktop to start Rare', ) parser.add_argument( - "--startmenu-shortcut", - action="store_true", - dest="startmenu_shortcut", - help="Use this, if there is no link in start menu to launch Rare", + '--startmenu-shortcut', + action='store_true', + dest='startmenu_shortcut', + help='Use this, if there is no link in start menu to launch Rare', ) - subparsers = parser.add_subparsers(title="Commands", dest="subparser") + subparsers = parser.add_subparsers(title='Commands', dest='subparser') # Launch command - launch_parser = subparsers.add_parser("launch", aliases=["start"]) - launch_parser.add_argument("--dry-run", action="store_true", help="Print arguments and exit") - launch_parser.add_argument("--offline", action="store_true", help="Launch game offline") - launch_parser.add_argument("--ask-sync-saves", action="store_true", help="Ask to sync cloud saves") - launch_parser.add_argument("--skip-update-check", action="store_true", help="Do not check for updates") + launch_parser = subparsers.add_parser('launch', aliases=['start']) + launch_parser.add_argument('--dry-run', action='store_true', help='Print arguments and exit') + launch_parser.add_argument('--offline', action='store_true', help='Launch game offline') + launch_parser.add_argument('--ask-sync-saves', action='store_true', help='Ask to sync cloud saves') + launch_parser.add_argument('--skip-update-check', action='store_true', help='Do not check for updates') launch_parser.add_argument( - "--show-console", - action="store_true", + '--show-console', + action='store_true', help="Show a console window to log the application's output", ) - if platform.system() != "Windows": + if platform.system() != 'Windows': launch_parser.add_argument( - "--wine-bin", - action="store", - dest="wine_bin", - default=os.environ.get("LGDRY_WINE_BINARY", None), - metavar="", - help="Set WINE binary to use to launch the app", + '--wine-bin', + action='store', + dest='wine_bin', + default=os.environ.get('LGDRY_WINE_BINARY', None), + metavar='', + help='Set WINE binary to use to launch the app', ) launch_parser.add_argument( - "--wine-prefix", - action="store", - dest="wine_pfx", - default=os.environ.get("LGDRY_WINE_PREFIX", None), - metavar="", - help="Set WINE prefix to use", + '--wine-prefix', + action='store', + dest='wine_pfx', + default=os.environ.get('LGDRY_WINE_PREFIX', None), + metavar='', + help='Set WINE prefix to use', ) launch_parser.add_argument( - "app_name", - action="store", - metavar="", - help="AppName of the game to launch", + 'app_name', + action='store', + metavar='', + help='AppName of the game to launch', ) # Login command - login_parser = subparsers.add_parser("login", aliases=["auth"]) + login_parser = subparsers.add_parser('login', aliases=['auth']) login_parser.add_argument( - "egl_version", - action="store", - metavar="", - help="Epic Games Launcher User Agent version", + 'egl_version', + action='store', + metavar='', + help='Epic Games Launcher User Agent version', ) # Subreaper command - subparsers.add_parser("subreaper", aliases=["reaper"]) + subparsers.add_parser('subreaper', aliases=['reaper']) args, other = parser.parse_known_args() @@ -120,34 +120,34 @@ def main() -> int: from rare.utils.paths import create_desktop_link if args.desktop_shortcut: - create_desktop_link(app_name="rare_shortcut", link_type="desktop") + create_desktop_link(app_name='rare_shortcut', link_type='desktop') if args.startmenu_shortcut: - create_desktop_link(app_name="rare_shortcut", link_type="start_menu") + create_desktop_link(app_name='rare_shortcut', link_type='start_menu') - print("Link created", file=sys.stderr) + print('Link created', file=sys.stderr) return 0 if args.version: from rare import __codename__, __version__ - print(f"Rare {__version__} Codename: {__codename__}", file=sys.stderr) + print(f'Rare {__version__} Codename: {__codename__}', file=sys.stderr) return 0 - if args.subparser in {"login", "auth"}: + if args.subparser in {'login', 'auth'}: from rare.commands.webview import webview return webview(args) - if args.subparser in {"launch", "start"}: + if args.subparser in {'launch', 'start'}: from rare.commands.launcher import launcher return launcher(args) - if args.subparser in {"subreaper", "reaper"}: + if args.subparser in {'subreaper', 'reaper'}: from rare.commands.subreaper import subreaper - sep = other.index("--") + sep = other.index('--') other = other[sep + 1 :] args.command = other.pop(0) args.workdir = os.getcwd() @@ -161,23 +161,29 @@ def main() -> int: me = singleton.SingleInstance() # noqa: F841 except singleton.SingleInstanceException: - print("Rare is already running", file=sys.stderr) + print('Rare is already running', file=sys.stderr) from rare.utils.paths import lock_file - with open(lock_file(), "w") as file: - file.write("show") + with open(lock_file(), 'w') as file: + file.write('show') file.close() return -1 from rare.components import start - return start(args) + ec = start(args) + if os.name == 'nt': + if not sys.stdout.closed: + sys.stdout.close() + if not sys.stderr.closed: + sys.stderr.close() + return ec -if __name__ == "__main__": +if __name__ == '__main__': # insert source directory if running `main.py` as python script # Required by AppImage - if "__compiled__" not in globals(): + if '__compiled__' not in globals(): sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) sys.exit(main()) diff --git a/rare/models/game.py b/rare/models/game.py index 837c38bb4f..eb2cd9ef10 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from datetime import datetime, timezone from threading import Lock -from typing import Dict, List, Optional, Set, Tuple +from typing import Optional from legendary.lfs import eos from legendary.models.game import Game, InstalledGame @@ -23,22 +23,22 @@ from rare.shared.image_manager import ImageManager from rare.utils import config_helper as config from rare.utils.paths import compat_shaders_dir, data_dir, get_rare_executable -from rare.utils.steam_grades import SteamGrades +from rare.utils.steam_grades import steam_grades from rare.utils.workarounds import apply_workarounds @dataclass class RareGameMetadata: queued: bool = False - queue_pos: Optional[int] = None + queue_pos: int | None = None last_played: datetime = datetime.min.replace(tzinfo=timezone.utc) achievements_date: datetime = datetime.min.replace(tzinfo=timezone.utc) grant_date: datetime = datetime.min.replace(tzinfo=timezone.utc) - steam_appid: Optional[str] = None - steam_grade: Optional[str] = None + steam_appid: str | None = None + steam_grade: str | None = None steam_date: datetime = datetime.min.replace(tzinfo=timezone.utc) - steam_shortcut: Optional[int] = None - tags: Tuple[str, ...] = field(default_factory=tuple) + steam_shortcut: int | None = None + tags: tuple[str, ...] = field(default_factory=tuple) # For compatibility with previously created game metadata @staticmethod @@ -47,18 +47,18 @@ def parse_date(strdate: str): return dt.replace(tzinfo=timezone.utc) @classmethod - def from_dict(cls, data: Dict): + def from_dict(cls, data: dict): return cls( - queued=data.get("queued", False), - queue_pos=data.get("queue_pos", None), - last_played=RareGameMetadata.parse_date(data.get("last_played", "")), - achievements_date=RareGameMetadata.parse_date(data.get("achievements_date", "")), - grant_date=RareGameMetadata.parse_date(data.get("grant_date", "")), - steam_appid=str(appid) if (appid := data.get("steam_appid", "")) else None, - steam_grade=data.get("steam_grade", None), - steam_date=RareGameMetadata.parse_date(data.get("steam_date", "")), - steam_shortcut=data.get("steam_shortcut", None), - tags=data.get("tags", ()), + queued=data.get('queued', False), + queue_pos=data.get('queue_pos'), + last_played=RareGameMetadata.parse_date(data.get('last_played', '')), + achievements_date=RareGameMetadata.parse_date(data.get('achievements_date', '')), + grant_date=RareGameMetadata.parse_date(data.get('grant_date', '')), + steam_appid=str(appid) if (appid := data.get('steam_appid', '')) else None, + steam_grade=data.get('steam_grade'), + steam_date=RareGameMetadata.parse_date(data.get('steam_date', '')), + steam_shortcut=data.get('steam_shortcut'), + tags=data.get('tags', ()), ) @property @@ -91,27 +91,27 @@ def __init__( game: Game, ): super(RareGame, self).__init__(settings, legendary_core, game) - self.__origin_install_path: Optional[str] = None - self.__origin_install_size: Optional[int] = None + self.__origin_install_path: str | None = None + self.__origin_install_size: int | None = None self.image_manager = image_manager # Update names for Unreal Engine - if self.game.app_title == "Unreal Engine": - self.game.app_title += f" {self.game.app_name.split('_')[-1]}" + if self.game.app_title == 'Unreal Engine': + self.game.app_title += f' {self.game.app_name.split("_")[-1]}' self.has_pixmap: bool = False self.metadata: RareGameMetadata = RareGameMetadata() self.__load_metadata() self.grant_date() - self.owned_dlcs: Set[RareGame] = set() - self.__parent_rgame: Optional[RareGame] = None + self.owned_dlcs: set[RareGame] = set() + self.__parent_rgame: RareGame | None = None if self.has_update: - self.logger.info(f"Update available for game: {self.app_name} ({self.app_title})") + self.logger.info(f'Update available for game: {self.app_name} ({self.app_title})') - self.__worker: Optional[QRunnable] = None + self.__worker: QRunnable | None = None self.progress: int = 0 self.signals.progress.start.connect(self.__on_progress_update) self.signals.progress.refresh.connect(self.__on_progress_update) @@ -135,11 +135,11 @@ def add_dlc(self, dlc) -> None: self.owned_dlcs.add(dlc) @property - def parent_rgame(self) -> Optional["RareGame"]: + def parent_rgame(self) -> Optional['RareGame']: return self.__parent_rgame if self.is_dlc else None @parent_rgame.setter - def parent_rgame(self, rgame: "RareGame") -> None: + def parent_rgame(self, rgame: 'RareGame') -> None: if self.is_dlc: self.__parent_rgame = rgame @@ -148,7 +148,7 @@ def parent_rgame(self, rgame: "RareGame") -> None: def __on_progress_update(self, progress: int = 0): self.progress = progress - def get_worker(self) -> Optional[QRunnable]: + def get_worker(self) -> QRunnable | None: return self.__worker @Slot(object) @@ -183,27 +183,27 @@ def _game_finished(self, exit_code: int): self.state = RareGame.State.IDLE self.signals.game.finished.emit(self.app_name) - __metadata_json: Optional[Dict] = None + __metadata_json: dict | None = None __metadata_lock: Lock = Lock() - def __load_metadata_json(self) -> Optional[Dict]: + def __load_metadata_json(self) -> dict | None: if RareGame.__metadata_json is None: metadata = {} - file = os.path.join(data_dir(), "game_meta.json") + file = os.path.join(data_dir(), 'game_meta.json') try: - with open(file, "r", encoding="utf-8") as f: + with open(file, encoding='utf-8') as f: metadata = json.load(f) except FileNotFoundError: - self.logger.info("%s does not exist", file) + self.logger.info('%s does not exist', file) except json.JSONDecodeError: - self.logger.warning("%s is corrupt", file) + self.logger.warning('%s is corrupt', file) finally: RareGame.__metadata_json = metadata return RareGame.__metadata_json def __load_metadata(self): with RareGame.__metadata_lock: - metadata: Dict = self.__load_metadata_json() + metadata: dict = self.__load_metadata_json() # pylint: disable=unsupported-membership-test if self.app_name in metadata: # pylint: disable=unsubscriptable-object @@ -211,10 +211,10 @@ def __load_metadata(self): def __save_metadata(self): with RareGame.__metadata_lock: - metadata: Dict = self.__load_metadata_json() + metadata: dict = self.__load_metadata_json() # pylint: disable=unsupported-assignment-operation metadata[self.app_name] = vars(self.metadata) - with open(os.path.join(data_dir(), "game_meta.json"), "w+", encoding="utf-8") as file: + with open(os.path.join(data_dir(), 'game_meta.json'), 'w+', encoding='utf-8') as file: json.dump(metadata, file, indent=2) def update_game(self): @@ -242,7 +242,7 @@ def developer(self) -> str: @return str """ - return self.game.metadata["developer"] + return self.game.metadata['developer'] @property def install_size(self) -> int: @@ -256,7 +256,7 @@ def install_size(self) -> int: return self.igame.install_size if self.igame is not None else 0 @property - def install_path(self) -> Optional[str]: + def install_path(self) -> str | None: if self.is_origin: # TODO Linux is also C:\\... return self.__origin_install_path @@ -300,7 +300,7 @@ def has_update(self) -> bool: if self.remote_version != self.igame.version: return True except ValueError: - self.logger.error(f"Asset error for {self.game.app_title}") + self.logger.error(f'Asset error for {self.game.app_title}') return False return False @@ -375,7 +375,7 @@ def is_foreign(self) -> bool: _ = self.core.get_asset(self.game.app_name, platform=self.igame.platform).build_version ret = False except ValueError: - self.logger.warning(f"Game {self.game.app_title} has no metadata. Set offline true") + self.logger.warning(f'Game {self.game.app_title} has no metadata. Set offline true') except AttributeError: ret = False return ret @@ -419,7 +419,7 @@ def needs_verification(self, needs: bool) -> None: @property def repair_file(self) -> str: - return os.path.join(self.core.lgd.get_tmp_path(), f"{self.app_name}.repair") + return os.path.join(self.core.lgd.get_tmp_path(), f'{self.app_name}.repair') @property def needs_repair(self) -> bool: @@ -437,7 +437,7 @@ def is_unreal(self) -> bool: @return bool """ - return False if self.is_non_asset else self.game.asset_infos["Windows"].namespace == "ue" + return False if self.is_non_asset else self.game.asset_infos['Windows'].namespace == 'ue' @property def is_non_asset(self) -> bool: @@ -460,18 +460,18 @@ def is_android_only(self) -> bool: @property def is_ubisoft(self) -> bool: - return self.game.partner_link_type == "ubisoft" + return self.game.partner_link_type == 'ubisoft' @property def folder_name(self) -> str: return ( folder_name - if (folder_name := self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value")) + if (folder_name := self.game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value')) else self.app_title ) @property - def save_path(self) -> Optional[str]: + def save_path(self) -> str | None: return super(RareGame, self).save_path @save_path.setter @@ -482,9 +482,9 @@ def save_path(self, path: str) -> None: self.signals.widget.refresh.emit() @property - def achievements(self) -> Optional[Namespace]: + def achievements(self) -> Namespace | None: if not self.game.achievements or not self.game.achievements.achievements: - self.logger.info("No achievements found for %s (%s)", self.app_name, self.app_title) + self.logger.info('No achievements found for %s (%s)', self.app_name, self.app_title) return None # if self.game.achievements is None: @@ -497,7 +497,7 @@ def achievements(self) -> Optional[Namespace]: update = not self.core.lgd.achievements or not self.core.lgd.achievements.get(self.game.namespace, None) update = update or self.metadata.achievements_date < self.metadata.last_played if update: - self.logger.info("Updating achievements for %s (%s)", self.app_name, self.app_title) + self.logger.info('Updating achievements for %s (%s)', self.app_name, self.app_title) ret = self.core.get_achievements(self.game, update=update) self.metadata.achievements_date = self.metadata.last_played self.__save_metadata() @@ -506,10 +506,10 @@ def achievements(self) -> Optional[Namespace]: return ret @property - def eulas(self) -> List: - eulas = self.game.metadata.get("eulaIds") or ["$"] + def eulas(self) -> list: + eulas = self.game.metadata.get('eulaIds') or ['$'] - pattern = r"\w+" + pattern = r'\w+' keys = [] for eula in eulas: keys += re.findall(pattern, eula) @@ -525,7 +525,7 @@ def eulas(self) -> List: return not_accepted_eulas - def sdl_data(self, platform: str) -> Optional[Dict[str, Dict]]: + def sdl_data(self, platform: str) -> dict[str, dict] | None: sdl_data = {} sdl_name = get_sdl_appname(self.app_name) @@ -533,7 +533,7 @@ def sdl_data(self, platform: str) -> Optional[Dict[str, Dict]]: sdl_data.update(data) known_install_tags = set() if sdl_data: - known_install_tags = set(tag for _, info in sdl_data.items() for tag in info["tags"]) + known_install_tags = set(tag for _, info in sdl_data.items() for tag in info['tags']) if self.igame is not None and not self.has_update: manifest_data = self.core.lgd.load_manifest(self.app_name, self.igame.version, self.igame.platform) @@ -549,8 +549,8 @@ def sdl_data(self, platform: str) -> Optional[Dict[str, Dict]]: manifest_install_tags.add(tag) extra_install_tags = manifest_install_tags.difference(known_install_tags) - for extra_tag in extra_install_tags: - sdl_data[extra_tag] = {"name": extra_tag, "description": "", "tags": [extra_tag]} + for extra_tag in sorted(extra_install_tags): + sdl_data[extra_tag] = {'name': extra_tag, 'description': '', 'tags': [extra_tag]} return sdl_data @@ -571,34 +571,34 @@ def reset_steam_date(self): self.signals.widget.refresh.emit() @property - def steam_appid(self) -> Optional[str]: + def steam_appid(self) -> str | None: return self.metadata.steam_appid @steam_appid.setter def steam_appid(self, appid: str) -> None: - config.adjust_envvar(self.app_name, "SteamAppId", appid) - config.adjust_envvar(self.app_name, "SteamGameId", appid) - config.adjust_envvar(self.app_name, "STEAM_COMPAT_APP_ID", f"rare_{self.app_name}") - config.adjust_envvar(self.app_name, "UMU_ID", f"umu-{appid}" if appid else "umu-default") + config.adjust_envvar(self.app_name, 'SteamAppId', appid) + config.adjust_envvar(self.app_name, 'SteamGameId', appid) + config.adjust_envvar(self.app_name, 'STEAM_COMPAT_APP_ID', f'rare_{self.app_name}') + config.adjust_envvar(self.app_name, 'UMU_ID', f'umu-{appid}' if appid else 'umu-default') self.metadata.steam_appid = appid self.__save_metadata() def get_steam_grade(self) -> str: - if platform.system() == "Windows" or self.is_unreal: - return "na" + if platform.system() == 'Windows' or self.is_unreal: + return 'na' if self.__steam_grade_pending: - return "pending" + return 'pending' elapsed_time = abs(datetime.now(timezone.utc) - self.metadata.steam_date) if elapsed_time.days > 3: - self.logger.info("Refreshing ProtonDB grade for %s", self.app_title) + self.logger.info('Refreshing ProtonDB grade for %s', self.app_title) worker = QRunnable.create(self.set_steam_grade) self.__steam_grade_pending = True QThreadPool.globalInstance().start(worker) - return "pending" + return 'pending' return self.metadata.steam_grade def set_steam_grade(self) -> None: - appid, grade = SteamGrades().get_rating(self.core, self.app_name, self.steam_appid) + appid, grade = steam_grades.get_rating(self.core, self.app_name, self.steam_appid) if appid and self.steam_appid is None: self.steam_appid = appid self.metadata.steam_grade = grade @@ -611,26 +611,25 @@ def grant_date(self, force=False) -> datetime: if not (entitlements := self.core.lgd.entitlements): return self.metadata.grant_date if self.metadata.grant_date == datetime.min.replace(tzinfo=timezone.utc) or force: - self.logger.info("Grant date for %s not found in metadata, resolving", self.app_name) - matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements) - entitlement = next(matching, None) + self.logger.info('Grant date for %s not found in metadata, resolving', self.app_name) + entitlement = next((ent for ent in entitlements if ent['namespace'] == self.game.namespace), None) grant_date = ( - datetime.fromisoformat(entitlement["grantDate"].replace("Z", "+00:00")) + datetime.fromisoformat(entitlement['grantDate'].replace('Z', '+00:00')) if entitlement - else datetime.fromisoformat(self.game.metadata["creationDate"].replace("Z", "+00:00")) + else datetime.fromisoformat(self.game.metadata['creationDate'].replace('Z', '+00:00')) ) self.metadata.grant_date = grant_date self.__save_metadata() return self.metadata.grant_date @property - def tags(self) -> Tuple[str, ...]: + def tags(self) -> tuple[str, ...]: return tuple(tag for tag in map(lambda x: x.lower().strip(), self.metadata.tags) if bool(tag)) @tags.setter - def tags(self, tags: Tuple[str, ...]) -> None: + def tags(self, tags: tuple[str, ...]) -> None: self.metadata.tags = tuple(tag for tag in map(lambda x: x.lower().strip(), tags) if bool(tag)) - self.logger.debug(f"Saving tags for {self.game.app_title}: {self.metadata.tags}") + self.logger.debug(f'Saving tags for {self.game.app_title}: {self.metadata.tags}') self.__save_metadata() def set_origin_attributes(self, path: str, size: int = 0) -> None: @@ -729,7 +728,7 @@ def uninstall(self) -> bool: UninstallOptionsModel( app_name=self.app_name, keep_folder=self.is_dlc, - keep_config=self.sdl_available or self.is_dlc or platform.system() not in {"Windows"}, + keep_config=self.sdl_available or self.is_dlc or platform.system() not in {'Windows'}, ) ) return True @@ -739,48 +738,48 @@ def launch( debug: bool = False, offline: bool = False, skip_update_check: bool = False, - wine_bin: Optional[str] = None, - wine_pfx: Optional[str] = None, + wine_bin: str | None = None, + wine_pfx: str | None = None, ) -> bool: if not self.can_launch: return False cmd_line = get_rare_executable() executable, args = cmd_line[0], cmd_line[1:] - args.append("launch") - if offline or config.get_boolean(self.app_name, "offline", fallback=False): - args.append("--offline") + args.append('launch') + if offline or config.get_boolean(self.app_name, 'offline', fallback=False): + args.append('--offline') if debug: - args.append("--debug") + args.append('--debug') if self.settings.get_value(app_settings.log_games): - args.append("--show-console") - if skip_update_check or config.get_boolean(self.app_name, "skip_update_check", fallback=False): - args.append("--skip-update-check") + args.append('--show-console') + if skip_update_check or config.get_boolean(self.app_name, 'skip_update_check', fallback=False): + args.append('--skip-update-check') if wine_bin: - args.extend(["--wine-bin", wine_bin]) + args.extend(['--wine-bin', wine_bin]) if wine_pfx: - args.extend(["--wine-prefix", wine_pfx]) + args.extend(['--wine-prefix', wine_pfx]) args.append(self.app_name) - if not config.get_envvar(self.app_name, "STORE", False): - config.set_envvar(self.app_name, "STORE", "egs") - if not config.get_envvar(self.app_name, "UMU_ID", False): + if not config.get_envvar(self.app_name, 'STORE', False): + config.set_envvar(self.app_name, 'STORE', 'egs') + if not config.get_envvar(self.app_name, 'UMU_ID', False): config.set_envvar( self.app_name, - "UMU_ID", - f"umu-{self.metadata.steam_appid}" if self.metadata.steam_appid else "umu-default", + 'UMU_ID', + f'umu-{self.metadata.steam_appid}' if self.metadata.steam_appid else 'umu-default', ) if self.settings.get_with_global(app_settings.local_shader_cache, self.app_name): config.set_envvar( self.app_name, - "STEAM_COMPAT_SHADER_PATH", + 'STEAM_COMPAT_SHADER_PATH', compat_shaders_dir(self.folder_name).as_posix(), ) else: - config.remove_envvar(self.app_name, "STEAM_COMPAT_SHADER_PATH") + config.remove_envvar(self.app_name, 'STEAM_COMPAT_SHADER_PATH') config.save_config() - self.logger.info(f"Starting game process: ({executable} {' '.join(args)})") + self.logger.info(f'Starting game process: ({executable} {" ".join(args)})') proc = QProcess() proc.setProgram(executable) proc.setArguments(args) @@ -801,7 +800,7 @@ def __init__( self.settings = settings self.image_manager = image_manager - self.igame: Optional[InstalledGame] = self.core.lgd.get_overlay_install_info() + self.igame: InstalledGame | None = self.core.lgd.get_overlay_install_info() self.image_manager.download_image(game, self._update_pixmap, 0, False) @Slot() @@ -829,31 +828,31 @@ def has_update(self) -> bool: # and legendary does it too during login return self.core.overlay_update_available - def is_enabled(self, prefix: Optional[str] = None) -> bool: + def is_enabled(self, prefix: str | None = None) -> bool: try: reg_paths = eos.query_registry_entries(prefix) except ValueError as e: - self.logger.info("%s %s", e, prefix) + self.logger.info('%s %s', e, prefix) return False - return reg_paths["overlay_path"] and self.core.is_overlay_install(reg_paths["overlay_path"]) + return reg_paths['overlay_path'] and self.core.is_overlay_install(reg_paths['overlay_path']) - def active_path(self, prefix: Optional[str] = None) -> str: + def active_path(self, prefix: str | None = None) -> str: try: - path = eos.query_registry_entries(prefix)["overlay_path"] + path = eos.query_registry_entries(prefix)['overlay_path'] except ValueError as e: - self.logger.info("%s %s", e, prefix) - return "" - return path if path and self.core.is_overlay_install(path) else "" + self.logger.info('%s %s', e, prefix) + return '' + return path if path and self.core.is_overlay_install(path) else '' - def available_paths(self, prefix: Optional[str] = None) -> List[str]: + def available_paths(self, prefix: str | None = None) -> list[str]: try: installs = self.core.search_overlay_installs(prefix) except ValueError as e: - self.logger.info("%s %s", e, prefix) + self.logger.info('%s %s', e, prefix) return [] return installs - def enable(self, prefix: Optional[str] = None, path: Optional[str] = None) -> bool: + def enable(self, prefix: str | None = None, path: str | None = None) -> bool: if self.is_enabled(prefix): return False if not path: @@ -862,9 +861,9 @@ def enable(self, prefix: Optional[str] = None, path: Optional[str] = None) -> bo else: path = self.available_paths(prefix)[-1] reg_paths = eos.query_registry_entries(prefix) - if old_path := reg_paths["overlay_path"]: + if old_path := reg_paths['overlay_path']: if os.path.normpath(old_path) == path: - self.logger.info("Overlay already enabled, nothing to do.") + self.logger.info('Overlay already enabled, nothing to do.') return True else: self.logger.info(f'Updating overlay registry entries from "{old_path}" to "{path}"') @@ -872,20 +871,20 @@ def enable(self, prefix: Optional[str] = None, path: Optional[str] = None) -> bo try: eos.add_registry_entries(path, prefix) except PermissionError as e: - self.logger.error("Exception while writing registry to enable the overlay.") + self.logger.error('Exception while writing registry to enable the overlay.') self.logger.error(e) return False - self.logger.info(f"Enabled overlay at: {path} for prefix: {prefix}") + self.logger.info(f'Enabled overlay at: {path} for prefix: {prefix}') return True - def disable(self, prefix: Optional[str] = None) -> bool: + def disable(self, prefix: str | None = None) -> bool: if not self.is_enabled(prefix): return False - self.logger.info(f"Disabling overlay (removing registry keys) for prefix: {prefix}") + self.logger.info(f'Disabling overlay (removing registry keys) for prefix: {prefix}') try: eos.remove_registry_entries(prefix) except PermissionError as e: - self.logger.error("Exception while writing registry to disable the overlay.") + self.logger.error('Exception while writing registry to disable the overlay.') self.logger.error(e) return False return True @@ -897,7 +896,7 @@ def install(self) -> bool: InstallOptionsModel( app_name=self.app_name, base_path=self.core.get_default_install_dir(), - platform="Windows", + platform='Windows', update=self.is_installed, overlay=True, ) @@ -910,10 +909,10 @@ def uninstall(self) -> bool: self.signals.game.uninstall.emit( UninstallOptionsModel( app_name=self.app_name, - keep_overlay_keys=platform.system() not in {"Windows"}, + keep_overlay_keys=platform.system() not in {'Windows'}, ) ) return True -__all__ = ["RareGame", "RareEosOverlay"] +__all__ = ['RareGame', 'RareEosOverlay'] diff --git a/rare/models/game_slim.py b/rare/models/game_slim.py index edf7b05ef3..f97b074982 100644 --- a/rare/models/game_slim.py +++ b/rare/models/game_slim.py @@ -4,7 +4,6 @@ from datetime import datetime from enum import IntEnum from logging import getLogger -from typing import List, Optional, Tuple from legendary.lfs import eos from legendary.models.game import Game, InstalledGame, SaveGameFile, SaveGameStatus @@ -17,11 +16,11 @@ @dataclass class RareSaveGame: - file: Optional[SaveGameFile] + file: SaveGameFile | None status: SaveGameStatus = SaveGameStatus.NO_SAVE - dt_local: Optional[datetime] = None - dt_remote: Optional[datetime] = None - description: Optional[str] = "" + dt_local: datetime | None = None + dt_remote: datetime | None = None + description: str | None = '' class RareGameSignalsProgress(QObject): @@ -98,11 +97,11 @@ def deleteLater(self): super(RareGameBase, self).deleteLater() @property - def state(self) -> "RareGameBase.State": + def state(self) -> 'RareGameBase.State': return self._state @state.setter - def state(self, state: "RareGameBase.State"): + def state(self, state: 'RareGameBase.State'): if state != self._state: self._state = state self.signals.widget.refresh.emit() @@ -129,7 +128,7 @@ def set_installed(self, installed: bool) -> None: pass @property - def platforms(self) -> Tuple: + def platforms(self) -> tuple: """! @brief Property that holds the platforms a game is available for @@ -139,7 +138,7 @@ def platforms(self) -> Tuple: @property def default_platform(self) -> str: - return self.core.default_platform if self.core.default_platform in self.platforms else "Windows" + return self.core.default_platform if self.core.default_platform in self.platforms else 'Windows' @property def is_mac(self) -> bool: @@ -148,7 +147,7 @@ def is_mac(self) -> bool: @return bool """ - return "Mac" in self.game.asset_infos.keys() + return 'Mac' in self.game.asset_infos @property def is_win32(self) -> bool: @@ -157,7 +156,7 @@ def is_win32(self) -> bool: @return bool """ - return "Win32" in self.game.asset_infos.keys() + return 'Win32' in self.game.asset_infos @property def is_origin(self) -> bool: @@ -170,14 +169,14 @@ def is_origin(self) -> bool: @return bool If the game is an Origin game """ - return self.game.third_party_store in {"Origin", "the EA app"} + return self.game.third_party_store in {'Origin', 'the EA app'} @property def is_android(self) -> bool: - release_info = self.game.metadata.get("releaseInfo") + release_info = self.game.metadata.get('releaseInfo') if not release_info: return False - return "Android" in release_info[0]["platform"] + return 'Android' in release_info[0]['platform'] @property def is_overlay(self): @@ -215,7 +214,7 @@ def version(self) -> str: return self.igame.version if self.igame is not None else self.game.app_version(self.default_platform) @property - def install_path(self) -> Optional[str]: + def install_path(self) -> str | None: if self.igame: return self.igame.install_path return None @@ -226,8 +225,8 @@ def __init__(self, settings: RareAppSettings, legendary_core: LegendaryCore, gam super(RareGameSlim, self).__init__(legendary_core, game) self.settings = settings # None if origin or not installed - self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name) - self.saves: List[RareSaveGame] = [] + self.igame: InstalledGame = self.core.get_installed_game(game.app_name) + self.saves: list[RareSaveGame] = [] @property def is_installed(self) -> bool: @@ -244,7 +243,7 @@ def auto_sync_saves(self): return self.supports_cloud_saves and auto_sync_cloud @property - def save_path(self) -> Optional[str]: + def save_path(self) -> str | None: if self.igame is not None: return self.igame.save_path return None @@ -259,7 +258,7 @@ def latest_save(self) -> RareSaveGame: @property def save_game_state( self, - ) -> Tuple[SaveGameStatus, Tuple[Optional[datetime], Optional[datetime]]]: + ) -> tuple[SaveGameStatus, tuple[datetime | None, datetime | None]]: if self.save_path: latest = self.latest_save # lk: if the save path wasn't known at startup, dt_local will be None @@ -318,7 +317,7 @@ def _download(): _download() self.state = RareGameSlim.State.IDLE - def load_saves(self, saves: List[SaveGameFile]): + def load_saves(self, saves: list[SaveGameFile]): """Use only in a thread""" self.saves.clear() for save in saves: @@ -350,14 +349,14 @@ def is_save_up_to_date(self): @property def raw_save_path(self) -> str: if self.game.supports_cloud_saves: - return self.game.metadata.get("customAttributes", {}).get("CloudSaveFolder", {}).get("value") - return "" + return self.game.metadata.get('customAttributes', {}).get('CloudSaveFolder', {}).get('value') + return '' @property def raw_save_path_mac(self) -> str: if self.game.supports_mac_cloud_saves: - return self.game.metadata.get("customAttributes", {}).get("CloudSaveFolder_MAC", {}).get("value") - return "" + return self.game.metadata.get('customAttributes', {}).get('CloudSaveFolder_MAC', {}).get('value') + return '' @property def supports_cloud_saves(self): diff --git a/rare/models/image.py b/rare/models/image.py index b995fb693a..154f06d387 100644 --- a/rare/models/image.py +++ b/rare/models/image.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Tuple from PySide6.QtCore import QSize @@ -18,7 +17,7 @@ def __init__( divisor: float, pixel_ratio: float, orientation: ImageType = ImageType.Tall, - base: "ImageSize.Preset" = None, + base: 'ImageSize.Preset' = None, ): self.__divisor = divisor self.__pixel_ratio = pixel_ratio @@ -37,7 +36,7 @@ def __init__( self.__smooth_transform = divisor <= 2 self.__base = base if base is not None else self - def __eq__(self, other: "ImageSize.Preset"): + def __eq__(self, other: 'ImageSize.Preset'): return ( self.__size == other.size and self.__divisor == other.divisor @@ -66,7 +65,7 @@ def orientation(self) -> ImageType: return self.__orientation @property - def aspect_ratio(self) -> Tuple[int, int]: + def aspect_ratio(self) -> tuple[int, int]: if self.__orientation == ImageType.Tall: return 3, 4 elif self.__orientation == ImageType.Wide: @@ -75,7 +74,7 @@ def aspect_ratio(self) -> Tuple[int, int]: return 0, 0 @property - def base(self) -> "ImageSize.Preset": + def base(self) -> 'ImageSize.Preset': return self.__base Tall = Preset(1, 1) diff --git a/rare/models/install.py b/rare/models/install.py index cc5d93f89f..321c6c8661 100644 --- a/rare/models/install.py +++ b/rare/models/install.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from pathlib import Path -from typing import Dict, List, Optional, Tuple from legendary.models.downloading import AnalysisResult, ConditionCheckResult from legendary.models.game import Game, InstalledGame @@ -13,12 +12,12 @@ @dataclass class InstallOptionsModel: app_name: str - base_path: str = "" + base_path: str = '' shared_memory: int = 1024 max_workers: int = os.cpu_count() * 2 force: bool = False - platform: str = "Windows" - install_tag: Optional[List[str]] = None + platform: str = 'Windows' + install_tag: list[str] | None = None read_files: bool = False order_opt: bool = False repair_mode: bool = False @@ -37,11 +36,11 @@ class InstallOptionsModel: silent: bool = False install_prereqs: bool = False - def as_install_kwargs(self) -> Dict: + def as_install_kwargs(self) -> dict: return { k: getattr(self, k) for k in vars(self) - if k not in ["update", "silent", "create_shortcut", "overlay", "install_prereqs"] + if k not in ['update', 'silent', 'create_shortcut', 'overlay', 'install_prereqs'] } @@ -58,19 +57,19 @@ class InstallDownloadModel: class InstallQueueItemModel: def __init__(self, options: InstallOptionsModel, download: InstallDownloadModel = None): - self.options: Optional[InstallOptionsModel] = options + self.options: InstallOptionsModel | None = options # lk: internal attribute holders - self.__download: Optional[InstallDownloadModel] = None - self.__date: Optional[datetime] = None + self.__download: InstallDownloadModel | None = None + self.__date: datetime | None = None self.download = download @property - def download(self) -> Optional[InstallDownloadModel]: + def download(self) -> InstallDownloadModel | None: return self.__download @download.setter - def download(self, download: Optional[InstallDownloadModel]): + def download(self, download: InstallDownloadModel | None): self.__download = download if download is not None: self.__date = datetime.now() @@ -93,7 +92,7 @@ class UninstallOptionsModel: keep_overlay_keys: bool = None @property - def __values(self) -> Tuple[bool, bool, bool, bool, bool]: + def __values(self) -> tuple[bool, bool, bool, bool, bool]: return ( self.accepted, self.keep_files, @@ -103,7 +102,7 @@ def __values(self) -> Tuple[bool, bool, bool, bool, bool]: ) @__values.setter - def __values(self, values: Tuple[bool, bool, bool, bool, bool]): + def __values(self, values: tuple[bool, bool, bool, bool, bool]): ( self.accepted, self.keep_files, @@ -129,7 +128,7 @@ def set_rejected(self): class SelectiveDownloadsModel: app_name: str accepted: bool = None - install_tag: Optional[List[str]] = None + install_tag: list[str] | None = None def __bool__(self): return bool(self.app_name) and (self.accepted is not None) and (self.install_tag is not None) @@ -180,7 +179,7 @@ def target_path(self, path: str): @property def target_name(self) -> str: if self._rename_path: - return "" + return '' if self._reset_name: return self.default_name return self._target_name diff --git a/rare/models/launcher.py b/rare/models/launcher.py index 2728873891..45993226fb 100644 --- a/rare/models/launcher.py +++ b/rare/models/launcher.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict class Actions: @@ -16,8 +15,8 @@ class BaseModel: app_name: str @classmethod - def from_json(cls, data: Dict): - return cls(action=data["action"], app_name=data["app_name"]) + def from_json(cls, data: dict): + return cls(action=data['action'], app_name=data['app_name']) @dataclass @@ -26,11 +25,11 @@ class FinishedModel(BaseModel): playtime: int # seconds @classmethod - def from_json(cls, data: Dict): + def from_json(cls, data: dict): return cls( **vars(BaseModel.from_json(data)), - exit_code=data["exit_code"], - playtime=data["playtime"], + exit_code=data['exit_code'], + playtime=data['playtime'], ) @@ -45,11 +44,11 @@ class States: new_state: int @classmethod - def from_json(cls, data: Dict): + def from_json(cls, data: dict): return cls( - action=data["action"], - app_name=data["app_name"], - new_state=data["new_state"], + action=data['action'], + app_name=data['app_name'], + new_state=data['new_state'], ) @@ -58,5 +57,5 @@ class ErrorModel(BaseModel): error_string: str @classmethod - def from_json(cls, data: Dict): - return cls(**vars(BaseModel.from_json(data)), error_string=data["error_string"]) + def from_json(cls, data: dict): + return cls(**vars(BaseModel.from_json(data)), error_string=data['error_string']) diff --git a/rare/models/pathspec.py b/rare/models/pathspec.py index b7e6bdf5b0..af90ee52e4 100644 --- a/rare/models/pathspec.py +++ b/rare/models/pathspec.py @@ -1,5 +1,4 @@ import os -from typing import List, Union from legendary.models.game import InstalledGame @@ -10,32 +9,32 @@ class PathSpec: @staticmethod def egl_appdata() -> str: - return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows" + return r'%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows' @staticmethod def egl_programdata() -> str: - return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests" + return r'%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests' @staticmethod def wine_programdata() -> str: - return r"ProgramData" + return r'ProgramData' @staticmethod def wine_egl_programdata() -> str: - return PathSpec.egl_programdata().replace("\\", "/").replace("%PROGRAMDATA%", PathSpec.wine_programdata()) + return PathSpec.egl_programdata().replace('\\', '/').replace('%PROGRAMDATA%', PathSpec.wine_programdata()) @staticmethod def prefix_egl_programdata(prefix: str) -> str: - return os.path.join(prefix, "dosdevices/c:", PathSpec.wine_egl_programdata()) + return os.path.join(prefix, 'dosdevices/c:', PathSpec.wine_egl_programdata()) @staticmethod - def wine_egl_prefixes(results: int = 0) -> Union[List[str], str]: + def wine_egl_prefixes(results: int = 0) -> list[str] | str: possible_prefixes = get_prefixes() prefixes = [ prefix for prefix, _ in possible_prefixes if os.path.exists(os.path.join(prefix, PathSpec.wine_egl_programdata())) ] if not prefixes: - return "" + return '' if not results: return prefixes elif results == 1: @@ -45,18 +44,18 @@ def wine_egl_prefixes(results: int = 0) -> Union[List[str], str]: def __init__(self, core: LegendaryCore = None, igame: InstalledGame = None): self.__egl_path_vars = { - "{appdata}": os.path.expandvars("%LOCALAPPDATA%"), - "{userdir}": os.path.expandvars("%USERPROFILE%/Documents"), - "{userprofile}": os.path.expandvars("%userprofile%"), # possibly wrong - "{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"), + '{appdata}': os.path.expandvars('%LOCALAPPDATA%'), + '{userdir}': os.path.expandvars('%USERPROFILE%/Documents'), + '{userprofile}': os.path.expandvars('%userprofile%'), # possibly wrong + '{usersavedgames}': os.path.expandvars('%USERPROFILE%/Saved Games'), } if core is not None: - self.__egl_path_vars["{epicid}"] = core.lgd.userdata["account_id"] + self.__egl_path_vars['{epicid}'] = core.lgd.userdata['account_id'] if igame is not None: - self.__egl_path_vars["{installdir}"] = igame.install_path + self.__egl_path_vars['{installdir}'] = igame.install_path - def resolve_egl_path_vars(self, path: str) -> Union[str, bytes]: - cooked_path = (self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")) + def resolve_egl_path_vars(self, path: str) -> str | bytes: + cooked_path = (self.__egl_path_vars.get(p.lower(), p) for p in path.split('/')) return os.path.join(*cooked_path) diff --git a/rare/models/settings.py b/rare/models/settings.py index 3d16b182fa..bf0071fb56 100644 --- a/rare/models/settings.py +++ b/rare/models/settings.py @@ -1,7 +1,7 @@ import locale import platform as pf from argparse import Namespace -from typing import Any, Optional, Type +from typing import Any, Optional from PySide6.QtCore import QSettings @@ -11,7 +11,7 @@ class Setting(Namespace): key: str default: Any - dtype: Type + dtype: type def __len__(self): return len(self.__dict__) @@ -22,85 +22,85 @@ def __iter__(self): # The key names are set to the existing option name class Settings(Namespace): - win32_meta = Setting(key="win32_meta", default=False, dtype=bool) - macos_meta = Setting(key="macos_meta", default=pf.system() == "Darwin", dtype=bool) - unreal_meta = Setting(key="unreal_meta", default=False, dtype=bool) - exclude_non_asset = Setting(key="exclude_non_asset", default=False, dtype=bool) - exclude_entitlements = Setting(key="exclude_entitlements", default=False, dtype=bool) - - language = Setting(key="language", default=locale.getlocale()[0], dtype=str) - - sys_tray_close = Setting(key="sys_tray", default=True, dtype=bool) - sys_tray_start = Setting(key="sys_tray_start", default=False, dtype=bool) - - auto_update = Setting(key="auto_update", default=False, dtype=bool) - auto_sync_cloud = Setting(key="auto_sync_cloud", default=False, dtype=bool) - confirm_start = Setting(key="confirm_start", default=False, dtype=bool) - restore_window = Setting(key="save_size", default=False, dtype=bool) - window_width = Setting(key="window_width", default=1280, dtype=int) - window_height = Setting(key="window_height", default=720, dtype=int) - notification = Setting(key="notification", default=True, dtype=bool) - log_games = Setting(key="show_console", default=pf.system() != "Windows", dtype=bool) - - color_scheme = Setting(key="color_scheme", default="", dtype=str) - style_sheet = Setting(key="style_sheet", default="RareStyle", dtype=str) - - library_view = Setting(key="library_view", default=int(LibraryView.COVER), dtype=int) + win32_meta = Setting(key='win32_meta', default=False, dtype=bool) + macos_meta = Setting(key='macos_meta', default=pf.system() == 'Darwin', dtype=bool) + unreal_meta = Setting(key='unreal_meta', default=False, dtype=bool) + exclude_non_asset = Setting(key='exclude_non_asset', default=False, dtype=bool) + exclude_entitlements = Setting(key='exclude_entitlements', default=False, dtype=bool) + + language = Setting(key='language', default=locale.getlocale()[0], dtype=str) + + sys_tray_close = Setting(key='sys_tray', default=True, dtype=bool) + sys_tray_start = Setting(key='sys_tray_start', default=False, dtype=bool) + + auto_update = Setting(key='auto_update', default=False, dtype=bool) + auto_sync_cloud = Setting(key='auto_sync_cloud', default=False, dtype=bool) + confirm_start = Setting(key='confirm_start', default=False, dtype=bool) + restore_window = Setting(key='save_size', default=False, dtype=bool) + window_width = Setting(key='window_width', default=1280, dtype=int) + window_height = Setting(key='window_height', default=720, dtype=int) + notification = Setting(key='notification', default=True, dtype=bool) + log_games = Setting(key='show_console', default=pf.system() != 'Windows', dtype=bool) + + color_scheme = Setting(key='color_scheme', default='', dtype=str) + style_sheet = Setting(key='style_sheet', default='RareStyle', dtype=str) + + library_view = Setting(key='library_view', default=int(LibraryView.COVER), dtype=int) library_filter = Setting( - key="library_filter", - default=int(LibraryFilter.MAC if pf.system() == "Darwin" else LibraryFilter.ALL), + key='library_filter', + default=int(LibraryFilter.MAC if pf.system() == 'Darwin' else LibraryFilter.ALL), dtype=int, ) - library_order = Setting(key="library_order", default=int(LibraryOrder.TITLE), dtype=int) + library_order = Setting(key='library_order', default=int(LibraryOrder.TITLE), dtype=int) - discord_rpc_mode = Setting(key="discord_rpc_mode", default=int(DiscordRPCMode.GAME_ONLY), dtype=int) - discord_rpc_game = Setting(key="discord_rpc_game", default=True, dtype=bool) - discord_rpc_time = Setting(key="discord_rpc_time", default=True, dtype=bool) - discord_rpc_os = Setting(key="discord_rpc_os", default=True, dtype=bool) + discord_rpc_mode = Setting(key='discord_rpc_mode', default=int(DiscordRPCMode.GAME_ONLY), dtype=int) + discord_rpc_game = Setting(key='discord_rpc_game', default=True, dtype=bool) + discord_rpc_time = Setting(key='discord_rpc_time', default=True, dtype=bool) + discord_rpc_os = Setting(key='discord_rpc_os', default=True, dtype=bool) - local_shader_cache = Setting(key="local_shader_cache", default=False, dtype=bool) - create_shortcut = Setting(key="create_shortcut", default=pf.system() == "Windows", dtype=bool) + local_shader_cache = Setting(key='local_shader_cache', default=False, dtype=bool) + create_shortcut = Setting(key='create_shortcut', default=pf.system() == 'Windows', dtype=bool) app_settings = Settings() class RareAppSettings(QSettings): - __instance: Optional["RareAppSettings"] = None + __instance: Optional['RareAppSettings'] = None def __init__(self, parent=None): if RareAppSettings.__instance is not None: - raise RuntimeError("RareSettings already initialized") + raise RuntimeError('RareSettings already initialized') super(RareAppSettings, self).__init__(parent) RareAppSettings.__instance = self @staticmethod - def instance() -> "RareAppSettings": + def instance() -> 'RareAppSettings': if RareAppSettings.__instance is None: - raise RuntimeError("Uninitialized use of RareSettings") + raise RuntimeError('Uninitialized use of RareSettings') return RareAppSettings.__instance def get_value(self, option: Setting, prefix: str = None) -> Any: if prefix: - return self.value(f"{prefix}/{option.key}", defaultValue=option.default, type=option.dtype) + return self.value(f'{prefix}/{option.key}', defaultValue=option.default, type=option.dtype) else: return self.value(option.key, defaultValue=option.default, type=option.dtype) def set_value(self, option: Setting, value: Any, prefix: str = None) -> None: if prefix: - self.setValue(f"{prefix}/{option.key}", option.dtype(value)) + self.setValue(f'{prefix}/{option.key}', option.dtype(value)) else: self.setValue(option.key, option.dtype(value)) def rem_value(self, option: Setting, prefix: str = None) -> None: if prefix: - self.remove(f"{prefix}/{option.key}") + self.remove(f'{prefix}/{option.key}') else: self.remove(option.key) def get_with_global(self, option: Setting, prefix: str) -> Any: _global = self.get_value(option, None) - return self.value(f"{prefix}/{option.key}", defaultValue=_global, type=option.dtype) + return self.value(f'{prefix}/{option.key}', defaultValue=_global, type=option.dtype) def set_with_global(self, option: Setting, value: Any, prefix: str) -> None: _global = self.get_value(option, None) @@ -115,10 +115,10 @@ def deleteLater(self): __all__ = [ - "app_settings", - "RareAppSettings", - "LibraryFilter", - "LibraryOrder", - "LibraryView", - "DiscordRPCMode", + 'app_settings', + 'RareAppSettings', + 'LibraryFilter', + 'LibraryOrder', + 'LibraryView', + 'DiscordRPCMode', ] diff --git a/rare/models/steam.py b/rare/models/steam.py index e1f656864b..5651c5516a 100644 --- a/rare/models/steam.py +++ b/rare/models/steam.py @@ -3,11 +3,11 @@ import shlex from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import Any, Dict, List, Type +from typing import Any class SteamUser: - def __init__(self, long_id: str, user: Dict): + def __init__(self, long_id: str, user: dict): super(SteamUser, self).__init__() self._long_id: str = long_id self._user = user.copy() @@ -22,19 +22,19 @@ def short_id(self) -> int: @property def account_name(self) -> str: - return self._user.get("AccountName", "") + return self._user.get('AccountName', '') @property def persona_name(self) -> str: - return self._user.get("PersonaName", "") + return self._user.get('PersonaName', '') @property def most_recent(self) -> bool: - return bool(int(self._user.get("MostRecent", "0"))) + return bool(int(self._user.get('MostRecent', '0'))) @property def last_login(self) -> datetime: - return datetime.fromtimestamp(float(self._user.get("Timestamp", "0")), timezone.utc) + return datetime.fromtimestamp(float(self._user.get('Timestamp', '0')), timezone.utc) @property def __dict__(self): @@ -69,46 +69,46 @@ class SteamShortcut: DevkitOverrideAppID: int LastPlayTime: int FlatpakAppID: str - tags: Dict = field(default_factory=dict) + tags: dict = field(default_factory=dict) @classmethod - def from_dict(cls: Type["SteamShortcut"], src: Dict[str, Any]) -> "SteamShortcut": + def from_dict(cls: type['SteamShortcut'], src: dict[str, Any]) -> 'SteamShortcut': d = src.copy() tmp = cls( - appid=d.pop("appid", 0), - AppName=d.pop("AppName", ""), - Exe=d.pop("Exe", ""), - StartDir=d.pop("StartDir", ""), - icon=d.pop("icon", ""), - ShortcutPath=d.pop("ShortcutPath", ""), - LaunchOptions=d.pop("LaunchOptions", ""), - IsHidden=bool(d.pop("IsHidden", 0)), - AllowDesktopConfig=bool(d.pop("AllowDesktopConfig", 1)), - AllowOverlay=bool(d.pop("AllowOverlay", 1)), - OpenVR=bool(d.pop("OpenVR", 0)), - Devkit=bool(d.pop("Devkit", 0)), - DevkitGameID=d.pop("DevkitGameID", ""), - DevkitOverrideAppID=d.pop("DevkitOverrideAppID", 0), - LastPlayTime=d.pop("LastPlayTime", 0), - FlatpakAppID=d.pop("FlatpakAppID", ""), - tags=d.pop("tags", {}), + appid=d.pop('appid', 0), + AppName=d.pop('AppName', ''), + Exe=d.pop('Exe', ''), + StartDir=d.pop('StartDir', ''), + icon=d.pop('icon', ''), + ShortcutPath=d.pop('ShortcutPath', ''), + LaunchOptions=d.pop('LaunchOptions', ''), + IsHidden=bool(d.pop('IsHidden', 0)), + AllowDesktopConfig=bool(d.pop('AllowDesktopConfig', 1)), + AllowOverlay=bool(d.pop('AllowOverlay', 1)), + OpenVR=bool(d.pop('OpenVR', 0)), + Devkit=bool(d.pop('Devkit', 0)), + DevkitGameID=d.pop('DevkitGameID', ''), + DevkitOverrideAppID=d.pop('DevkitOverrideAppID', 0), + LastPlayTime=d.pop('LastPlayTime', 0), + FlatpakAppID=d.pop('FlatpakAppID', ''), + tags=d.pop('tags', {}), ) return tmp @classmethod def create( - cls: Type["SteamShortcut"], + cls: type['SteamShortcut'], app_name: str, app_title: str, executable: str, start_dir: str, icon: str, - launch_options: List[str], - ) -> "SteamShortcut": + launch_options: list[str], + ) -> 'SteamShortcut': shortcut = cls.from_dict({}) shortcut.appid = cls.calculate_appid(app_name) shortcut.AppName = app_title - shortcut.Exe = executable if platform.system() == "Windows" else shlex.quote(executable) + shortcut.Exe = executable if platform.system() == 'Windows' else shlex.quote(executable) shortcut.StartDir = start_dir shortcut.icon = icon shortcut.LaunchOptions = shlex.join(launch_options) @@ -116,8 +116,8 @@ def create( @staticmethod def calculate_appid(app_name) -> int: - key = "rare_steam_shortcut_" + app_name - top = binascii.crc32(str.encode(key, "utf-8")) | 0x80000000 + key = 'rare_steam_shortcut_' + app_name + top = binascii.crc32(str.encode(key, 'utf-8')) | 0x80000000 return (((top << 32) | 0x02000000) >> 32) - 0x100000000 def shortcut_appid(self) -> int: @@ -128,19 +128,19 @@ def coverart_appid(self) -> int: @property def grid_wide(self) -> str: - return f"{self.coverart_appid()}.png" + return f'{self.coverart_appid()}.png' @property def grid_tall(self) -> str: - return f"{self.coverart_appid()}p.png" + return f'{self.coverart_appid()}p.png' @property def game_hero(self) -> str: - return f"{self.coverart_appid()}_hero.png" + return f'{self.coverart_appid()}_hero.png' @property def game_logo(self) -> str: - return f"{self.coverart_appid()}_logo.png" + return f'{self.coverart_appid()}_logo.png' @property def last_played(self): diff --git a/rare/models/wrapper.py b/rare/models/wrapper.py index 057b6b01b9..50d9567ede 100644 --- a/rare/models/wrapper.py +++ b/rare/models/wrapper.py @@ -2,7 +2,6 @@ import shlex from enum import IntEnum from hashlib import md5 -from typing import Dict, List, Union class WrapperType(IntEnum): @@ -15,12 +14,12 @@ class WrapperType(IntEnum): class Wrapper: def __init__( self, - command: Union[str, List[str]], + command: str | list[str], name: str = None, wtype: WrapperType = None, enabled: bool = True, ): - self.__command: List[str] = shlex.split(command) if isinstance(command, str) else command + self.__command: list[str] = shlex.split(command) if isinstance(command, str) else command self.__name: str = name if name is not None else os.path.basename(self.__command[0]) self.__wtype: WrapperType = wtype if wtype is not None else WrapperType.USER_DEFINED self.__enabled: bool = enabled or self.__wtype == WrapperType.COMPAT_TOOL @@ -43,20 +42,20 @@ def is_enabled(self, state: bool) -> None: @property def checksum(self) -> str: - return md5(self.as_str.encode("utf-8")).hexdigest() + return md5(self.as_str.encode('utf-8')).hexdigest() @property def executable(self) -> str: return shlex.quote(self.__command[0]) @property - def command(self) -> List[str]: + def command(self) -> list[str]: return self.__command @property def as_str(self) -> str: # return " ".join(shlex.quote(part) for part in self.__command) - return " ".join(map(shlex.quote, self.__command)) + return ' '.join(map(shlex.quote, self.__command)) @property def name(self) -> str: @@ -76,11 +75,11 @@ def __bool__(self) -> bool: return True if not self.is_editable else bool(self.as_str.strip()) @classmethod - def from_dict(cls, data: Dict): + def from_dict(cls, data: dict): return cls( - command=data.get("command"), - name=data.get("name"), - wtype=WrapperType(data.get("wtype", WrapperType.USER_DEFINED)), + command=data.get('command'), + name=data.get('name'), + wtype=WrapperType(data.get('wtype', WrapperType.USER_DEFINED)), ) @property diff --git a/rare/resources/__init__.py b/rare/resources/__init__.py index c18e049eea..7b08215d28 100644 --- a/rare/resources/__init__.py +++ b/rare/resources/__init__.py @@ -4,4 +4,4 @@ import rare.resources.static_css as static_css import rare.resources.stylesheets as stylesheets -__all__ = ["resources", "static_css", "stylesheets"] +__all__ = ['resources', 'static_css', 'stylesheets'] diff --git a/rare/resources/resources.py b/rare/resources/resources.py index 274ea4bf8a..5916bab475 100644 --- a/rare/resources/resources.py +++ b/rare/resources/resources.py @@ -35633,7 +35633,7 @@ \xab\xe0\xa3\ " -qt_resource_name = b"\ +qt_resource_name = b'\ \x00\x07\ \x09\x9e\xc3#\ \x00s\ @@ -35682,9 +35682,9 @@ \x04\xa8\x91\xc2\ \x00D\ \x00a\x00r\x00k\x00e\x00r\ -" +' -qt_resource_struct = b"\ +qt_resource_struct = b'\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x14\x00\x02\x00\x00\x00\x02\x00\x00\x00\x0b\ @@ -35711,12 +35711,15 @@ \x00\x00\x01\x9c\xc3\x1e\xdd\x86\ \x00\x00\x00&\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x905 \x19\x03\ -" +' + def qInitResources(): QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + def qCleanupResources(): QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + qInitResources() diff --git a/rare/resources/static_css/stylesheet.py b/rare/resources/static_css/stylesheet.py index b9b4cb635c..0b3a86c35b 100644 --- a/rare/resources/static_css/stylesheet.py +++ b/rare/resources/static_css/stylesheet.py @@ -1,5 +1,4 @@ import os -from typing import Type, Union import qstylizer.style from PySide6.QtCore import QObject @@ -11,12 +10,12 @@ verbose = True compressLevel = 6 -compressAlgo = "zlib" +compressAlgo = 'zlib' compressThreshold = 0 -def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): - return f"#{widget_object_name(widget, '')}{subwidget}" +def css_name(widget: wrappertype | QObject | type, subwidget: str = ''): + return f'#{widget_object_name(widget, "")}{subwidget}' style = qstylizer.style.StyleSheet() @@ -30,89 +29,89 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): style.QLineEdit, style.QComboBox, ): - elem.setValues(minHeight="2.75ex") + elem.setValues(minHeight='2.75ex') # Generic flat button style['QPushButton[flat="true"]'].setValues( - border="0px", - borderRadius="5px", - backgroundColor="rgba(255, 255, 255, 5%)", + border='0px', + borderRadius='5px', + backgroundColor='rgba(255, 255, 255, 5%)', ) # InfoLabel -style.QLabel["#InfoLabel"].setValues( - color="#999", - fontStyle="italic", - fontWeight="normal", +style.QLabel['#InfoLabel'].setValues( + color='#999', + fontStyle='italic', + fontWeight='normal', ) -verify_color = QColor("#d6af57") -move_color = QColor("#41cad9") -cloud_sync_color = QColor("#dbd3d1") +verify_color = QColor('#d6af57') +move_color = QColor('#41cad9') +cloud_sync_color = QColor('#dbd3d1') # [Un]InstallButton -style.QPushButton["#InstallButton"].setValues(borderColor=QColor(0, 180, 0).name(), backgroundColor=QColor(0, 100, 0).name()) -style.QPushButton["#InstallButton"].hover.setValues(borderColor=QColor(0, 135, 0).name(), backgroundColor=QColor(0, 70, 0).name()) -style.QPushButton["#InstallButton"].disabled.setValues( +style.QPushButton['#InstallButton'].setValues(borderColor=QColor(0, 180, 0).name(), backgroundColor=QColor(0, 100, 0).name()) +style.QPushButton['#InstallButton'].hover.setValues(borderColor=QColor(0, 135, 0).name(), backgroundColor=QColor(0, 70, 0).name()) +style.QPushButton['#InstallButton'].disabled.setValues( borderColor=QColor(0, 60, 0).name(), backgroundColor=QColor(0, 40, 0).name() ) -style.QPushButton["#UninstallButton"].setValues(borderColor=QColor(180, 0, 0).name(), backgroundColor=QColor(100, 0, 0).name()) -style.QPushButton["#UninstallButton"].hover.setValues( +style.QPushButton['#UninstallButton'].setValues(borderColor=QColor(180, 0, 0).name(), backgroundColor=QColor(100, 0, 0).name()) +style.QPushButton['#UninstallButton'].hover.setValues( borderColor=QColor(135, 0, 0).name(), backgroundColor=QColor(70, 0, 0).name() ) -style.QPushButton["#UninstallButton"].disabled.setValues( +style.QPushButton['#UninstallButton'].disabled.setValues( borderColor=QColor(60, 0, 0).name(), backgroundColor=QColor(40, 0, 0).name() ) # VerifyButton -style.QPushButton["#VerifyButton"].setValues( +style.QPushButton['#VerifyButton'].setValues( borderColor=verify_color.darker(200).name(), backgroundColor=verify_color.darker(400).name() ) -style.QPushButton["#VerifyButton"].hover.setValues( +style.QPushButton['#VerifyButton'].hover.setValues( borderColor=verify_color.darker(300).name(), backgroundColor=verify_color.darker(500).name() ) -style.QPushButton["#VerifyButton"].disabled.setValues( +style.QPushButton['#VerifyButton'].disabled.setValues( borderColor=verify_color.darker(400).name(), backgroundColor=verify_color.darker(600).name() ) # MoveButton -style.QPushButton["#MoveButton"].setValues( +style.QPushButton['#MoveButton'].setValues( borderColor=move_color.darker(200).name(), backgroundColor=move_color.darker(400).name() ) -style.QPushButton["#MoveButton"].hover.setValues( +style.QPushButton['#MoveButton'].hover.setValues( borderColor=move_color.darker(300).name(), backgroundColor=move_color.darker(500).name() ) -style.QPushButton["#MoveButton"].disabled.setValues( +style.QPushButton['#MoveButton'].disabled.setValues( borderColor=move_color.darker(400).name(), backgroundColor=move_color.darker(600).name() ) # QueueWorkerLabel -style.QLabel["#QueueWorkerLabel"].setValues( - borderWidth="1px", - borderStyle="solid", - borderRadius="3px", +style.QLabel['#QueueWorkerLabel'].setValues( + borderWidth='1px', + borderStyle='solid', + borderRadius='3px', ) from rare.shared.workers.verify import VerifyWorker # noqa: E402 -style.QLabel["#QueueWorkerLabel"][f'[workertype="{widget_object_name(VerifyWorker, "")}"]'].setValues( +style.QLabel['#QueueWorkerLabel'][f'[workertype="{widget_object_name(VerifyWorker, "")}"]'].setValues( borderColor=verify_color.darker(200).name(), backgroundColor=verify_color.darker(400).name(), ) from rare.shared.workers.move import MoveWorker # noqa: E402 -style.QLabel["#QueueWorkerLabel"][f'[workertype="{widget_object_name(MoveWorker, "")}"]'].setValues( +style.QLabel['#QueueWorkerLabel'][f'[workertype="{widget_object_name(MoveWorker, "")}"]'].setValues( borderColor=move_color.darker(200).name(), backgroundColor=move_color.darker(400).name(), ) from rare.shared.workers.cloud_sync import CloudSyncWorker # noqa: E402 -style.QLabel["#QueueWorkerLabel"][f'[workertype="{widget_object_name(CloudSyncWorker, "")}"]'].setValues( +style.QLabel['#QueueWorkerLabel'][f'[workertype="{widget_object_name(CloudSyncWorker, "")}"]'].setValues( borderColor=cloud_sync_color.darker(200).name(), backgroundColor=cloud_sync_color.darker(400).name(), ) @@ -121,10 +120,10 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): from rare.components.tabs.library.widgets.library_widget import ProgressLabel # noqa: E402 style.QLabel[css_name(ProgressLabel)].setValues( - borderWidth="1px", - borderRadius="5%", - fontWeight="bold", - fontSize="16pt", + borderWidth='1px', + borderRadius='5%', + fontWeight='bold', + fontSize='16pt', ) @@ -132,127 +131,127 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): from rare.components.tabs.library.widgets.icon_widget import IconWidget # noqa: E402 icon_background_props = { - "backgroundColor": "rgba(0, 0, 0, 65%)", + 'backgroundColor': 'rgba(0, 0, 0, 65%)', } -style.QLabel[css_name(IconWidget, "StatusLabel")].setValues( - fontWeight="bold", - color="white", +style.QLabel[css_name(IconWidget, 'StatusLabel')].setValues( + fontWeight='bold', + color='white', **icon_background_props, - borderRadius="5%", - borderTopLeftRadius="11%", - borderTopRightRadius="11%", + borderRadius='5%', + borderTopLeftRadius='11%', + borderTopRightRadius='11%', ) -style.QWidget[css_name(IconWidget, "MiniWidget")].setValues( - color="rgb(238, 238, 238)", +style.QWidget[css_name(IconWidget, 'MiniWidget')].setValues( + color='rgb(238, 238, 238)', **icon_background_props, - borderRadius="5%", - borderBottomLeftRadius="9%", - borderBottomRightRadius="9%", + borderRadius='5%', + borderBottomLeftRadius='9%', + borderBottomRightRadius='9%', ) icon_bottom_label_props = { - "color": "white", - "backgroundColor": "rgba(0, 0, 0, 0%)", + 'color': 'white', + 'backgroundColor': 'rgba(0, 0, 0, 0%)', } -style.QLabel[css_name(IconWidget, "TitleLabel")].setValues( - fontWeight="bold", +style.QLabel[css_name(IconWidget, 'TitleLabel')].setValues( + fontWeight='bold', **icon_bottom_label_props, ) -style.QLabel[css_name(IconWidget, "TooltipLabel")].setValues( +style.QLabel[css_name(IconWidget, 'TooltipLabel')].setValues( **icon_bottom_label_props, ) icon_square_button_props = { - "border": "1px solid black", - "borderRadius": "10%", + 'border': '1px solid black', + 'borderRadius': '10%', } icon_square_button_props.update(icon_background_props) -style.QPushButton[css_name(IconWidget, "Button")].setValues(**icon_square_button_props) -style.QPushButton[css_name(IconWidget, "Button")].hover.borderColor.setValue("gray") +style.QPushButton[css_name(IconWidget, 'Button')].setValues(**icon_square_button_props) +style.QPushButton[css_name(IconWidget, 'Button')].hover.borderColor.setValue('gray') # ListGameWidget from rare.components.tabs.library.widgets.list_widget import ListWidget # noqa: E402 -style.QLabel[css_name(ListWidget, "TitleLabel")].fontWeight.setValue("bold") +style.QLabel[css_name(ListWidget, 'TitleLabel')].fontWeight.setValue('bold') list_status_label_props = { - "color": "white", - "backgroundColor": "rgba(0, 0, 0, 75%)", - "border": "0px solid black", - "borderRadius": "3px", - "paddingLeft": "0.3em", - "paddingRight": "0.3em", + 'color': 'white', + 'backgroundColor': 'rgba(0, 0, 0, 75%)', + 'border': '0px solid black', + 'borderRadius': '3px', + 'paddingLeft': '0.3em', + 'paddingRight': '0.3em', } -style.QLabel[css_name(ListWidget, "StatusLabel")].setValues( - fontWeight="bold", +style.QLabel[css_name(ListWidget, 'StatusLabel')].setValues( + fontWeight='bold', **list_status_label_props, ) -style.QLabel[css_name(ListWidget, "TooltipLabel")].setValues( +style.QLabel[css_name(ListWidget, 'TooltipLabel')].setValues( **list_status_label_props, ) -style.QPushButton[css_name(ListWidget, "Button")].textAlign.setValue("left") -style.QLabel[css_name(ListWidget, "InfoLabel")].color.setValue("#999") +style.QPushButton[css_name(ListWidget, 'Button')].textAlign.setValue('left') +style.QLabel[css_name(ListWidget, 'InfoLabel')].color.setValue('#999') # MainTabBar from rare.components.tabs.tab_widgets import MainTabBar # noqa: E402 -style.QTabBar[css_name(MainTabBar, "")].tab.disabled.setValues( - border="0px", - backgroundColor="transparent", +style.QTabBar[css_name(MainTabBar, '')].tab.disabled.setValues( + border='0px', + backgroundColor='transparent', ) # SelectViewWidget from rare.components.tabs.library.head_bar import SelectViewWidget # noqa: E402 -style.QPushButton[css_name(SelectViewWidget, "Button")].setValues( - border="none", - backgroundColor="transparent", +style.QPushButton[css_name(SelectViewWidget, 'Button')].setValues( + border='none', + backgroundColor='transparent', ) # ButtonLineEdit from rare.widgets.button_edit import ButtonLineEdit # noqa: E402 -style.QPushButton[css_name(ButtonLineEdit, "Button")].setValues( - backgroundColor="transparent", - border="0px", - padding="0px", +style.QPushButton[css_name(ButtonLineEdit, 'Button')].setValues( + backgroundColor='transparent', + border='0px', + padding='0px', ) # GameTagCheckBox from rare.components.tabs.library.details.details import GameTagCheckBox # noqa: E402 -style.QCheckBox[css_name(GameTagCheckBox, "")].setValues( - margin="1px", - borderWidth="1px", - borderStyle="solid", - borderColor="black", - borderRadius="3px", - padding="3px", +style.QCheckBox[css_name(GameTagCheckBox, '')].setValues( + margin='1px', + borderWidth='1px', + borderStyle='solid', + borderColor='black', + borderRadius='3px', + padding='3px', ) -if __name__ == "__main__": - with open(os.path.join(os.path.dirname(__file__), "stylesheet.qss"), "w", encoding="utf-8") as stylesheet: +if __name__ == '__main__': + with open(os.path.join(os.path.dirname(__file__), 'stylesheet.qss'), 'w', encoding='utf-8') as stylesheet: stylesheet.write(f'/* This file is auto-generated from "{os.path.basename(__file__)}". DO NOT EDIT!!! */\n\n') stylesheet.write(style.toString(recursive=True)) qt_tool_wrapper( - "rcc", + 'rcc', [ - "-g", - "python", - "--compress", + '-g', + 'python', + '--compress', str(compressLevel), - "--compress-algo", + '--compress-algo', compressAlgo, - "--threshold", + '--threshold', str(compressThreshold), - "--verbose" if verbose else "", - os.path.join(os.path.dirname(__file__), "stylesheet.qrc"), - "-o", - os.path.join(os.path.dirname(__file__), "__init__.py"), + '--verbose' if verbose else '', + os.path.join(os.path.dirname(__file__), 'stylesheet.qrc'), + '-o', + os.path.join(os.path.dirname(__file__), '__init__.py'), ], True, ) diff --git a/rare/resources/stylesheets/RareStyle/stylesheet.py b/rare/resources/stylesheets/RareStyle/stylesheet.py index 6060e06de2..679a331b9c 100644 --- a/rare/resources/stylesheets/RareStyle/stylesheet.py +++ b/rare/resources/stylesheets/RareStyle/stylesheet.py @@ -1,5 +1,4 @@ import os -from typing import Type, Union import qstylizer.parser import qstylizer.style @@ -13,16 +12,16 @@ verbose = True compressLevel = 6 -compressAlgo = "zlib" +compressAlgo = 'zlib' compressThreshold = 0 -def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): - return f"#{widget_object_name(widget, '')}{subwidget}" +def css_name(widget: wrappertype | QObject | type, subwidget: str = ''): + return f'#{widget_object_name(widget, "")}{subwidget}' # style = qstylizer.style.StyleSheet() -with open(os.path.join(os.path.dirname(__file__), "template.qss"), "r", encoding="utf-8") as template: +with open(os.path.join(os.path.dirname(__file__), 'template.qss'), encoding='utf-8') as template: style = qstylizer.parser.parse(template.read()) background_color_base = QColor(32, 34, 37) @@ -33,64 +32,64 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): border_color_editable = QColor(47, 79, 79) common_attributes = { - "borderWidth": "1px", - "borderStyle": "solid", - "borderRadius": "2px", - "padding": "1px", + 'borderWidth': '1px', + 'borderStyle': 'solid', + 'borderRadius': '2px', + 'padding': '1px', } common_attributes_editable = { - "borderColor": border_color_editable.name(), - "backgroundColor": background_color_editable.name(), - "selectionBackgroundColor": background_color_selection.name(), + 'borderColor': border_color_editable.name(), + 'backgroundColor': background_color_editable.name(), + 'selectionBackgroundColor': background_color_selection.name(), } -style.QPushButton.paddingTop.setValue("2px") -style.QPushButton.paddingBottom.setValue("2px") -style.QToolButton.paddingTop.setValue("2px") -style.QToolButton.paddingBottom.setValue("2px") +style.QPushButton.paddingTop.setValue('2px') +style.QPushButton.paddingBottom.setValue('2px') +style.QToolButton.paddingTop.setValue('2px') +style.QToolButton.paddingBottom.setValue('2px') -style.QScrollBar.vertical.width.setValue("1em") -style.QScrollBar.horizontal.height.setValue("1em") +style.QScrollBar.vertical.width.setValue('1em') +style.QScrollBar.horizontal.height.setValue('1em') -style["QTableView QTableCornerButton::section"].setValues(**common_attributes, **common_attributes_editable) +style['QTableView QTableCornerButton::section'].setValues(**common_attributes, **common_attributes_editable) style.QComboBox.comboboxPopup.setValue(0) -style["QComboBox QAbstractItemView"].setValues(**common_attributes, **common_attributes_editable) +style['QComboBox QAbstractItemView'].setValues(**common_attributes, **common_attributes_editable) style[f'QLabel[frameShape="{int(QFrame.Shape.StyledPanel)}"][frameShadow="{int(QFrame.Shadow.Sunken)}"]'].setValues( - color="#bbb", + color='#bbb', borderColor=background_color_base.darker(200).name(), backgroundColor=background_color_base.darker(110).name(), ) for selector in ( - "QListView QLineEdit", - "QTreeView QLineEdit", - "QTableView QLineEdit", + 'QListView QLineEdit', + 'QTreeView QLineEdit', + 'QTableView QLineEdit', ): - style[selector].padding.setValue("0px") + style[selector].padding.setValue('0px') -if __name__ == "__main__": - with open(os.path.join(os.path.dirname(__file__), "stylesheet.qss"), "w", encoding="utf-8") as stylesheet: +if __name__ == '__main__': + with open(os.path.join(os.path.dirname(__file__), 'stylesheet.qss'), 'w', encoding='utf-8') as stylesheet: stylesheet.write(f'/* This file is auto-generated from "{os.path.basename(__file__)}". DO NOT EDIT!!! */\n\n') stylesheet.write(style.toString(recursive=True)) qt_tool_wrapper( - "rcc", + 'rcc', [ - "-g", - "python", - "--compress", + '-g', + 'python', + '--compress', str(compressLevel), - "--compress-algo", + '--compress-algo', compressAlgo, - "--threshold", + '--threshold', str(compressThreshold), - "--verbose" if verbose else "", - os.path.join(os.path.dirname(__file__), "stylesheet.qrc"), - "-o", - os.path.join(os.path.dirname(__file__), "__init__.py"), + '--verbose' if verbose else '', + os.path.join(os.path.dirname(__file__), 'stylesheet.qrc'), + '-o', + os.path.join(os.path.dirname(__file__), '__init__.py'), ], True, ) diff --git a/rare/resources/stylesheets/__init__.py b/rare/resources/stylesheets/__init__.py index 29183dfb53..83846f6205 100644 --- a/rare/resources/stylesheets/__init__.py +++ b/rare/resources/stylesheets/__init__.py @@ -3,4 +3,4 @@ # Incomplete # import rare.resources.stylesheets.ChildOfMetropolis as ChildOfMetropolis -__all__ = ["RareStyle"] +__all__ = ['RareStyle'] diff --git a/rare/shared/__init__.py b/rare/shared/__init__.py index b31a33d034..5036b4d357 100644 --- a/rare/shared/__init__.py +++ b/rare/shared/__init__.py @@ -7,4 +7,4 @@ from .rare_core import RareCore -__all__ = ["RareCore"] +__all__ = ['RareCore'] diff --git a/rare/shared/game_process.py b/rare/shared/game_process.py index f28b3860da..a05905ef36 100644 --- a/rare/shared/game_process.py +++ b/rare/shared/game_process.py @@ -1,3 +1,4 @@ +import contextlib import json from enum import IntEnum from logging import getLogger @@ -9,7 +10,7 @@ from rare.models.launcher import Actions, ErrorModel, FinishedModel, StateChangedModel -logger = getLogger("GameProcess") +logger = getLogger('GameProcess') class GameProcess(QObject): @@ -33,7 +34,7 @@ def __init__(self, game: Game): self.timer = QTimer(self) self.timer.timeout.connect(self.__connect) self.socket = QLocalSocket() - self.socket_name = f"rare_{self.game.app_name}" + self.socket_name = f'rare_{self.game.app_name}' self.socket.connected.connect(self.__on_connected) try: @@ -43,7 +44,7 @@ def __init__(self, game: Game): # 100, # lambda: self._error_occurred(QLocalSocket.UnknownSocketError) if self.socket.error() else None # ) - logger.warning("Do not handle errors on QLocalSocket, because of an old qt version") + logger.warning('Do not handle errors on QLocalSocket, because of an old qt version') self.socket.readyRead.connect(self.__on_message) self.socket.disconnected.connect(self.__close) @@ -53,10 +54,8 @@ def connect_to_server(self, on_startup: bool): @Slot() def __close(self): - try: + with contextlib.suppress(RuntimeError): self.socket.close() - except RuntimeError: - pass @Slot() def __connect(self): @@ -66,9 +65,9 @@ def __connect(self): if self.tried_connections > 50: # 10 seconds QMessageBox.warning( None, - self.tr("Error - {}").format(self.game.app_title), + self.tr('Error - {}').format(self.game.app_title), self.tr( - "Connection to game launcher failed for {}.\nThis normally means that the game is already running." + 'Connection to game launcher failed for {}.\nThis normally means that the game is already running.' ).format(self.game.app_title), ) self.timer.stop() @@ -78,47 +77,47 @@ def __connect(self): @Slot() def __on_message(self): message = self.socket.readAll().data() - if not message.startswith(b"{"): - logger.error(f"Received unsupported message: {message.decode('utf-8')}") + if not message.startswith(b'{'): + logger.error(f'Received unsupported message: {message.decode("utf-8")}') return try: data = json.loads(message) except json.JSONDecodeError as e: logger.error(e) - logger.error("Could not load json data") + logger.error('Could not load json data') return - action = data.get("action", False) + action = data.get('action', False) if not action: - logger.error("Got unexpected action") + logger.error('Got unexpected action') elif action == Actions.finished: - logger.info(f"{self.game.app_name} {self.game.app_title} finished") + logger.info(f'{self.game.app_name} {self.game.app_title} finished') model = FinishedModel.from_json(data) self.socket.close() self.__game_finished(model.exit_code) elif action == Actions.error: model = ErrorModel.from_json(data) - logger.error(f"Error in game {self.game.app_title}: {model.error_string}") + logger.error(f'Error in game {self.game.app_title}: {model.error_string}') self.socket.close() self.__game_finished(GameProcess.Code.ERROR) QMessageBox.warning( None, - "Error", - self.tr("Error in game {}:\n{}").format(self.game.app_title, model.error_string), + 'Error', + self.tr('Error in game {}:\n{}').format(self.game.app_title, model.error_string), ) elif action == Actions.state_update: model = StateChangedModel.from_json(data) if model.new_state == StateChangedModel.States.started: - logger.info("Launched Game") + logger.info('Launched Game') self.launched.emit(GameProcess.Code.SUCCESS) @Slot() def __on_connected(self): self.timer.stop() - logger.info(f"Connection established for {self.game.app_name} ({self.game.app_title})") + logger.info(f'Connection established for {self.game.app_name} ({self.game.app_title})') if self.on_startup: - logger.info(f"Found {self.game.app_name} ({self.game.app_title}) running at startup") + logger.info(f'Found {self.game.app_name} ({self.game.app_title}) running at startup') self.launched.emit(GameProcess.Code.ON_STARTUP) @Slot(QLocalSocket.LocalSocketError) @@ -128,7 +127,7 @@ def __on_error(self, _: QLocalSocket.LocalSocketError): self.__close() self.__game_finished(GameProcess.Code.ON_STARTUP) # 1234 is exit code for startup else: - logger.error(f"{self.game.app_name} ({self.game.app_title}): {self.socket.errorString()}") + logger.error(f'{self.game.app_name} ({self.game.app_title}): {self.socket.errorString()}') def __game_finished(self, exit_code: int): self.finished.emit(exit_code) diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index f8718dc1f2..b2a0a9e87e 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -3,10 +3,11 @@ import pickle import threading import zlib +from collections.abc import Callable from logging import getLogger from multiprocessing import cpu_count from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Any import requests from legendary.lfs.eos import EOSOverlayApp @@ -78,22 +79,22 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore): # lk: the ordering in _img_types matters for the order of fallbacks # {'AndroidIcon', 'DieselGameBox', 'DieselGameBoxLogo', 'DieselGameBoxTall', 'DieselGameBoxWide', # 'ESRB', 'Featured', 'OfferImageTall', 'OfferImageWide', 'Screenshot', 'Thumbnail'} - self._img_tall_types: Tuple = ( - "DieselGameBoxTall", - "OfferImageTall", - "Thumbnail", + self._img_tall_types: tuple = ( + 'DieselGameBoxTall', + 'OfferImageTall', + 'Thumbnail', ) - self._img_wide_types: Tuple = ( - "DieselGameBoxWide", - "DieselGameBox", - "OfferImageWide", - "Screenshot", + self._img_wide_types: tuple = ( + 'DieselGameBoxWide', + 'DieselGameBox', + 'OfferImageWide', + 'Screenshot', ) - self._img_logo_types: Tuple = ("DieselGameBoxLogo",) - self._img_types: Tuple = self._img_tall_types + self._img_wide_types + self._img_logo_types + self._img_logo_types: tuple = ('DieselGameBoxLogo',) + self._img_types: tuple = self._img_tall_types + self._img_wide_types + self._img_logo_types self._dl_retries = 1 self._worker_lock = threading.Lock() - self._worker_app_names: Set[str] = set() + self._worker_app_names: set[str] = set() super(ImageManager, self).__init__() self.signals = signals self.core = core @@ -101,21 +102,21 @@ def __init__(self, signals: GlobalSignals, core: LegendaryCore): self.image_dir: Path = image_dir() if not self.image_dir.is_dir(): self.image_dir.mkdir() - self.logger.info("Created image directory at %s", self.image_dir) + self.logger.info('Created image directory at %s', self.image_dir) self.threadpool = QThreadPool(self) self.threadpool.setMaxThreadCount(min(cpu_count() * 2, 16)) @staticmethod def _img_json(app_name: str) -> Path: - return image_dir_game(app_name).joinpath("image.json") + return image_dir_game(app_name).joinpath('image.json') @staticmethod def _img_cache(app_name: str) -> Path: - return image_dir_game(app_name).joinpath("image.cache") + return image_dir_game(app_name).joinpath('image.cache') @staticmethod - def _img_all(app_name: str) -> Tuple: + def _img_all(app_name: str) -> tuple: return ( image_tall_path(app_name), image_tall_path(app_name, color=False), @@ -129,7 +130,7 @@ def _img_all(app_name: str) -> Tuple: def has_pixmaps(self, app_name: str) -> bool: return all(file.is_file() for file in self._img_all(app_name)) - def _prepare_download(self, game: Game, force: bool = False) -> Tuple[List, Dict]: + def _prepare_download(self, game: Game, force: bool = False) -> tuple[list, dict]: if force and image_dir_game(game.app_name).exists(): for file in self._img_all(game.app_name): file.unlink(missing_ok=True) @@ -138,16 +139,17 @@ def _prepare_download(self, game: Game, force: bool = False) -> Tuple[List, Dict # Load image checksums if not self._img_json(game.app_name).is_file(): - json_data: Dict = dict(zip(self._img_types, [None] * len(self._img_types))) - json_data["version"] = self._cache_version + json_data: dict = dict(zip(self._img_types, [None] * len(self._img_types), strict=False)) + json_data['version'] = self._cache_version else: - json_data = json.load(open(self._img_json(game.app_name), "r")) + with self._img_json(game.app_name).open() as fd: + json_data = json.load(fd) # Only download the best matching candidate for each image category - def best_match(key_images: List, image_types: Tuple) -> Dict: + def best_match(key_images: list, image_types: tuple) -> dict: matches = sorted( - filter(lambda image: image["type"] in image_types, key_images), - key=lambda x: image_types.index(x["type"]) if x["type"] in image_types else len(image_types), + filter(lambda image: image['type'] in image_types, key_images), + key=lambda x: image_types.index(x['type']) if x['type'] in image_types else len(image_types), reverse=False, ) try: @@ -159,9 +161,9 @@ def best_match(key_images: List, image_types: Tuple) -> Dict: candidates = tuple( image for image in [ - best_match(game.metadata.get("keyImages", []), self._img_tall_types), - best_match(game.metadata.get("keyImages", []), self._img_wide_types), - best_match(game.metadata.get("keyImages", []), self._img_logo_types), + best_match(game.metadata.get('keyImages', []), self._img_tall_types), + best_match(game.metadata.get('keyImages', []), self._img_wide_types), + best_match(game.metadata.get('keyImages', []), self._img_logo_types), ] if bool(image) ) @@ -171,40 +173,40 @@ def best_match(key_images: List, image_types: Tuple) -> Dict: # lk: so everything below it is skipped # TODO: Move this into the thread, maybe, concurrency could help here too updates = [] - if (not self.has_pixmaps(game.app_name)): + if not self.has_pixmaps(game.app_name): if not candidates: - cover = "epic.png" if game.app_name == EOSOverlayApp.app_name else "cover.png" + cover = 'epic.png' if game.app_name == EOSOverlayApp.app_name else 'cover.png' # lk: fast path for games without images, convert Rare's logo - cache_data: Dict = dict(zip(self._img_types, [None] * len(self._img_types))) - with open(resources_path.joinpath("images", cover), "rb") as fd: - cache_data["DieselGameBoxTall"] = fd.read() - with open(resources_path.joinpath("images", cover), "rb") as fd: - cache_data["DieselGameBoxWide"] = fd.read() + cache_data: dict = dict(zip(self._img_types, [None] * len(self._img_types), strict=False)) + with open(resources_path.joinpath('images', cover), 'rb') as fd: + cache_data['DieselGameBoxTall'] = fd.read() + with open(resources_path.joinpath('images', cover), 'rb') as fd: + cache_data['DieselGameBoxWide'] = fd.read() # cache_data["DieselGameBoxLogo"] = open( # resources_path.joinpath("images", "logo.png"), "rb").read() self._convert(game, cache_data) - json_data["cache"] = None - json_data["scale"] = ImageSize.Tall.pixel_ratio - json_data["size"] = { - "w": ImageSize.Tall.size.width(), - "h": ImageSize.Tall.size.height(), + json_data['cache'] = None + json_data['scale'] = ImageSize.Tall.pixel_ratio + json_data['size'] = { + 'w': ImageSize.Tall.size.width(), + 'h': ImageSize.Tall.size.height(), } - with open(self._img_json(game.app_name), "w", encoding="utf-8") as file: + with open(self._img_json(game.app_name), 'w', encoding='utf-8') as file: json.dump(json_data, file) else: - updates = [image for image in candidates if image["type"] in self._img_types] + updates = [image for image in candidates if image['type'] in self._img_types] else: for image in candidates: - if image["type"] in self._img_types: - if image["type"] not in json_data.keys() or json_data[image["type"]] != image["md5"]: + if image['type'] in self._img_types: + if image['type'] not in json_data or json_data[image['type']] != image['md5']: updates.append(image) return updates, json_data - def _download(self, updates: List, json_data: Dict, game: Game) -> bool: + def _download(self, updates: list, json_data: dict, game: Game) -> bool: # Decompress existing image.cache if not self._img_cache(game.app_name).is_file(): - cache_data: Dict[str, Any] = dict(zip(self._img_types, [None] * len(self._img_types))) + cache_data: dict[str, Any] = dict(zip(self._img_types, [None] * len(self._img_types), strict=False)) else: cache_data = self._decompress(game) @@ -213,38 +215,38 @@ def _download(self, updates: List, json_data: Dict, game: Game) -> bool: downloads = [ image for image in updates - if (cache_data.get(image["type"], None) is None or json_data[image["type"]] != image["md5"]) + if (cache_data.get(image['type'], None) is None or json_data[image['type']] != image['md5']) ] for image in downloads: self.logger.debug( - "Downloading %s for %s (%s)", - image["type"], + 'Downloading %s for %s (%s)', + image['type'], game.app_name, game.app_title, ) - json_data[image["type"]] = image["md5"] - if image["type"] in self._img_tall_types: + json_data[image['type']] = image['md5'] + if image['type'] in self._img_tall_types: payload = { - "resize": 1, - "w": ImageSize.Tall.size.width(), - "h": ImageSize.Tall.size.height(), + 'resize': 1, + 'w': ImageSize.Tall.size.width(), + 'h': ImageSize.Tall.size.height(), } - elif image["type"] in self._img_wide_types: + elif image['type'] in self._img_wide_types: payload = { - "resize": 1, - "w": ImageSize.Wide.size.width(), - "h": ImageSize.Wide.size.height(), + 'resize': 1, + 'w': ImageSize.Wide.size.width(), + 'h': ImageSize.Wide.size.height(), } else: # Set the larger of the sizes for everything else payload = { - "resize": 1, - "w": ImageSize.Wide.size.width(), - "h": ImageSize.Wide.size.height(), + 'resize': 1, + 'w': ImageSize.Wide.size.width(), + 'h': ImageSize.Wide.size.height(), } try: - cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=10).content + cache_data[image['type']] = requests.get(image['url'], params=payload, timeout=10).content except Exception as e: self.logger.error(e) return False @@ -252,14 +254,14 @@ def _download(self, updates: List, json_data: Dict, game: Game) -> bool: # lk: test the cached and downloaded data if they describe an image with valid dimensions # I do not like this, it should add a bunch of processing for something simple but I am out of ideas for image in updates: - image_data = QImage().fromData(cache_data[image["type"]]) + image_data = QImage().fromData(cache_data[image['type']]) if not (image_data.width() and image_data.height()): - with open(resources_path.joinpath("images", "cover.png"), "rb") as fd: - cache_data[image["type"]] = fd.read() - json_data[image["type"]] = None + with open(resources_path.joinpath('images', 'cover.png'), 'rb') as fd: + cache_data[image['type']] = fd.read() + json_data[image['type']] = None self.logger.error( - "Invalid image %s data for %s (%s)", - image["type"], + 'Invalid image %s data for %s (%s)', + image['type'], game.app_name, game.app_title, ) @@ -273,25 +275,25 @@ def _download(self, updates: List, json_data: Dict, game: Game) -> bool: # hash image cache try: - with open(self._img_cache(game.app_name), "rb") as archive: + with open(self._img_cache(game.app_name), 'rb') as archive: archive_hash = hashlib.md5(archive.read()).hexdigest() except FileNotFoundError: archive_hash = None - json_data["cache"] = archive_hash - json_data["scale"] = ImageSize.Tall.pixel_ratio - json_data["size"] = { - "w": ImageSize.Tall.size.width(), - "h": ImageSize.Tall.size.height(), + json_data['cache'] = archive_hash + json_data['scale'] = ImageSize.Tall.pixel_ratio + json_data['size'] = { + 'w': ImageSize.Tall.size.width(), + 'h': ImageSize.Tall.size.height(), } # write image.json - with open(self._img_json(game.app_name), "w", encoding="utf-8") as file: + with open(self._img_json(game.app_name), 'w', encoding='utf-8') as file: json.dump(json_data, file) return bool(updates) - _icon_overlay: Optional[QPainterPath] = None + _icon_overlay: QPainterPath | None = None def _generate_icon_overlay(self, rect: QRect) -> QPainterPath: if self._icon_overlay is not None: @@ -376,21 +378,21 @@ def _save_image(self, image: QImage, color_path: Path, gray_path: Path): # image = image.convertToFormat(QImage.Format_Indexed8) # add the alpha channel back to the cover image = image.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) - image.save(color_path.as_posix(), format="PNG") + image.save(color_path.as_posix(), format='PNG') # quick way to convert to grayscale, but keep the alpha channel alpha = image.convertToFormat(QImage.Format.Format_Alpha8) image = image.convertToFormat(QImage.Format.Format_Grayscale8) # add the alpha channel back to the grayscale cover image = image.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) image.setAlphaChannel(alpha) - image.save(gray_path.as_posix(), format="PNG") + image.save(gray_path.as_posix(), format='PNG') def _convert(self, game, images, force=False) -> None: for file in self._img_all(game.app_name): if force and file.exists(): file.unlink(missing_ok=True) - def find_image_data(image_types: Tuple): + def find_image_data(image_types: tuple): data = None for image_type in image_types: if images.get(image_type, None) is not None: @@ -402,7 +404,7 @@ def find_image_data(image_types: Tuple): wide_data = find_image_data(self._img_wide_types) logo_data = find_image_data(self._img_logo_types) - icon_source = "wide" if tall_data is None else "tall" + icon_source = 'wide' if tall_data is None else 'tall' if tall_data is None and wide_data is not None: tall_data = wide_data @@ -424,7 +426,7 @@ def find_image_data(image_types: Tuple): image_wide_path(game.app_name, color=False), ) - icon = self._convert_icon(tall if icon_source == "tall" else wide) + icon = self._convert_icon(tall if icon_source == 'tall' else wide) self._save_image( icon, image_icon_path(game.app_name), @@ -435,19 +437,19 @@ def find_image_data(image_types: Tuple): format=desktop_icon_suffix().upper(), ) - def _compress(self, game: Game, data: Dict) -> None: - archive = open(self._img_cache(game.app_name), "wb") + def _compress(self, game: Game, data: dict) -> None: + archive = open(self._img_cache(game.app_name), 'wb') # noqa: SIM115 cdata = zlib.compress(pickle.dumps(data), level=-1) archive.write(cdata) archive.close() - def _decompress(self, game: Game) -> Dict: - archive = open(self._img_cache(game.app_name), "rb") + def _decompress(self, game: Game) -> dict: + archive = open(self._img_cache(game.app_name), 'rb') # noqa: SIM115 try: data = zlib.decompress(archive.read()) data = pickle.loads(data) except zlib.error: - data = dict(zip(self._img_types, [None] * len(self._img_types))) + data = dict(zip(self._img_types, [None] * len(self._img_types), strict=False)) finally: archive.close() return data @@ -471,7 +473,7 @@ def _download_image(self, game, force: bool): updates, json_data = self._prepare_download(game, force) if updates: self._download(updates, json_data, game) - self.logger.debug("Emitting singal for %s (%s)", game.app_name, game.app_title) + self.logger.debug('Emitting singal for %s (%s)', game.app_name, game.app_title) def download_image(self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False) -> None: if game.app_name in self._worker_app_names: @@ -499,11 +501,11 @@ def download_image_blocking(self, game: Game, load_callback: Callable[[], None], @staticmethod def _get_cover( - container: Union[Type[QPixmap], Type[QImage]], + container: type[QPixmap] | type[QImage], app_name: str, preset: ImageSize.Preset, color: bool, - ) -> Union[QPixmap, QImage]: + ) -> QPixmap | QImage: ret = container() if preset.orientation == ImageType.Icon: if image_icon_path(app_name, color).is_file(): @@ -515,7 +517,7 @@ def _get_cover( if image_wide_path(app_name, color).is_file(): ret.load(image_wide_path(app_name, color).as_posix()) else: - raise RuntimeError("Unknown image preset") + raise RuntimeError('Unknown image preset') if not ret.isNull(): device = ImageSize.Preset( divisor=preset.base.divisor, diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 0456029cf3..0e7307ffa8 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -2,9 +2,10 @@ import os import time from argparse import Namespace +from collections.abc import Callable, Iterable, Iterator from itertools import chain from logging import getLogger -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union +from typing import Optional from legendary.lfs.eos import EOSOverlayApp from legendary.models.game import Game, SaveGameFile @@ -30,7 +31,7 @@ QueueWorker, VerifyWorker, ) -from .workers.fetch import SteamAppIdsWorker +from .workers.fetch import RuntimeAssetsWorker from .workers.uninstall import uninstall_game from .workers.worker import QueueWorkerInfo, QueueWorkerState from .wrappers import Wrappers @@ -45,19 +46,19 @@ class RareCore(QObject): # completed_entitlements = Signal() # lk: special case class attribute, this has to be here - __instance: Optional["RareCore"] = None + __instance: Optional['RareCore'] = None def __init__(self, settings: RareAppSettings, args: Namespace): if self.__instance is not None: - raise RuntimeError("RareCore already initialized") + raise RuntimeError('RareCore already initialized') super(RareCore, self).__init__() self.logger = getLogger(type(self).__name__) self.__settings = settings - self.__args: Optional[Namespace] = None - self.__signals: Optional[GlobalSignals] = None - self.__core: Optional[LegendaryCore] = None - self.__image_manager: Optional[ImageManager] = None - self.__wrappers: Optional[Wrappers] = None + self.__args: Namespace | None = None + self.__signals: GlobalSignals | None = None + self.__core: LegendaryCore | None = None + self.__image_manager: ImageManager | None = None + self.__wrappers: Wrappers | None = None self.__start_time = time.perf_counter() @@ -68,14 +69,14 @@ def __init__(self, settings: RareAppSettings, args: Namespace): self.image_manager(init=True) self.__wrappers = Wrappers() - self.workers_disk: List[QueueWorker] = [] + self.workers_disk: list[QueueWorker] = [] self.threadpool_disk = QThreadPool() self.threadpool_disk.setMaxThreadCount(2) - self.workers_net: List[QueueWorker] = [] + self.workers_net: list[QueueWorker] = [] self.threadpool_net = QThreadPool() self.threadpool_net.setMaxThreadCount(2) - self.__library: Dict[str, RareGame] = {} + self.__library: dict[str, RareGame] = {} self.__eos_overlay = RareEosOverlay(self.__settings, self.__core, self.__image_manager, EOSOverlayApp) self.__eos_overlay.signals.game.install.connect(self.__signals.game.install) self.__eos_overlay.signals.game.uninstall.connect(self.__signals.game.uninstall) @@ -103,7 +104,7 @@ def enqueue_worker(self, rgame: RareGame, worker: QueueWorker): self.workers_disk.append(worker) self.threadpool_disk.start(worker, priority=0) else: - raise RuntimeError(f"Cannot enqueue unkown worker type {type(worker).__name__}") + raise RuntimeError(f'Cannot enqueue unkown worker type {type(worker).__name__}') self.__signals.application.update_statusbar.emit() def _on_worker_finished(self, worker: QueueWorker): @@ -135,98 +136,98 @@ def queue_info(self) -> Iterable[QueueWorkerInfo]: yield w.worker_info() @staticmethod - def instance() -> "RareCore": - raise RuntimeError("RareCore.instance() method is deprecated") + def instance() -> 'RareCore': + raise RuntimeError('RareCore.instance() method is deprecated') def signals(self, init: bool = False) -> GlobalSignals: if self.__signals is None and not init: - raise RuntimeError("Uninitialized use of GlobalSignalsSingleton") + raise RuntimeError('Uninitialized use of GlobalSignalsSingleton') if self.__signals is not None and init: - raise RuntimeError("GlobalSignals already initialized") + raise RuntimeError('GlobalSignals already initialized') if init: self.__signals = GlobalSignals() return self.__signals - def args(self, args: Namespace = None) -> Optional[Namespace]: + def args(self, args: Namespace = None) -> Namespace | None: if self.__args is None and args is None: - raise RuntimeError("Uninitialized use of ArgumentsSingleton") + raise RuntimeError('Uninitialized use of ArgumentsSingleton') if self.__args is not None and args is not None: - raise RuntimeError("Arguments already initialized") + raise RuntimeError('Arguments already initialized') if args is not None: self.__args = args return self.__args def core(self, init: bool = False) -> LegendaryCore: if self.__core is None and not init: - raise RuntimeError("Uninitialized use of LegendaryCoreSingleton") + raise RuntimeError('Uninitialized use of LegendaryCoreSingleton') if self.__core is not None and init: - raise RuntimeError("LegendaryCore already initialized") + raise RuntimeError('LegendaryCore already initialized') if init: try: self.__core = LegendaryCore() except configparser.MissingSectionHeaderError as e: - self.logger.warning("Config is corrupt: %s", e) - if config_path := os.environ.get("LEGENDARY_CONFIG_PATH"): + self.logger.warning('Config is corrupt: %s', e) + if config_path := os.environ.get('LEGENDARY_CONFIG_PATH'): path = config_path - elif config_path := os.environ.get("XDG_CONFIG_HOME"): - path = os.path.join(config_path, "legendary") + elif config_path := os.environ.get('XDG_CONFIG_HOME'): + path = os.path.join(config_path, 'legendary') else: - path = os.path.expanduser("~/.config/legendary") - self.logger.info("Creating config in path: %s", config_path) - with open(os.path.join(path, "config.ini"), "w", encoding="utf-8") as config_file: - config_file.write("[Legendary]") + path = os.path.expanduser('~/.config/legendary') + self.logger.info('Creating config in path: %s', config_path) + with open(os.path.join(path, 'config.ini'), 'w', encoding='utf-8') as config_file: + config_file.write('[Legendary]') self.__core = LegendaryCore() - self.__core.egs._store_gql_host = "launcher.store.epicgames.com" + self.__core.egs._store_gql_host = 'launcher.store.epicgames.com' # Initialize sections if they don't exist - for section in ["Legendary", "default", "default.env"]: + for section in ['Legendary', 'default', 'default.env']: if section not in self.__core.lgd.config.sections(): self.__core.lgd.config.add_section(section) # Set some platform defaults if unset - def check_config(option: str, accepted: Set = None) -> bool: - _exists = self.__core.lgd.config.has_option("Legendary", option) - _value = self.__core.lgd.config.get("Legendary", option, fallback="") + def check_config(option: str, accepted: set = None) -> bool: + _exists = self.__core.lgd.config.has_option('Legendary', option) + _value = self.__core.lgd.config.get('Legendary', option, fallback='') _accepted = _value in accepted if accepted is not None else True return _exists and bool(_value) and _accepted - if not check_config("default_platform", {"Windows", "Win32", "Mac"}): - self.__core.lgd.config.set("Legendary", "default_platform", self.__core.default_platform) - if not check_config("install_dir"): - self.__core.lgd.config.set("Legendary", "install_dir", self.__core.get_default_install_dir()) - if not check_config("mac_install_dir"): + if not check_config('default_platform', {'Windows', 'Win32', 'Mac'}): + self.__core.lgd.config.set('Legendary', 'default_platform', self.__core.default_platform) + if not check_config('install_dir'): + self.__core.lgd.config.set('Legendary', 'install_dir', self.__core.get_default_install_dir()) + if not check_config('mac_install_dir'): self.__core.lgd.config.set( - "Legendary", - "mac_install_dir", + 'Legendary', + 'mac_install_dir', self.__core.get_default_install_dir(self.__core.default_platform), ) # Always set these options # Avoid implicitly falling back to Windows games on macOS - self.__core.lgd.config.set("Legendary", "install_platform_fallback", str(False)) + self.__core.lgd.config.set('Legendary', 'install_platform_fallback', str(False)) # Force-disable automatic use of crossover on macOS (remove this when we support crossover) - self.__core.lgd.config.set("Legendary", "disable_auto_crossover", str(True)) + self.__core.lgd.config.set('Legendary', 'disable_auto_crossover', str(True)) # Force-disable automatic sync with EGL, it seems to have issues - self.__core.lgd.config.set("Legendary", "egl_sync", str(False)) + self.__core.lgd.config.set('Legendary', 'egl_sync', str(False)) # workaround if egl sync enabled, but no programdata_path # programdata_path might be unset if logging in through the browser if self.__core.egl_sync_enabled: if self.__core.egl.programdata_path is None: - self.__core.lgd.config.remove_option("Legendary", "egl_sync") + self.__core.lgd.config.remove_option('Legendary', 'egl_sync') else: if not os.path.exists(self.__core.egl.programdata_path): - self.__core.lgd.config.remove_option("Legendary", "egl_sync") + self.__core.lgd.config.remove_option('Legendary', 'egl_sync') self.__core.lgd.save_config() return self.__core def image_manager(self, init: bool = False) -> ImageManager: if self.__image_manager is None and not init: - raise RuntimeError("Uninitialized use of ImageManagerSingleton") + raise RuntimeError('Uninitialized use of ImageManagerSingleton') if self.__image_manager is not None and init: - raise RuntimeError("ImageManager already initialized") + raise RuntimeError('ImageManager already initialized') if self.__image_manager is None: self.__image_manager = ImageManager(self.signals(), self.core()) return self.__image_manager @@ -270,26 +271,26 @@ def __validate_install(self, rgame: RareGame): dlc.igame = None self.logger.info(f'Removing "{rgame.app_title}" because "{rgame.igame.install_path}" does not exist...') uninstall_game(self.__core, rgame, self.logger, keep_files=True, keep_config=True) - self.logger.info(f"Uninstalled {rgame.app_title}, because no game files exist") + self.logger.info(f'Uninstalled {rgame.app_title}, because no game files exist') rgame.igame = None return # lk: games that don't have an override and can't find their executable due to case sensitivity # lk: will still erroneously require verification. This might need to be removed completely # lk: or be decoupled from the verification requirement - if override_exe := self.__core.lgd.config.get(rgame.app_name, "override_exe", fallback=""): + if override_exe := self.__core.lgd.config.get(rgame.app_name, 'override_exe', fallback=''): igame_executable = override_exe else: igame_executable = rgame.igame.executable # lk: Case-insensitive search for the game's executable (example: Brothers - A Tale of two Sons) - executable_path = os.path.join(rgame.igame.install_path, igame_executable.replace("\\", "/").lstrip("/")) + executable_path = os.path.join(rgame.igame.install_path, igame_executable.replace('\\', '/').lstrip('/')) file_list = map(str.lower, os.listdir(os.path.dirname(executable_path))) if os.path.basename(executable_path).lower() not in file_list: rgame.igame.needs_verification = True self.__core.lgd.set_installed_game(rgame.app_name, rgame.igame) rgame.update_igame() - self.logger.info(f"{rgame.app_title} needs verification") + self.logger.info(f'{rgame.app_title} needs verification') - def get_game(self, app_name: str) -> Union[RareEosOverlay, RareGame]: + def get_game(self, app_name: str) -> RareEosOverlay | RareGame: if app_name == EOSOverlayApp.app_name: return self.__eos_overlay return self.__library[app_name] @@ -320,15 +321,15 @@ def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[Rare def __create_or_update_rgame(self, game: Game) -> RareGame: if rgame := self.__library.get(game.app_name, False): - self.logger.warning(f"{rgame.app_name} already present in {type(self).__name__}") - self.logger.info(f"Updating Game for {rgame.app_name}") + self.logger.warning(f'{rgame.app_name} already present in {type(self).__name__}') + self.logger.info(f'Updating Game for {rgame.app_name}') rgame.update_rgame() else: rgame = RareGame(self.__settings, 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: + 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) @@ -347,8 +348,8 @@ def __add_games_and_dlcs(self, games: List[Game], dlcs_dict: Dict[str, List]) -> self.logger.info(f'Marking "{rgame.app_title}" as not installed because an exception has occurred...') self.logger.error(e) rgame.set_installed(False) - progress = int(idx / length * self.__fetch_progress) + (100 - self.__fetch_progress) - self.progress.emit(progress, self.tr("Loaded {}").format(rgame.app_title)) + progress = int((idx / length) * (100 - self.__fetch_progress)) + self.__fetch_progress + self.progress.emit(progress, self.tr('Loaded {}').format(rgame.app_title)) @Slot(int, str) def __on_fetch_progress(self, increment: int, message: str): @@ -356,7 +357,7 @@ def __on_fetch_progress(self, increment: int, message: str): self.progress.emit(self.__fetch_progress, message) @Slot(object, int) - def __on_fetch_result(self, result: Tuple, result_type: int): + def __on_fetch_result(self, result: tuple, result_type: int): if result_type == FetchWorker.Result.GAMESDLCS: self.__add_games_and_dlcs(*result) self.__fetched_games_dlcs = True @@ -365,10 +366,10 @@ def __on_fetch_result(self, result: Tuple, result_type: int): self.__core.lgd.entitlements = result self.__fetched_entitlements = True - if result_type == FetchWorker.Result.STEAMAPPIDS: + if result_type == FetchWorker.Result.EXTRAS: self.__fetched_steamappids = True - self.logger.info("Acquired data from %s worker", FetchWorker.Result(result_type).name) + self.logger.info('Acquired data from %s worker', FetchWorker.Result(result_type).name) # Return early if there are still things to fetch if not all( @@ -380,54 +381,54 @@ def __on_fetch_result(self, result: Tuple, result_type: int): ): return - self.logger.debug("Fetch time %s seconds", time.perf_counter() - self.__start_time) + self.logger.debug('Fetch time %s seconds', time.perf_counter() - self.__start_time) self.__wrappers.import_wrappers(self.__settings, self.__core, [rgame.app_name for rgame in self.games]) # Look for Rare shortcuts in Steam steam_shortcuts.load_steam_shortcuts() - self.progress.emit(100, self.tr("Launching Rare")) + self.progress.emit(100, self.tr('Launching Rare')) self.completed.emit() QTimer.singleShot(100, self.__post_init) def fetch(self): self.__start_time = time.perf_counter() - games_dlcs_worker = GamesDlcsWorker(self.__settings, self.__core, self.__args) + games_dlcs_worker = GamesDlcsWorker(self.__settings, self.__core, self.__args, segment=40) games_dlcs_worker.signals.progress.connect(self.__on_fetch_progress) games_dlcs_worker.signals.result.connect(self.__on_fetch_result) - entitlements_worker = EntitlementsWorker(self.__settings, self.__core, self.__args) + entitlements_worker = EntitlementsWorker(self.__settings, self.__core, self.__args, segment=5) entitlements_worker.signals.progress.connect(self.__on_fetch_progress) entitlements_worker.signals.result.connect(self.__on_fetch_result) - steamappids_worker = SteamAppIdsWorker(self.__settings, self.__core, self.__args) - steamappids_worker.signals.progress.connect(self.__on_fetch_progress) - steamappids_worker.signals.result.connect(self.__on_fetch_result) + runtime_assets_worker = RuntimeAssetsWorker(self.__settings, self.__core, self.__args, segment=10) + runtime_assets_worker.signals.progress.connect(self.__on_fetch_progress) + runtime_assets_worker.signals.result.connect(self.__on_fetch_result) QThreadPool.globalInstance().start(games_dlcs_worker) QThreadPool.globalInstance().start(entitlements_worker) - QThreadPool.globalInstance().start(steamappids_worker) + QThreadPool.globalInstance().start(runtime_assets_worker) def __fetch_saves(self) -> None: - saves_dict: Dict[str, List[SaveGameFile]] = {} + saves_dict: dict[str, list[SaveGameFile]] = {} try: - with timelogger(self.logger, "Request saves"): + with timelogger(self.logger, 'Request saves'): saves_list = self.__core.get_save_games() for s in saves_list: - if s.app_name not in saves_dict.keys(): + if s.app_name not in saves_dict: saves_dict[s.app_name] = [s] else: saves_dict[s.app_name].append(s) for app_name, saves in saves_dict.items(): - if app_name not in self.__library.keys(): + if app_name not in self.__library: continue self.__library[app_name].load_saves(saves) except (HTTPError, ConnectionError) as e: - self.logger.error("Exception while fetching saves from EGS.") + self.logger.error('Exception while fetching saves from EGS.') self.logger.error(e) return - self.logger.info(f"Saves: {len(saves_dict)}") + self.logger.info(f'Saves: {len(saves_dict)}') def fetch_saves(self): saves_worker = QRunnable.create(self.__fetch_saves) @@ -443,8 +444,8 @@ def __post_init(self) -> None: self.resolve_origin() @property - def game_tags(self) -> Tuple[str, ...]: - default_tags = ("favorite", "backlog", "completed", "hidden") + def game_tags(self) -> tuple[str, ...]: + default_tags = ('favorite', 'backlog', 'completed', 'hidden') custom_tags = set() for rgame in self.games: custom_tags.update(rgame.tags) @@ -478,7 +479,7 @@ def game_list(self) -> Iterator[Game]: yield game.game @property - def dlcs(self) -> Dict[str, set[RareGame]]: + def dlcs(self) -> dict[str, set[RareGame]]: """! RareGames that ARE DLCs themselves """ diff --git a/rare/shared/workers/__init__.py b/rare/shared/workers/__init__.py index b4e725abaf..92382c4156 100644 --- a/rare/shared/workers/__init__.py +++ b/rare/shared/workers/__init__.py @@ -8,16 +8,16 @@ from .worker import QueueWorker, Worker __all__ = [ - "CloudSyncWorker", - "EntitlementsWorker", - "FetchWorker", - "GamesDlcsWorker", - "InstallInfoWorker", - "MoveInfoWorker", - "MoveWorker", - "OriginWineWorker", - "QueueWorker", - "UninstallWorker", - "VerifyWorker", - "Worker", + 'CloudSyncWorker', + 'EntitlementsWorker', + 'FetchWorker', + 'GamesDlcsWorker', + 'InstallInfoWorker', + 'MoveInfoWorker', + 'MoveWorker', + 'OriginWineWorker', + 'QueueWorker', + 'UninstallWorker', + 'VerifyWorker', + 'Worker', ] diff --git a/rare/shared/workers/cloud_sync.py b/rare/shared/workers/cloud_sync.py index c9ad9b8934..89113b5390 100644 --- a/rare/shared/workers/cloud_sync.py +++ b/rare/shared/workers/cloud_sync.py @@ -33,7 +33,7 @@ def worker_info(self) -> QueueWorkerInfo: app_name=self.rgame.app_name, app_title=self.rgame.app_title, type=type(self).__name__, - prefix="Syncing", + prefix='Syncing', state=self.state, ) diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index 4c2dad2adc..2ff332a3af 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -1,5 +1,7 @@ +import os import platform from argparse import Namespace +from datetime import datetime from enum import IntEnum from PySide6.QtCore import QObject, Signal @@ -8,7 +10,9 @@ from rare.lgndr.core import LegendaryCore from rare.models.settings import RareAppSettings, app_settings from rare.utils.metrics import timelogger -from rare.utils.steam_grades import SteamGrades +from rare.utils.steam_grades import steam_grades +from rare.utils.workarounds import workarounds +from rare.utils.wrapper_exe import download_wrapper_exe from .worker import Worker @@ -23,44 +27,65 @@ class Result(IntEnum): ERROR = 0 GAMESDLCS = 1 ENTITLEMENTS = 2 - STEAMAPPIDS = 3 + EXTRAS = 3 - def __init__(self, settings: RareAppSettings, core: LegendaryCore, args: Namespace): + def __init__(self, settings: RareAppSettings, core: LegendaryCore, args: Namespace, segment: int): super(FetchWorker, self).__init__() self.signals = FetchWorkerSignals() self.settings = settings self.core = core self.args = args + self.segment = segment -class SteamAppIdsWorker(FetchWorker): +class RuntimeAssetsWorker(FetchWorker): def run_real(self): - if platform.system() != "Windows" and not self.args.offline: - self.signals.progress.emit(0, self.signals.tr("Updating Steam AppIds")) - with timelogger(self.logger, "Request Steam AppIds"): + increment = self.segment // 3 + + if not self.args.offline and platform.system() not in {'Windows'}: + self.signals.progress.emit(increment, self.signals.tr('Updating Steam AppIds')) + with timelogger(self.logger, 'Request Steam AppIds'): + try: + steam_grades.load_steam_appids() + except Exception as e: + self.logger.warning(e) + if not self.args.offline: + self.signals.progress.emit(increment, self.signals.tr('Updating workarounds')) + with timelogger(self.logger, 'Request workarounds'): try: - SteamGrades().load_steam_appids() + workarounds.load_workarounds() except Exception as e: self.logger.warning(e) - self.signals.result.emit((), FetchWorker.Result.STEAMAPPIDS) + if not self.args.offline: + self.signals.progress.emit(increment, self.signals.tr('Updating workarounds')) + with timelogger(self.logger, 'Request wrapper exe'): + try: + download_wrapper_exe() + except Exception as e: + self.logger.warning(e) + self.signals.result.emit((), FetchWorker.Result.EXTRAS) return class EntitlementsWorker(FetchWorker): def run_real(self): - want_entitlements = not self.settings.get_value(app_settings.exclude_entitlements) + mod_time = datetime.fromtimestamp(os.path.getmtime(os.path.join(self.core.lgd.path, 'entitlements.json'))) + elapsed_days = abs(datetime.now() - mod_time).days + + want_entitlements = not self.settings.get_value(app_settings.exclude_entitlements) and elapsed_days > 1 + want_entitlements = want_entitlements and not self.args.offline entitlements = () - if want_entitlements and not self.args.offline: + if want_entitlements: # Get entitlements, Ubisoft integration also uses them - self.signals.progress.emit(0, self.signals.tr("Updating entitlements")) - with timelogger(self.logger, "Request entitlements"): + self.signals.progress.emit(self.segment, self.signals.tr('Updating entitlements')) + with timelogger(self.logger, 'Request entitlements'): try: entitlements = self.core.egs.get_user_entitlements_full() except Exception as e: self.logger.warning(e) self.core.lgd.entitlements = entitlements - self.logger.info("Entitlements: %s", len(list(entitlements))) + self.logger.info('Entitlements: %s', len(list(entitlements))) self.signals.result.emit(entitlements, FetchWorker.Result.ENTITLEMENTS) return @@ -75,67 +100,70 @@ def run_real(self): want_win32 = self.settings.get_value(app_settings.win32_meta) want_macos = self.settings.get_value(app_settings.macos_meta) want_non_asset = not self.settings.get_value(app_settings.exclude_non_asset) - need_macos = platform.system() == "Darwin" + need_macos = platform.system() == 'Darwin' need_windows = not any([want_win32, want_macos, need_macos]) and not self.args.offline + # One more step for the "Preparing library" message + steps = sum((want_win32, need_macos or want_macos, want_non_asset)) + 2 + increment = self.segment // steps + if want_win32: self.logger.info( - "Requesting Win32 metadata due to %s, %s Unreal engine", - "settings" if want_win32 else "debug", - "with" if want_unreal else "without", + 'Requesting Win32 metadata due to %s, %s Unreal engine', + 'settings' if want_win32 else 'debug', + 'with' if want_unreal else 'without', ) - self.signals.progress.emit(00, self.signals.tr("Updating game metadata for Windows")) - with timelogger(self.logger, "Request Win32 games"): + self.signals.progress.emit(increment, self.signals.tr('Updating game metadata for Windows')) + with timelogger(self.logger, 'Request Win32 games'): self.core.get_game_and_dlc_list( update_assets=not self.args.offline, - platform="Win32", + platform='Win32', skip_ue=not want_unreal, ) if need_macos or want_macos: self.logger.info( - "Requesting macOS metadata due to %s, %s Unreal engine", - "platform" if need_macos else "settings" if want_macos else "debug", - "with" if want_unreal else "without", + 'Requesting macOS metadata due to %s, %s Unreal engine', + 'platform' if need_macos else 'settings' if want_macos else 'debug', + 'with' if want_unreal else 'without', ) - self.signals.progress.emit(15, self.signals.tr("Updating game metadata for macOS")) - with timelogger(self.logger, "Request macOS games"): + self.signals.progress.emit(increment, self.signals.tr('Updating game metadata for macOS')) + with timelogger(self.logger, 'Request macOS games'): self.core.get_game_and_dlc_list( update_assets=not self.args.offline, - platform="Mac", + platform='Mac', skip_ue=not want_unreal, ) - if need_windows: - self.signals.progress.emit(00, self.signals.tr("Updating game metadata for Windows")) - self.logger.info( - "Requesting Windows metadata, %s Unreal engine", - "with" if want_unreal else "without", - ) - with timelogger(self.logger, "Request Windows games"): + self.signals.progress.emit(increment, self.signals.tr('Updating game metadata for Windows')) + self.logger.info( + 'Requesting Windows metadata, %s Unreal engine', + 'with' if want_unreal else 'without', + ) + with timelogger(self.logger, 'Request Windows games'): games, dlc_dict = self.core.get_game_and_dlc_list( - update_assets=need_windows, platform="Windows", skip_ue=not want_unreal + update_assets=need_windows, platform='Windows', skip_ue=not want_unreal ) - self.logger.info("Games: %s. Games with DLCs: %s", len(games), len(dlc_dict)) + self.logger.info('Games: %s. Games with DLCs: %s', len(games), len(dlc_dict)) # Fetch non-asset games if want_non_asset: - self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata")) + self.signals.progress.emit(increment, self.signals.tr('Updating non-asset game metadata')) try: - with timelogger(self.logger, "Request non-asset"): + with timelogger(self.logger, 'Request non-asset'): na_games, na_dlc_dict = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False) except (HTTPError, ConnectionError) as e: - self.logger.error("Network error while updating non-asset games") + self.logger.error('Network error while updating non-asset games') self.logger.error(e) na_games, na_dlc_dict = ([], {}) # NOTE: This is here because of broken appIds from Epic # https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729 except Exception as e: - self.logger.error("General exception while updating non-asset games from EGS.") + self.logger.error('General exception while updating non-asset games from EGS.') self.logger.error(e) na_games, na_dlc_dict = ([], {}) self.logger.info( - "Non-asset: %s. Non-asset with DLCs: %s", + 'Non-asset: %s. Non-asset with DLCs: %s', len(na_games), len(na_dlc_dict), ) @@ -143,11 +171,11 @@ def run_real(self): # Combine the two games lists and the two dlc dictionaries between regular and non-asset results games += na_games for catalog_id, dlcs in na_dlc_dict.items(): - if catalog_id in dlc_dict.keys(): + if catalog_id in dlc_dict: dlc_dict[catalog_id] += dlcs else: dlc_dict[catalog_id] = dlcs - self.logger.info(f"Games: {len(games)}. Games with DLCs: {len(dlc_dict)}") + self.logger.info(f'Games: {len(games)}. Games with DLCs: {len(dlc_dict)}') - self.signals.progress.emit(40, self.signals.tr("Preparing library")) + self.signals.progress.emit(increment, self.signals.tr('Preparing library')) self.signals.result.emit((games, dlc_dict), FetchWorker.Result.GAMESDLCS) diff --git a/rare/shared/workers/install.py b/rare/shared/workers/install.py index f83bfb6ce8..6c7a9a0dd0 100644 --- a/rare/shared/workers/install.py +++ b/rare/shared/workers/install.py @@ -44,7 +44,7 @@ def run_real(self): igame=igame, game=EOSOverlayApp, repair=False, - repair_file="", + repair_file='', res=ConditionCheckResult(), # empty ) @@ -52,7 +52,7 @@ def run_real(self): self.signals.result.emit(download) else: # self.signals.failed.emit("\n".join(str(i) for i in download.res.failures)) - self.signals.failed.emit("\n".join(map(str, download.res.failures))) + self.signals.failed.emit('\n'.join(map(str, download.res.failures))) except LgndrException as ret: self.signals.failed.emit(ret.message) except Exception as e: diff --git a/rare/shared/workers/move.py b/rare/shared/workers/move.py index ed95f6c499..a1a517f510 100644 --- a/rare/shared/workers/move.py +++ b/rare/shared/workers/move.py @@ -1,7 +1,7 @@ import os import shutil +from collections.abc import Iterator from enum import auto -from typing import Iterator from legendary.lfs.utils import validate_files from legendary.models.game import VerifyResult @@ -125,7 +125,7 @@ def worker_info(self) -> QueueWorkerInfo: app_name=self.rgame.app_name, app_title=self.rgame.app_title, type=type(self).__name__, - prefix="Moving", + prefix='Moving', state=self.state, ) @@ -176,8 +176,8 @@ def copy_with_progress(src, dst): manifest.file_manifest_list.elements, key=lambda a: a.filename.lower(), ) - if config_tags := self.core.lgd.config.get(self.rgame.app_name, "install_tags", fallback=None): - install_tags = set(i.strip() for i in config_tags.split(",")) + if config_tags := self.core.lgd.config.get(self.rgame.app_name, 'install_tags', fallback=None): + install_tags = set(i.strip() for i in config_tags.split(',')) file_list = [ (f.filename, f.sha_hash.hex()) for f in files @@ -207,7 +207,8 @@ def copy_dir_structure(src, dst): dirs_exist_ok=True, ) - for result, path, _, _ in validate_files(self.dst_path, file_list): + is_win = self.rgame.igame.platform.startswith('Win') + for result, path, _, _ in validate_files(self.dst_path, file_list, case_insensitive=is_win): dst_path = os.path.join(self.dst_path, path) src_path = os.path.join(self.rgame.install_path, path) if os.path.isfile(src_path): @@ -217,15 +218,15 @@ def copy_dir_structure(src, dst): try: shutil.copy2(src_path, dst_path) dst_size += os.stat(dst_path).st_size - except (IOError, OSError) as e: + except OSError as e: self.rgame.signals.progress.finish.emit(True) self.signals.error.emit(self.rgame, str(e)) return else: - self.logger.warning(f"Copying file {src_path} to {dst_path} failed") + self.logger.warning(f'Copying file {src_path} to {dst_path} failed') self.progress(total_size, dst_size) else: - self.logger.warning(f"Source dir does not have file {src_path}. File will be missing in the destination dir.") + self.logger.warning(f'Source dir does not have file {src_path}. File will be missing in the destination dir.') self.rgame.needs_verification = True shutil.rmtree(self.rgame.install_path) diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py index 0dea7b2c10..146f4da919 100644 --- a/rare/shared/workers/uninstall.py +++ b/rare/shared/workers/uninstall.py @@ -1,7 +1,6 @@ import logging import platform import shutil -from typing import Tuple from legendary.lfs.eos import remove_registry_entries from PySide6.QtCore import QObject, Signal @@ -31,18 +30,18 @@ def uninstall_game( keep_folder=True, keep_config=False, keep_overlay_keys=False, -) -> Tuple[bool, str]: +) -> tuple[bool, str]: if rgame.is_overlay: - logger.info("Deleting overlay installation...") + logger.info('Deleting overlay installation...') core.remove_overlay_install() if keep_overlay_keys: - return True, "" + return True, '' - logger.info("Removing registry entries...") - if platform.system() != "Window": + logger.info('Removing registry entries...') + if platform.system() != 'Window': prefixes = config.get_prefixes() - if platform.system() == "Darwin": + if platform.system() == 'Darwin': # TODO: add crossover support pass if len(prefixes): @@ -50,18 +49,18 @@ def uninstall_game( try: remove_registry_entries(prefix) except Exception as e: - logger.error("%s %s", e, prefix) - logger.debug("Removed registry entries for prefix %s", prefix) + logger.error('%s %s', e, prefix) + logger.debug('Removed registry entries for prefix %s', prefix) else: remove_registry_entries() - return True, "" + return True, '' # remove shortcuts link if desktop_links_supported(): for link_type in desktop_link_types(): link_path = desktop_link_path( - rgame.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value"), + rgame.game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value'), link_type, ) if link_path.exists(): @@ -82,13 +81,13 @@ def uninstall_game( keep_folder = keep_files if keep_files else keep_folder if not keep_folder: - logger.info("Removing game install directory") + logger.info('Removing game install directory') shutil.rmtree(install_path, ignore_errors=True) if not keep_config: - logger.info("Removing sections in config file") + logger.info('Removing sections in config file') config.remove_section(rgame.app_name) - config.remove_section(f"{rgame.app_name}.env") + config.remove_section(f'{rgame.app_name}.env') config.save_config() diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py index a34ab5d643..857f729e20 100644 --- a/rare/shared/workers/verify.py +++ b/rare/shared/workers/verify.py @@ -38,7 +38,7 @@ def worker_info(self) -> QueueWorkerInfo: app_name=self.rgame.app_name, app_title=self.rgame.app_title, type=type(self).__name__, - prefix="Verifying", + prefix='Verifying', state=self.state, ) @@ -86,7 +86,7 @@ def run_real(self): if success: # lk: if verification was successful we delete the repair file and run the clean procedure # lk: this could probably be cut down to what is relevant for this use-case and skip the `cli` call - repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.repair") + repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.rgame.app_name}.repair') cli.install_game_cleanup( game=self.rgame.game, igame=self.rgame.igame, diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py index 13b4667785..6ffb3ddd49 100644 --- a/rare/shared/workers/wine_resolver.py +++ b/rare/shared/workers/wine_resolver.py @@ -1,8 +1,8 @@ import os import platform import time +from collections.abc import Iterable from configparser import ConfigParser -from typing import Dict, Iterable, List, Tuple, Union from PySide6.QtCore import QObject, Signal @@ -15,7 +15,7 @@ from .worker import Worker -if platform.system() == "Windows": +if platform.system() == 'Windows': # noinspection PyUnresolvedReferences import winreg # pylint: disable=E0401 @@ -38,10 +38,10 @@ def __init__(self, core: LegendaryCore, app_name: str, path: str): self.path = path @staticmethod - def _configure_process(core: LegendaryCore, app_name: str) -> Tuple[List, Dict]: + def _configure_process(core: LegendaryCore, app_name: str) -> tuple[list, dict]: tool: steam.CompatibilityTool = None - if config.get_boolean(app_name, "no_wine"): + if config.get_boolean(app_name, 'no_wine'): wrappers = Wrappers() for w in wrappers.get_wrappers(app_name): if w.is_compat_tool: @@ -53,17 +53,17 @@ def _configure_process(core: LegendaryCore, app_name: str) -> Tuple[List, Dict]: cmd = core.get_app_launch_command( app_name, wrapper=tool.as_str(steam.SteamVerb.RUN_IN_PREFIX) if tool is not None else None, - disable_wine=config.get_boolean(app_name, "no_wine"), + disable_wine=config.get_boolean(app_name, 'no_wine'), ) - env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, "no_wine")) + env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, 'no_wine')) # pylint: disable=E0606 env = compat_utils.get_host_environment(env, silent=True) env.update( { - "SteamAppId": config.get_envvar_with_global(app_name, "SteamAppId", "default"), - "SteamGameId": config.get_envvar_with_global(app_name, "SteamGameId", "default"), - "STEAM_COMPAT_APP_ID": config.get_envvar_with_global(app_name, "STEAM_COMPAT_APP_ID", "default"), - "UMU_ID": config.get_envvar_with_global(app_name, "UMU_ID", "default"), + 'SteamAppId': config.get_envvar_with_global(app_name, 'SteamAppId', 'default'), + 'SteamGameId': config.get_envvar_with_global(app_name, 'SteamGameId', 'default'), + 'STEAM_COMPAT_APP_ID': config.get_envvar_with_global(app_name, 'STEAM_COMPAT_APP_ID', 'default'), + 'UMU_ID': config.get_envvar_with_global(app_name, 'UMU_ID', 'default'), } ) @@ -71,9 +71,9 @@ def _configure_process(core: LegendaryCore, app_name: str) -> Tuple[List, Dict]: _cmd = core.get_app_launch_command( app_name, wrapper=tool.as_str(steam.SteamVerb.WAIT_FOR_EXIT_AND_RUN) if tool is not None else None, - disable_wine=config.get_boolean(app_name, "no_wine"), + disable_wine=config.get_boolean(app_name, 'no_wine'), ) - compat_utils.execute(_cmd, ["c:\\windows\\system32\\wineboot.exe", "-u"], env) + compat_utils.execute(_cmd, ['c:\\windows\\system32\\wineboot.exe', '-u'], env) return cmd, env @@ -88,8 +88,8 @@ def _resolve_unix_path(self, cmd, env, path: str) -> str: def run_real(self): command, environ = self._configure_process(self.core, self.app_name) if not (command and environ): - self.logger.error("Cannot setup %s, missing infomation", {type(self).__name__}) - self.signals.result_ready.emit("", self.app_name) + self.logger.error('Cannot setup %s, missing infomation', {type(self).__name__}) + self.signals.result_ready.emit('', self.app_name) path = self._resolve_unix_path(command, environ, self.path) self.signals.result_ready.emit(path, self.app_name) @@ -103,11 +103,11 @@ def __init__(self, core: LegendaryCore, rgame: RareGame): self.rgame = rgame def run_real(self): - self.logger.info("Resolving save path for %s (%s)", self.rgame.app_title, self.rgame.app_name) + self.logger.info('Resolving save path for %s (%s)', self.rgame.app_title, self.rgame.app_name) command, environ = self._configure_process(self.core, self.rgame.app_name) if not (command and environ): - self.logger.error("Cannot setup %s, missing infomation", {type(self).__name__}) - self.signals.result_ready.emit("", self.rgame.app_name) + self.logger.error('Cannot setup %s, missing infomation', {type(self).__name__}) + self.signals.result_ready.emit('', self.rgame.app_name) path = self._resolve_unix_path(command, environ, self.path) # Clean wine output @@ -119,8 +119,8 @@ def run_real(self): class OriginWineWorker(WinePathResolver): - def __init__(self, core: LegendaryCore, games: Union[Iterable[RareGame], RareGame]): - super(OriginWineWorker, self).__init__(core, "", "") + def __init__(self, core: LegendaryCore, games: Iterable[RareGame] | RareGame): + super(OriginWineWorker, self).__init__(core, '', '') self.__cache: dict[str, ConfigParser] = {} self.core = core self.games = [games] if isinstance(games, RareGame) else games @@ -129,15 +129,15 @@ def run_real(self) -> None: t = time.time() for rgame in self.games: - reg_path: str = rgame.game.metadata.get("customAttributes", {}).get("RegistryPath", {}).get("value", "") + reg_path: str = rgame.game.metadata.get('customAttributes', {}).get('RegistryPath', {}).get('value', '') if not reg_path: continue - reg_key: str = rgame.game.metadata.get("customAttributes", {}).get("RegistryKey", {}).get("value", "") + reg_key: str = rgame.game.metadata.get('customAttributes', {}).get('RegistryKey', {}).get('value', '') if not reg_key: continue - if platform.system() == "Windows": + if platform.system() == 'Windows': install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key) else: command, environ = self._configure_process(self.core, rgame.app_name) @@ -149,23 +149,23 @@ def run_real(self) -> None: use_wine = True if not use_wine: # lk: this is the original way of getting the path by parsing "system.reg" - reg = self.__cache.get(prefix, None) or compat_utils.read_registry("system.reg", prefix) + reg = self.__cache.get(prefix, None) or compat_utils.read_registry('system.reg', prefix) self.__cache[prefix] = reg - reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node") + reg_path = reg_path.replace('SOFTWARE', 'Software').replace('WOW6432Node', 'Wow6432Node') # lk: split and rejoin the registry path to avoid slash expansion - reg_path = "\\\\".join([x for x in reg_path.split("\\") if bool(x)]) + reg_path = '\\\\'.join([x for x in reg_path.split('\\') if bool(x)]) install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None) else: # lk: this is the alternative way of getting the path by using wine itself - install_dir = compat_utils.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key) + install_dir = compat_utils.query_reg_key(command, environ, f'HKLM\\{reg_path}', reg_key) if install_dir: - self.logger.debug("Found Wine install directory %s", install_dir) + self.logger.debug('Found Wine install directory %s', install_dir) install_dir = compat_utils.convert_to_unix_path(command, environ, install_dir) if install_dir: - self.logger.debug("Found Unix install directory %s", install_dir) + self.logger.debug('Found Unix install directory %s', install_dir) else: self.logger.info( "Could not find Unix install directory for '%s'", @@ -195,4 +195,4 @@ def run_real(self) -> None: ) else: self.logger.info("Origin game '%s' is not installed", rgame.app_title) - self.logger.info("Origin worker finished in %ss", time.time() - t) + self.logger.info('Origin worker finished in %ss', time.time() - t) diff --git a/rare/shared/wrappers.py b/rare/shared/wrappers.py index 908a7aed6c..67ae27968a 100644 --- a/rare/shared/wrappers.py +++ b/rare/shared/wrappers.py @@ -1,8 +1,8 @@ import json import os import shlex +from collections.abc import Iterable from logging import getLogger -from typing import Dict, Iterable, List, Set from rare.lgndr.core import LegendaryCore from rare.models.settings import RareAppSettings @@ -10,7 +10,7 @@ from rare.utils import config_helper as config from rare.utils.paths import config_dir -logger = getLogger("Wrappers") +logger = getLogger('Wrappers') class WrapperEntry: @@ -27,8 +27,8 @@ def enabled(self) -> bool: return self.__enabled @classmethod - def from_dict(cls, data: Dict): - return cls(checksum=data.get("checksum"), enabled=data.get("enabled", True)) + def from_dict(cls, data: dict): + return cls(checksum=data.get('checksum'), enabled=data.get('enabled', True)) @property def __dict__(self): @@ -40,24 +40,24 @@ def __dict__(self): class Wrappers: def __init__(self): - self._file = os.path.join(config_dir(), "wrappers.json") + self._file = os.path.join(config_dir(), 'wrappers.json') self._wrappers_dict = {} try: - with open(self._file, "r", encoding="utf-8") as f: + with open(self._file, encoding='utf-8') as f: self._wrappers_dict = json.load(f) except FileNotFoundError: - logger.info("%s does not exist", self._file) + logger.info('%s does not exist', self._file) except json.JSONDecodeError: - logger.warning("%s is corrupt", self._file) + logger.warning('%s is corrupt', self._file) - self._version = self._wrappers_dict.get("version", 1) + self._version = self._wrappers_dict.get('version', 1) - self._wrappers: Dict[str, Wrapper] = {} - for wrap_id, wrapper in self._wrappers_dict.get("wrappers", {}).items(): + self._wrappers: dict[str, Wrapper] = {} + for wrap_id, wrapper in self._wrappers_dict.get('wrappers', {}).items(): self._wrappers.update({wrap_id: Wrapper.from_dict(wrapper)}) - self._applists: Dict[str, List[WrapperEntry]] = {} - for app_name, wrapper_list in self._wrappers_dict.get("applists", {}).items(): + self._applists: dict[str, list[WrapperEntry]] = {} + for app_name, wrapper_list in self._wrappers_dict.get('applists', {}).items(): if all(isinstance(x, str) for x in wrapper_list): wlist = [WrapperEntry(y) for y in wrapper_list] elif all(isinstance(x, dict) for x in wrapper_list): @@ -69,35 +69,35 @@ def __init__(self): # set current file version self._version = 2 - def import_wrappers(self, settings: RareAppSettings, core: LegendaryCore, app_names: List): + def import_wrappers(self, settings: RareAppSettings, core: LegendaryCore, app_names: list): for app_name in app_names: wrappers = self.get_wrappers(app_name) - if not wrappers and (commands := settings.value(f"{app_name}/wrapper", [], type=list)): + if not wrappers and (commands := settings.value(f'{app_name}/wrapper', [], type=list)): logger.info("Importing wrappers from Rare's config") - settings.remove(f"{app_name}/wrapper") + settings.remove(f'{app_name}/wrapper') for command in commands: wrapper = Wrapper(command=shlex.split(command)) wrappers.append(wrapper) self.set_wrappers(app_name, wrappers) logger.debug( - "Imported previous wrappers in %s Rare: %s", + 'Imported previous wrappers in %s Rare: %s', app_name, wrapper.name, ) # NOTE: compatibility with Legendary # No Rare settings wrappers, but legendary config wrappers, for backwards compatibility - if not wrappers and (command := core.lgd.config.get(app_name, "wrapper", fallback="")): + if not wrappers and (command := core.lgd.config.get(app_name, 'wrapper', fallback='')): logger.info("Importing wrappers from legendary's config") wrapper = Wrapper( command=shlex.split(command), - name="Imported from Legendary", + name='Imported from Legendary', wtype=WrapperType.LEGENDARY_IMPORT, ) wrappers = [wrapper] self.set_wrappers(app_name, wrappers) logger.debug( - "Imported existing wrappers in %s legendary: %s", + 'Imported existing wrappers in %s legendary: %s', app_name, wrapper.name, ) @@ -111,12 +111,12 @@ def user_wrappers(self) -> Iterable[Wrapper]: def wrapper_command(self, app_name: str) -> str: commands = [wrapper.as_str for wrapper in self.get_wrappers(app_name) if wrapper.is_enabled] - return " ".join(commands) + return ' '.join(commands) - def get_checksums(self, app_name: str) -> Set[str]: + def get_checksums(self, app_name: str) -> set[str]: return {entry.checksum for entry in self._applists.get(app_name, [])} - def get_wrappers(self, app_name: str) -> List[Wrapper]: + def get_wrappers(self, app_name: str) -> list[Wrapper]: wrappers = [] for entry in self._applists.get(app_name, []): if wrap := self._wrappers.get(entry.checksum, None): @@ -124,13 +124,13 @@ def get_wrappers(self, app_name: str) -> List[Wrapper]: wrappers.append(wrap) return wrappers - def set_wrappers(self, app_name: str, wrappers: List[Wrapper]) -> None: + def set_wrappers(self, app_name: str, wrappers: list[Wrapper]) -> None: _wrappers = sorted(wrappers, key=lambda w: w.is_compat_tool) for w in _wrappers: - if (md5sum := w.checksum) in self._wrappers.keys(): + if (md5sum := w.checksum) in self._wrappers: if w != self._wrappers[md5sum]: logger.error( - "Equal csum for unequal wrappers %s, %s", + 'Equal csum for unequal wrappers %s, %s', w.name, self._wrappers[md5sum].name, ) @@ -144,24 +144,24 @@ def set_wrappers(self, app_name: str, wrappers: List[Wrapper]) -> None: def __save_config(self, app_name: str): command = self.wrapper_command(app_name) - config.adjust_option(app_name, "wrapper", command) + config.adjust_option(app_name, 'wrapper', command) def __save_wrappers(self): - existing = {csum for csum in self._wrappers.keys()} + existing = {csum for csum in self._wrappers} in_use = {entry.checksum for wrappers in self._applists.values() for entry in wrappers} for redudant in existing.difference(in_use): del self._wrappers[redudant] - self._wrappers_dict["version"] = self._version - self._wrappers_dict["wrappers"] = self._wrappers - self._wrappers_dict["applists"] = self._applists + self._wrappers_dict['version'] = self._version + self._wrappers_dict['wrappers'] = self._wrappers + self._wrappers_dict['applists'] = self._applists - with open(os.path.join(self._file), "w+", encoding="utf-8") as f: + with open(os.path.join(self._file), 'w+', encoding='utf-8') as f: json.dump(self._wrappers_dict, f, default=lambda o: vars(o), indent=2) -if __name__ == "__main__": +if __name__ == '__main__': from argparse import Namespace from pprint import pprint @@ -171,40 +171,40 @@ def __save_wrappers(self): config_dir = os.getcwd global config config = Namespace() - config.set_option = lambda x, y, z: print("set_option:", x, y, z) - config.remove_option = lambda x, y: print("remove_option:", x, y) - config.save_config = lambda: print("save_config:") - config.adjust_option = lambda x, y, z: print("save_option:", x, y, z) + config.set_option = lambda x, y, z: print('set_option:', x, y, z) + config.remove_option = lambda x, y: print('remove_option:', x, y) + config.save_config = lambda: print('save_config:') + config.adjust_option = lambda x, y, z: print('save_option:', x, y, z) wr = Wrappers() - w1 = Wrapper(command=["/usr/bin/w1"], wtype=WrapperType.NONE) - w2 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL) - w3 = Wrapper(command=["/usr/bin/w3"], wtype=WrapperType.USER_DEFINED, enabled=False) - w4 = Wrapper(command=["/usr/bin/w4"], wtype=WrapperType.USER_DEFINED) - wr.set_wrappers("testgame", [w1, w2, w3, w4]) + w1 = Wrapper(command=['/usr/bin/w1'], wtype=WrapperType.NONE) + w2 = Wrapper(command=['/usr/bin/w2'], wtype=WrapperType.COMPAT_TOOL) + w3 = Wrapper(command=['/usr/bin/w3'], wtype=WrapperType.USER_DEFINED, enabled=False) + w4 = Wrapper(command=['/usr/bin/w4'], wtype=WrapperType.USER_DEFINED) + wr.set_wrappers('testgame', [w1, w2, w3, w4]) - w5 = Wrapper(command=["/usr/bin/w5"], wtype=WrapperType.COMPAT_TOOL) - wr.set_wrappers("testgame2", [w2, w1, w5]) + w5 = Wrapper(command=['/usr/bin/w5'], wtype=WrapperType.COMPAT_TOOL) + wr.set_wrappers('testgame2', [w2, w1, w5]) - w6 = Wrapper(command=["/usr/bin/w 6", "-w", "-t"], wtype=WrapperType.USER_DEFINED) - wr.set_wrappers("testgame", [w1, w2, w3, w6]) + w6 = Wrapper(command=['/usr/bin/w 6', '-w', '-t'], wtype=WrapperType.USER_DEFINED) + wr.set_wrappers('testgame', [w1, w2, w3, w6]) - w7 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL) - app_wrappers = wr.get_wrappers("testgame") + w7 = Wrapper(command=['/usr/bin/w2'], wtype=WrapperType.COMPAT_TOOL) + app_wrappers = wr.get_wrappers('testgame') pprint([w.as_str for w in app_wrappers]) # item = next(item for item in app_wrappers if item.checksum == w3.checksum) app_wrappers.remove(w3) - wr.set_wrappers("testgame", app_wrappers) + wr.set_wrappers('testgame', app_wrappers) - game_wrappers = wr.get_wrappers("testgame") + game_wrappers = wr.get_wrappers('testgame') pprint([w.as_str for w in game_wrappers]) - game_wrappers = wr.get_wrappers("testgame2") + game_wrappers = wr.get_wrappers('testgame2') pprint([w.as_str for w in game_wrappers]) for i, tool in enumerate(steam.find_tools()): wt = Wrapper(command=tool.command(), name=tool.name, wtype=WrapperType.COMPAT_TOOL) - wr.set_wrappers(f"compat_game_{i}", [wt]) + wr.set_wrappers(f'compat_game_{i}', [wt]) print(wt.as_str) for wrp in wr.user_wrappers: diff --git a/rare/utils/compat/miniproton.py b/rare/utils/compat/miniproton.py index d69ae55fab..aeaf69839e 100644 --- a/rare/utils/compat/miniproton.py +++ b/rare/utils/compat/miniproton.py @@ -4,51 +4,51 @@ import subprocess import sys from pathlib import Path -from typing import Any, Dict, List +from typing import Any def log(msg: Any): - sys.stderr.write(str(msg) + "\n") + sys.stderr.write(str(msg) + '\n') sys.stderr.flush() -def run_proc(args: List, env: Dict): +def run_proc(args: list, env: dict): return subprocess.call(args, env=env, stderr=sys.stderr, stdout=sys.stdout) -if __name__ == "__main__": - if "STEAM_COMPAT_DATA_PATH" not in os.environ: - log("No compat data path?") +if __name__ == '__main__': + if 'STEAM_COMPAT_DATA_PATH' not in os.environ: + log('No compat data path?') sys.exit(1) - wine_bin = "/usr/bin/wine" - wineserver_bin = "/usr/bin/wineserver" + wine_bin = '/usr/bin/wine' + wineserver_bin = '/usr/bin/wineserver' - compat_data = Path(os.environ["STEAM_COMPAT_DATA_PATH"]).resolve(strict=False) + compat_data = Path(os.environ['STEAM_COMPAT_DATA_PATH']).resolve(strict=False) compat_data.mkdir(parents=True, exist_ok=True) - compat_data.joinpath("creation_sync_guard").touch(exist_ok=True) - compat_data.joinpath("tracked_files").touch(exist_ok=True) + compat_data.joinpath('creation_sync_guard').touch(exist_ok=True) + compat_data.joinpath('tracked_files').touch(exist_ok=True) - wine_prefix = compat_data.joinpath("pfx").resolve(strict=False) + wine_prefix = compat_data.joinpath('pfx').resolve(strict=False) if not wine_prefix.exists(): wine_prefix.mkdir(parents=True, exist_ok=True) dlls = { - "d3d8": "n,b", - "d3d9": "n,b", - "dxgi": "n,b", - "d3d10core": "n,b", - "d3d11": "n,b", - "d3d12": "n,b;", - "d3d12core": "n,b", + 'd3d8': 'n,b', + 'd3d9': 'n,b', + 'dxgi': 'n,b', + 'd3d10core': 'n,b', + 'd3d11': 'n,b', + 'd3d12': 'n,b;', + 'd3d12core': 'n,b', } - dlloverrides = ";".join(("=".join((k, v)) for k, v in dlls.items())) - dlloverrides = ";".join((os.environ.get("WINEDLLOVERRIDES", ""), dlloverrides)) + dlloverrides = ';'.join(('='.join((k, v)) for k, v in dlls.items())) + dlloverrides = ';'.join((os.environ.get('WINEDLLOVERRIDES', ''), dlloverrides)) env = { - "WINE": wine_bin, - "WINEPREFIX": wine_prefix.as_posix(), - "WINEDLLOVERRIDES": dlloverrides, + 'WINE': wine_bin, + 'WINEPREFIX': wine_prefix.as_posix(), + 'WINEDLLOVERRIDES': dlloverrides, } log(env) @@ -57,30 +57,30 @@ def run_proc(args: List, env: Dict): # determine mode rc = 0 - if sys.argv[1] == "run": + if sys.argv[1] == 'run': # start target app rc = run_proc([wine_bin] + sys.argv[2:], local_env) - elif sys.argv[1] == "waitforexitandrun": + elif sys.argv[1] == 'waitforexitandrun': # wait for wineserver to shut down - run_proc([wineserver_bin, "-w"], local_env) + run_proc([wineserver_bin, '-w'], local_env) # then run rc = run_proc([wine_bin] + sys.argv[2:], local_env) - elif sys.argv[1] == "runinprefix": + elif sys.argv[1] == 'runinprefix': rc = run_proc([wine_bin] + sys.argv[2:], local_env) - elif sys.argv[1] == "getcompatpath": + elif sys.argv[1] == 'getcompatpath': # linux -> windows path - path = subprocess.check_output([wine_bin, "winepath", "-w", sys.argv[2]], env=local_env, stderr=sys.stderr) + path = subprocess.check_output([wine_bin, 'winepath', '-w', sys.argv[2]], env=local_env, stderr=sys.stderr) sys.stdout.buffer.write(path) sys.stdout.buffer.flush() sys.stderr.buffer.flush() - elif sys.argv[1] == "getnativepath": + elif sys.argv[1] == 'getnativepath': # windows -> linux path - path = subprocess.check_output([wine_bin, "winepath", sys.argv[2]], env=local_env, stderr=sys.stderr) + path = subprocess.check_output([wine_bin, 'winepath', sys.argv[2]], env=local_env, stderr=sys.stderr) sys.stdout.buffer.write(path) sys.stdout.buffer.flush() sys.stderr.buffer.flush() else: - log("Need a verb.") + log('Need a verb.') sys.exit(1) sys.exit(rc) diff --git a/rare/utils/compat/steam.py b/rare/utils/compat/steam.py index 580592cb84..df3353a8fe 100644 --- a/rare/utils/compat/steam.py +++ b/rare/utils/compat/steam.py @@ -4,48 +4,47 @@ from enum import Enum from hashlib import md5 from logging import getLogger -from typing import Dict, List, Optional, Set, Union import vdf from rare.utils.paths import data_dir -logger = getLogger("SteamTools") +logger = getLogger('SteamTools') -steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] -umu_install_paths = [os.path.expanduser("~/.local/share/umu")] +steam_client_install_paths = [os.path.expanduser('~/.local/share/Steam')] +umu_install_paths = [os.path.expanduser('~/.local/share/umu')] -def find_steam() -> Optional[str]: +def find_steam() -> str | None: # return the first valid path for path in steam_client_install_paths: - if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")): + if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'steam.sh')): return path return None -def find_libraries(steam_path: str) -> Set[str]: - vdf_path = os.path.join(steam_path, "config", "libraryfolders.vdf") +def find_libraries(steam_path: str) -> set[str]: + vdf_path = os.path.join(steam_path, 'config', 'libraryfolders.vdf') if not os.path.isfile(vdf_path): return set() - with open(vdf_path, "r", encoding="utf-8") as f: - libraryfolders = vdf.load(f)["libraryfolders"] + with open(vdf_path, encoding='utf-8') as f: + libraryfolders = vdf.load(f)['libraryfolders'] # libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()] - libraries = {os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()} + libraries = {os.path.join(folder['path'], 'steamapps') for key, folder in libraryfolders.items()} libraries = set(filter(lambda x: os.path.isdir(x), libraries)) return libraries UMU_RUNTIMES = { - "1391110": "steamrt2", - "1628350": "steamrt3", - "3810310": "steamrt3-arm64", - "4183110": "steamrt4", - "4185400": "steamrt4-arm64", + '1391110': 'steamrt2', + '1628350': 'steamrt3', + '3810310': 'steamrt3-arm64', + '4183110': 'steamrt4', + '4185400': 'steamrt4-arm64', } -def find_umu() -> Optional[str]: +def find_umu() -> str | None: for path in umu_install_paths: if os.path.isdir(path) and any(rt in os.listdir(path) for rt in UMU_RUNTIMES.values()): return path @@ -64,12 +63,12 @@ def find_umu() -> Optional[str]: class SteamVerb(Enum): - RUN = "run" - WAIT_FOR_EXIT_AND_RUN = "waitforexitandrun" - RUN_IN_PREFIX = "runinprefix" - DESTROY_PREFIX = "destroyprefix" - GET_COMPAT_PATH = "getcompatpath" - GET_NATIVE_PATH = "getnativepath" + RUN = 'run' + WAIT_FOR_EXIT_AND_RUN = 'waitforexitandrun' + RUN_IN_PREFIX = 'runinprefix' + DESTROY_PREFIX = 'destroyprefix' + GET_COMPAT_PATH = 'getcompatpath' + GET_NATIVE_PATH = 'getnativepath' DEFAULT = WAIT_FOR_EXIT_AND_RUN @@ -77,7 +76,7 @@ class SteamVerb(Enum): class SteamBase: steam_path: str tool_path: str - toolmanifest: Dict + toolmanifest: dict def __eq__(self, other): return self.tool_path == other.tool_path @@ -86,41 +85,41 @@ def __hash__(self): return hash(self.tool_path) @property - def required_tool(self) -> Optional[str]: - return self.toolmanifest.get("require_tool_appid", None) + def required_tool(self) -> str | None: + return self.toolmanifest.get('require_tool_appid', None) @property - def layer(self) -> Optional[str]: - return self.toolmanifest.get("compatmanager_layer_name", None) + def layer(self) -> str | None: + return self.toolmanifest.get('compatmanager_layer_name', None) - def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]: + def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> list[str]: tool_path = os.path.normpath(self.tool_path) - cmd = "".join([shlex.quote(tool_path), self.toolmanifest["commandline"]]) + cmd = ''.join([shlex.quote(tool_path), self.toolmanifest['commandline']]) # NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff # `run` is used when setting up the environment, so use that if we are setting up the prefix. - cmd = cmd.replace("%verb%", verb.value) + cmd = cmd.replace('%verb%', verb.value) return shlex.split(cmd) def as_str(self, verb: SteamVerb = SteamVerb.DEFAULT): - return " ".join(map(shlex.quote, self.command(verb))) + return ' '.join(map(shlex.quote, self.command(verb))) @property def checksum(self) -> str: - return md5(self.as_str().encode("utf-8")).hexdigest() + return md5(self.as_str().encode('utf-8')).hexdigest() @dataclass class SteamRuntime(SteamBase): steam_library: str - appmanifest: Dict + appmanifest: dict @property def name(self) -> str: - return self.appmanifest["AppState"]["name"] + return self.appmanifest['AppState']['name'] @property def appid(self) -> str: - return self.appmanifest["AppState"]["appid"] + return self.appmanifest['AppState']['appid'] @dataclass @@ -134,7 +133,7 @@ class SteamAntiCheat: steam_path: str tool_path: str steam_library: str - appmanifest: Dict + appmanifest: dict def __eq__(self, other): return self.tool_path == other.tool_path @@ -144,24 +143,24 @@ def __hash__(self): @property def name(self) -> str: - return self.appmanifest["AppState"]["name"] + return self.appmanifest['AppState']['name'] @property def appid(self) -> str: - return self.appmanifest["AppState"]["appid"] + return self.appmanifest['AppState']['appid'] @dataclass class ProtonTool(SteamRuntime): - runtime: Union[SteamRuntime, UmuRuntime] = None - anticheat: Dict[str, SteamAntiCheat] = None + runtime: SteamRuntime | UmuRuntime = None + anticheat: dict[str, SteamAntiCheat] = None def __bool__(self) -> bool: if appid := self.required_tool: return self.runtime is not None and self.runtime.appid == appid return True - def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]: + def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> list[str]: cmd = self.runtime.command(verb) cmd.extend(super().command(verb)) return cmd @@ -169,9 +168,9 @@ def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]: @dataclass class CompatibilityTool(SteamBase): - compatibilitytool: Dict - runtime: Union[SteamRuntime, UmuRuntime] = None - anticheat: Dict[str, SteamAntiCheat] = None + compatibilitytool: dict + runtime: SteamRuntime | UmuRuntime = None + anticheat: dict[str, SteamAntiCheat] = None def __bool__(self) -> bool: if appid := self.required_tool: @@ -180,41 +179,41 @@ def __bool__(self) -> bool: @property def name(self) -> str: - return self.compatibilitytool["display_name"] + return self.compatibilitytool['display_name'] - def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> List[str]: + def command(self, verb: SteamVerb = SteamVerb.DEFAULT) -> list[str]: cmd = self.runtime.command(verb) if self.runtime is not None else [] cmd.extend(super().command(verb)) return cmd -def find_appmanifests(library: str) -> List[dict]: +def find_appmanifests(library: str) -> list[dict]: appmanifests = [] for entry in os.scandir(library): - if entry.is_file() and entry.name.endswith(".acf"): - with open(os.path.join(library, entry.name), "r", encoding="utf-8") as f: + if entry.is_file() and entry.name.endswith('.acf'): + with open(os.path.join(library, entry.name), encoding='utf-8') as f: appmanifest = vdf.load(f) appmanifests.append(appmanifest) return appmanifests ANTICHEAT_RUNTIMES = { - "eac_runtime": "1826330", - "battleye_runtime": "1161040", + 'eac_runtime': '1826330', + 'battleye_runtime': '1161040', } def find_anticheat(steam_path: str, library: str): runtimes = {} appmanifests = find_appmanifests(library) - common = os.path.join(library, "common") + common = os.path.join(library, 'common') for appmanifest in appmanifests: - if appmanifest["AppState"]["appid"] not in ANTICHEAT_RUNTIMES.values(): + if appmanifest['AppState']['appid'] not in ANTICHEAT_RUNTIMES.values(): continue - folder = appmanifest["AppState"]["installdir"] + folder = appmanifest['AppState']['installdir'] runtimes.update( { - appmanifest["AppState"]["appid"]: SteamAntiCheat( + appmanifest['AppState']['appid']: SteamAntiCheat( steam_path=steam_path, steam_library=library, appmanifest=appmanifest, @@ -225,51 +224,51 @@ def find_anticheat(steam_path: str, library: str): return runtimes -def find_steam_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]: +def find_steam_runtimes(steam_path: str, library: str) -> dict[str, SteamRuntime]: runtimes = {} appmanifests = find_appmanifests(library) - common = os.path.join(library, "common") + common = os.path.join(library, 'common') for appmanifest in appmanifests: - folder = appmanifest["AppState"]["installdir"] + folder = appmanifest['AppState']['installdir'] tool_path = os.path.join(common, folder) - if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): - with open(vdf_file, "r", encoding="utf-8") as f: + if os.path.isfile(vdf_file := os.path.join(tool_path, 'toolmanifest.vdf')): + with open(vdf_file, encoding='utf-8') as f: toolmanifest = vdf.load(f) - if toolmanifest["manifest"].get("version") != "2": + if toolmanifest['manifest'].get('version') != '2': continue - if toolmanifest["manifest"].get("compatmanager_layer_name") == "container-runtime": + if toolmanifest['manifest'].get('compatmanager_layer_name') == 'container-runtime': runtimes.update( { - appmanifest["AppState"]["appid"]: SteamRuntime( + appmanifest['AppState']['appid']: SteamRuntime( steam_path=steam_path, steam_library=library, appmanifest=appmanifest, tool_path=tool_path, - toolmanifest=toolmanifest["manifest"], + toolmanifest=toolmanifest['manifest'], ) } ) return runtimes -def find_umu_runtimes(umu_path: str) -> Dict[str, UmuRuntime]: +def find_umu_runtimes(umu_path: str) -> dict[str, UmuRuntime]: runtimes = {} for appid, folder in UMU_RUNTIMES.items(): tool_path = os.path.join(umu_path, folder) - if os.path.isdir(tool_path) and os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): - with open(vdf_file, "r", encoding="utf-8") as f: + if os.path.isdir(tool_path) and os.path.isfile(vdf_file := os.path.join(tool_path, 'toolmanifest.vdf')): + with open(vdf_file, encoding='utf-8') as f: toolmanifest = vdf.load(f) - if toolmanifest["manifest"].get("version") != "2": + if toolmanifest['manifest'].get('version') != '2': continue - if toolmanifest["manifest"].get("compatmanager_layer_name") == "container-runtime": + if toolmanifest['manifest'].get('compatmanager_layer_name') == 'container-runtime': runtimes.update( { appid: UmuRuntime( steam_path=None, - name=f"umu-{folder}", + name=f'umu-{folder}', appid=appid, tool_path=tool_path, - toolmanifest=toolmanifest["manifest"], + toolmanifest=toolmanifest['manifest'], ) } ) @@ -277,42 +276,42 @@ def find_umu_runtimes(umu_path: str) -> Dict[str, UmuRuntime]: return runtimes -def find_steam_tools(steam_path: str, library: str) -> List[ProtonTool]: +def find_steam_tools(steam_path: str, library: str) -> list[ProtonTool]: tools = [] appmanifests = find_appmanifests(library) - common = os.path.join(library, "common") + common = os.path.join(library, 'common') for appmanifest in appmanifests: - folder = appmanifest["AppState"]["installdir"] + folder = appmanifest['AppState']['installdir'] tool_path = os.path.join(common, folder) - if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): - with open(vdf_file, "r", encoding="utf-8") as f: + if os.path.isfile(vdf_file := os.path.join(tool_path, 'toolmanifest.vdf')): + with open(vdf_file, encoding='utf-8') as f: toolmanifest = vdf.load(f) - if toolmanifest["manifest"].get("version") != "2": + if toolmanifest['manifest'].get('version') != '2': continue - if toolmanifest["manifest"].get("compatmanager_layer_name") == "proton": + if toolmanifest['manifest'].get('compatmanager_layer_name') == 'proton': tools.append( ProtonTool( steam_path=steam_path, steam_library=library, appmanifest=appmanifest, tool_path=tool_path, - toolmanifest=toolmanifest["manifest"], + toolmanifest=toolmanifest['manifest'], ) ) return tools -def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: +def find_compatibility_tools(steam_path: str) -> list[CompatibilityTool]: compatibilitytools_paths = { - data_dir().joinpath("tools").as_posix(), - os.path.expanduser("~/.local/share/umu/compatibilitytools"), - os.path.expanduser("~/.steam/compatibilitytools.d"), - os.path.expanduser("~/.steam/root/compatibilitytools.d"), - "/usr/share/steam/compatibilitytools.d", + data_dir().joinpath('tools').as_posix(), + os.path.expanduser('~/.local/share/umu/compatibilitytools'), + os.path.expanduser('~/.steam/compatibilitytools.d'), + os.path.expanduser('~/.steam/root/compatibilitytools.d'), + '/usr/share/steam/compatibilitytools.d', } if steam_path: compatibilitytools_paths.add( - os.path.expanduser(os.path.join(steam_path, "compatibilitytools.d")), + os.path.expanduser(os.path.join(steam_path, 'compatibilitytools.d')), ) compatibilitytools_paths = {os.path.realpath(path) for path in compatibilitytools_paths if os.path.isdir(path)} tools = [] @@ -320,8 +319,8 @@ def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: for entry in os.scandir(path): entry_path = os.path.join(path, entry.name) if entry.is_dir(): - tool_vdf = os.path.join(entry_path, "compatibilitytool.vdf") - elif entry.is_file() and entry.name.endswith(".vdf"): + tool_vdf = os.path.join(entry_path, 'compatibilitytool.vdf') + elif entry.is_file() and entry.name.endswith('.vdf'): tool_vdf = entry_path else: continue @@ -329,31 +328,31 @@ def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: if not os.path.isfile(tool_vdf): continue - with open(tool_vdf, "r", encoding="utf-8") as f: + with open(tool_vdf, encoding='utf-8') as f: compatibilitytool = vdf.load(f) - entry_tools = compatibilitytool["compatibilitytools"]["compat_tools"] + entry_tools = compatibilitytool['compatibilitytools']['compat_tools'] for entry_tool in entry_tools.values(): - if entry_tool.get("from_oslist") != "windows" or entry_tool.get("to_oslist") != "linux": + if entry_tool.get('from_oslist') != 'windows' or entry_tool.get('to_oslist') != 'linux': continue - install_path = entry_tool["install_path"] + install_path = entry_tool['install_path'] # lk: os.path.join understands the leading '/' in the last argument and it returns # only the last argument if it starts with one. tool_path = os.path.abspath(os.path.join(os.path.dirname(tool_vdf), install_path)) - manifest_vdf = os.path.join(tool_path, "toolmanifest.vdf") + manifest_vdf = os.path.join(tool_path, 'toolmanifest.vdf') if not os.path.isfile(manifest_vdf): continue - with open(manifest_vdf, "r", encoding="utf-8") as f: + with open(manifest_vdf, encoding='utf-8') as f: manifest = vdf.load(f) tools.append( CompatibilityTool( steam_path=steam_path, tool_path=tool_path, - toolmanifest=manifest["manifest"], + toolmanifest=manifest['manifest'], compatibilitytool=entry_tool, ) ) @@ -361,84 +360,84 @@ def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: def get_runtime( - tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, Union[SteamRuntime, UmuRuntime]] -) -> Union[SteamRuntime, UmuRuntime, None]: + tool: ProtonTool | CompatibilityTool, runtimes: dict[str, SteamRuntime | UmuRuntime] +) -> SteamRuntime | UmuRuntime | None: required_tool = tool.required_tool if required_tool is None: return None - return runtimes.get(required_tool, None) + return runtimes.get(required_tool) -def get_umu_environment(tool: Optional[ProtonTool] = None, compat_path: Optional[str] = None) -> Dict: +def get_umu_environment(tool: ProtonTool | None = None, compat_path: str | None = None) -> dict: # If the tool is unset, return all affected env variable names # IMPORTANT: keep this in sync with the code below - environ = {"WINEPREFIX": compat_path if compat_path else ""} + environ = {'WINEPREFIX': compat_path if compat_path else ''} if tool is None: - environ["WINEPREFIX"] = "" - environ["PROTONPATH"] = "" - environ["GAMEID"] = "" - environ["STORE"] = "" + environ['WINEPREFIX'] = '' + environ['PROTONPATH'] = '' + environ['GAMEID'] = '' + environ['STORE'] = '' return environ - environ["PROTONPATH"] = tool.tool_path - environ["STORE"] = "egs" + environ['PROTONPATH'] = tool.tool_path + environ['STORE'] = 'egs' return environ def get_steam_environment( - tool: Optional[Union[ProtonTool, CompatibilityTool]] = None, - compat_path: Optional[str] = None, -) -> Dict: + tool: ProtonTool | CompatibilityTool | None = None, + compat_path: str | None = None, +) -> dict: # If the tool is unset, return all affected env variable names # IMPORTANT: keep this in sync with the code below - environ = {"STEAM_COMPAT_DATA_PATH": compat_path if compat_path else ""} - - environ["STORE"] = "" - environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" - environ["STEAM_COMPAT_LAUNCHER_SERVICE"] = "" - environ["STEAM_COMPAT_LIBRARY_PATHS"] = "" - environ["STEAM_COMPAT_MOUNTS"] = "" - environ["STEAM_COMPAT_TOOL_PATHS"] = "" - environ["PROTON_EAC_RUNTIME"] = "" - environ["PROTON_BATTLEYE_RUNTIME"] = "" + environ = {'STEAM_COMPAT_DATA_PATH': compat_path if compat_path else ''} + + environ['STORE'] = '' + environ['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = '' + environ['STEAM_COMPAT_LAUNCHER_SERVICE'] = '' + environ['STEAM_COMPAT_LIBRARY_PATHS'] = '' + environ['STEAM_COMPAT_MOUNTS'] = '' + environ['STEAM_COMPAT_TOOL_PATHS'] = '' + environ['PROTON_EAC_RUNTIME'] = '' + environ['PROTON_BATTLEYE_RUNTIME'] = '' if tool is None: - environ["STEAM_COMPAT_DATA_PATH"] = "" - environ["STEAM_COMPAT_INSTALL_PATH"] = "" + environ['STEAM_COMPAT_DATA_PATH'] = '' + environ['STEAM_COMPAT_INSTALL_PATH'] = '' return environ - environ["STORE"] = "egs" + environ['STORE'] = 'egs' if tool.steam_path: - environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = tool.steam_path - environ["STEAM_COMPAT_LAUNCHER_SERVICE"] = tool.layer + environ['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = tool.steam_path + environ['STEAM_COMPAT_LAUNCHER_SERVICE'] = tool.layer if isinstance(tool, ProtonTool): - environ["STEAM_COMPAT_LIBRARY_PATHS"] = tool.steam_library + environ['STEAM_COMPAT_LIBRARY_PATHS'] = tool.steam_library if tool.runtime is not None: compat_mounts = [tool.tool_path, tool.runtime.tool_path] - environ["STEAM_COMPAT_MOUNTS"] = ":".join(compat_mounts) + environ['STEAM_COMPAT_MOUNTS'] = ':'.join(compat_mounts) tool_paths = [tool.tool_path] if tool.runtime is not None: tool_paths.append(tool.runtime.tool_path) - environ["STEAM_COMPAT_TOOL_PATHS"] = ":".join(tool_paths) + environ['STEAM_COMPAT_TOOL_PATHS'] = ':'.join(tool_paths) if tool.anticheat is not None: - if (appid := ANTICHEAT_RUNTIMES["eac_runtime"]) in tool.anticheat.keys(): - environ["PROTON_EAC_RUNTIME"] = tool.anticheat[appid].tool_path - if (appid := ANTICHEAT_RUNTIMES["battleye_runtime"]) in tool.anticheat.keys(): - environ["PROTON_BATTLEYE_RUNTIME"] = tool.anticheat[appid].tool_path + if (appid := ANTICHEAT_RUNTIMES['eac_runtime']) in tool.anticheat: + environ['PROTON_EAC_RUNTIME'] = tool.anticheat[appid].tool_path + if (appid := ANTICHEAT_RUNTIMES['battleye_runtime']) in tool.anticheat: + environ['PROTON_BATTLEYE_RUNTIME'] = tool.anticheat[appid].tool_path return environ -def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: +def _find_tools() -> list[ProtonTool | CompatibilityTool]: steam_path = find_steam() if steam_path is None: - logger.info("Steam folder could not be found") + logger.info('Steam folder could not be found') else: - logger.info("Found Steam folder in %s", steam_path) + logger.info('Found Steam folder in %s', steam_path) steam_libraries = set() if steam_path: steam_libraries.update(find_libraries(steam_path)) - logger.debug("Searching for tools in Steam libraries:") - logger.debug("%s", steam_libraries) + logger.debug('Searching for tools in Steam libraries:') + logger.debug('%s', steam_libraries) runtimes = {} for library in steam_libraries: @@ -446,9 +445,9 @@ def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: umu_path = find_umu() if umu_path is None: - logger.info("UMU folder could not be found") + logger.info('UMU folder could not be found') else: - logger.info("Found UMU folder in %s", umu_path) + logger.info('Found UMU folder in %s', umu_path) if umu_path: runtimes.update(find_umu_runtimes(umu_path)) @@ -465,7 +464,7 @@ def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: for tool in tools: runtime = get_runtime(tool, runtimes) tool.runtime = runtime - if tool.layer == "proton": + if tool.layer == 'proton': tool.anticheat = anticheat tools = list(filter(lambda t: bool(t), tools)) @@ -474,25 +473,25 @@ def _find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: return tools -_tools: Optional[List[Union[ProtonTool, CompatibilityTool]]] = None +_tools: list[ProtonTool | CompatibilityTool] | None = None -def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: +def find_tools() -> list[ProtonTool | CompatibilityTool]: global _tools if _tools is None: _tools = _find_tools() - return list(filter(lambda t: t.layer != "umu-launcher", _tools)) + return list(filter(lambda t: t.layer != 'umu-launcher', _tools)) -def find_umu_launcher() -> Optional[CompatibilityTool]: +def find_umu_launcher() -> CompatibilityTool | None: global _tools if _tools is None: _tools = _find_tools() - _umu = list(filter(lambda t: t.layer == "umu-launcher", _tools)) + _umu = list(filter(lambda t: t.layer == 'umu-launcher', _tools)) return _umu[0] if _umu else None -if __name__ == "__main__": +if __name__ == '__main__': from pprint import pprint tools = find_tools() @@ -505,4 +504,4 @@ def find_umu_launcher() -> Optional[CompatibilityTool]: print(get_steam_environment(tool)) print(tool.name) print(tool.command(SteamVerb.RUN)) - print(" ".join(tool.command(SteamVerb.RUN_IN_PREFIX))) + print(' '.join(tool.command(SteamVerb.RUN_IN_PREFIX))) diff --git a/rare/utils/compat/utils.py b/rare/utils/compat/utils.py index 1c7a692e2d..8aae8e4033 100644 --- a/rare/utils/compat/utils.py +++ b/rare/utils/compat/utils.py @@ -1,52 +1,52 @@ import os import platform as pf import subprocess +from collections.abc import Mapping from configparser import ConfigParser from getpass import getuser from logging import getLogger -from typing import Dict, List, Mapping, Tuple from PySide6.QtCore import QProcess, QProcessEnvironment -if pf.system() != "Windows": - if pf.system() in {"Linux", "FreeBSD"}: +if pf.system() != 'Windows': + if pf.system() in {'Linux', 'FreeBSD'}: pass -logger = getLogger("CompatUtils") +logger = getLogger('CompatUtils') # this is a copied function from legendary.utils.wine_helpers, but registry file can be specified def read_registry(registry: str, prefix: str) -> ConfigParser: - accepted = ["system.reg", "user.reg"] + accepted = ['system.reg', 'user.reg'] if registry not in accepted: raise RuntimeError(f'Unknown target "{registry}" not in {accepted}') - reg = ConfigParser(comment_prefixes=(";", "#", "/", "WINE"), allow_no_value=True, strict=False) + reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True, strict=False) reg.optionxform = str - reg.read(os.path.join(prefix, "system.reg")) + reg.read(os.path.join(prefix, 'system.reg')) return reg -def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[str], Dict]: - logger.debug("Preparing process: %s", command) +def prepare_process(command: list[str], environment: dict) -> tuple[str, list[str], dict]: + logger.debug('Preparing process: %s', command) _env = os.environ.copy() _command = command.copy() - if os.environ.get("container") == "flatpak": - flatpak_command = ["flatpak-spawn", "--host"] - flatpak_command.extend(f"--env={name}={value}" for name, value in environment.items()) + if os.environ.get('container') == 'flatpak': # noqa: SIM112 + flatpak_command = ['flatpak-spawn', '--host'] + flatpak_command.extend(f'--env={name}={value}' for name, value in environment.items()) _command = flatpak_command + command else: _env.update(environment) return _command[0], _command[1:] if len(_command) > 1 else [], _env -def dict_to_qprocenv(env: Dict) -> QProcessEnvironment: +def dict_to_qprocenv(env: dict) -> QProcessEnvironment: _env = QProcessEnvironment() for name, value in env.items(): _env.insert(name, value) return _env -def get_configured_qprocess(command: List[str], environment: Dict) -> QProcess: +def get_configured_qprocess(command: list[str], environment: dict) -> QProcess: cmd, args, env = prepare_process(command, environment) proc = QProcess() proc.setProcessChannelMode(QProcess.ProcessChannelMode.SeparateChannels) @@ -56,7 +56,7 @@ def get_configured_qprocess(command: List[str], environment: Dict) -> QProcess: return proc -def get_configured_subprocess(command: List[str], environment: Dict) -> subprocess.Popen: +def get_configured_subprocess(command: list[str], environment: dict) -> subprocess.Popen: cmd, args, env = prepare_process(command, environment) return subprocess.Popen( (cmd, *args), @@ -69,34 +69,34 @@ def get_configured_subprocess(command: List[str], environment: Dict) -> subproce ) -def execute_subprocess(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]: +def execute_subprocess(command: list[str], arguments: list[str], environment: Mapping) -> tuple[str, str]: proc = get_configured_subprocess(command + arguments, environment) out, err = proc.communicate() out, err = ( - out.decode("utf-8", "ignore") if out else "", - err.decode("utf-8", "ignore") if err else "", + out.decode('utf-8', 'ignore') if out else '', + err.decode('utf-8', 'ignore') if err else '', ) # lk: the following is a work-around for wineserver sometimes hanging around after - proc = get_configured_subprocess(command + ["wineboot", "-e"], environment) + proc = get_configured_subprocess(command + ['wineboot', '-e'], environment) _, _ = proc.communicate() return out, err -def execute_qprocess(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]: - logger.debug("WINEPREFIX: %s", environment.get("WINEPREFIX", None)) - logger.debug("STEAM_COMPAT_DATA_PATH: %s", environment.get("STEAM_COMPAT_DATA_PATH", None)) +def execute_qprocess(command: list[str], arguments: list[str], environment: Mapping) -> tuple[str, str]: + logger.debug('WINEPREFIX: %s', environment.get('WINEPREFIX', None)) + logger.debug('STEAM_COMPAT_DATA_PATH: %s', environment.get('STEAM_COMPAT_DATA_PATH', None)) proc = get_configured_qprocess(command + arguments, environment) proc.start() proc.waitForFinished(-1) out, err = ( - proc.readAllStandardOutput().data().decode("utf-8", "ignore"), - proc.readAllStandardError().data().decode("utf-8", "ignore"), + proc.readAllStandardOutput().data().decode('utf-8', 'ignore'), + proc.readAllStandardError().data().decode('utf-8', 'ignore'), ) proc.deleteLater() # lk: the following is a work-around for wineserver sometimes hanging around after - proc = get_configured_qprocess(command + ["wineboot", "-e"], environment) + proc = get_configured_qprocess(command + ['wineboot', '-e'], environment) proc.start() proc.waitForFinished(-1) proc.deleteLater() @@ -104,19 +104,19 @@ def execute_qprocess(command: List[str], arguments: List[str], environment: Mapp return out, err -def execute(command: List[str], arguments: List[str], environment: Mapping) -> Tuple[str, str]: +def execute(command: list[str], arguments: list[str], environment: Mapping) -> tuple[str, str]: try: out, err = execute_qprocess(command, arguments, environment) except Exception as e: - out, err = "", str(e) + out, err = '', str(e) return out, err -def resolve_path(command: List[str], environment: Mapping, path: str) -> str: - path = path.strip().replace("/", "\\") +def resolve_path(command: list[str], environment: Mapping, path: str) -> str: + path = path.strip().replace('/', '\\') # lk: if path does not exist form - arguments = ["c:\\windows\\system32\\cmd.exe", "/c", "echo", path] + arguments = ['c:\\windows\\system32\\cmd.exe', '/c', 'echo', path] # lk: if path exists and needs a case-sensitive interpretation form # cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd'] out, err = execute(command, arguments, environment) @@ -131,29 +131,29 @@ def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str): raise NotImplementedError -def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str: - arguments = ["c:\\windows\\system32\\reg.exe", "query", reg_path, "/v", reg_key] +def query_reg_key(command: list[str], environment: Mapping, reg_path: str, reg_key) -> str: + arguments = ['c:\\windows\\system32\\reg.exe', 'query', reg_path, '/v', reg_key] out, err = execute(command, arguments, environment) out, err = out.strip(), err.strip() if not out: logger.error('Failed to query registry key due to "%s"', err) return out - lines = out.split("\n") - keys: Dict = {} + lines = out.split('\n') + keys: dict = {} for line in lines: - if line.startswith(" " * 4): - key = [x for x in line.split(" " * 4, 3) if bool(x)] + if line.startswith(' ' * 4): + key = [x for x in line.split(' ' * 4, 3) if bool(x)] keys.update({key[0]: key[2]}) - return keys.get(reg_key, "") + return keys.get(reg_key, '') def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str: raise NotImplementedError -def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> str: +def convert_to_unix_path(command: list[str], environment: Mapping, path: str) -> str: path = path.strip().strip('"') - arguments = ["c:\\windows\\system32\\winepath.exe", "-u", path] + arguments = ['c:\\windows\\system32\\winepath.exe', '-u', path] out, err = execute(command, arguments, environment) out, err = out.strip(), err.strip() if not out: @@ -161,33 +161,33 @@ def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> return os.path.realpath(out) if (out := out.strip()) else out -def get_host_environment(app_environment: Dict, silent: bool = False) -> Dict: +def get_host_environment(app_environment: dict, silent: bool = False) -> dict: # Get a clean environment if we are in flatpak, this environment will be passed # to `flatpak-spawn`, otherwise use the system's. _environ = app_environment.copy() if silent: - _environ["WINEESYNC"] = "0" - _environ["WINEFSYNC"] = "0" - _environ["WINE_DISABLE_FAST_SYNC"] = "1" - _environ["WINEDEBUG"] = "-all" - _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;" - _environ["WINEDLLOVERRIDES"] += "winex11.drv,winewayland.drv=d;" + _environ['WINEESYNC'] = '0' + _environ['WINEFSYNC'] = '0' + _environ['WINE_DISABLE_FAST_SYNC'] = '1' + _environ['WINEDEBUG'] = '-all' + _environ['WINEDLLOVERRIDES'] = 'winemenubuilder=d;mscoree=d;mshtml=d;' + _environ['WINEDLLOVERRIDES'] += 'winex11.drv,winewayland.drv=d;' # lk: pressure-vessel complains about this but it doesn't fail due to it # _environ["DISPLAY"] = "" return _environ def create_compat_users(pfx: str): - pfx_users = os.path.join(pfx, "drive_c", "users") + pfx_users = os.path.join(pfx, 'drive_c', 'users') os.makedirs(pfx_users, exist_ok=True) - steam_user = os.path.join(pfx_users, "steamuser") + steam_user = os.path.join(pfx_users, 'steamuser') unix_user = os.path.join(pfx_users, getuser()) if not os.path.exists(steam_user) and not os.path.exists(unix_user): os.makedirs(steam_user, exist_ok=True) pwd = os.getcwd() os.chdir(pfx_users) if os.path.exists(steam_user) and not os.path.exists(unix_user): - os.symlink("steamuser", getuser()) + os.symlink('steamuser', getuser()) if not os.path.exists(steam_user) and os.path.exists(unix_user): - os.symlink(getuser(), "steamuser") + os.symlink(getuser(), 'steamuser') os.chdir(pwd) diff --git a/rare/utils/compat/wine.py b/rare/utils/compat/wine.py index c92189f4b5..2dd441ea61 100644 --- a/rare/utils/compat/wine.py +++ b/rare/utils/compat/wine.py @@ -1,93 +1,92 @@ import os from dataclasses import dataclass from logging import getLogger -from typing import Dict, List, Optional, Tuple -logger = getLogger("Wine") +logger = getLogger('Wine') -lutris_runtime_paths = [os.path.expanduser("~/.local/share/lutris")] +lutris_runtime_paths = [os.path.expanduser('~/.local/share/lutris')] __lutris_runtime: str = None __lutris_wine: str = None -def find_lutris() -> Tuple[str, str]: +def find_lutris() -> tuple[str, str]: global __lutris_runtime, __lutris_wine for path in lutris_runtime_paths: - runtime_path = os.path.join(path, "runtime") - wine_path = os.path.join(path, "runners", "wine") + runtime_path = os.path.join(path, 'runtime') + wine_path = os.path.join(path, 'runners', 'wine') if os.path.isdir(path) and os.path.isdir(runtime_path) and os.path.isdir(wine_path): __lutris_runtime, __lutris_wine = runtime_path, wine_path return runtime_path, wine_path - return "", "" + return '', '' @dataclass class WineRuntime: name: str path: str - environ: Dict + environ: dict @dataclass class WineRunner: name: str path: str - environ: Dict - runtime: Optional[WineRuntime] = None + environ: dict + runtime: WineRuntime | None = None -def find_lutris_wines(runtime_path: str = None, wine_path: str = None) -> List[WineRunner]: +def find_lutris_wines(runtime_path: str = None, wine_path: str = None) -> list[WineRunner]: runners = [] if not runtime_path and not wine_path: return runners return runners -def __get_lib_path(executable: str, basename: str = "") -> str: +def __get_lib_path(executable: str, basename: str = '') -> str: path = os.path.dirname(os.path.dirname(executable)) - lib32 = os.path.realpath(os.path.join(path, "lib32", basename)) - lib64 = os.path.realpath(os.path.join(path, "lib64", basename)) - lib = os.path.realpath(os.path.join(path, "lib", basename)) + lib32 = os.path.realpath(os.path.join(path, 'lib32', basename)) + lib64 = os.path.realpath(os.path.join(path, 'lib64', basename)) + lib = os.path.realpath(os.path.join(path, 'lib', basename)) if lib32 == lib or not os.path.exists(lib32): - ldpath = ":".join([lib64, lib]) + ldpath = ':'.join([lib64, lib]) elif lib64 == lib or not os.path.exists(lib64): - ldpath = ":".join([lib, lib32]) + ldpath = ':'.join([lib, lib32]) else: ldpath = lib if os.path.exists(lib) else lib64 return ldpath -def get_wine_environment(executable: str = None, prefix: str = None) -> Dict: +def get_wine_environment(executable: str = None, prefix: str = None) -> dict: # If the tool is unset, return all affected env variable names # IMPORTANT: keep this in sync with the code below - environ = {"WINEPREFIX": prefix if prefix is not None else ""} + environ = {'WINEPREFIX': prefix if prefix is not None else ''} if executable is None: - environ["WINEDLLPATH"] = "" - environ["LD_LIBRARY_PATH"] = "" + environ['WINEDLLPATH'] = '' + environ['LD_LIBRARY_PATH'] = '' else: - winedllpath = __get_lib_path(executable, "wine") - environ["WINEDLLPATH"] = winedllpath - librarypath = __get_lib_path(executable, "") - environ["LD_LIBRARY_PATH"] = librarypath + winedllpath = __get_lib_path(executable, 'wine') + environ['WINEDLLPATH'] = winedllpath + librarypath = __get_lib_path(executable, '') + environ['LD_LIBRARY_PATH'] = librarypath return environ -if __name__ == "__main__": +if __name__ == '__main__': from pprint import pprint - pprint(get_wine_environment("/opt/wine-ge-custom/bin/wine", None)) - pprint(get_wine_environment("/usr/bin/wine", None)) - pprint(get_wine_environment("/usr/share/steam/compatitiblitytools.d/dist/bin/wine", None)) + pprint(get_wine_environment('/opt/wine-ge-custom/bin/wine', None)) + pprint(get_wine_environment('/usr/bin/wine', None)) + pprint(get_wine_environment('/usr/share/steam/compatitiblitytools.d/dist/bin/wine', None)) pprint( get_wine_environment( - os.path.expanduser("~/.local/share/Steam/compatibilitytools.d/GE-Proton8-14/files/bin/wine"), + os.path.expanduser('~/.local/share/Steam/compatibilitytools.d/GE-Proton8-14/files/bin/wine'), None, ) ) pprint( get_wine_environment( - os.path.expanduser("~/.local/share/lutris/runners/wine/lutris-GE-Proton8-14-x86_64/bin/wine"), + os.path.expanduser('~/.local/share/lutris/runners/wine/lutris-GE-Proton8-14-x86_64/bin/wine'), None, ) ) diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index 2f0747cf34..683b0e202a 100644 --- a/rare/utils/config_helper.py +++ b/rare/utils/config_helper.py @@ -1,13 +1,14 @@ import os +from collections.abc import Callable from configparser import SectionProxy -from typing import Any, Callable, Optional, Set, Tuple +from typing import Any from legendary.models.config import LGDConf from rare.lgndr.core import LegendaryCore -_config: Optional[LGDConf] = None -_save_config: Optional[Callable[[], None]] = None +_config: LGDConf | None = None +_save_config: Callable[[], None] | None = None def init_config_handler(core: LegendaryCore): @@ -18,12 +19,12 @@ def init_config_handler(core: LegendaryCore): def save_config(): if _save_config is None: - raise RuntimeError("Uninitialized use of config_helper") + raise RuntimeError('Uninitialized use of config_helper') _save_config() def set_option(app_name: str, option: str, value: str) -> None: - value = value.replace("%%", "%").replace("%", "%%") + value = value.replace('%%', '%').replace('%', '%%') if not _config.has_section(app_name): _config[app_name] = {} _config.set(app_name, option, value) @@ -35,7 +36,7 @@ def set_boolean(app_name: str, option: str, value: bool) -> None: def set_envvar(app_name: str, envvar: str, value: str) -> None: - set_option(f"{app_name}.env", envvar, value) + set_option(f'{app_name}.env', envvar, value) def remove_section(app_name: str) -> None: @@ -55,7 +56,7 @@ def remove_option(app_name: str, option: str) -> None: def remove_envvar(app_name: str, option: str) -> None: - remove_option(f"{app_name}.env", option) + remove_option(f'{app_name}.env', option) def adjust_option(app_name: str, option: str, value: str) -> None: @@ -70,7 +71,7 @@ def get_option(app_name: str, option: str, fallback: Any = None) -> str: def get_option_with_global(app_name: str, option: str, fallback: Any = None) -> str: - _option = get_option("default", option, fallback=fallback) + _option = get_option('default', option, fallback=fallback) _option = get_option(app_name, option, fallback=_option) return _option @@ -87,90 +88,90 @@ def adjust_envvar(app_name: str, option: str, value: str) -> None: def get_envvar(app_name: str, option: str, fallback: Any = None) -> str: - return get_option(f"{app_name}.env", option, fallback=fallback) + return get_option(f'{app_name}.env', option, fallback=fallback) def get_envvar_with_global(app_name: str, option: str, fallback: Any = None) -> str: - _option = _config.get("default.env", option, fallback=fallback) - _option = _config.get(f"{app_name}.env", option, fallback=_option) + _option = _config.get('default.env', option, fallback=fallback) + _option = _config.get(f'{app_name}.env', option, fallback=_option) return _option def adjust_wine_prefix(app_name: str, value: str) -> None: - adjust_envvar(app_name, "WINEPREFIX", value) - adjust_option(app_name, "wine_prefix", value) + adjust_envvar(app_name, 'WINEPREFIX', value) + adjust_option(app_name, 'wine_prefix', value) def get_wine_prefix(app_name: str, fallback: Any = None): - _prefix = get_envvar(app_name, "WINEPREFIX", fallback=fallback) - _prefix = get_option(app_name, "wine_prefix", fallback=_prefix) + _prefix = get_envvar(app_name, 'WINEPREFIX', fallback=fallback) + _prefix = get_option(app_name, 'wine_prefix', fallback=_prefix) return _prefix def get_wine_prefix_with_global(app_name: str, fallback: Any = None) -> str: - _prefix = get_wine_prefix("default", fallback) + _prefix = get_wine_prefix('default', fallback) _prefix = get_wine_prefix(app_name, fallback=_prefix) return _prefix def adjust_compat_data_path(app_name: str, value: str) -> None: - adjust_envvar(app_name, "STEAM_COMPAT_DATA_PATH", value) + adjust_envvar(app_name, 'STEAM_COMPAT_DATA_PATH', value) -def get_compat_data_path(app_name: Optional[str] = None, fallback: Any = None) -> str: - _compat = get_envvar(app_name, "STEAM_COMPAT_DATA_PATH", fallback=fallback) +def get_compat_data_path(app_name: str | None = None, fallback: Any = None) -> str: + _compat = get_envvar(app_name, 'STEAM_COMPAT_DATA_PATH', fallback=fallback) # return os.path.join(_compat, "pfx") if _compat else fallback return _compat -def get_compat_data_path_with_global(app_name: Optional[str] = None, fallback: Any = None) -> str: - _compat = get_envvar_with_global(app_name, "STEAM_COMPAT_DATA_PATH", fallback=fallback) +def get_compat_data_path_with_global(app_name: str | None = None, fallback: Any = None) -> str: + _compat = get_envvar_with_global(app_name, 'STEAM_COMPAT_DATA_PATH', fallback=fallback) # return os.path.join(_compat, "pfx") if _compat else fallback return _compat def prefix_exists(pfx: str) -> bool: - return os.path.isdir(pfx) and os.path.isfile(os.path.join(pfx, "user.reg")) + return os.path.isdir(pfx) and os.path.isfile(os.path.join(pfx, 'user.reg')) -def _get_prefixes(lookup_fn: Callable[[SectionProxy], str]) -> Set[Tuple[str, str]]: - _prefixes: Set[Tuple[str, str]] = set() +def _get_prefixes(lookup_fn: Callable[[SectionProxy], str]) -> set[tuple[str, str]]: + _prefixes: set[tuple[str, str]] = set() for name, section in _config.items(): pfx = lookup_fn(section) if pfx: - _prefixes.update([(pfx, name[: -len(".env")] if name.endswith(".env") else name)]) + _prefixes.update([(pfx, name[: -len('.env')] if name.endswith('.env') else name)]) _prefixes = {(os.path.expanduser(p), n) for p, n in _prefixes} return {(p, n) for p, n in _prefixes if prefix_exists(p)} -def get_wine_prefixes() -> Set[Tuple[str, str]]: - return _get_prefixes(lambda s: s.get("WINEPREFIX") or s.get("wine_prefix")) +def get_wine_prefixes() -> set[tuple[str, str]]: + return _get_prefixes(lambda s: s.get('WINEPREFIX') or s.get('wine_prefix')) -def get_proton_prefixes() -> Set[Tuple[str, str]]: - return _get_prefixes(lambda s: os.path.join(compat_path, "pfx") if (compat_path := s.get("STEAM_COMPAT_DATA_PATH")) else "") +def get_proton_prefixes() -> set[tuple[str, str]]: + return _get_prefixes(lambda s: os.path.join(compat_path, 'pfx') if (compat_path := s.get('STEAM_COMPAT_DATA_PATH')) else '') -def get_prefixes() -> Set[Tuple[str, str]]: +def get_prefixes() -> set[tuple[str, str]]: return get_wine_prefixes().union(get_proton_prefixes()) -def get_prefix(app_name: str = "default") -> Optional[str]: - _compat_path = _config.get(f"{app_name}.env", "STEAM_COMPAT_DATA_PATH", fallback=None) - if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, "pfx")): +def get_prefix(app_name: str = 'default') -> str | None: + _compat_path = _config.get(f'{app_name}.env', 'STEAM_COMPAT_DATA_PATH', fallback=None) + if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, 'pfx')): return _compat_prefix - _wine_prefix = _config.get(f"{app_name}.env", "WINEPREFIX", fallback=None) - _wine_prefix = _config.get(app_name, "wine_prefix", fallback=_wine_prefix) + _wine_prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=None) + _wine_prefix = _config.get(app_name, 'wine_prefix', fallback=_wine_prefix) if _wine_prefix and prefix_exists(_wine_prefix): return _wine_prefix - _compat_path = _config.get("default.env", "STEAM_COMPAT_DATA_PATH", fallback=None) - if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, "pfx")): + _compat_path = _config.get('default.env', 'STEAM_COMPAT_DATA_PATH', fallback=None) + if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, 'pfx')): return _compat_prefix - _wine_prefix = _config.get("default.env", "WINEPREFIX", fallback=None) - _wine_prefix = _config.get("default", "wine_prefix", fallback=_wine_prefix) + _wine_prefix = _config.get('default.env', 'WINEPREFIX', fallback=None) + _wine_prefix = _config.get('default', 'wine_prefix', fallback=_wine_prefix) if _wine_prefix and prefix_exists(_wine_prefix): return _wine_prefix diff --git a/rare/utils/discord_rpc.py b/rare/utils/discord_rpc.py index 88bad87bda..de9457738e 100644 --- a/rare/utils/discord_rpc.py +++ b/rare/utils/discord_rpc.py @@ -1,7 +1,6 @@ import platform import time from logging import getLogger -from typing import List from pypresence import Presence, exceptions from PySide6.QtCore import QObject, Slot @@ -9,8 +8,8 @@ from rare.models.settings import DiscordRPCMode, RareAppSettings, app_settings from rare.shared import RareCore -client_id = "830732538225360908" -logger = getLogger("DiscordRPC") +client_id = '830732538225360908' +logger = getLogger('DiscordRPC') class DiscordRPC(QObject): @@ -40,7 +39,7 @@ def remove_presence(self, app_name: str): @Slot() @Slot(list) - def update_settings(self, game_running: List = None): + def update_settings(self, game_running: list = None): rpc_mode = DiscordRPCMode(self.settings.get_value(app_settings.discord_rpc_mode)) if rpc_mode == DiscordRPCMode.NEVER: self.remove_rpc() @@ -60,10 +59,10 @@ def remove_rpc(self): try: self.rpc.close() except Exception: - logger.warning("Already closed") + logger.warning('Already closed') del self.rpc self.rpc = None - logger.info("Remove RPC") + logger.info('Remove RPC') else: self.set_discord_rpc() @@ -73,15 +72,15 @@ def set_discord_rpc(self, app_name=None): self.rpc = Presence(client_id) # Rare app: https://discord.com/developers/applications self.rpc.connect() except ConnectionRefusedError as e: - logger.warning(f"Discord is not active\n{e}") + logger.warning(f'Discord is not active\n{e}') self.rpc = None return except FileNotFoundError as e: - logger.warning(f"File not found error\n{e}") + logger.warning(f'File not found error\n{e}') self.rpc = None return except exceptions.InvalidPipe as e: - logger.error(f"Is Discord running? \n{e}") + logger.error(f'Is Discord running? \n{e}') self.rpc = None return except Exception as e: @@ -97,19 +96,19 @@ def update_rpc(self, app_name=None): return title = None if not app_name: - self.rpc.update(large_image="logo", details="https://github.com/RareDevs/Rare") + self.rpc.update(large_image='logo', details='https://github.com/RareDevs/Rare') return if self.settings.get_value(app_settings.discord_rpc_game): try: title = self.core.get_installed_game(app_name).title except AttributeError: - logger.error(f"Could not get title of game: {app_name}") + logger.error(f'Could not get title of game: {app_name}') title = app_name start = None if self.settings.get_value(app_settings.discord_rpc_time): - start = str(time.time()).split(".")[0] + start = str(time.time()).split('.')[0] os = None if self.settings.get_value(app_settings.discord_rpc_os): - os = f"via Rare on {platform.system()}" + os = f'via Rare on {platform.system()}' - self.rpc.update(large_image="logo", details=title, large_text=title, state=os, start=start) + self.rpc.update(large_image='logo', details=title, large_text=title, state=os, start=start) diff --git a/rare/utils/json_formatter.py b/rare/utils/json_formatter.py index d9e60dbb77..dc8e1a084e 100644 --- a/rare/utils/json_formatter.py +++ b/rare/utils/json_formatter.py @@ -42,12 +42,12 @@ from PySide6 import QtCore, QtWidgets -class QJsonTreeItem(object): +class QJsonTreeItem: def __init__(self, parent=None): self._parent = parent - self._key = "" - self._value = "" + self._key = '' + self._value = '' self._type = None self._children = list() @@ -93,22 +93,22 @@ def type(self, typ): @classmethod def load(self, value, parent=None, sort=True): rootItem = QJsonTreeItem(parent) - rootItem.key = "root" + rootItem.key = 'root' if isinstance(value, dict): items = sorted(value.items()) if sort else value.items() - for key, value in items: - child = self.load(value, rootItem) + for key, val in items: + child = self.load(val, rootItem) child.key = key - child.type = type(value) + child.type = type(val) rootItem.appendChild(child) elif isinstance(value, list): - for index, value in enumerate(value): - child = self.load(value, rootItem) - child.key = index - child.type = type(value) + for idx, val in enumerate(value): + child = self.load(val, rootItem) + child.key = idx + child.type = type(val) rootItem.appendChild(child) else: @@ -123,7 +123,7 @@ def __init__(self, parent=None): super(QJsonModel, self).__init__(parent) self._rootItem = QJsonTreeItem() - self._headers = ("key", "value") + self._headers = ('key', 'value') def clear(self): self.load({}) @@ -136,7 +136,7 @@ def load(self, document): """ - assert isinstance(document, (dict, list, tuple)), "`document` must be of dict, list or tuple, not %s" % type(document) + assert isinstance(document, (dict, list, tuple)), f'`document` must be of dict, list or tuple, not {type(document)}' self.beginResetModel() @@ -179,6 +179,8 @@ def data(self, index, role): if index.column() == 1: return item.value + return None + def setData(self, index, value, role): if role == QtCore.Qt.ItemDataRole.EditRole: if index.column() == 1: @@ -198,7 +200,12 @@ def headerData(self, section, orientation, role): if orientation == QtCore.Qt.Orientation.Horizontal: return self._headers[section] - def index(self, row, column, parent=QtCore.QModelIndex()): + return None + + def index(self, row, column, parent=None): + if parent is None: + parent = QtCore.QModelIndex() + if not self.hasIndex(row, column, parent): return QtCore.QModelIndex() @@ -225,7 +232,9 @@ def parent(self, index): return self.createIndex(parentItem.row(), 0, parentItem) - def rowCount(self, parent=QtCore.QModelIndex()): + def rowCount(self, parent=None): + if parent is None: + parent = QtCore.QModelIndex() if parent.column() > 0: return 0 @@ -236,7 +245,9 @@ def rowCount(self, parent=QtCore.QModelIndex()): return parentItem.childCount() - def columnCount(self, parent=QtCore.QModelIndex()): + def columnCount(self, parent=None): + if parent is None: + parent = QtCore.QModelIndex() return 2 def flags(self, index): @@ -251,24 +262,24 @@ def genJson(self, item): nchild = item.childCount() if item.type is dict: - document = {} + _document = {} for i in range(nchild): ch = item.child(i) - document[ch.key] = self.genJson(ch) - return document + _document[ch.key] = self.genJson(ch) + return _document elif item.type is list: - document = [] + _document = [] for i in range(nchild): ch = item.child(i) - document.append(self.genJson(ch)) - return document + _document.append(self.genJson(ch)) + return _document else: return item.value -if __name__ == "__main__": +if __name__ == '__main__': import sys app = QtWidgets.QApplication(sys.argv) diff --git a/rare/utils/metrics.py b/rare/utils/metrics.py index bcc969d781..cd95db1f0d 100644 --- a/rare/utils/metrics.py +++ b/rare/utils/metrics.py @@ -7,4 +7,4 @@ def timelogger(logger: Logger, title: str): start = time.perf_counter() yield - logger.debug("%s: %s seconds", title, time.perf_counter() - start) + logger.debug('%s: %s seconds', title, time.perf_counter() - start) diff --git a/rare/utils/misc.py b/rare/utils/misc.py index 065385f16d..7d06dcecd0 100644 --- a/rare/utils/misc.py +++ b/rare/utils/misc.py @@ -1,9 +1,9 @@ import functools import os +from collections.abc import Iterable from datetime import UTC, datetime from enum import IntEnum from logging import getLogger -from typing import Dict, Iterable, Tuple, Type, Union import qtawesome from PySide6.QtCore import ( @@ -20,7 +20,7 @@ from rare.utils.paths import resources_path -logger = getLogger("Utils") +logger = getLogger('Utils') class ExitCodes(IntEnum): @@ -28,35 +28,35 @@ class ExitCodes(IntEnum): LOGOUT = -133742 -color_role_map: Dict[int, str] = { - 0: "WindowText", - 1: "Button", - 2: "Light", - 3: "Midlight", - 4: "Dark", - 5: "Mid", - 6: "Text", - 7: "BrightText", - 8: "ButtonText", - 9: "Base", - 10: "Window", - 11: "Shadow", - 12: "Highlight", - 13: "HighlightedText", - 14: "Link", - 15: "LinkVisited", - 16: "AlternateBase", +color_role_map: dict[int, str] = { + 0: 'WindowText', + 1: 'Button', + 2: 'Light', + 3: 'Midlight', + 4: 'Dark', + 5: 'Mid', + 6: 'Text', + 7: 'BrightText', + 8: 'ButtonText', + 9: 'Base', + 10: 'Window', + 11: 'Shadow', + 12: 'Highlight', + 13: 'HighlightedText', + 14: 'Link', + 15: 'LinkVisited', + 16: 'AlternateBase', # 17: "NoRole", - 18: "ToolTipBase", - 19: "ToolTipText", - 20: "PlaceholderText", + 18: 'ToolTipBase', + 19: 'ToolTipText', + 20: 'PlaceholderText', # 21: "NColorRoles", } -color_group_map: Dict[int, str] = { - 0: "Active", - 1: "Disabled", - 2: "Inactive", +color_group_map: dict[int, str] = { + 0: 'Active', + 1: 'Disabled', + 2: 'Inactive', } @@ -64,7 +64,7 @@ def load_color_scheme(path: str) -> QPalette: palette = QPalette() scheme = QSettings(path, QSettings.Format.IniFormat) try: - scheme.beginGroup("ColorScheme") + scheme.beginGroup('ColorScheme') for g in color_group_map: scheme.beginGroup(color_group_map[g]) group = QPalette.ColorGroup(g) @@ -83,9 +83,9 @@ def load_color_scheme(path: str) -> QPalette: def get_static_style() -> str: - file = QFile(":/static_css/stylesheet.qss") + file = QFile(':/static_css/stylesheet.qss') file.open(QFile.OpenModeFlag.ReadOnly) - static = file.readAll().data().decode("utf-8") + static = file.readAll().data().decode('utf-8') file.close() return static @@ -95,13 +95,13 @@ def set_color_pallete(color_scheme: str) -> None: qApp: QApplication = QApplication.instance() if not color_scheme: - qApp.setStyle(QStyleFactory.create(qApp.property("rareDefaultQtStyle"))) + qApp.setStyle(QStyleFactory.create(qApp.property('rareDefaultQtStyle'))) qApp.setPalette(qApp.style().standardPalette()) qApp.setStyleSheet(static) return - qApp.setStyle(QStyleFactory.create("Fusion")) - custom_palette = load_color_scheme(f":/schemes/{color_scheme}") + qApp.setStyle(QStyleFactory.create('Fusion')) + custom_palette = load_color_scheme(f':/schemes/{color_scheme}') if custom_palette is not None: qApp.setPalette(custom_palette) qApp.setStyleSheet(static) @@ -111,7 +111,7 @@ def set_color_pallete(color_scheme: str) -> None: def get_color_schemes() -> Iterable[str]: - it = QDirIterator(":/schemes/") + it = QDirIterator(':/schemes/') while it.hasNext(): it.next() yield it.fileName() @@ -122,44 +122,44 @@ def set_style_sheet(style_sheet: str) -> None: qApp: QApplication = QApplication.instance() if not style_sheet: - qApp.setStyle(QStyleFactory.create(qApp.property("rareDefaultQtStyle"))) + qApp.setStyle(QStyleFactory.create(qApp.property('rareDefaultQtStyle'))) qApp.setStyleSheet(static) return - qApp.setStyle(QStyleFactory.create("Fusion")) - file = QFile(f":/stylesheets/{style_sheet}/stylesheet.qss") + qApp.setStyle(QStyleFactory.create('Fusion')) + file = QFile(f':/stylesheets/{style_sheet}/stylesheet.qss') file.open(QFile.OpenModeFlag.ReadOnly) - stylesheet = file.readAll().data().decode("utf-8") + stylesheet = file.readAll().data().decode('utf-8') file.close() qApp.setStyleSheet(stylesheet + static) icon_color_normal = qApp.palette().color(QPalette.ColorRole.Text).name() # noqa: F841 icon_color_disabled = qApp.palette().color(QPalette.ColorRole.Text).name() # noqa: F841 - qtawesome.set_defaults(color="#eee", color_disabled="#eee") + qtawesome.set_defaults(color='#eee', color_disabled='#eee') def get_style_sheets() -> Iterable[str]: - it = QDirIterator(":/stylesheets/") + it = QDirIterator(':/stylesheets/') while it.hasNext(): it.next() yield it.fileName() -def get_translations() -> Tuple[Tuple[str, str], ...]: +def get_translations() -> tuple[tuple[str, str], ...]: langs = [] - for i in os.listdir(os.path.join(resources_path, "languages")): - if i.endswith(".qm") and i.startswith("rare_"): - locale = QLocale(i.removesuffix(".qm").removeprefix("rare_")) + for i in os.listdir(os.path.join(resources_path, 'languages')): + if i.endswith('.qm') and i.startswith('rare_'): + locale = QLocale(i.removesuffix('.qm').removeprefix('rare_')) langs.append( ( locale.name(), - f"{locale.nativeLanguageName()} ({locale.nativeCountryName()})", + f'{locale.nativeLanguageName()} ({locale.nativeCountryName()})', ) ) return tuple(langs) -def path_size(path: Union[str, os.PathLike]) -> int: +def path_size(path: str | os.PathLike) -> int: return sum( os.stat(os.path.join(dp, f)).st_size for dp, dn, filenames in os.walk(path) @@ -168,10 +168,10 @@ def path_size(path: Union[str, os.PathLike]) -> int: ) -def format_size(b: Union[int, float]) -> str: - for s in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"): +def format_size(b: int | float) -> str: + for s in ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'): if b < 1024: - return f"{b:.2f} {s}B" + return f'{b:.2f} {s}B' b /= 1024 return str(b) @@ -180,23 +180,23 @@ def relative_date(date): diff = datetime.now(UTC) - date s = diff.seconds if diff.days > 7 or diff.days < 0: - return date.strftime("%d %b %y") + return date.strftime('%d %b %y') elif diff.days == 1: - return "1 day ago" + return '1 day ago' elif diff.days > 1: - return "{} days ago".format(diff.days) + return f'{diff.days} days ago' elif s <= 1: - return "just now" + return 'just now' elif s < 60: - return "{} seconds ago".format(s) + return f'{s} seconds ago' elif s < 120: - return "1 minute ago" + return '1 minute ago' elif s < 3600: - return "{} minutes ago".format(s // 60) + return f'{s // 60} minutes ago' elif s < 7200: - return "1 hour ago" + return '1 hour ago' else: - return "{} hours ago".format(s // 3600) + return f'{s // 3600} hours ago' def qta_icon(icn_str: str, fallback: str = None, **kwargs): @@ -204,15 +204,15 @@ def qta_icon(icn_str: str, fallback: str = None, **kwargs): return qtawesome.icon(icn_str, **kwargs) except Exception as e: if not fallback: - logger.warning(f"{e} {icn_str}") + logger.warning(f'{e} {icn_str}') if fallback: try: return qtawesome.icon(fallback, **kwargs) except Exception as e: - logger.error(f"{e} {icn_str}") - if kwargs.get("color"): - kwargs["color"] = "red" - return qtawesome.icon("ei.error", **kwargs) + logger.error(f'{e} {icn_str}') + if kwargs.get('color'): + kwargs['color'] = 'red' + return qtawesome.icon('ei.error', **kwargs) # Source - https://stackoverflow.com/a @@ -226,14 +226,14 @@ def partial_bound_method(bound_method, *args, **kwargs): return (lambda *args: f(*args)).__get__(bound_method.__self__) -def widget_object_name(widget: Union[QObject, ShibokenObject, Type], suffix: str) -> str: - suffix = f"_{suffix}" if suffix else "" +def widget_object_name(widget: QObject | ShibokenObject | type, suffix: str) -> str: + suffix = f'_{suffix}' if suffix else '' if isinstance(widget, QObject): - return f"{type(widget).__name__}{suffix}" - elif isinstance(widget, ShibokenObject) or isinstance(widget, type): - return f"{widget.__name__}{suffix}" + return f'{type(widget).__name__}{suffix}' + elif isinstance(widget, (ShibokenObject, type)): + return f'{widget.__name__}{suffix}' else: - raise RuntimeError(f"Argument {widget} not a QObject or type of QObject") + raise RuntimeError(f'Argument {widget} not a QObject or type of QObject') def elide_text(label: QLabel, text: str) -> str: @@ -242,4 +242,4 @@ def elide_text(label: QLabel, text: str) -> str: def style_hyperlink(link: str, title: str) -> str: - return "{}".format(link, title) + return f"{title}" diff --git a/rare/utils/paths.py b/rare/utils/paths.py index 9f50db1aab..6bb64dc0af 100644 --- a/rare/utils/paths.py +++ b/rare/utils/paths.py @@ -5,36 +5,35 @@ import sys from logging import getLogger from pathlib import Path -from typing import Dict, List from PySide6.QtCore import QStandardPaths -if platform.system() == "Windows": +if platform.system() == 'Windows': # noinspection PyUnresolvedReferences from win32com.client import Dispatch # pylint: disable=E0401 -logger = getLogger("Paths") +logger = getLogger('Paths') # This depends on the location of this file (obviously) -resources_path = Path(__file__).absolute().parent.parent.joinpath("resources") +resources_path = Path(__file__).absolute().parent.parent.joinpath('resources') # lk: delete old Rare directories for old_dir in [ Path( QStandardPaths.writableLocation(QStandardPaths.StandardLocation.CacheLocation), - "rare", - ).joinpath("tmp"), + 'rare', + ).joinpath('tmp'), Path( QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation), - "rare", - ).joinpath("images"), + 'rare', + ).joinpath('images'), Path( QStandardPaths.writableLocation(QStandardPaths.StandardLocation.CacheLocation), - "rare", + 'rare', ), Path( QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation), - "rare", + 'rare', ), ]: if old_dir.exists(): @@ -48,7 +47,7 @@ def lock_file() -> Path: return Path( QStandardPaths.writableLocation(QStandardPaths.StandardLocation.TempLocation), - "Rare.lock", + 'Rare.lock', ) @@ -67,7 +66,7 @@ def cache_dir() -> Path: def image_dir() -> Path: - return data_dir().joinpath("images") + return data_dir().joinpath('images') def image_dir_game(app_name: str) -> Path: @@ -75,30 +74,34 @@ def image_dir_game(app_name: str) -> Path: def image_tall_path(app_name: str, color: bool = True) -> Path: - return image_dir_game(app_name).joinpath("tall.png" if color else "tall_gray.png") + return image_dir_game(app_name).joinpath('tall.png' if color else 'tall_gray.png') def image_wide_path(app_name: str, color: bool = True) -> Path: - return image_dir_game(app_name).joinpath("wide.png" if color else "wide_gray.png") + return image_dir_game(app_name).joinpath('wide.png' if color else 'wide_gray.png') def image_icon_path(app_name: str, color: bool = True) -> Path: - return image_dir_game(app_name).joinpath("icon.png" if color else "icon_gray.png") + return image_dir_game(app_name).joinpath('icon.png' if color else 'icon_gray.png') + + +def runtime_assets_json() -> Path: + return data_dir().joinpath('runtime_assets.json') def log_dir() -> Path: - return cache_dir().joinpath("logs") + return cache_dir().joinpath('logs') def tmp_dir() -> Path: - return cache_dir().joinpath("tmp") + return cache_dir().joinpath('tmp') def create_dirs() -> None: for path in (data_dir(), cache_dir(), image_dir(), log_dir(), tmp_dir()): if not path.exists(): path.mkdir(parents=True) - logger.info(f"Created directory at {path}") + logger.info(f'Created directory at {path}') def home_dir() -> Path: @@ -114,66 +117,66 @@ def applications_dir() -> Path: def proton_compat_dir(name: str) -> Path: - if not (compat_dir := data_dir().joinpath(f"compatdata/{name}")).is_dir(): + if not (compat_dir := data_dir().joinpath(f'compatdata/{name}')).is_dir(): compat_dir.mkdir(parents=True) - if not (prefix_dir := compat_dir.joinpath("pfx")).is_dir(): + if not (prefix_dir := compat_dir.joinpath('pfx')).is_dir(): prefix_dir.mkdir(parents=True) return compat_dir def wine_prefix_dir(name: str) -> Path: - if not (prefix_dir := proton_compat_dir(name).joinpath("pfx")).is_dir(): + if not (prefix_dir := proton_compat_dir(name).joinpath('pfx')).is_dir(): prefix_dir.mkdir(parents=True) return prefix_dir def compat_shaders_dir(name: str) -> Path: - if not (shader_dir := proton_compat_dir(name).joinpath("shadercache")).is_dir(): + if not (shader_dir := proton_compat_dir(name).joinpath('shadercache')).is_dir(): shader_dir.mkdir(parents=True) return shader_dir def compat_logs_dir(name: str) -> Path: - if not (logs_dir := proton_compat_dir(name).joinpath("logs")).is_dir(): + if not (logs_dir := proton_compat_dir(name).joinpath('logs')).is_dir(): logs_dir.mkdir(parents=True) return logs_dir -def setup_compat_shaders_dir(path: str) -> Dict: +def setup_compat_shaders_dir(path: str) -> dict: """Setup per-game shader cache if shader pre-caching is disabled""" environ = {} - shader_cache_name = "steamapp_shader_cache" + shader_cache_name = 'steamapp_shader_cache' shader_cache_vars = { # Nvidia - "__GL_SHADER_DISK_CACHE_APP_NAME": shader_cache_name, - "__GL_SHADER_DISK_CACHE_PATH": os.path.join(path, "nvidiav1"), - "__GL_SHADER_DISK_CACHE_READ_ONLY_APP_NAME": "steam_shader_cache;steamapp_merged_shader_cache", - "__GL_SHADER_DISK_CACHE_SIZE": "10737418240", # 10GiB + '__GL_SHADER_DISK_CACHE_APP_NAME': shader_cache_name, + '__GL_SHADER_DISK_CACHE_PATH': os.path.join(path, 'nvidiav1'), + '__GL_SHADER_DISK_CACHE_READ_ONLY_APP_NAME': 'steam_shader_cache;steamapp_merged_shader_cache', + '__GL_SHADER_DISK_CACHE_SIZE': '10737418240', # 10GiB # "__GL_SHADER_DISK_CACHE_SKIP_CLEANUP": "1", # Mesa - "MESA_DISK_CACHE_READ_ONLY_FOZ_DBS": "steam_cache,steam_precompiled", - "MESA_DISK_CACHE_SINGLE_FILE": "1", - "MESA_GLSL_CACHE_DIR": path, - "MESA_GLSL_CACHE_MAX_SIZE": "10G", - "MESA_SHADER_CACHE_DIR": path, - "MESA_SHADER_CACHE_MAX_SIZE": "10G", + 'MESA_DISK_CACHE_READ_ONLY_FOZ_DBS': 'steam_cache,steam_precompiled', + 'MESA_DISK_CACHE_SINGLE_FILE': '1', + 'MESA_GLSL_CACHE_DIR': path, + 'MESA_GLSL_CACHE_MAX_SIZE': '10G', + 'MESA_SHADER_CACHE_DIR': path, + 'MESA_SHADER_CACHE_MAX_SIZE': '10G', # AMD VK - "AMD_VK_PIPELINE_CACHE_FILENAME": shader_cache_name, - "AMD_VK_PIPELINE_CACHE_PATH": os.path.join(path, "AMDv1"), - "AMD_VK_USE_PIPELINE_CACHE": "1", + 'AMD_VK_PIPELINE_CACHE_FILENAME': shader_cache_name, + 'AMD_VK_PIPELINE_CACHE_PATH': os.path.join(path, 'AMDv1'), + 'AMD_VK_USE_PIPELINE_CACHE': '1', # DXVK - "DXVK_STATE_CACHE_PATH": os.path.join(path, "DXVK_state_cache"), + 'DXVK_STATE_CACHE_PATH': os.path.join(path, 'DXVK_state_cache'), # VKD3D - "VKD3D_SHADER_CACHE_PATH": os.path.join(path, "VKD3D_shader_cache"), + 'VKD3D_SHADER_CACHE_PATH': os.path.join(path, 'VKD3D_shader_cache'), } for key, value in shader_cache_vars.items(): if key in { - "__GL_SHADER_DISK_CACHE_PATH", - "MESA_GLSL_CACHE_DIR", - "MESA_SHADER_CACHE_DIR", - "AMD_VK_PIPELINE_CACHE_PATH", - "DXVK_STATE_CACHE_PATH", - "VKD3D_SHADER_CACHE_PATH", + '__GL_SHADER_DISK_CACHE_PATH', + 'MESA_GLSL_CACHE_DIR', + 'MESA_SHADER_CACHE_DIR', + 'AMD_VK_PIPELINE_CACHE_PATH', + 'DXVK_STATE_CACHE_PATH', + 'VKD3D_SHADER_CACHE_PATH', }: os.makedirs(value, exist_ok=True) environ[key] = value @@ -222,7 +225,7 @@ def desktop_icon_path(app_name: str) -> Path: } -def desktop_link_types() -> List: +def desktop_link_types() -> list: return list(__link_type.keys()) @@ -240,10 +243,10 @@ def desktop_link_path(link_name: str, link_type: str) -> Path: :return Path: shortcut path """ - return __link_type[link_type].joinpath(f"{link_name}.{__link_suffix[platform.system()]['link']}") + return __link_type[link_type].joinpath(f'{link_name}.{__link_suffix[platform.system()]["link"]}') -def get_rare_executable(*, external: bool = False) -> List[str]: +def get_rare_executable(*, external: bool = False) -> list[str]: """ Returns the command list to invoke Rare for different platforms and packaging solutions When used with container based packaging, such as Flatpak or Snap, returns the command @@ -252,39 +255,39 @@ def get_rare_executable(*, external: bool = False) -> List[str]: :param external: if True return the command to invoke Rare through Flatpak or Snap, defaults to false :return: command list """ - logger.debug(f"Trying to find executable: {sys.executable}, {sys.argv}") + logger.debug(f'Trying to find executable: {sys.executable}, {sys.argv}') - if os.environ.get("SNAP") and external: - return ["snap", "run", "rare"] - if os.environ.get("container") == "flatpak" and external: - return ["flatpak", "run", "io.github.dummerle.rare"] + if os.environ.get('SNAP') and external: + return ['snap', 'run', 'rare'] + if os.environ.get('container') == 'flatpak' and external: # noqa: SIM112 + return ['flatpak', 'run', 'io.github.dummerle.rare'] # lk: detect if nuitka - if "__compiled__" in globals(): + if '__compiled__' in globals(): executable = [sys.executable] - elif sys.argv[0].endswith("__main__.py"): - executable = [sys.executable, "-m", "rare"] - elif platform.system() in {"Linux", "FreeBSD", "Darwin"}: - if p := os.environ.get("APPIMAGE"): + elif sys.argv[0].endswith('__main__.py'): + executable = [sys.executable, '-m', 'rare'] + elif platform.system() in {'Linux', 'FreeBSD', 'Darwin'}: + if p := os.environ.get('APPIMAGE'): executable = [p] else: if sys.executable == os.path.abspath(sys.argv[0]): executable = [sys.executable] else: executable = [os.path.abspath(sys.argv[0])] - elif platform.system() == "Windows": + elif platform.system() == 'Windows': executable = [sys.executable] if sys.executable != os.path.abspath(sys.argv[0]): executable.append(os.path.abspath(sys.argv[0])) - if executable[0].endswith("python.exe"): + if executable[0].endswith('python.exe'): # be sure to start console-less then - executable[0] = executable[0].replace("python.exe", "pythonw.exe") + executable[0] = executable[0].replace('python.exe', 'pythonw.exe') - if executable[0].endswith("pythonw.exe"): - if executable[1].endswith("rare"): - executable[1] = executable[1] + ".exe" + if executable[0].endswith('pythonw.exe'): + if executable[1].endswith('rare'): + executable[1] = executable[1] + '.exe' else: executable = [sys.executable] @@ -292,7 +295,7 @@ def get_rare_executable(*, external: bool = False) -> List[str]: return executable -def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "", link_type="desktop") -> bool: +def create_desktop_link(app_name: str, app_title: str = '', link_name: str = '', link_type='desktop') -> bool: """ Creates a desktop or start menu shortcut link @@ -311,55 +314,55 @@ def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "", """ # macOS is based on Darwin if not desktop_links_supported(): - logger.error(f"Shortcut creation is not available on {platform.system()}") + logger.error(f'Shortcut creation is not available on {platform.system()}') return False - if link_type not in ["desktop", "start_menu"]: - logger.error(f"Invalid link type {link_type}") + if link_type not in ['desktop', 'start_menu']: + logger.error(f'Invalid link type {link_type}') return False - for_rare = app_name == "rare_shortcut" + for_rare = app_name == 'rare_shortcut' if for_rare: - icon_path = resources_path.joinpath("images", f"Rare.{desktop_icon_suffix()}") - app_title = "Rare" - link_name = "Rare" + icon_path = resources_path.joinpath('images', f'Rare.{desktop_icon_suffix()}') + app_title = 'Rare' + link_name = 'Rare' else: icon_path = desktop_icon_path(app_name) if not app_title or not link_name: - logger.error("Missing app_title or link_name") + logger.error('Missing app_title or link_name') return False shortcut_path = desktop_link_path(link_name, link_type) if not shortcut_path.parent.exists(): - logger.error(f"Parent directory {shortcut_path.parent} does not exist") + logger.error(f'Parent directory {shortcut_path.parent} does not exist') return False else: - logger.info(f"Creating shortcut for {app_title} at {shortcut_path}") + logger.info(f'Creating shortcut for {app_title} at {shortcut_path}') - if platform.system() in {"Linux", "FreeBSD"}: + if platform.system() in {'Linux', 'FreeBSD'}: executable = get_rare_executable(external=True) executable = shlex.join(executable) if not for_rare: - executable = f"{executable} launch {app_name}" + executable = f'{executable} launch {app_name}' - with shortcut_path.open(mode="w") as desktop_file: + with shortcut_path.open(mode='w') as desktop_file: desktop_file.write( - "[Desktop Entry]\n" - f"Name={app_title}\n" - "Type=Application\n" - "Categories=Game;\n" - f"Icon={icon_path}\n" - f"Exec={executable}\n" - "Terminal=false\n" - "StartupWMClass=Rare\n" + '[Desktop Entry]\n' + f'Name={app_title}\n' + 'Type=Application\n' + 'Categories=Game;\n' + f'Icon={icon_path}\n' + f'Exec={executable}\n' + 'Terminal=false\n' + 'StartupWMClass=Rare\n' ) # shortcut_path.chmod(0o755) return True - elif platform.system() == "Windows": + elif platform.system() == 'Windows': # Add shortcut - shell = Dispatch("WScript.Shell") + shell = Dispatch('WScript.Shell') shortcut = shell.CreateShortCut(str(shortcut_path)) executable = get_rare_executable() @@ -370,7 +373,7 @@ def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "", executable = executable[0] if not for_rare: - arguments.extend(["launch", app_name]) + arguments.extend(['launch', app_name]) shortcut.Targetpath = executable # Maybe there is a better solution, but windows does not accept single quotes (Windows is weird) shortcut.Arguments = shlex.join(arguments).replace("'", '"') diff --git a/rare/utils/qt_requests.py b/rare/utils/qrequests.py similarity index 73% rename from rare/utils/qt_requests.py rename to rare/utils/qrequests.py index 74b6bb7700..279b0f568a 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qrequests.py @@ -1,7 +1,8 @@ +from collections.abc import Callable from dataclasses import dataclass, field from email.message import Message from logging import getLogger -from typing import Callable, Dict, List, Tuple, TypeVar, Union +from typing import TypeVar import orjson from PySide6.QtCore import QObject, QUrl, QUrlQuery, Signal, Slot @@ -12,42 +13,42 @@ QNetworkRequest, ) -user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" +user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' # user_agent = f'UELauncher/{version} Windows/10.0.19041.1.256.64bit' -RequestHandler = TypeVar("RequestHandler", bound=Callable[[Union[Dict, bytes]], None]) +RequestHandler = TypeVar('RequestHandler', bound=Callable[[dict | bytes], None]) @dataclass class RequestQueueItem: method: str = None url: QUrl = None - payload: Dict = field(default_factory=dict) - params: Dict = field(default_factory=dict) - handlers: List[RequestHandler] = field(default_factory=list) + payload: dict = field(default_factory=dict) + params: dict = field(default_factory=dict) + handlers: list[RequestHandler] = field(default_factory=list) def __eq__(self, other): return self.method == other.method and self.url == other.url -class QtRequests(QObject): +class QRequests(QObject): data_ready = Signal(object) def __init__(self, cache: str = None, token: str = None, parent=None): - super(QtRequests, self).__init__(parent=parent) - self.logger = getLogger(f"{type(self).__name__}_{type(parent).__name__}") + super(QRequests, self).__init__(parent=parent) + self.logger = getLogger(f'{type(self).__name__}_{type(parent).__name__}') self._manager = QNetworkAccessManager(self) self._manager.finished.connect(self.__on_finished) self._cache = None if cache is not None: - self.logger.debug("Using cache dir %s", cache) + self.logger.debug('Using cache dir %s', cache) self._cache = QNetworkDiskCache(self) self._cache.setCacheDirectory(cache) self._manager.setCache(self._cache) if token is not None: - self.logger.debug("Manager is authorized") + self.logger.debug('Manager is authorized') self._token = token - self.__active_requests: Dict[QNetworkReply, RequestQueueItem] = {} + self.__active_requests: dict[QNetworkReply, RequestQueueItem] = {} @staticmethod def __prepare_query(url, params) -> QUrl: @@ -62,7 +63,7 @@ def __prepare_request(self, item: RequestQueueItem) -> QNetworkRequest: request = QNetworkRequest(item.url) request.setHeader( QNetworkRequest.KnownHeaders.ContentTypeHeader, - "application/json; charset=UTF-8", + 'application/json; charset=UTF-8', ) request.setHeader(QNetworkRequest.KnownHeaders.UserAgentHeader, user_agent) request.setAttribute( @@ -75,7 +76,7 @@ def __prepare_request(self, item: RequestQueueItem) -> QNetworkRequest: QNetworkRequest.CacheLoadControl.PreferCache, ) if self._token is not None: - request.setRawHeader(b"Authorization", self._token.encode()) + request.setRawHeader(b'Authorization', self._token.encode()) return request def __post(self, item: RequestQueueItem): @@ -86,7 +87,7 @@ def __post(self, item: RequestQueueItem): self.__active_requests[reply] = item def post(self, url: str, handler: RequestHandler, payload: dict): - item = RequestQueueItem(method="post", url=QUrl(url), payload=payload, handlers=[handler]) + item = RequestQueueItem(method='post', url=QUrl(url), payload=payload, handlers=[handler]) self.__post(item) def __get(self, item: RequestQueueItem): @@ -99,37 +100,37 @@ def get( self, url: str, handler: RequestHandler, - payload: Dict = None, - params: Dict = None, + payload: dict = None, + params: dict = None, ): url = self.__prepare_query(url, params) if params is not None else QUrl(url) - item = RequestQueueItem(method="get", url=url, payload=payload, handlers=[handler]) + item = RequestQueueItem(method='get', url=url, payload=payload, handlers=[handler]) self.__get(item) def __on_error(self, error: QNetworkReply.NetworkError) -> None: self.logger.error(error) @staticmethod - def __parse_content_type(header) -> Tuple[str, str]: + def __parse_content_type(header) -> tuple[str, str]: # lk: this looks weird but `cgi` is deprecated, PEP 594 suggests this way of parsing MIME m = Message() - m["content-type"] = header + m['content-type'] = header return m.get_content_type(), m.get_content_charset() @Slot(QNetworkReply) def __on_finished(self, reply: QNetworkReply): item = self.__active_requests.pop(reply, None) if item is None: - self.logger.error("QNetworkReply: %s without associated item", reply.url().toString()) + self.logger.error('QNetworkReply: %s without associated item', reply.url().toString()) elif reply.error() != QNetworkReply.NetworkError.NoError: self.logger.error(reply.errorString()) else: mimetype, charset = self.__parse_content_type(reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader)) - maintype, subtype = mimetype.split("/") + maintype, subtype = mimetype.split('/') bin_data = reply.readAll().data() - if mimetype == "application/json": + if mimetype == 'application/json': data = orjson.loads(bin_data) - elif maintype == "image": + elif maintype == 'image': data = bin_data else: data = None diff --git a/rare/utils/singleton.py b/rare/utils/singleton.py index 6dad3c59c6..f1f09392cf 100644 --- a/rare/utils/singleton.py +++ b/rare/utils/singleton.py @@ -6,9 +6,9 @@ import sys import tempfile -logger = logging.getLogger("tendo.singleton") +logger = logging.getLogger('tendo.singleton') -if sys.platform != "win32": +if sys.platform != 'win32': import fcntl @@ -16,62 +16,65 @@ class SingleInstanceException(BaseException): pass -class SingleInstance(object): +class SingleInstance: """Class that can be instantiated only once per machine. - If you want to prevent your script from running in parallel just instantiate SingleInstance() class. If is there another instance already running it will throw a `SingleInstanceException`. + If you want to prevent your script from running in parallel just instantiate SingleInstance() class. If is there + another instance already running it will throw a `SingleInstanceException`. This option is very useful if you have scripts executed by crontab at small amounts of time. Remember that this works by creating a lock file with a filename based on the full path to the script file. - Providing a flavor_id will augment the filename with the provided flavor_id, allowing you to create multiple singleton instances from the same file. This is particularly useful if you want specific functions to have their own singleton instances. + Providing a flavor_id will augment the filename with the provided flavor_id, allowing you to create multiple + singleton instances from the same file. This is particularly useful if you want specific functions to have their + own singleton instances. """ - def __init__(self, flavor_id="", lockfile=""): + def __init__(self, flavor_id='', lockfile=''): self.initialized = False if lockfile: self.lockfile = lockfile else: basename = ( - os.path.splitext(os.path.abspath(sys.argv[0]))[0].replace("/", "-").replace(":", "").replace("\\", "-") - + "-%s" % flavor_id - + ".lock" + os.path.splitext(os.path.abspath(sys.argv[0]))[0].replace('/', '-').replace(':', '').replace('\\', '-') + + f'-{flavor_id}' + + '.lock' ) - self.lockfile = os.path.normpath(f"{tempfile.gettempdir()}/{basename}") + self.lockfile = os.path.normpath(f'{tempfile.gettempdir()}/{basename}') - logger.debug(f"SingleInstance lockfile: {self.lockfile}") - if sys.platform == "win32": + logger.debug(f'SingleInstance lockfile: {self.lockfile}') + if sys.platform == 'win32': try: # file already exists, we try to remove (in case previous # execution was interrupted) if os.path.exists(self.lockfile): os.unlink(self.lockfile) self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) - except OSError: + except OSError as exc: type, e, tb = sys.exc_info() if e.errno == 13: - logger.error("Another instance is already running, quitting.") - raise SingleInstanceException() + logger.error('Another instance is already running, quitting.') + raise SingleInstanceException() from exc print(e.errno) raise else: # non Windows - self.fp = open(self.lockfile, "w") + self.fp = open(self.lockfile, 'w') # noqa: SIM115 self.fp.flush() try: fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: - logger.warning("Another instance is already running, quitting.") - raise SingleInstanceException() + except OSError as exc: + logger.warning('Another instance is already running, quitting.') + raise SingleInstanceException() from exc self.initialized = True def __del__(self): if not self.initialized: return try: - if sys.platform == "win32": - if hasattr(self, "fd"): + if sys.platform == 'win32': + if hasattr(self, 'fd'): os.close(self.fd) os.unlink(self.lockfile) else: @@ -83,5 +86,5 @@ def __del__(self): if logger: logger.warning(e) else: - print("Unloggable error: %s" % e) + print(f'Unloggable error: {e}', file=sys.stderr) sys.exit(-1) diff --git a/rare/utils/slot_adapters.py b/rare/utils/slot_adapters.py index 3e97ca2b9c..78c2509111 100644 --- a/rare/utils/slot_adapters.py +++ b/rare/utils/slot_adapters.py @@ -1,5 +1,5 @@ import types -from typing import Callable +from collections.abc import Callable from PySide6 import QtCore, QtGui, QtWidgets diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 46605f3115..01449913a8 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -1,31 +1,27 @@ import difflib import lzma -import os from datetime import datetime from enum import Enum from logging import getLogger -from typing import Dict, Tuple import orjson import requests from rare.lgndr.core import LegendaryCore -from rare.utils.paths import cache_dir - -logger = getLogger("SteamGrades") +from rare.utils.paths import data_dir class ProtondbRatings(int, Enum): # internal - PENDING = ("pending", -2) - FAIL = ("fail", -1) + PENDING = ('pending', -2) + FAIL = ('fail', -1) # protondb - NA = ("na", 0) - BORKED = ("borked", 1) - BRONZE = ("bronze", 2) - SILVER = ("silver", 3) - GOLD = ("gold", 4) - PLATINUM = ("platinum", 5) + NA = ('na', 0) + BORKED = ('borked', 1) + BRONZE = ('bronze', 2) + SILVER = ('silver', 3) + GOLD = ('gold', 4) + PLATINUM = ('platinum', 5) def __new__(cls, name: str, value: int): obj = int.__new__(cls, value) @@ -41,68 +37,69 @@ def __int__(self): class SteamGrades: - __steam_appids: Dict[str, str] = {} - __steam_appids_version: int = 3 - __active_download: bool = False - __replace_chars = ",;.:-_ " - __steamids_url = "https://raredevs.github.io/wring/steam_appids.json.xz" - __protondb_url = "https://www.protondb.com/api/v1/reports/summaries/" + _steam_appids_version: int = 3 + _replace_chars = ',;.:-_ ' + _steamids_url = 'https://raredevs.github.io/wring/steam_appids.json.xz' + _protondb_url = 'https://www.protondb.com/api/v1/reports/summaries/' def __init__(self): - pass + self.logger = getLogger(type(self).__name__) + self._steam_appids: dict[str, str] = {} + self._active_download: bool = False def _download_steam_appids(self) -> bytes: - if SteamGrades.__active_download: - return b"" - SteamGrades.__active_download = True - resp = requests.get(self.__steamids_url) - SteamGrades.__active_download = False + if self._active_download: + return b'' + self._active_download = True + resp = requests.get(self._steamids_url) + self._active_download = False return resp.content - def load_steam_appids(self) -> Dict: - if SteamGrades.__steam_appids: - return SteamGrades.__steam_appids + def load_steam_appids(self) -> dict: + if self._steam_appids: + return self._steam_appids - file = os.path.join(cache_dir(), "steam_appids.json") - version = SteamGrades.__steam_appids_version + file = data_dir().joinpath('steam_appids.json') + version = self._steam_appids_version elapsed_days = 0 - if os.path.exists(file): - mod_time = datetime.fromtimestamp(os.path.getmtime(file)) + if file.is_file(): + mod_time = datetime.fromtimestamp(file.stat().st_mtime) elapsed_days = abs(datetime.now() - mod_time).days - json = orjson.loads(open(file, "r").read()) - version = json.get("version", 0) - if version >= SteamGrades.__steam_appids_version: - SteamGrades.__steam_appids = json["games"] + with file.open('r') as fd: + json = orjson.loads(fd.read()) + version = json.get('version', 0) + if version >= self._steam_appids_version: + self._steam_appids = json['games'] - if not os.path.exists(file) or elapsed_days > 3 or version < SteamGrades.__steam_appids_version: + if not file.is_file() or elapsed_days > 3 or version < self._steam_appids_version: if content := self._download_steam_appids(): - text = lzma.decompress(content).decode("utf-8") - with open(file, "w", encoding="utf-8") as f: - f.write(text) - json = orjson.loads(text) - SteamGrades.__steam_appids = json["games"] + data = lzma.decompress(content).decode('utf-8') + with file.open('w', encoding='utf-8') as fd: + fd.write(data) + json = orjson.loads(data) + self._steam_appids = json['games'] - return SteamGrades.__steam_appids + return self._steam_appids @property - def steam_appids(self) -> Dict[str, str]: - if not SteamGrades.__steam_appids: - SteamGrades.__steam_appids = self.load_steam_appids() - return SteamGrades.__steam_appids + def steam_appids(self) -> dict[str, str]: + if not self._steam_appids: + self.load_steam_appids() + return self._steam_appids @property - def steam_titles(self) -> Dict: + def steam_titles(self) -> dict: return {v: k for k, v in self.steam_appids.items()} def _get_steam_appid(self, title: str) -> str: # workarounds for satisfactory # FIXME: This has to be made smarter. - title = title.replace("Early Access", "").replace("Experimental", "").strip() + title = title.replace('Early Access', '').replace('Experimental', '').strip() # title = title.split(":")[0] # title = title.split("-")[0] - if title in self.steam_titles.keys(): + if title in self.steam_titles: steam_name = [title] else: steam_name = difflib.get_close_matches(title, self.steam_appids.keys(), n=1, cutoff=0.5) @@ -110,23 +107,23 @@ def _get_steam_appid(self, title: str) -> str: if steam_name: return self.steam_appids[steam_name[0]] else: - return "0" + return '0' def _get_grade(self, steam_appid: str): - if steam_appid == "0": - return "fail" + if steam_appid == '0': + return 'fail' steam_appid = str(steam_appid) - res = requests.get(f"{self.__protondb_url}/{steam_appid}.json") + res = requests.get(f'{self._protondb_url}/{steam_appid}.json') try: app = orjson.loads(res.text) except orjson.JSONDecodeError as e: - logger.error(repr(e)) - logger.error("Failed to get ProtonDB response for %s", steam_appid) - return "fail" + self.logger.error(repr(e)) + self.logger.error('Failed to get ProtonDB response for %s', steam_appid) + return 'fail' - return app.get("tier", "fail") + return app.get('tier', 'fail') - def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: str = None) -> Tuple[str, str]: + def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: str = None) -> tuple[str, str]: game = core.get_game(app_name) try: if steam_appid is None: @@ -135,8 +132,13 @@ def get_rating(self, core: LegendaryCore, app_name: str, steam_appid: str = None raise RuntimeError grade = self._get_grade(steam_appid) except Exception as e: - logger.error(repr(e)) - logger.error("Failed to get ProtonDB rating for %s", game.app_title) - return "0", "fail" + self.logger.error(repr(e)) + self.logger.error('Failed to get ProtonDB rating for %s', game.app_title) + return '0', 'fail' else: return steam_appid, grade + + +steam_grades = SteamGrades() + +__all__ = ['steam_grades'] diff --git a/rare/utils/steam_shortcuts.py b/rare/utils/steam_shortcuts.py index 4510d37ee1..0555f7abb6 100644 --- a/rare/utils/steam_shortcuts.py +++ b/rare/utils/steam_shortcuts.py @@ -4,7 +4,6 @@ from dataclasses import asdict from logging import getLogger from pathlib import Path -from typing import Dict, List, Optional import vdf @@ -17,68 +16,68 @@ image_wide_path, ) -if platform.system() == "Windows": +if platform.system() == 'Windows': # noinspection PyUnresolvedReferences import winreg # pylint: disable=E0401 -logger = getLogger("SteamShortcuts") +logger = getLogger('SteamShortcuts') -steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] +steam_client_install_paths = [os.path.expanduser('~/.local/share/Steam')] -def find_steam() -> Optional[str]: - if platform.system() == "Windows": +def find_steam() -> str | None: + if platform.system() == 'Windows': # Find the Steam install directory or raise an error try: # 32-bit - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Valve\\Steams") + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Valve\\Steams') except FileNotFoundError: try: # 64-bit - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Wow6432Node\\Valve\\Steam") + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Wow6432Node\\Valve\\Steam') except FileNotFoundError: return None - return winreg.QueryValueEx(key, "InstallPath")[0] + return winreg.QueryValueEx(key, 'InstallPath')[0] # return the first valid path - elif platform.system() in {"Linux", "FreeBSD"}: + elif platform.system() in {'Linux', 'FreeBSD'}: for path in steam_client_install_paths: - if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")): + if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'steam.sh')): return path return None -def find_steam_users(steam_path: str) -> List[SteamUser]: +def find_steam_users(steam_path: str) -> list[SteamUser]: _users = [] - vdf_path = os.path.join(steam_path, "config", "loginusers.vdf") + vdf_path = os.path.join(steam_path, 'config', 'loginusers.vdf') if not os.path.exists(vdf_path): return _users - with open(vdf_path, "r", encoding="utf-8") as f: - users = vdf.load(f).get("users", {}) + with open(vdf_path, encoding='utf-8') as f: + users = vdf.load(f).get('users', {}) for long_id, user in users.items(): _users.append(SteamUser(long_id, user)) return _users -def _load_shortcuts(steam_path: str, user: SteamUser) -> Dict[str, SteamShortcut]: +def _load_shortcuts(steam_path: str, user: SteamUser) -> dict[str, SteamShortcut]: _shortcuts = {} - vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf") + vdf_path = os.path.join(steam_path, 'userdata', str(user.short_id), 'config', 'shortcuts.vdf') if not os.path.exists(vdf_path): return _shortcuts - with open(vdf_path, "rb") as f: - shortcuts = vdf.binary_load(f).get("shortcuts", {}) + with open(vdf_path, 'rb') as f: + shortcuts = vdf.binary_load(f).get('shortcuts', {}) for idx, shortcut in shortcuts.items(): _shortcuts[idx] = SteamShortcut.from_dict(shortcut) return _shortcuts -def _save_shortcuts(steam_path: str, user: SteamUser, shortcuts: Dict[str, SteamShortcut]) -> None: +def _save_shortcuts(steam_path: str, user: SteamUser, shortcuts: dict[str, SteamShortcut]) -> None: _shortcuts = {k: asdict(v) for k, v in shortcuts.items()} - vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf") - with open(vdf_path, "wb") as f: - vdf.binary_dump({"shortcuts": _shortcuts}, f) + vdf_path = os.path.join(steam_path, 'userdata', str(user.short_id), 'config', 'shortcuts.vdf') + with open(vdf_path, 'wb') as f: + vdf.binary_dump({'shortcuts': _shortcuts}, f) -__steam_dir: Optional[str] = None -__steam_user: Optional[SteamUser] = None -__steam_shortcuts: Optional[Dict] = None +__steam_dir: str | None = None +__steam_user: SteamUser | None = None +__steam_shortcuts: dict | None = None def steam_shortcuts_supported() -> bool: @@ -93,13 +92,13 @@ def load_steam_shortcuts(): steam_dir = find_steam() if not steam_dir: - logger.error("Failed to find Steam install directory") + logger.error('Failed to find Steam install directory') return __steam_dir = steam_dir steam_users = find_steam_users(steam_dir) if not steam_users: - logger.error("Failed to find any Steam users") + logger.error('Failed to find any Steam users') return else: steam_user = next( @@ -107,7 +106,7 @@ def load_steam_shortcuts(): sorted(steam_users, key=lambda x: x.last_login, reverse=True)[0], ) logger.info( - "Found most recently logged-in user %s(%s) (%s)", + 'Found most recently logged-in user %s(%s) (%s)', steam_user.account_name, steam_user.persona_name, steam_user.last_login, @@ -118,9 +117,11 @@ def load_steam_shortcuts(): def save_steam_shortcuts(): + if not (__steam_dir and __steam_user): + return logger.info( - "%s Steam shortcuts for user %s(%s)", - "Saving" if __steam_shortcuts else "Removing", + '%s Steam shortcuts for user %s(%s)', + 'Saving' if __steam_shortcuts else 'Removing', __steam_user.account_name, __steam_user.persona_name, ) @@ -131,7 +132,7 @@ def steam_shortcut_exists(app_name: str) -> bool: return SteamShortcut.calculate_appid(app_name) in {s.appid for s in __steam_shortcuts.values()} -def remove_steam_shortcut(app_name: str) -> Optional[SteamShortcut]: +def remove_steam_shortcut(app_name: str) -> SteamShortcut | None: global __steam_shortcuts if not steam_shortcut_exists(app_name): @@ -149,29 +150,31 @@ def add_steam_shortcut(app_name: str, app_title: str) -> SteamShortcut: global __steam_shortcuts if steam_shortcut_exists(app_name): - logger.info("Removing old Steam shortcut for %s", app_name) + logger.info('Removing old Steam shortcut for %s', app_name) remove_steam_shortcut(app_name) command = get_rare_executable(external=True) - arguments = ["launch", app_name] + arguments = ['launch', app_name] if len(command) > 1: arguments = command[1:] + arguments shortcut = SteamShortcut.create( app_name=app_name, - app_title=f"{app_title} (Rare)", + app_title=f'{app_title} (Rare)', executable=command[0], start_dir=os.path.dirname(command[0]), icon=desktop_icon_path(app_name).as_posix(), launch_options=arguments, ) - key = int(max(__steam_shortcuts.keys(), default="0")) + key = int(max(__steam_shortcuts.keys(), default='0')) __steam_shortcuts[str(key + 1)] = shortcut return shortcut def add_steam_coverart(app_name: str, shortcut: SteamShortcut): - steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid") + if not __steam_dir: + return + steam_grid_dir = os.path.join(__steam_dir, 'userdata', str(__steam_user.short_id), 'config', 'grid') if not os.path.exists(steam_grid_dir): os.mkdir(steam_grid_dir) shutil.copy(image_wide_path(app_name), os.path.join(steam_grid_dir, shortcut.game_hero)) @@ -181,9 +184,11 @@ def add_steam_coverart(app_name: str, shortcut: SteamShortcut): def remove_steam_coverart(shortcut: SteamShortcut): - steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid") + if not __steam_dir: + return + steam_grid_dir = os.path.join(__steam_dir, 'userdata', str(__steam_user.short_id), 'config', 'grid') if not os.path.exists(steam_grid_dir): - logger.warning("Path does not exist %s", steam_grid_dir) + logger.warning('Path does not exist %s', steam_grid_dir) return Path(steam_grid_dir).joinpath(shortcut.game_hero).unlink(missing_ok=True) Path(steam_grid_dir).joinpath(shortcut.game_logo).unlink(missing_ok=True) @@ -191,7 +196,7 @@ def remove_steam_coverart(shortcut: SteamShortcut): Path(steam_grid_dir).joinpath(shortcut.grid_tall).unlink(missing_ok=True) -if __name__ == "__main__": +if __name__ == '__main__': load_steam_shortcuts() print(__steam_dir) @@ -206,11 +211,11 @@ def print_shortcuts(): print_shortcuts() - add_steam_shortcut("test1", "Test1") - add_steam_shortcut("test2", "Test2") - add_steam_shortcut("test3", "Test3") - add_steam_shortcut("test1", "Test1") + add_steam_shortcut('test1', 'Test1') + add_steam_shortcut('test2', 'Test2') + add_steam_shortcut('test3', 'Test3') + add_steam_shortcut('test1', 'Test1') - remove_steam_shortcut("test2") + remove_steam_shortcut('test2') print_shortcuts() diff --git a/rare/utils/workarounds.py b/rare/utils/workarounds.py index 3eaa75cf08..ee3dec7f79 100644 --- a/rare/utils/workarounds.py +++ b/rare/utils/workarounds.py @@ -1,175 +1,102 @@ import platform -from typing import Dict, Set, Union +from logging import getLogger +import requests +from orjson import orjson from PySide6.QtWidgets import QApplication from rare.utils import config_helper as config +from rare.utils.paths import data_dir -__os_compat: Set = {"Linux", "Darwin", "FreeBSD"} -__os_native: Set = {"Windows"} -__os_all: Set = {*__os_compat, *__os_native} - - -def __screen_height() -> int: - return QApplication.instance().primaryScreen().geometry().height() - - -def __screen_width() -> int: - return QApplication.instance().primaryScreen().geometry().width() - - -# Keeps a dictionary of workarounds. -# Can use the following placeholders: res_width, res_height -__workarounds: Dict[str, Dict[str, Dict[str, Dict[str, Union[str, Set]]]]] = { - # XCOM2 - "3be3c4d681bc46b3b8b26c5df3ae0a18": { - "options": { - "override_exe": { - "value": "Binaries/Win64/XCom2.exe", - "os": __os_all, - }, - }, - }, - # Civilization VI - "Kinglet": { - "options": { - "override_exe": { - "value": "Base/Binaries/Win64EOS/CivilizationVI.exe", - "os": __os_all, - }, - }, - }, - # Bioshock 2 Remastered - "b22ce34b4ce0408c97a888554447479b": { - "options": { - "override_exe": { - "value": "Build/FinalEpic/Bioshock2HD.exe", - "os": __os_all, - }, - }, - }, - # Bioshock 1 Remastered - "bc2c95c6ff564a16b26644f1d3ac3c55": { - "options": { - "override_exe": { - "value": "Build/FinalEpic/BioshockHD.exe", - "os": __os_all, - }, - }, - }, - # Eternal Threads - "ff1d9bf6b1304cb9a12b8754afa78ae5": { - "options": { - "override_exe": { - "value": "EternalThreadsBuild/EternalThreads.exe", - "os": __os_compat, - }, - }, - }, - # Celeste - "Salt": { - "options": { - "start_params": { - "value": "/gldevice:OpenGL", - "os": __os_compat, - }, - }, - }, - # Borderlands: The Pre Sequel - "Turkey": { - "options": { - "start_params": { - "value": "-NoLauncher", - "os": __os_compat, - }, - }, - }, - # Borderlands 2 - "Dodo": { - "options": { - "start_params": { - "value": "-NoLauncher", - "os": __os_compat, - }, - # "override_exe": { "value": "Binaries/Win32/Borderlands2.exe", "os": __os_compat, }, - }, - }, - # Tiny Tina's Assault on Dragon Keep: A Wonderlands One shot Adventure - "9e296d276ad447108f12c654c3341d59": { - "options": { - "start_params": { - "value": "-NoLauncher", - "os": __os_compat, - }, - }, - }, - # Brothers: A Tale of Two Sons - "Tamarind": { - "options": { - "start_params": { - # value set at runtime - "value": "ResX={res_width} ResY={res_height} -nomovies -nosplash", - "os": __os_compat, - }, - "override_exe": { - "value": "Binaries/Win32/Brothers.exe", - "os": __os_compat, - }, - }, - }, - # Borderlands: The Pre Sequel - "9c203b6ed35846e8a4a9ff1e314f6593": { - "options": { - "start_params": { - "value": "/autorun /ed /autoquit", - "os": __os_compat, - }, - }, - }, - # F1® Manager 2024 - "03c9fe3b2869452ba8433ee7708a3e93": { - "options": { - "override_exe": { - "value": "F1Manager24/Binaries/Win64/F1Manage", - "os": __os_all, - }, - }, - }, - # Cities Skylines - "bcbc03d8812a44c18f41cf7d5f849265": { - "options": { - "override_exe": { - "value": "Cities.exe", - "os": __os_all, - }, - }, - }, -} - - -def __subst(text: str) -> str: - return text.format( - res_width=__screen_width(), - res_height=__screen_height(), - ) + +class Workarounds: + _workarounds_url = 'https://raredevs.github.io/wring/workarounds.json' + _workarounds_version_url = 'https://raredevs.github.io/wring/workarounds_version.json' + + def __init__(self): + self.logger = getLogger(type(self).__name__) + self._workarounds: dict[str, dict[str, dict[str, dict[str, str | tuple]]]] = {} + self._active_download: bool = False + + def _download_workarounds(self) -> bytes: + if self._active_download: + return b'' + self._active_download = True + resp = requests.get(self._workarounds_url) + self._active_download = False + return resp.content + + def load_workarounds(self) -> dict[str, dict[str, dict[str, dict[str, str | tuple]]]]: + if self._workarounds: + return self._workarounds + + try: + resp = requests.get(self._workarounds_version_url, timeout=1) + data = resp.content.decode('utf-8') + remote_version = orjson.loads(data).get('version', 1) + except requests.exceptions.Timeout: + remote_version = 1 + + file = data_dir().joinpath('workarounds.json') + + if file.is_file(): + json = orjson.loads(file.open('r').read()) + version = json.get('version', 1) + if version >= remote_version: + self._workarounds = json.get('workarounds', {}) + else: + version = 0 + + if not file.is_file() or version < remote_version: + if content := self._download_workarounds(): + data = content.decode('utf-8') + with file.open('w', encoding='utf-8') as fd: + fd.write(data) + json = orjson.loads(data) + self._workarounds = json.get('workarounds', {}) + + return self._workarounds + + def get(self, app_name: str) -> dict: + if not self._workarounds: + self.load_workarounds() + return self._workarounds.get(app_name, {}) + + @staticmethod + def screen_height() -> int: + return QApplication.instance().primaryScreen().geometry().height() + + @staticmethod + def screen_width() -> int: + return QApplication.instance().primaryScreen().geometry().width() + + @staticmethod + def subst(text: str) -> str: + return text.format( + res_width=Workarounds.screen_width(), + res_height=Workarounds.screen_height(), + ) + + +workarounds = Workarounds() def apply_workarounds(app_name: str): - if workaround := __workarounds.get(app_name): + if wa := workarounds.get(app_name): # apply options - for opt in (options := workaround.get("options", {})): + for opt in (options := wa.get('options', {})): if config.get_option(app_name, opt, None) is not None: continue - if platform.system() not in options[opt].get("os", set()): + if platform.system() not in options[opt].get('os', tuple()): continue - config.set_option(app_name, opt, __subst(options[opt]["value"])) + config.set_option(app_name, opt, Workarounds.subst(options[opt]['value'])) # apply environment - for var in (environ := workaround.get("environ", {})): + for var in (environ := wa.get('environ', {})): if config.get_envvar(app_name, var, None) is not None: continue - if platform.system() not in environ[var].get("os", set()): + if platform.system() not in environ[var].get('os', tuple()): continue - config.set_envvar(app_name, var, __subst(environ[var]["value"])) + config.set_envvar(app_name, var, Workarounds.subst(environ[var]['value'])) -__all__ = ["apply_workarounds"] +__all__ = ['apply_workarounds', 'workarounds'] diff --git a/rare/utils/wrapper_exe.py b/rare/utils/wrapper_exe.py new file mode 100644 index 0000000000..7bab3b24b8 --- /dev/null +++ b/rare/utils/wrapper_exe.py @@ -0,0 +1,74 @@ +from datetime import datetime + +import requests +from orjson import orjson + +from rare.utils import config_helper as config +from rare.utils.paths import data_dir, runtime_assets_json + + +def version_tuple(version: str) -> tuple: + return tuple(version.lstrip('v').split('.')) + + +_github_api_url = 'https://api.github.com/repos/Etaash-mathamsetty/heroic-epic-integration/releases/latest' + + +def download_wrapper_exe(): + wrapper_exe = data_dir().joinpath('EpicGamesLauncher.exe') + + if not wrapper_exe.exists(): + config.remove_envvar('default', 'LEGENDARY_WRAPPER_EXE') + + runtime_assets = { + wrapper_exe.name: { + 'version': 'v0.0', + 'date': datetime.isoformat(datetime.min), + } + } + version = runtime_assets[wrapper_exe.name]['version'] + + if runtime_assets_json().exists(): + runtime_assets = orjson.loads(runtime_assets_json().open('r').read()) + version = runtime_assets.get(wrapper_exe.name, {}).get('version', 'v0.0') + + try: + resp = requests.get(_github_api_url, timeout=5) + data = resp.content.decode('utf-8') + latest_release = orjson.loads(data) + except requests.exceptions.Timeout: + return + + remote_assets = latest_release['assets'] + remote_version = latest_release['tag_name'] + + if version_tuple(version) >= version_tuple(remote_version): + if wrapper_exe.exists() and config.get_envvar('default', 'LEGENDARY_WRAPPER_EXE', '') == str(wrapper_exe): + return + + if wrapper_exe.exists(): + config.set_envvar('default', 'LEGENDARY_WRAPPER_EXE', str(wrapper_exe)) + return + + download_url = remote_assets[0]['browser_download_url'] + try: + resp = requests.get(download_url, timeout=5) + wrapper_exe.write_bytes(resp.content) + config.set_envvar('default', 'LEGENDARY_WRAPPER_EXE', str(wrapper_exe)) + except requests.exceptions.Timeout: + return + + runtime_assets[wrapper_exe.name] = { + 'version': remote_version, + 'date': datetime.isoformat(datetime.fromisoformat(remote_assets[0]['created_at'].replace('Z', '+00:00'))), + } + + runtime_assets_json().write_text(orjson.dumps(runtime_assets).decode('utf-8'), encoding='utf-8') + return + + +if __name__ == '__main__': + download_wrapper_exe() + + +__all__ = ['download_wrapper_exe'] diff --git a/rare/widgets/button_edit.py b/rare/widgets/button_edit.py index 505aff6ece..019055019a 100644 --- a/rare/widgets/button_edit.py +++ b/rare/widgets/button_edit.py @@ -12,7 +12,7 @@ def __init__(self, icon_name, placeholder_text: str, parent=None): self.setObjectName(type(self).__name__) self.button = QPushButton(self) - self.button.setObjectName(f"{type(self).__name__}Button") + self.button.setObjectName(f'{type(self).__name__}Button') self.button.setIcon(qta_icon(icon_name)) self.button.setCursor(Qt.CursorShape.ArrowCursor) self.button.clicked.connect(self.buttonClicked) diff --git a/rare/widgets/collapsible_widget.py b/rare/widgets/collapsible_widget.py index 0546a0216a..e1ed8095ec 100644 --- a/rare/widgets/collapsible_widget.py +++ b/rare/widgets/collapsible_widget.py @@ -1,5 +1,4 @@ from abc import abstractmethod -from typing import Optional from PySide6.QtCore import ( QAbstractAnimation, @@ -22,7 +21,7 @@ from rare.utils.misc import qta_icon -class CollapsibleBase(object): +class CollapsibleBase: """ References: # Adapted from c++ version @@ -39,13 +38,13 @@ def __init__(self, parent=None): def setup(self, animation_duration: int = 200): self.animation_duration = animation_duration - self.content_area: Optional[QWidget] = None - self.content_toggle_animation: Optional[QPropertyAnimation] = None + self.content_area: QWidget | None = None + self.content_toggle_animation: QPropertyAnimation | None = None # let the entire widget grow and shrink with its content self.toggle_animation = QParallelAnimationGroup(self) - self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight")) - self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight")) + self.toggle_animation.addAnimation(QPropertyAnimation(self, b'minimumHeight')) + self.toggle_animation.addAnimation(QPropertyAnimation(self, b'maximumHeight')) @abstractmethod def isChecked(self) -> bool: @@ -89,7 +88,7 @@ def setWidget(self, widget: QWidget): self.content_area.setMaximumHeight(0) self.content_area.setMinimumHeight(0) - self.content_toggle_animation = QPropertyAnimation(self.content_area, b"maximumHeight") + self.content_toggle_animation = QPropertyAnimation(self.content_area, b'maximumHeight') self.toggle_animation.addAnimation(self.content_toggle_animation) self.addToLayout(self.content_area) collapsed_height = self.sizeHint().height() @@ -115,7 +114,7 @@ def __init__(self, animation_duration: int = 200, parent=None): self.toggle_button = QToolButton(self) self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) - self.toggle_button.setIcon(qta_icon("fa.arrow-right", "fa5s.arrow-right")) + self.toggle_button.setIcon(qta_icon('fa.arrow-right', 'fa5s.arrow-right')) self.toggle_button.setCheckable(True) self.toggle_button.setChecked(False) @@ -157,7 +156,7 @@ def sizeHint(self) -> QSize: return super(CollapsibleFrame, self).sizeHint() def animationStart(self, checked): - arrow_type = qta_icon("fa.arrow-down", "fa5s.arrow-down") if checked else qta_icon("fa.arrow-right", "fa5s.arrow-right") + arrow_type = qta_icon('fa.arrow-down', 'fa5s.arrow-down') if checked else qta_icon('fa.arrow-right', 'fa5s.arrow-right') self.toggle_button.setIcon(arrow_type) super(CollapsibleFrame, self).animationStart(checked) diff --git a/rare/widgets/dialogs.py b/rare/widgets/dialogs.py index dc5643bb84..4bca2b4df3 100644 --- a/rare/widgets/dialogs.py +++ b/rare/widgets/dialogs.py @@ -26,7 +26,7 @@ def game_title(text: str, app_title: str) -> str: def dialog_title(text: str) -> str: - return f"{text} - {QCoreApplication.instance().applicationName()}" + return f'{text} - {QCoreApplication.instance().applicationName()}' class BaseDialog(QDialog): @@ -82,8 +82,8 @@ def __init__(self, parent=None): self.subtitle_label.setVisible(False) self.reject_button = QPushButton(self) - self.reject_button.setText(self.tr("Cancel")) - self.reject_button.setIcon(qta_icon("fa.remove", "fa5s.times")) + self.reject_button.setText(self.tr('Cancel')) + self.reject_button.setIcon(qta_icon('fa.remove', 'fa5s.times')) self.reject_button.setAutoDefault(False) self.reject_button.clicked.connect(self.reject) @@ -118,7 +118,7 @@ def close(self): raise RuntimeError(f"Don't use `close()` with {type(self).__name__}") def setSubtitle(self, text: str): - self.subtitle_label.setText(f"{text}") + self.subtitle_label.setText(f'{text}') self.subtitle_label.setVisible(True) def setCentralWidget(self, widget: QWidget): @@ -226,16 +226,16 @@ def closeEvent(self, a0: QCloseEvent) -> None: super(BaseDialog, self).closeEvent(a0) -__all__ = ["dialog_title", "game_title", "BaseDialog", "ButtonDialog", "ActionDialog"] +__all__ = ['dialog_title', 'game_title', 'BaseDialog', 'ButtonDialog', 'ActionDialog'] class TestDialog(BaseDialog): def __init__(self, parent=None): super(TestDialog, self).__init__(parent=parent) - self.accept_button = QPushButton("accept", self) - self.reject_button = QPushButton("reject", self) - self.action_button = QPushButton("action", self) + self.accept_button = QPushButton('accept', self) + self.reject_button = QPushButton('reject', self) + self.action_button = QPushButton('action', self) self.button_box = QDialogButtonBox(Qt.Orientation.Horizontal, self) self.button_box.addButton(self.accept_button, QDialogButtonBox.ButtonRole.AcceptRole) self.button_box.addButton(self.reject_button, QDialogButtonBox.ButtonRole.RejectRole) @@ -253,13 +253,13 @@ def setWindowTitle(self, a0): super().setWindowTitle(dialog_title(a0)) def close(self): - print("in close") + print('in close') super().close() def closeEvent(self, a0: QCloseEvent) -> None: - print("in closeEvent") + print('in closeEvent') if a0.spontaneous(): - print("is spontaneous") + print('is spontaneous') a0.ignore() return if self.reject_close: @@ -270,31 +270,31 @@ def closeEvent(self, a0: QCloseEvent) -> None: # super().closeEvent(a0) def done(self, a0): - print(f"in done {a0}") + print(f'in done {a0}') return super().done(a0) def accept(self): - print("in accept") + print('in accept') self._on_accept() # return # super().accept() def reject(self): - print("in reject") + print('in reject') self._on_reject() # return # super().reject() def _on_close(self): - print("in _on_close") + print('in _on_close') def _on_accept(self): - print("in _on_accepted") + print('in _on_accepted') # self.close() def _on_reject(self): - print("in _on_rejected") + print('in _on_rejected') self.close() def keyPressEvent(self, a0: QKeyEvent) -> None: @@ -309,5 +309,5 @@ def test_dialog(): sys.exit(ret) -if __name__ == "__main__": +if __name__ == '__main__': test_dialog() diff --git a/rare/widgets/elide_label.py b/rare/widgets/elide_label.py index d5d4d96007..63e0fa025f 100644 --- a/rare/widgets/elide_label.py +++ b/rare/widgets/elide_label.py @@ -1,16 +1,14 @@ -from typing import Union - from PySide6.QtCore import Qt from PySide6.QtGui import QFontMetrics, QResizeEvent from PySide6.QtWidgets import QLabel class ElideLabel(QLabel): - def __init__(self, text="", parent=None): + def __init__(self, text='', parent=None): super(ElideLabel, self).__init__(parent=parent) self.__text = text self.__fm = QFontMetrics(self.font()) - self.__tooltip = "" + self.__tooltip = '' self.setFixedHeight(True) self.setWordWrap(True) self.setText(text) @@ -29,7 +27,7 @@ def __setElideText(self, a0: str): if self.__fm.boundingRect(elided_text).width() < self.__fm.boundingRect(self.__text).width(): super(ElideLabel, self).setToolTip(self.__text) else: - super(ElideLabel, self).setToolTip("") + super(ElideLabel, self).setToolTip('') super(ElideLabel, self).setText(elided_text) def setToolTip(self, a0: str) -> None: @@ -40,7 +38,7 @@ def resizeEvent(self, a0: QResizeEvent) -> None: self.__setElideText(self.__text) super(ElideLabel, self).resizeEvent(a0) - def setFixedHeight(self, h: Union[int, bool]) -> None: + def setFixedHeight(self, h: int | bool) -> None: if isinstance(h, bool): # FIXME: figure out 'else' case super(ElideLabel, self).setFixedHeight(self.__fm.height() if h else 16777215) diff --git a/rare/widgets/flow_layout.py b/rare/widgets/flow_layout.py index b4777bacd4..d9db452702 100644 --- a/rare/widgets/flow_layout.py +++ b/rare/widgets/flow_layout.py @@ -1,4 +1,4 @@ -from typing import List, Optional, overload +from typing import overload from PySide6.QtCore import QPoint, QRect, QSize, Qt from PySide6.QtWidgets import QLayout, QLayoutItem, QSizePolicy, QStyle, QWidget @@ -10,7 +10,7 @@ def __init__(self, parent=None): self.setObjectName(type(self).__name__) self._hspacing = -1 self._vspacing = -1 - self._items: List[QLayoutItem] = [] + self._items: list[QLayoutItem] = [] def __del__(self): del self._items[:] @@ -70,12 +70,12 @@ def verticalSpacing(self): def count(self) -> int: return len(self._items) - def itemAt(self, index: int) -> Optional[QLayoutItem]: + def itemAt(self, index: int) -> QLayoutItem | None: if 0 <= index < len(self._items): return self._items[index] return None - def takeAt(self, index: int) -> Optional[QLayoutItem]: + def takeAt(self, index: int) -> QLayoutItem | None: if 0 <= index < len(self._items): item = self._items.pop(index) self.invalidate() diff --git a/rare/widgets/image_widget.py b/rare/widgets/image_widget.py index 74d14b52c4..5e23f824db 100644 --- a/rare/widgets/image_widget.py +++ b/rare/widgets/image_widget.py @@ -1,5 +1,5 @@ +from contextlib import suppress from enum import Enum -from typing import Dict, Optional, Tuple, Union from PySide6.QtCore import QRectF, QSize, Qt from PySide6.QtGui import ( @@ -17,11 +17,11 @@ from PySide6.QtWidgets import QWidget from rare.models.image import ImageSize -from rare.utils.qt_requests import QtRequests +from rare.utils.qrequests import QRequests from .loading_widget import LoadingWidget -OverlayPath = Tuple[QPainterPath, Union[QColor, QLinearGradient]] +OverlayPath = tuple[QPainterPath, QColor | QLinearGradient] class ImageWidget(QWidget): @@ -29,16 +29,16 @@ class Border(Enum): Rounded = 0 Squared = 1 - _rounded_overlay: Optional[OverlayPath] = None - _squared_overlay: Optional[OverlayPath] = None + _rounded_overlay: OverlayPath | None = None + _squared_overlay: OverlayPath | None = None def __init__(self, parent=None) -> None: super(ImageWidget, self).__init__(parent=parent) - self._pixmap: Optional[QPixmap] = None + self._pixmap: QPixmap | None = None self._opacity: float = 1.0 self._transform: QTransform = None self._smooth_transform: bool = False - self._image_size: Optional[ImageSize.Preset] = None + self._image_size: ImageSize.Preset | None = None self.setObjectName(type(self).__name__) self.setContentsMargins(0, 0, 0, 0) @@ -65,7 +65,10 @@ def setPixmap(self, pixmap: QPixmap) -> None: ) else: self.paint_image = self.paint_image_empty - self.update() + # FIXME: this suppresss the RuntimeError raised by Qt if the widget has already + # deleted. Temporary until I look into it again. + with suppress(RuntimeError): + self.update() def sizeHint(self) -> QSize: return self._image_size.size if self._image_size else super(ImageWidget, self).sizeHint() @@ -160,11 +163,11 @@ def paintEvent(self, a0: QPaintEvent) -> None: class LoadingImageWidget(ImageWidget): - def __init__(self, manager: QtRequests, parent=None): + def __init__(self, manager: QRequests, parent=None): super(LoadingImageWidget, self).__init__(parent=parent) self.manager = manager - def fetchPixmap(self, url: str, params: Dict = None): + def fetchPixmap(self, url: str, params: dict = None): self.setPixmap(QPixmap()) self.manager.get(url, self._on_image_ready, params=params) @@ -182,22 +185,16 @@ def _on_image_ready(self, data): class LoadingSpinnerImageWidget(LoadingImageWidget): - def __init__(self, manager: QtRequests, parent=None): + def __init__(self, manager: QRequests, parent=None): super(LoadingSpinnerImageWidget, self).__init__(manager, parent=parent) self.spinner = LoadingWidget(parent=self) self.spinner.setVisible(False) - def fetchPixmap(self, url: str, params: Dict = None): + def fetchPixmap(self, url: str, params: dict = None): self.spinner.setFixedSize(self._image_size.size) self.spinner.start() params = ( - { - "resize": 1, - "w": self._image_size.base.size.width(), - "h": self._image_size.base.size.height(), - } - if not params - else params + params if params else {'resize': 1, 'w': self._image_size.base.size.width(), 'h': self._image_size.base.size.height()} ) super().fetchPixmap(url, params=params) @@ -206,4 +203,4 @@ def _on_image_ready(self, data): self.spinner.stop() -__all__ = ["ImageSize", "ImageWidget", "LoadingImageWidget", "LoadingSpinnerImageWidget"] +__all__ = ['ImageSize', 'ImageWidget', 'LoadingImageWidget', 'LoadingSpinnerImageWidget'] diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py index 107b2d2b97..feec79732b 100644 --- a/rare/widgets/indicator_edit.py +++ b/rare/widgets/indicator_edit.py @@ -1,7 +1,7 @@ import os +from collections.abc import Callable from enum import Enum, IntEnum from logging import getLogger -from typing import Callable, Dict, Optional, Tuple from PySide6.QtCore import ( QDir, @@ -32,7 +32,7 @@ from rare.utils.misc import qta_icon -logger = getLogger("IndicatorEdit") +logger = getLogger('IndicatorEdit') class IndicatorReasonsCommon(IntEnum): @@ -72,18 +72,18 @@ class IndicatorReasonsStrings(QObject): def __init__(self, parent=None): super(IndicatorReasonsStrings, self).__init__(parent=parent) self.__text = { - IndicatorReasonsCommon.VALID: self.tr("Ok!"), - IndicatorReasonsCommon.INVALID: self.tr("Unknown error occurred"), - IndicatorReasonsCommon.IS_EMPTY: self.tr("Value can not be empty"), - IndicatorReasonsCommon.WRONG_FORMAT: self.tr("Wrong format"), - IndicatorReasonsCommon.WRONG_PATH: self.tr("Wrong file or directory"), - IndicatorReasonsCommon.DIR_NOT_EMPTY: self.tr("Directory is not empty"), - IndicatorReasonsCommon.DIR_NOT_EXISTS: self.tr("Directory does not exist"), - IndicatorReasonsCommon.FILE_NOT_EXISTS: self.tr("File does not exist"), - IndicatorReasonsCommon.GAME_NOT_INSTALLED: self.tr("Game is not installed"), - IndicatorReasonsCommon.GAME_NOT_EXISTS: self.tr("Game does not exist"), + IndicatorReasonsCommon.VALID: self.tr('Ok!'), + IndicatorReasonsCommon.INVALID: self.tr('Unknown error occurred'), + IndicatorReasonsCommon.IS_EMPTY: self.tr('Value can not be empty'), + IndicatorReasonsCommon.WRONG_FORMAT: self.tr('Wrong format'), + IndicatorReasonsCommon.WRONG_PATH: self.tr('Wrong file or directory'), + IndicatorReasonsCommon.DIR_NOT_EMPTY: self.tr('Directory is not empty'), + IndicatorReasonsCommon.DIR_NOT_EXISTS: self.tr('Directory does not exist'), + IndicatorReasonsCommon.FILE_NOT_EXISTS: self.tr('File does not exist'), + IndicatorReasonsCommon.GAME_NOT_INSTALLED: self.tr('Game is not installed'), + IndicatorReasonsCommon.GAME_NOT_EXISTS: self.tr('Game does not exist'), IndicatorReasonsCommon.PERM_NO_WRITE: self.tr("No 'write' access to folder"), - IndicatorReasonsCommon.UNDEFINED: self.tr("Unset"), + IndicatorReasonsCommon.UNDEFINED: self.tr('Unset'), } def __getitem__(self, item: int) -> str: @@ -92,10 +92,10 @@ def __getitem__(self, item: int) -> str: def __setitem__(self, key: int, value: str): self.__text[key] = value - def extend(self, reasons: Dict): - for k in self.__text.keys(): - if k in reasons.keys(): - raise RuntimeError(f"{reasons} contains existing values") + def extend(self, reasons: dict): + for k in self.__text: + if k in reasons: + raise RuntimeError(f'{reasons} contains existing values') self.__text.update(reasons) @@ -104,7 +104,7 @@ class EditFuncRunnableSignals(QObject): class EditFuncRunnable(QRunnable): - def __init__(self, func: Callable[[str], Tuple[bool, str, int]], args: str): + def __init__(self, func: Callable[[str], tuple[bool, str, int]], args: str): super(EditFuncRunnable, self).__init__() self.setAutoDelete(True) self.signals = EditFuncRunnableSignals() @@ -118,9 +118,9 @@ def run(self): self.signals.deleteLater() @staticmethod - def __wrap_edit_function(func: Callable[[str], Tuple[bool, str, int]]): + def __wrap_edit_function(func: Callable[[str], tuple[bool, str, int]]): if func: - return lambda text: func(os.path.expanduser(text) if text.startswith("~") else text) + return lambda text: func(os.path.expanduser(text) if text.startswith('~') else text) else: return func @@ -131,10 +131,10 @@ class IndicatorLineEdit(QWidget): def __init__( self, - text: str = "", - placeholder: str = "", + text: str = '', + placeholder: str = '', completer: QCompleter = None, - edit_func: Callable[[str], Tuple[bool, str, int]] = None, + edit_func: Callable[[str], tuple[bool, str, int]] = None, save_func: Callable[[str], None] = None, horiz_policy: QSizePolicy.Policy = QSizePolicy.Policy.Expanding, parent=None, @@ -142,18 +142,18 @@ def __init__( super(IndicatorLineEdit, self).__init__(parent=parent) self.setObjectName(type(self).__name__) layout = QHBoxLayout(self) - layout.setObjectName(f"{self.objectName()}Layout") + layout.setObjectName(f'{self.objectName()}Layout') layout.setContentsMargins(0, 0, 0, 0) layout.setSizeConstraint(QHBoxLayout.SizeConstraint.SetDefaultConstraint) # Add line_edit self.line_edit = QLineEdit(self) - self.line_edit.setObjectName(f"{type(self).__name__}Edit") - self.line_edit.setPlaceholderText(placeholder if placeholder else self.tr("Use global/default settings")) - self.line_edit.setToolTip(placeholder if placeholder else "") + self.line_edit.setObjectName(f'{type(self).__name__}Edit') + self.line_edit.setPlaceholderText(placeholder if placeholder else self.tr('Use global/default settings')) + self.line_edit.setToolTip(placeholder if placeholder else '') self.line_edit.setSizePolicy(horiz_policy, QSizePolicy.Policy.Fixed) # Add informative label self.info_label = QLabel(self) - self.info_label.setObjectName(f"{self.objectName()}Label") + self.info_label.setObjectName(f'{self.objectName()}Label') self.info_label.setFrameStyle(QLabel.Shape.StyledPanel | QLabel.Shadow.Plain) self.info_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) line_edit_layout = QHBoxLayout(self.line_edit) @@ -166,7 +166,7 @@ def __init__( layout.addWidget(self.line_edit) if edit_func is not None: self.indicator_label = QLabel(self) - self.indicator_label.setPixmap(qta_icon("ei.info-circle", color="gray").pixmap(16, 16)) + self.indicator_label.setPixmap(qta_icon('ei.info-circle', color='gray').pixmap(16, 16)) self.indicator_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) layout.addWidget(self.indicator_label) @@ -174,7 +174,7 @@ def __init__( self.__threadpool = QThreadPool(self) self.__threadpool.setMaxThreadCount(1) - self.__thread: Optional[EditFuncRunnable] = None + self.__thread: EditFuncRunnable | None = None self.is_valid = False self.edit_func = edit_func @@ -210,7 +210,7 @@ def setInfo(self, text: str): self.info_label.setVisible(bool(text)) self.info_label.setText(text) - def setCompleter(self, completer: Optional[QCompleter]): + def setCompleter(self, completer: QCompleter | None): if old := self.line_edit.completer(): old.deleteLater() if not completer: @@ -228,7 +228,7 @@ def reasons(self): return self.__reasons @reasons.setter - def reasons(self, reasons: Dict): + def reasons(self, reasons: dict): self.__reasons.extend(reasons) def refresh(self): @@ -236,8 +236,8 @@ def refresh(self): self.__threadpool.waitForDone() def __indicator(self, valid, reason: int = 0): - color = "gray" if reason == IndicatorReasonsCommon.UNDEFINED else "green" if valid else "red" - self.indicator_label.setPixmap(qta_icon("ei.info-circle", color=color).pixmap(16, 16)) + color = 'gray' if reason == IndicatorReasonsCommon.UNDEFINED else 'green' if valid else 'red' + self.indicator_label.setPixmap(qta_icon('ei.info-circle', color=color).pixmap(16, 16)) if not valid: self.indicator_label.setToolTip(self.__reasons[reason]) else: @@ -276,33 +276,33 @@ class CustomIconType(Enum): Executable = -2 icons = { - CustomIconType.Unknown: ("mdi.file-cancel", "fa5.file-excel"), # Unknown + CustomIconType.Unknown: ('mdi.file-cancel', 'fa5.file-excel'), # Unknown QAbstractFileIconProvider.IconType.Computer: ( - "mdi.desktop-classic", - "fa5s.desktop", + 'mdi.desktop-classic', + 'fa5s.desktop', ), # Computer QAbstractFileIconProvider.IconType.Desktop: ( - "mdi.desktop-mac", - "fa5s.desktop", + 'mdi.desktop-mac', + 'fa5s.desktop', ), # Desktop QAbstractFileIconProvider.IconType.Trashcan: ( - "mdi.trash-can", - "fa5s.trash", + 'mdi.trash-can', + 'fa5s.trash', ), # Trashcan QAbstractFileIconProvider.IconType.Network: ( - "mdi.server-network", - "fa5s.server", + 'mdi.server-network', + 'fa5s.server', ), # Network QAbstractFileIconProvider.IconType.Drive: ( - "mdi.harddisk", - "fa5s.desktop", + 'mdi.harddisk', + 'fa5s.desktop', ), # Drive QAbstractFileIconProvider.IconType.Folder: ( - "mdi.folder", - "fa5.folder", + 'mdi.folder', + 'fa5.folder', ), # Folder - QAbstractFileIconProvider.IconType.File: ("mdi.file", "fa5.file"), # File - CustomIconType.Executable: ("mdi.cog", "fa5s.cog"), # Executable + QAbstractFileIconProvider.IconType.File: ('mdi.file', 'fa5.file'), # File + CustomIconType.Executable: ('mdi.cog', 'fa5s.cog'), # Executable } def __init__(self): @@ -310,7 +310,7 @@ def __init__(self): self.setOptions(QAbstractFileIconProvider.Option.DontUseCustomDirectoryIcons) self.icon_types = {} for idx, (icn, fallback) in PathEditIconProvider.icons.items(): - self.icon_types.update({idx: qta_icon(icn, fallback, color="#eeeeee")}) + self.icon_types.update({idx: qta_icon(icn, fallback, color='#eeeeee')}) def icon(self, info_type): if isinstance(info_type, QFileInfo): @@ -329,17 +329,17 @@ def icon(self, info_type): class PathEdit(IndicatorLineEdit): def __init__( self, - path: str = "", + path: str = '', file_mode: QFileDialog.FileMode = QFileDialog.FileMode.AnyFile, file_filter: QDir.Filter = 0, - name_filters: Tuple[str, ...] = None, - placeholder: str = "", - edit_func: Callable[[str], Tuple[bool, str, int]] = None, + name_filters: tuple[str, ...] = None, + placeholder: str = '', + edit_func: Callable[[str], tuple[bool, str, int]] = None, save_func: Callable[[str], None] = None, horiz_policy: QSizePolicy.Policy = QSizePolicy.Policy.Expanding, parent=None, ): - self.__root_path = path if path else os.path.expanduser("~/") + self.__root_path = path if path else os.path.expanduser('~/') self.__completer = QCompleter() self.__completer_model = QFileSystemModel(self.__completer) try: @@ -370,11 +370,11 @@ def __init__( self.setObjectName(type(self).__name__) self.line_edit.setMinimumSize(QSize(250, 0)) self.path_select = QPushButton(self) - self.path_select.setObjectName(f"{type(self).__name__}Button") + self.path_select.setObjectName(f'{type(self).__name__}Button') layout = self.layout() layout.addWidget(self.path_select) - self.path_select.setText(self.tr("Browse...")) + self.path_select.setText(self.tr('Browse...')) self.__file_mode = file_mode self.__file_filter = file_filter @@ -393,7 +393,7 @@ def __set_path(self): dlg_path = self.line_edit.text() if not dlg_path or not os.path.isabs(dlg_path): dlg_path = self.__root_path - dlg = QFileDialog(self, self.tr("Choose path"), dlg_path) + dlg = QFileDialog(self, self.tr('Choose path'), dlg_path) dlg.setOption(QFileDialog.Option.DontUseCustomDirectoryIcons) dlg.setIconProvider(PathEditIconProvider()) dlg.setFileMode(self.__file_mode) @@ -402,7 +402,7 @@ def __set_path(self): if self.__file_filter: dlg.setFilter(self.__file_filter) if self.__name_filter: - dlg.setNameFilter(" ".join(self.__name_filter)) + dlg.setNameFilter(' '.join(self.__name_filter)) if dlg.exec_(): name = dlg.selectedFiles()[0] self.__completer_model.setRootPath(name) @@ -410,7 +410,7 @@ def __set_path(self): class ColumnCompleter(QCompleter): - def __init__(self, items: Optional[Dict[str, str]], parent=None): + def __init__(self, items: dict[str, str] | None, parent=None): super(ColumnCompleter, self).__init__(parent) self._treeview = QTreeView() @@ -427,7 +427,7 @@ def __init__(self, items: Optional[Dict[str, str]], parent=None): self.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) # self.setCompletionMode(QCompleter.UnfilteredPopupCompletion) - def setModel(self, items: Dict[str, str]): + def setModel(self, items: dict[str, str]): model = QStandardItemModel(len(items), 2, self) for idx, item in enumerate(items.items()): app_title, app_name = item diff --git a/rare/widgets/library_layout.py b/rare/widgets/library_layout.py index f40d2842bd..df0cb2ddab 100644 --- a/rare/widgets/library_layout.py +++ b/rare/widgets/library_layout.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable from PySide6.QtCore import QPoint, QRect, Qt from PySide6.QtWidgets import QSizePolicy diff --git a/rare/widgets/loading_widget.py b/rare/widgets/loading_widget.py index adabecf1d8..ab61707ae9 100644 --- a/rare/widgets/loading_widget.py +++ b/rare/widgets/loading_widget.py @@ -8,7 +8,7 @@ def __init__(self, autostart=False, parent=None): super(LoadingWidget, self).__init__(parent=parent) self.setObjectName(type(self).__name__) self.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) - self.movie = QMovie(":/images/loader.webp", parent=self) + self.movie = QMovie(':/images/loader.webp', parent=self) # The animation's exact size is 94x94 self.setFixedSize(96, 96) self.setMovie(self.movie) diff --git a/rare/widgets/rare_app.py b/rare/widgets/rare_app.py index c31f1d8191..bdbb925011 100644 --- a/rare/widgets/rare_app.py +++ b/rare/widgets/rare_app.py @@ -48,7 +48,7 @@ def deleteLater(self): @Slot(object, object, object) def _on_exception(self, exc_type, exc_value, exc_tb): - message = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) + message = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) if self._handler(exc_type, exc_value, exc_tb): return self.logger.fatal(message) @@ -56,10 +56,10 @@ def _on_exception(self, exc_type, exc_value, exc_tb): QMessageBox.Icon.Critical, exc_type.__name__, self.tr( - "An error has occurred!\n\n" - "You can report this issue at\n" - "https://github.com/RareDevs/Rare/issues\n\n" - "Be sure to include the detailed text in your report." + 'An error has occurred!\n\n' + 'You can report this issue at\n' + 'https://github.com/RareDevs/Rare/issues\n\n' + 'Be sure to include the detailed text in your report.' ), detailedText=message, buttons=QMessageBox.StandardButton.Ignore | QMessageBox.StandardButton.Abort, @@ -78,9 +78,9 @@ def __init__(self, args: Namespace, log_file: str): self.setQuitOnLastWindowClosed(False) self.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, True) - self.setDesktopFileName("rare") - self.setApplicationName("Rare") - self.setOrganizationName("Rare") + self.setDesktopFileName('rare') + self.setApplicationName('Rare') + self.setOrganizationName('Rare') # Create directories after QStandardPaths has been initialized paths.create_dirs() @@ -91,33 +91,33 @@ def __init__(self, args: Namespace, log_file: str): # Set up common logging channel to stderr logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", + format='[%(name)s] %(levelname)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO, stream=sys.stderr, ) - start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute + start_time = time.strftime('%y-%m-%d--%H-%M') # year-month-day-hour-minute file_handler = logging.FileHandler( filename=os.path.join(paths.log_dir(), log_file.format(start_time)), - encoding="utf-8", + encoding='utf-8', ) - file_handler.setFormatter(fmt=logging.Formatter("[%(name)s] %(levelname)s: %(message)s")) + file_handler.setFormatter(fmt=logging.Formatter('[%(name)s] %(levelname)s: %(message)s')) file_handler.setLevel(logging.DEBUG if args.debug else logging.INFO) logging.root.addHandler(file_handler) logging.getLogger().setLevel(logging.DEBUG if args.debug else logging.INFO) # keep requests, asyncio and pillow quiet - logging.getLogger("requests").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("asyncio").setLevel(logging.WARNING) + logging.getLogger('requests').setLevel(logging.WARNING) + logging.getLogger('urllib3').setLevel(logging.WARNING) + logging.getLogger('asyncio').setLevel(logging.WARNING) self.logger.info( - f"Launching Rare version {rare.__version__} Codename: {rare.__codename__}\n" - f" - Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n" - f" - Operating System: {platform.system()}, Python version: {platform.python_version()}\n" - f" - Running {sys.executable} {' '.join(sys.argv)}\n" - f" - Qt version: {QT_VERSION_STR}, PySide6 version: {PYSIDE_VERSION_STR}" + f'Launching Rare version {rare.__version__} Codename: {rare.__codename__}\n' + f' - Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n' + f' - Operating System: {platform.system()}, Python version: {platform.python_version()}\n' + f' - Running {sys.executable} {" ".join(sys.argv)}\n' + f' - Qt version: {QT_VERSION_STR}, PySide6 version: {PYSIDE_VERSION_STR}' ) self.settings = RareAppSettings(self) @@ -128,29 +128,29 @@ def __init__(self, args: Namespace, log_file: str): # Style # lk: this is a bit silly but serves well until we have a class # lk: store the default qt style name from the system on startup as a property for later reference - self.setProperty("rareDefaultQtStyle", self.style().objectName()) + self.setProperty('rareDefaultQtStyle', self.style().objectName()) if color_scheme := self.settings.get_value(app_settings.color_scheme): - self.settings.set_value(app_settings.style_sheet, "") + self.settings.set_value(app_settings.style_sheet, '') set_color_pallete(str(color_scheme)) elif style_sheet := self.settings.get_value(app_settings.style_sheet): - self.settings.set_value(app_settings.color_scheme, "") + self.settings.set_value(app_settings.color_scheme, '') set_style_sheet(str(style_sheet)) else: self.setStyleSheet(get_static_style()) - self.setWindowIcon(QIcon(":/images/icon.png")) + self.setWindowIcon(QIcon(':/images/icon.png')) def load_translator(self, lang: str): # translator for qt stuff locale = QLocale(lang) - self.logger.info("Using locale: %s", locale.name()) + self.logger.info('Using locale: %s', locale.name()) translations = { - "qtbase": QLibraryInfo.location(QLibraryInfo.LibraryPath.TranslationsPath), - "rare": os.path.join(paths.resources_path, "languages"), + 'qtbase': QLibraryInfo.location(QLibraryInfo.LibraryPath.TranslationsPath), + 'rare': os.path.join(paths.resources_path, 'languages'), } for filename, path in translations.items(): translator = QTranslator(self) - if translator.load(locale, filename, "_", path): - self.logger.debug("Loaded translation file: %s", translator.filePath()) + if translator.load(locale, filename, '_', path): + self.logger.debug('Loaded translation file: %s', translator.filePath()) self.installTranslator(translator) else: self.logger.info("Couldn't find translation for locale: %s", locale.name()) diff --git a/rare/widgets/side_tab.py b/rare/widgets/side_tab.py index 11bbd9b475..6581737d91 100644 --- a/rare/widgets/side_tab.py +++ b/rare/widgets/side_tab.py @@ -1,5 +1,5 @@ from logging import getLogger -from typing import Protocol, Union +from typing import Protocol from PySide6.QtCore import ( QSize, @@ -23,7 +23,7 @@ from rare.utils.misc import qta_icon -logger = getLogger("SideTab") +logger = getLogger('SideTab') class SideTabBar(QTabBar): @@ -56,7 +56,7 @@ def paintEvent(self, event): painter.restore() -class SideTabContents(object): +class SideTabContents: # str: title set_title = Signal(str) implements_scrollarea: bool = False @@ -78,8 +78,8 @@ def sizeHint(self) -> QSize: class SideTabContainer(QWidget): def __init__( self, - widget: Union[QWidget, SideTabContentsProtocol], - title: str = "", + widget: QWidget | SideTabContentsProtocol, + title: str = '', parent: QWidget = None, ): super(SideTabContainer, self).__init__(parent=parent) @@ -89,13 +89,13 @@ def __init__( if widget.layout(): widget.layout().setAlignment(Qt.AlignmentFlag.AlignTop) widget.layout().setContentsMargins(0, 0, 3, 0) - if hasattr(widget, "set_title"): + if hasattr(widget, 'set_title'): widget.set_title.connect(self.setTitle) layout = QVBoxLayout(self) layout.addWidget(self.title) - if not hasattr(widget, "implements_scrollarea") or not widget.implements_scrollarea: + if not hasattr(widget, 'implements_scrollarea') or not widget.implements_scrollarea: scrollarea = QScrollArea(self) scrollarea.setSizeAdjustPolicy(QScrollArea.SizeAdjustPolicy.AdjustToContents) scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -113,7 +113,7 @@ def __init__( layout.setAlignment(Qt.AlignmentFlag.AlignTop) def setTitle(self, text: str) -> None: - self.title.setText(f"

{text}

") + self.title.setText(f'

{text}

') self.title.setVisible(bool(text)) @@ -128,8 +128,8 @@ def __init__(self, show_back: bool = False, padding: int = -1, parent=None): if show_back: super(SideTabWidget, self).addTab( QWidget(self), - qta_icon("mdi.keyboard-backspace", "ei.backward"), - self.tr("Back"), + qta_icon('mdi.keyboard-backspace', 'ei.backward'), + self.tr('Back'), ) self.tabBarClicked.connect(self.back_func) @@ -138,6 +138,6 @@ def back_func(self, tab): if not tab: self.back_clicked.emit() - def addTab(self, widget: Union[QWidget, SideTabContentsProtocol], a1: str, title: str = "") -> int: + def addTab(self, widget: QWidget | SideTabContentsProtocol, a1: str, title: str = '') -> int: container = SideTabContainer(widget, title, parent=self) return super(SideTabWidget, self).addTab(container, a1) diff --git a/rare/widgets/sliding_stack.py b/rare/widgets/sliding_stack.py index 8a7d51d93b..5264d843a0 100644 --- a/rare/widgets/sliding_stack.py +++ b/rare/widgets/sliding_stack.py @@ -75,7 +75,7 @@ def slideInWidget(self, newwidget): offsetx, offsety = self.frameRect().width(), self.frameRect().height() self.widget(_next).setGeometry(self.frameRect()) - if not self.m_direction == Qt.Orientation.Horizontal: + if self.m_direction != Qt.Orientation.Horizontal: if _now < _next: offsetx, offsety = 0, -offsety else: @@ -97,10 +97,10 @@ def slideInWidget(self, newwidget): animgroup = QParallelAnimationGroup(self, finished=self.animationDoneSlot) - for index, start, end in zip((_now, _next), (pnow, pnext - offset), (pnow + offset, pnext)): + for index, start, end in zip((_now, _next), (pnow, pnext - offset), (pnow + offset, pnext), strict=False): animation = QPropertyAnimation( self.widget(index), - b"pos", + b'pos', duration=self.m_speed, easingCurve=self.m_animationtype, startValue=start,