1
0
Fork 0
mirror of synced 2024-05-22 13:32:55 +12:00

Merge pull request #381 from loathingKernel/next

Merge a lot of WIP
This commit is contained in:
Stelios Tsampas 2024-02-18 13:00:05 +02:00 committed by GitHub
commit 557189f41b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 4055 additions and 3971 deletions

View file

@ -14,39 +14,34 @@ on:
type: string
name2:
type: string
default: ""
file2:
type: string
default: ""
jobs:
release:
name: Upload
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: ${{ inputs.name1 }}
file: ${{ inputs.file1 }}
- name: ${{ inputs.name2 }}
file: ${{ inputs.file2 }}
steps:
- name: Download ${{ inputs.name1 }} artifact
- name: Download ${{ matrix.name }} from artifact
uses: actions/download-artifact@v3
if: ${{ matrix.name != '' }}
with:
name: ${{ inputs.name1 }}
- name: Upload ${{ inputs.name1 }} to release
name: ${{ matrix.name }}
- name: Upload ${{ matrix.name }} to release
uses: svenstaro/upload-release-action@v2
if: ${{ matrix.name != '' }}
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ inputs.file1 }}
asset_name: ${{ inputs.name1 }}
file: ${{ matrix.file }}
asset_name: ${{ matrix.name }}
tag: ${{ inputs.version }}
overwrite: true
- name: Download ${{ inputs.name2 }} artifact
uses: actions/download-artifact@v3
if: ${{ inputs.name2 != '' }}
with:
name: ${{ inputs.name2 }}
- name: Upload ${{ inputs.name2 }} to release
uses: svenstaro/upload-release-action@v2
if: ${{ inputs.name2 != '' }}
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ inputs.file2 }}
asset_name: ${{ inputs.name2 }}
tag: ${{ inputs.version }}
overwrite: true

View file

@ -2,9 +2,9 @@
host = https://www.transifex.com
[o:rare-1:p:rare:r:placeholder-ts]
file_filter = rare/resources/languages/<lang>.ts
source_file = rare/resources/languages/translation_source.ts
source_lang = en_US
file_filter = rare/resources/languages/rare_<lang>.ts
source_file = rare/resources/languages/source.ts
source_lang = en
type = QT
minimum_perc = 50

View file

@ -2,4 +2,4 @@ import pkg_resources
from subprocess import call
for dist in pkg_resources.working_set:
call("python -m pip install --upgrade " + dist.project_name, shell=True)
call(f"python -m pip install --upgrade {dist.project_name}", shell=True)

View file

@ -3,6 +3,9 @@
cwd="$(pwd)"
cd "$(dirname "$0")"/.. || exit
pylupdate5 -noobsolete $(find rare/ -iname "*.py") -ts rare/resources/languages/translation_source.ts
#py_files=$(find rare -iname "*.py" -not -path rare/ui)
#ui_files=$(find rare/ui -iname "*.ui")
pylupdate5 -noobsolete $(find rare/ -iname "*.py") -ts rare/resources/languages/source.ts
cd "$cwd" || exit

View file

@ -17,6 +17,7 @@ from legendary.models.game import SaveGameStatus
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameSlim
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
from rare.models.options import options
from rare.widgets.rare_app import RareApp, RareAppException
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog
@ -142,11 +143,11 @@ class RareLauncher(RareApp):
return
self.rgame = RareGameSlim(self.core, game)
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
language = self.settings.value(*options.language)
self.load_translator(language)
if QSettings().value("show_console", False, bool):
self.console = ConsoleDialog()
if QSettings(self).value(*options.log_games):
self.console = ConsoleDialog(game.app_title)
self.console.show()
self.game_process.finished.connect(self.__process_finished)
@ -294,10 +295,10 @@ class RareLauncher(RareApp):
))
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
self.game_process.deleteLater()
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
env={i: args.environment.value(i) for i in args.environment.keys()})
if self.console:
self.console.log("Launching game as a detached process")
subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
env={i: args.environment.value(i) for i in args.environment.keys()})
self.stop()
return
if self.args.dry_run:

View file

@ -10,7 +10,7 @@ from legendary.models.game import InstalledGame
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
from rare.utils.misc import icon
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
from rare.widgets.dialogs import ButtonDialog, game_title
logger = getLogger("CloudSyncDialog")
@ -28,9 +28,9 @@ 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")
self.setWindowTitle(dialog_title_game(header, igame.title))
self.setWindowTitle(game_title(header, igame.title))
title_label = QLabel(f"<h4>{dialog_title_game(header, igame.title)}</h4>", self)
title_label = QLabel(f"<h4>{game_title(header, igame.title)}</h4>", self)
sync_widget = QWidget(self)
self.sync_ui = Ui_CloudSyncWidget()

View file

@ -1,4 +1,3 @@
import platform
from typing import Union
from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize, Qt
@ -15,6 +14,7 @@ from PyQt5.QtWidgets import (
)
from rare.ui.launcher.console_env import Ui_ConsoleEnv
from rare.widgets.dialogs import dialog_title, game_title
class ConsoleDialog(QDialog):
@ -22,10 +22,12 @@ class ConsoleDialog(QDialog):
kill = pyqtSignal()
env: QProcessEnvironment
def __init__(self, parent=None):
def __init__(self, app_title: str, parent=None):
super(ConsoleDialog, self).__init__(parent=parent)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.setWindowTitle("Rare - Console")
self.setWindowTitle(
dialog_title(game_title(self.tr("Console"), app_title))
)
self.setGeometry(0, 0, 640, 480)
layout = QVBoxLayout()
@ -62,7 +64,7 @@ class ConsoleDialog(QDialog):
self.setLayout(layout)
self.env_variables = ConsoleEnv(self)
self.env_variables = ConsoleEnv(app_title, self)
self.env_variables.hide()
self.accept_close = False
@ -140,11 +142,14 @@ class ConsoleDialog(QDialog):
class ConsoleEnv(QDialog):
def __init__(self, parent=None):
def __init__(self, app_title: str, parent=None):
super(ConsoleEnv, self).__init__(parent=parent)
self.setAttribute(Qt.WA_DeleteOnClose, False)
self.ui = Ui_ConsoleEnv()
self.ui.setupUi(self)
self.setWindowTitle(
dialog_title(game_title(self.tr("Environment"), app_title))
)
def setTable(self, env: QProcessEnvironment):
self.ui.table.clearContents()
@ -168,12 +173,12 @@ class ConsoleEdit(QPlainTextEdit):
self._cursor_output = self.textCursor()
def log(self, text):
html = f"<p style=\"color:#BBB;white-space:pre\">{text}</p>"
html = f"<p style=\"color:#aaa;white-space:pre\">{text}</p>"
self._cursor_output.insertHtml(html)
self.scroll_to_last_line()
def error(self, text):
html = f"<p style=\"color:#eee;white-space:pre\">{text}</p>"
html = f"<p style=\"color:#a33;white-space:pre\">{text}</p>"
self._cursor_output.insertHtml(html)
self.scroll_to_last_line()

0
rare/commands/reaper.py Normal file
View file

13
rare/commands/webview.py Normal file
View file

@ -0,0 +1,13 @@
import sys
from argparse import Namespace
from legendary.utils import webview_login
def launch(args: Namespace) -> int:
if webview_login.do_webview_login(
callback_code=sys.stdout.write, user_agent=f'EpicGamesLauncher/{args.egl_version}'
):
return 0
else:
return 1

View file

@ -9,6 +9,7 @@ from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
from PyQt5.QtWidgets import QApplication, QMessageBox
from requests import HTTPError
from rare.models.options import options
from rare.components.dialogs.launch_dialog import LaunchDialog
from rare.components.main_window import MainWindow
from rare.shared import RareCore
@ -45,8 +46,8 @@ class Rare(RareApp):
self.signals = RareCore.instance().signals()
self.core = RareCore.instance().core()
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
language = self.settings.value(*options.language)
self.load_translator(language)
# set Application name for settings
self.main_window: Optional[MainWindow] = None

View file

@ -15,7 +15,7 @@ from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
from rare.utils.misc import format_size, icon
from rare.widgets.collapsible_widget import CollapsibleFrame
from rare.widgets.dialogs import ActionDialog, dialog_title_game
from rare.widgets.dialogs import ActionDialog, game_title
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.selective_widget import SelectiveWidget
@ -74,8 +74,8 @@ class InstallDialog(ActionDialog):
elif options.reset_sdl:
header = self.tr("Modify")
bicon = icon("fa.gear")
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
self.setSubtitle(dialog_title_game(header, rgame.app_title))
self.setWindowTitle(game_title(header, rgame.app_title))
self.setSubtitle(game_title(header, rgame.app_title))
install_widget = QWidget(self)
self.ui = Ui_InstallDialog()

View file

@ -2,14 +2,15 @@ import json
from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtCore import pyqtSignal, QUrl, QProcess, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QFrame, qApp, QFormLayout, QLineEdit
from legendary.core import LegendaryCore
from legendary.utils import webview_login
from rare.lgndr.core import LegendaryCore
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
from rare.utils.misc import icon
from rare.utils.paths import get_rare_executable
from rare.widgets.indicator_edit import IndicatorLineEdit, IndicatorReasonsCommon
logger = getLogger("BrowserLogin")
@ -43,6 +44,7 @@ class BrowserLogin(QFrame):
self.ui.open_button.clicked.connect(self.open_browser)
self.sid_edit.textChanged.connect(self.changed.emit)
@pyqtSlot()
def copy_link(self):
clipboard = qApp.clipboard()
clipboard.setText(self.login_url)
@ -79,12 +81,24 @@ class BrowserLogin(QFrame):
except Exception as e:
logger.warning(e)
@pyqtSlot()
def open_browser(self):
if not webview_login.webview_available:
logger.warning("You don't have webengine installed, you will need to manually copy the authorizationCode.")
QDesktopServices.openUrl(QUrl(self.login_url))
else:
if webview_login.do_webview_login(callback_code=self.core.auth_ex_token):
cmd = get_rare_executable() + ["login", self.core.get_egl_version()]
proc = QProcess(self)
proc.start(cmd[0], cmd[1:])
proc.waitForFinished(-1)
out, err = (
proc.readAllStandardOutput().data().decode("utf-8", "ignore"),
proc.readAllStandardError().data().decode("utf-8", "ignore")
)
proc.deleteLater()
if out:
self.core.auth_ex_token(out)
logger.info("Successfully logged in as %s", {self.core.lgd.userdata['displayName']})
self.success.emit()
else:

View file

@ -11,7 +11,7 @@ from rare.models.install import MoveGameModel
from rare.models.game import RareGame
from rare.shared import RareCore
from rare.utils.misc import path_size, format_size, icon
from rare.widgets.dialogs import ActionDialog, dialog_title_game
from rare.widgets.dialogs import ActionDialog, game_title
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasons, IndicatorReasonsCommon
@ -33,8 +33,8 @@ class MoveDialog(ActionDialog):
def __init__(self, rgame: RareGame, parent=None):
super(MoveDialog, self).__init__(parent=parent)
header = self.tr("Move")
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
self.setSubtitle(dialog_title_game(header, rgame.app_title))
self.setWindowTitle(game_title(header, rgame.app_title))
self.setSubtitle(game_title(header, rgame.app_title))
self.rcore = RareCore.instance()
self.core = RareCore.instance().core()
@ -135,7 +135,7 @@ class MoveDialog(ActionDialog):
if not os.access(path, os.W_OK) or not os.access(self.rgame.install_path, os.W_OK):
return helper_func(MovePathEditReasons.NO_WRITE_PERM)
if src_path == dst_path or src_path == dst_install_path:
if src_path in {dst_path, dst_install_path}:
return helper_func(MovePathEditReasons.SAME_DIR)
if str(src_path) in str(dst_path):

View file

@ -1,10 +1,10 @@
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QLayout, QGroupBox
from PyQt5.QtWidgets import QVBoxLayout, QGroupBox
from rare.models.game import RareGame
from rare.models.install import SelectiveDownloadsModel
from rare.utils.misc import icon
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
from rare.widgets.dialogs import ButtonDialog, game_title
from rare.widgets.selective_widget import SelectiveWidget
@ -14,8 +14,8 @@ class SelectiveDialog(ButtonDialog):
def __init__(self, rgame: RareGame, parent=None):
super(SelectiveDialog, self).__init__(parent=parent)
header = self.tr("Optional downloads for")
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
self.setSubtitle(dialog_title_game(header, rgame.app_title))
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)

View file

@ -7,7 +7,7 @@ from PyQt5.QtWidgets import (
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
from rare.utils.misc import icon
from rare.widgets.dialogs import ButtonDialog, dialog_title_game
from rare.widgets.dialogs import ButtonDialog, game_title
class UninstallDialog(ButtonDialog):
@ -16,8 +16,8 @@ class UninstallDialog(ButtonDialog):
def __init__(self, rgame: RareGame, options: UninstallOptionsModel, parent=None):
super(UninstallDialog, self).__init__(parent=parent)
header = self.tr("Uninstall")
self.setWindowTitle(dialog_title_game(header, rgame.app_title))
self.setSubtitle(dialog_title_game(header, rgame.app_title))
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.setChecked(bool(options.keep_files))

View file

@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
QHBoxLayout,
)
from rare.models.options import options
from rare.components.tabs import MainTabWidget
from rare.components.tray_icon import TrayIcon
from rare.shared import RareCore
@ -93,8 +94,8 @@ class MainWindow(QMainWindow):
# self.status_timer.start()
width, height = 1280, 720
if self.settings.value("save_size", False, bool):
width, height = self.settings.value("window_size", (width, height), tuple)
if self.settings.value(*options.save_size):
width, height = self.settings.value(*options.window_size)
self.resize(width, height)
@ -151,9 +152,9 @@ class MainWindow(QMainWindow):
self._window_launched = True
def hide(self) -> None:
if self.settings.value("save_size", False, bool):
if self.settings.value(*options.save_size):
size = self.size().width(), self.size().height()
self.settings.setValue("window_size", size)
self.settings.setValue(options.window_size.key, size)
super(MainWindow, self).hide()
def toggle(self):
@ -214,7 +215,7 @@ class MainWindow(QMainWindow):
# lk: `accept_close` is set to `True` by the `close()` method, overrides exiting to tray in `closeEvent()`
# lk: ensures exiting instead of hiding when `close()` is called programmatically
if not self.__accept_close:
if self.settings.value("sys_tray", True, bool):
if self.settings.value(*options.sys_tray):
self.hide()
e.ignore()
return

View file

@ -31,13 +31,13 @@ class MainTabWidget(QTabWidget):
self.games_index = self.addTab(self.games_tab, self.tr("Games"))
# Downloads Tab after Games Tab to use populated RareCore games list
if not self.args.offline:
self.downloads_tab = DownloadsTab(self)
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)
self.downloads_tab = DownloadsTab(self)
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 = Shop(self.core)
self.store_index = self.addTab(self.store_tab, self.tr("Store (Beta)"))
self.setTabEnabled(self.store_index, not self.args.offline)

View file

@ -15,6 +15,7 @@ from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.options import options
from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker
@ -105,9 +106,13 @@ class DownloadsTab(QWidget):
def __add_update(self, update: Union[str, RareGame]):
if isinstance(update, str):
update = self.rcore.get_game(update)
if QSettings().value(
f"{update.app_name}/auto_update", False, bool
) or QSettings().value("auto_update", False, bool):
auto_update = QSettings(self).value(
f"{update.app_name}/{options.auto_update.key}",
QSettings(self).value(*options.auto_update),
options.auto_update.dtype
)
if auto_update:
self.__get_install_options(
InstallOptionsModel(app_name=update.app_name, update=True, silent=True)
)

View file

@ -55,7 +55,7 @@ class DlThread(QThread):
if result.code == DlResultCode.FINISHED:
self.rgame.set_installed(True)
self.rgame.state = RareGame.State.IDLE
self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED)
self.rgame.signals.progress.finish.emit(result.code != DlResultCode.FINISHED)
self.result.emit(result)
def __status_callback(self, status: UIUpdate):
@ -125,15 +125,14 @@ class DlThread(QThread):
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
if dlcs and not self.item.options.skip_dlcs:
result.dlcs = []
for dlc in dlcs:
result.dlcs.append(
{
"app_name": dlc.app_name,
"app_title": dlc.app_title,
"app_version": dlc.app_version(self.item.options.platform),
}
)
result.dlcs.extend(
{
"app_name": dlc.app_name,
"app_title": dlc.app_title,
"app_version": dlc.app_version(self.item.options.platform),
}
for dlc in dlcs
)
if (
self.item.download.game.supports_cloud_saves
or self.item.download.game.supports_mac_cloud_saves

View file

@ -12,10 +12,9 @@ from rare.shared import (
ImageManagerSingleton,
)
from rare.shared import RareCore
from rare.widgets.library_layout import LibraryLayout
from rare.widgets.sliding_stack import SlidingStackedWidget
from rare.models.options import options
from .game_info import GameInfoTabs
from .game_widgets import LibraryWidgetController
from .game_widgets import LibraryWidgetController, LibraryFilter, LibraryOrder, LibraryView
from .game_widgets.icon_game_widget import IconGameWidget
from .game_widgets.list_game_widget import ListGameWidget
from .head_bar import GameListHeadBar
@ -53,51 +52,22 @@ class GamesTab(QStackedWidget):
self.integrations_page.back_clicked.connect(lambda: self.setCurrentWidget(self.games_page))
self.addWidget(self.integrations_page)
self.view_stack = SlidingStackedWidget(self.games_page)
self.view_stack.setFrameStyle(QFrame.NoFrame)
self.view_scroll = QScrollArea(self.games_page)
self.view_scroll.setWidgetResizable(True)
self.view_scroll.setFrameShape(QFrame.StyledPanel)
self.view_scroll.horizontalScrollBar().setDisabled(True)
self.icon_view_scroll = QScrollArea(self.view_stack)
self.icon_view_scroll.setWidgetResizable(True)
self.icon_view_scroll.setFrameShape(QFrame.StyledPanel)
self.icon_view_scroll.horizontalScrollBar().setDisabled(True)
library_view = LibraryView(self.settings.value(*options.library_view))
self.library_controller = LibraryWidgetController(library_view, self.view_scroll)
games_page_layout.addWidget(self.view_scroll)
self.list_view_scroll = QScrollArea(self.view_stack)
self.list_view_scroll.setWidgetResizable(True)
self.list_view_scroll.setFrameShape(QFrame.StyledPanel)
self.list_view_scroll.horizontalScrollBar().setDisabled(True)
self.icon_view = QWidget(self.icon_view_scroll)
icon_view_layout = LibraryLayout(self.icon_view)
icon_view_layout.setSpacing(9)
icon_view_layout.setContentsMargins(0, 13, 0, 13)
icon_view_layout.setAlignment(Qt.AlignTop)
self.list_view = QWidget(self.list_view_scroll)
list_view_layout = QVBoxLayout(self.list_view)
list_view_layout.setContentsMargins(3, 3, 9, 3)
list_view_layout.setAlignment(Qt.AlignTop)
self.library_controller = LibraryWidgetController(self.icon_view, self.list_view, self)
self.icon_view_scroll.setWidget(self.icon_view)
self.list_view_scroll.setWidget(self.list_view)
self.view_stack.addWidget(self.icon_view_scroll)
self.view_stack.addWidget(self.list_view_scroll)
games_page_layout.addWidget(self.view_stack)
if not self.settings.value("icon_view", True, bool):
self.view_stack.setCurrentWidget(self.list_view_scroll)
self.head_bar.view.list()
else:
self.view_stack.setCurrentWidget(self.icon_view_scroll)
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
self.head_bar.search_bar.textChanged.connect(self.search_games)
self.head_bar.search_bar.textChanged.connect(self.scroll_to_top)
self.head_bar.filterChanged.connect(self.filter_games)
self.head_bar.filterChanged.connect(self.scroll_to_top)
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_list)
self.head_bar.view.toggled.connect(self.toggle_view)
self.active_filter: str = self.head_bar.filter.currentData(Qt.UserRole)
self.head_bar.orderChanged.connect(self.order_games)
self.head_bar.orderChanged.connect(self.scroll_to_top)
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_game_views)
# signals
self.signals.game.installed.connect(self.update_count_games_label)
@ -114,11 +84,8 @@ class GamesTab(QStackedWidget):
@pyqtSlot()
def scroll_to_top(self):
self.icon_view_scroll.verticalScrollBar().setSliderPosition(
self.icon_view_scroll.verticalScrollBar().minimum()
)
self.list_view_scroll.verticalScrollBar().setSliderPosition(
self.list_view_scroll.verticalScrollBar().minimum()
self.view_scroll.verticalScrollBar().setSliderPosition(
self.view_scroll.verticalScrollBar().minimum()
)
@pyqtSlot()
@ -139,8 +106,8 @@ class GamesTab(QStackedWidget):
@pyqtSlot(RareGame)
def show_game_info(self, rgame):
self.setCurrentWidget(self.game_info_page)
self.game_info_page.update_game(rgame)
self.setCurrentWidget(self.game_info_page)
@pyqtSlot()
def update_count_games_label(self):
@ -151,42 +118,38 @@ class GamesTab(QStackedWidget):
def setup_game_list(self):
for rgame in self.rcore.games:
icon_widget, list_widget = self.add_library_widget(rgame)
if not icon_widget or not list_widget:
widget = self.add_library_widget(rgame)
if not widget:
logger.warning("Excluding %s from the game list", rgame.app_title)
continue
self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_widget)
self.filter_games(self.active_filter)
self.filter_games(self.head_bar.current_filter())
self.update_count_games_label()
def add_library_widget(self, rgame: RareGame):
try:
icon_widget, list_widget = self.library_controller.add_game(rgame)
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)
return None, None
icon_widget.show_info.connect(self.show_game_info)
list_widget.show_info.connect(self.show_game_info)
return icon_widget, list_widget
return None
widget.show_info.connect(self.show_game_info)
return widget
@pyqtSlot(str)
@pyqtSlot(str, str)
def filter_games(self, filter_name="all", search_text: str = ""):
def search_games(self, search_text: str = ""):
self.filter_games(self.head_bar.current_filter(), search_text)
@pyqtSlot(object)
@pyqtSlot(object, str)
def filter_games(self, library_filter: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t
if filter_name:
self.active_filter = filter_name
if not filter_name and (t := self.active_filter):
filter_name = t
self.library_controller.filter_game_views(library_filter, search_text.lower())
self.library_controller.filter_list(filter_name, search_text.lower())
@pyqtSlot(object)
@pyqtSlot(object, str)
def order_games(self, library_order: LibraryOrder = LibraryFilter.ALL, search_text: str = ""):
if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t
def toggle_view(self):
self.settings.setValue("icon_view", not self.head_bar.view.isChecked())
if not self.head_bar.view.isChecked():
self.view_stack.slideInWidget(self.icon_view_scroll)
else:
self.view_stack.slideInWidget(self.list_view_scroll)
self.library_controller.order_game_views(library_order, search_text.lower())

View file

@ -1,6 +1,6 @@
from typing import Optional
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QTreeView
@ -64,8 +64,8 @@ class GameInfoTabs(SideTabWidget):
self.setCurrentIndex(self.info_index)
def keyPressEvent(self, e: QKeyEvent):
if e.key() == Qt.Key_Escape:
def keyPressEvent(self, a0: QKeyEvent):
if a0.key() == Qt.Key_Escape:
self.back_clicked.emit()

View file

@ -3,7 +3,7 @@ import platform
from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import QThreadPool, QSettings
from PyQt5.QtCore import QThreadPool, QSettings, pyqtSlot
from PyQt5.QtWidgets import (
QWidget,
QFileDialog,
@ -19,15 +19,16 @@ from legendary.models.game import SaveGameStatus
from rare.models.game import RareGame
from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WineResolver
from rare.shared.workers.wine_resolver import WineSavePathResolver
from rare.ui.components.tabs.games.game_info.cloud_settings_widget import Ui_CloudSettingsWidget
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
from rare.utils.misc import icon
from rare.utils.metrics import timelogger
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.loading_widget import LoadingWidget
from rare.widgets.side_tab import SideTabContents
logger = getLogger("CloudWidget")
logger = getLogger("CloudSaves")
class CloudSaves(QWidget, SideTabContents):
@ -114,30 +115,32 @@ class CloudSaves(QWidget, SideTabContents):
def compute_save_path(self):
if self.rgame.is_installed and self.rgame.game.supports_cloud_saves:
try:
new_path = self.core.get_save_path(self.rgame.app_name)
with timelogger(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):
raise ValueError(f'Path "{new_path}" does not exist.')
except Exception as e:
logger.warning(str(e))
resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name)
if not resolver.wine_env.get("WINEPREFIX"):
del resolver
self.cloud_save_path_edit.setText("")
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
return
resolver = WineSavePathResolver(self.core, self.rgame)
# if not resolver.environ.get("WINEPREFIX"):
# del resolver
# 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.setDisabled(True)
self.compute_save_path_button.setDisabled(True)
app_name = self.rgame.app_name
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(resolver)
return
else:
self.cloud_save_path_edit.setText(new_path)
def wine_resolver_finished(self, path, app_name):
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
@pyqtSlot(str, str)
def __on_wine_resolver_result(self, path, app_name):
logger.info("Wine resolver finished for %s", app_name)
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)
@ -158,8 +161,6 @@ class CloudSaves(QWidget, SideTabContents):
self.cloud_save_path_edit.setText("")
return
self.cloud_save_path_edit.setText(path)
elif path:
self.rcore.get_game(app_name).save_path = path
def __update_widget(self):
supports_saves = self.rgame.igame is not None and (

View file

@ -20,7 +20,7 @@ from rare.models.game import RareGame
from rare.shared import RareCore
from rare.shared.workers import VerifyWorker, MoveWorker
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.misc import format_size, icon
from rare.utils.misc import format_size, icon, style_hyperlink
from rare.widgets.image_widget import ImageWidget, ImageSize
from rare.widgets.side_tab import SideTabContents
from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir
@ -302,7 +302,12 @@ class GameInfo(QWidget, SideTabContents):
self.ui.grade.setDisabled(
self.rgame.is_unreal or platform.system() == "Windows"
)
self.ui.grade.setText(self.steam_grade_ratings[self.rgame.steam_grade()])
self.ui.grade.setText(
style_hyperlink(
f"https://www.protondb.com/app/{self.rgame.steam_appid}",
self.steam_grade_ratings[self.rgame.steam_grade()]
)
)
self.ui.install_button.setEnabled(
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle

View file

@ -1,64 +1,117 @@
import os.path
import platform
import platform as pf
from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QFileDialog
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QFileDialog, QComboBox, QLineEdit
from legendary.models.game import Game, InstalledGame
from rare.components.tabs.settings import DefaultGameSettings
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
from rare.components.tabs.settings.widgets.env_vars import EnvVars
from rare.components.tabs.settings.widgets.game import GameSettingsBase
from rare.components.tabs.settings.widgets.launch import LaunchSettingsBase
from rare.components.tabs.settings.widgets.overlay import DxvkSettings
from rare.components.tabs.settings.widgets.wrappers import WrapperSettings
from rare.models.game import RareGame
from rare.utils import config_helper
from rare.widgets.side_tab import SideTabContents
from rare.utils import config_helper as config
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
if pf.system() != "Windows":
from rare.components.tabs.settings.widgets.wine import WineSettings
if pf.system() in {"Linux", "FreeBSD"}:
from rare.components.tabs.settings.widgets.proton import ProtonSettings
from rare.components.tabs.settings.widgets.overlay import MangoHudSettings
logger = getLogger("GameSettings")
class GameSettings(DefaultGameSettings, SideTabContents):
class GameWrapperSettings(WrapperSettings):
def __init__(self, parent=None):
super(GameSettings, self).__init__(False, parent=parent)
self.pre_launch_settings = PreLaunchSettings()
self.ui.launch_settings_group.layout().addRow(
QLabel(self.tr("Pre-launch command")), self.pre_launch_settings
)
super().__init__(parent=parent)
self.ui.skip_update.currentIndexChanged.connect(
lambda x: self.update_combobox("skip_update_check", x)
)
self.ui.offline.currentIndexChanged.connect(
lambda x: self.update_combobox("offline", x)
)
self.ui.launch_params.textChanged.connect(
lambda x: self.line_edit_save_callback("start_params", x)
)
def load_settings(self, app_name: str):
self.app_name = app_name
self.override_exe_edit = PathEdit(
file_mode=QFileDialog.ExistingFile,
name_filters=["*.exe", "*.app"],
placeholder=self.tr("Relative path to launch executable"),
edit_func=self.override_exe_edit_callback,
save_func=self.override_exe_save_callback,
parent=self
)
self.ui.launch_settings_layout.insertRow(
self.ui.launch_settings_layout.getWidgetPosition(self.ui.launch_params)[0] + 1,
QLabel(self.tr("Override executable"), self), self.override_exe_edit
)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
class GameLaunchSettings(LaunchSettingsBase):
def __init__(self, parent=None):
super(GameLaunchSettings, self).__init__(GameWrapperSettings, parent=parent)
self.game: Game = None
self.igame: InstalledGame = None
def override_exe_edit_callback(self, path: str) -> Tuple[bool, str, int]:
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.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.currentIndexChanged.connect(self.__offline_changed)
self.override_exe_edit = PathEdit(
file_mode=QFileDialog.ExistingFile,
name_filters=["*.exe", "*.app"],
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.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)
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)
self.skip_update_combo.setCurrentIndex(self.offline_combo.findData(skip_update, Qt.UserRole))
offline = config.get_option(self.app_name, "offline", fallback=None)
self.offline_combo.setCurrentIndex(self.offline_combo.findData(offline, Qt.UserRole))
if self.igame:
self.offline_combo.setEnabled(self.igame.can_run_offline)
self.override_exe_edit.set_root(self.igame.install_path)
else:
self.offline_combo.setEnabled(False)
self.override_exe_edit.set_root("")
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="")
self.override_exe_edit.setText(override_exe)
return super().showEvent(a0)
@pyqtSlot(int)
def __skip_update_changed(self, index):
data = self.skip_update_combo.itemData(index, Qt.UserRole)
config.save_option(self.app_name, "skip_update_check", data)
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):
path = os.path.join(self.igame.install_path, path)
if self.igame.install_path not in path:
# lk: Compare paths through python's commonpath because in windows we
# cannot compare as strings
# antonia disapproves of this
if os.path.commonpath([self.igame.install_path, path]) != self.igame.install_path:
return False, self.igame.install_path, IndicatorReasonsCommon.WRONG_PATH
if not os.path.exists(path):
return False, path, IndicatorReasonsCommon.WRONG_PATH
@ -68,72 +121,80 @@ class GameSettings(DefaultGameSettings, SideTabContents):
path = os.path.relpath(path, self.igame.install_path)
return True, path, IndicatorReasonsCommon.VALID
def override_exe_save_callback(self, path: str):
self.line_edit_save_callback("override_exe", path)
def __override_exe_save_callback(self, path: str):
config.save_option(self.app_name, "override_exe", path)
def line_edit_save_callback(self, option, value) -> None:
if value:
config_helper.add_option(self.game.app_name, option, value)
else:
config_helper.remove_option(self.game.app_name, option)
config_helper.save_config()
@pyqtSlot(int)
def __offline_changed(self, index):
data = self.skip_update_combo.itemData(index, Qt.UserRole)
config.save_option(self.app_name, "offline", data)
def update_combobox(self, option, index):
if self.change:
# remove section
if index:
if index == 1:
config_helper.add_option(self.game.app_name, option, "true")
if index == 2:
config_helper.add_option(self.game.app_name, option, "false")
else:
config_helper.remove_option(self.game.app_name, option)
config_helper.save_config()
def __launch_params_changed(self, value) -> None:
config.save_option(self.app_name, "start_params", value)
def load_settings(self, rgame: RareGame):
self.change = False
# FIXME: Use RareGame for the rest of the code
app_name = rgame.app_name
super(GameSettings, self).load_settings(app_name)
self.game = rgame.game
self.igame = rgame.igame
if self.igame:
if self.igame.can_run_offline:
offline = self.core.lgd.config.get(self.game.app_name, "offline", fallback="unset")
if offline == "true":
self.ui.offline.setCurrentIndex(1)
elif offline == "false":
self.ui.offline.setCurrentIndex(2)
else:
self.ui.offline.setCurrentIndex(0)
self.app_name = rgame.app_name
self.wrappers_widget.load_settings(rgame.app_name)
self.ui.offline.setEnabled(True)
else:
self.ui.offline.setEnabled(False)
self.override_exe_edit.set_root(self.igame.install_path)
if pf.system() != "Windows":
class GameWineSettings(WineSettings):
def load_settings(self, app_name):
self.app_name = app_name
if pf.system() in {"Linux", "FreeBSD"}:
class GameProtonSettings(ProtonSettings):
def load_settings(self, app_name: str):
self.app_name = app_name
class GameMangoHudSettings(MangoHudSettings):
def load_settings(self, app_name: str):
self.app_name = app_name
class GameDxvkSettings(DxvkSettings):
def load_settings(self, app_name: str):
self.app_name = app_name
class GameEnvVars(EnvVars):
def load_settings(self, app_name):
self.app_name = app_name
class GameSettings(GameSettingsBase):
def __init__(self, parent=None):
if pf.system() in {"Linux", "FreeBSD"}:
super(GameSettings, self).__init__(
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
GameWineSettings, GameProtonSettings, GameMangoHudSettings,
parent=parent
)
elif pf.system() != "Windows":
super(GameSettings, self).__init__(
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
GameWineSettings,
parent=parent
)
else:
self.ui.offline.setEnabled(False)
self.override_exe_edit.set_root("")
super(GameSettings, self).__init__(
GameLaunchSettings, GameDxvkSettings, GameEnvVars,
parent=parent
)
skip_update = self.core.lgd.config.get(self.game.app_name, "skip_update_check", fallback="unset")
if skip_update == "true":
self.ui.skip_update.setCurrentIndex(1)
elif skip_update == "false":
self.ui.skip_update.setCurrentIndex(2)
else:
self.ui.skip_update.setCurrentIndex(0)
self.set_title.emit(self.game.app_title)
if platform.system() != "Windows":
if self.igame and self.igame.platform == "Mac":
self.ui.linux_settings_widget.setVisible(False)
else:
self.ui.linux_settings_widget.setVisible(True)
self.ui.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText(
self.core.lgd.config.get(self.game.app_name, "override_exe", fallback="")
)
self.pre_launch_settings.load_settings(app_name)
self.change = True
def load_settings(self, rgame: RareGame):
self.set_title.emit(rgame.app_title)
self.app_name = rgame.app_name
self.launch.load_settings(rgame)
if pf.system() != "Windows":
self.wine.load_settings(rgame.app_name)
if pf.system() in {"Linux", "FreeBSD"}:
self.proton_tool.load_settings(rgame.app_name)
self.mangohud.load_settings(rgame.app_name)
self.dxvk.load_settings(rgame.app_name)
self.env_vars.load_settings(rgame.app_name)

View file

@ -1,55 +1,50 @@
from typing import Tuple, List, Union, Optional
from abc import abstractmethod
from typing import Tuple, List, Union, Type, TypeVar
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import QObject, pyqtSlot, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QScrollArea
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.signals import GlobalSignals
from rare.models.library import LibraryFilter, LibraryOrder, LibraryView
from rare.shared import RareCore
from rare.widgets.library_layout import LibraryLayout
from .icon_game_widget import IconGameWidget
from .list_game_widget import ListGameWidget
ViewWidget = TypeVar("ViewWidget", IconGameWidget, ListGameWidget)
class LibraryWidgetController(QObject):
def __init__(self, icon_container: QWidget, list_container: QWidget, parent: QWidget = None):
super(LibraryWidgetController, self).__init__(parent=parent)
self._icon_container: QWidget = icon_container
self._list_container: QWidget = list_container
self.rcore = RareCore.instance()
self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals()
self.signals.game.installed.connect(self.sort_list)
self.signals.game.uninstalled.connect(self.sort_list)
class ViewContainer(QWidget):
def __init__(self, rcore: RareCore, parent=None):
super().__init__(parent=parent)
self.rcore: RareCore = rcore
def add_game(self, rgame: RareGame):
return self.add_widgets(rgame)
def add_widgets(self, rgame: RareGame) -> Tuple[IconGameWidget, ListGameWidget]:
icon_widget = IconGameWidget(rgame, self._icon_container)
list_widget = ListGameWidget(rgame, self._list_container)
return icon_widget, list_widget
def _add_widget(self, widget_type: Type[ViewWidget], rgame: RareGame) -> ViewWidget:
widget = widget_type(rgame, self)
self.layout().addWidget(widget)
return widget
@staticmethod
def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]:
if filter_name == "hidden":
def __visibility(widget: ViewWidget, library_filter, search_text) -> Tuple[bool, float]:
if library_filter == LibraryFilter.HIDDEN:
visible = "hidden" in widget.rgame.metadata.tags
elif "hidden" in widget.rgame.metadata.tags:
visible = False
elif filter_name == "installed":
elif library_filter == LibraryFilter.INSTALLED:
visible = widget.rgame.is_installed and not widget.rgame.is_unreal
elif filter_name == "offline":
elif library_filter == LibraryFilter.OFFLINE:
visible = widget.rgame.can_run_offline and not widget.rgame.is_unreal
elif filter_name == "32bit":
elif library_filter == LibraryFilter.WIN32:
visible = widget.rgame.is_win32 and not widget.rgame.is_unreal
elif filter_name == "mac":
elif library_filter == LibraryFilter.MAC:
visible = widget.rgame.is_mac and not widget.rgame.is_unreal
elif filter_name == "installable":
elif library_filter == LibraryFilter.INSTALLABLE:
visible = not widget.rgame.is_non_asset and not widget.rgame.is_unreal
elif filter_name == "include_ue":
elif library_filter == LibraryFilter.INCLUDE_UE:
visible = True
elif filter_name == "all":
elif library_filter == LibraryFilter.ALL:
visible = not widget.rgame.is_unreal
else:
visible = True
@ -64,74 +59,159 @@ class LibraryWidgetController(QObject):
return visible, opacity
def filter_list(self, filter_name="all", search_text: str = ""):
icon_widgets = self._icon_container.findChildren(IconGameWidget)
list_widgets = self._list_container.findChildren(ListGameWidget)
for iw in icon_widgets:
visibility, opacity = self.__visibility(iw, filter_name, search_text)
def _filter_view(self, widget_type: Type[ViewWidget], filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
widgets = self.findChildren(widget_type)
for iw in widgets:
visibility, opacity = self.__visibility(iw, filter_by, search_text)
iw.setOpacity(opacity)
iw.setVisible(visibility)
for lw in list_widgets:
visibility, opacity = self.__visibility(lw, filter_name, search_text)
lw.setOpacity(opacity)
lw.setVisible(visibility)
self.sort_list(search_text)
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)
game_app_names = {g.app_name for g in games}
new_app_names = game_app_names.difference(app_names)
for app_name in new_app_names:
game = self.rcore.get_game(app_name)
w = widget_type(game, self)
self.layout().addWidget(w)
def _find_widget(self, widget_type: Type[ViewWidget], app_name: str) -> ViewWidget:
w = self.findChild(widget_type, app_name)
return w
@abstractmethod
def order_view(self):
pass
class IconViewContainer(ViewContainer):
def __init__(self, rcore: RareCore, parent=None):
super().__init__(rcore, parent=parent)
view_layout = LibraryLayout(self)
view_layout.setSpacing(9)
view_layout.setContentsMargins(0, 13, 0, 13)
view_layout.setAlignment(Qt.AlignTop)
self.setLayout(view_layout)
def add_widget(self, rgame: RareGame) -> IconGameWidget:
return self._add_widget(IconGameWidget, rgame)
def filter_view(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
self._filter_view(IconGameWidget, filter_by, search_text)
def update_view(self):
self._update_view(IconGameWidget)
def find_widget(self, app_name: str) -> ViewWidget:
return self._find_widget(IconGameWidget, app_name)
def order_view(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
if search_text:
self.layout().sort(
lambda x: (search_text not in x.widget().rgame.app_title.lower(),)
)
else:
if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
# Sort by grant date
self.layout().sort(
key=lambda x: (x.widget().rgame.is_installed, not x.widget().rgame.is_non_asset, x.widget().rgame.grant_date()),
reverse=newest,
)
elif order_by == LibraryOrder.RECENT:
# Sort by recently played
self.layout().sort(
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
reverse=True,
)
else:
# Sort by title
self.layout().sort(
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.app_title)
)
class ListViewContainer(ViewContainer):
def __init__(self, rcore, parent=None):
super().__init__(rcore, parent=parent)
view_layout = QVBoxLayout(self)
view_layout.setContentsMargins(3, 3, 9, 3)
view_layout.setAlignment(Qt.AlignTop)
self.setLayout(view_layout)
def add_widget(self, rgame: RareGame) -> ListGameWidget:
return self._add_widget(ListGameWidget, rgame)
def filter_view(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
self._filter_view(ListGameWidget, filter_by, search_text)
def update_view(self):
self._update_view(ListGameWidget)
def find_widget(self, app_name: str) -> ViewWidget:
return self._find_widget(ListGameWidget, app_name)
def order_view(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
list_widgets = self.findChildren(ListGameWidget)
if search_text:
list_widgets.sort(key=lambda x: (search_text not in x.rgame.app_title.lower(),))
else:
if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
list_widgets.sort(
key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()),
reverse=newest,
)
elif order_by == LibraryOrder.RECENT:
list_widgets.sort(
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.metadata.last_played),
reverse=True,
)
else:
list_widgets.sort(
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
)
for idx, wl in enumerate(list_widgets):
self.layout().insertWidget(idx, wl)
class LibraryWidgetController(QObject):
def __init__(self, view: LibraryView, parent: QScrollArea = None):
super(LibraryWidgetController, self).__init__(parent=parent)
self.rcore = RareCore.instance()
self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals()
if view == LibraryView.COVER:
self._container: IconViewContainer = IconViewContainer(self.rcore, parent)
else:
self._container: ListViewContainer = ListViewContainer(self.rcore, parent)
parent.setWidget(self._container)
self.signals.game.installed.connect(self.order_game_views)
self.signals.game.uninstalled.connect(self.order_game_views)
def add_game(self, rgame: RareGame):
return self.add_widgets(rgame)
def add_widgets(self, rgame: RareGame) -> ViewWidget:
return self._container.add_widget(rgame)
def filter_game_views(self, filter_by: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
self._container.filter_view(filter_by, search_text)
self.order_game_views(search_text=search_text)
@pyqtSlot()
def sort_list(self, sort_by: str = ""):
# lk: this is the existing sorting implemenation
# lk: it sorts by installed then by title
if sort_by:
self._icon_container.layout().sort(lambda x: (sort_by not in x.widget().rgame.app_title.lower(),))
else:
self._icon_container.layout().sort(
key=lambda x: (
# Sort by grant date
# x.widget().rgame.is_installed,
# not x.widget().rgame.is_non_asset,
# x.widget().rgame.grant_date(),
# ), reverse=True
not x.widget().rgame.is_installed,
x.widget().rgame.is_non_asset,
x.widget().rgame.app_title,
)
)
list_widgets = self._list_container.findChildren(ListGameWidget)
if sort_by:
list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),))
else:
list_widgets.sort(
# Sort by grant date
# key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
)
for idx, wl in enumerate(list_widgets):
self._list_container.layout().insertWidget(idx, wl)
def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
self._container.order_view(order_by, search_text)
@pyqtSlot()
@pyqtSlot(list)
def update_list(self, app_names: List[str] = None):
if not app_names:
# lk: base it on icon widgets, the two lists should be identical
icon_widgets = self._icon_container.findChildren(IconGameWidget)
list_widgets = self._list_container.findChildren(ListGameWidget)
icon_app_names = set([iw.rgame.app_name for iw in icon_widgets])
list_app_names = set([lw.rgame.app_name for lw in list_widgets])
games = list(self.rcore.games)
game_app_names = set([g.app_name for g in games])
new_icon_app_names = game_app_names.difference(icon_app_names)
new_list_app_names = game_app_names.difference(list_app_names)
for app_name in new_icon_app_names:
game = self.rcore.get_game(app_name)
iw = IconGameWidget(game)
self._icon_container.layout().addWidget(iw)
for app_name in new_list_app_names:
game = self.rcore.get_game(app_name)
lw = ListGameWidget(game)
self._list_container.layout().addWidget(lw)
self.sort_list()
def update_game_views(self, app_names: List[str] = None):
if app_names:
return
self._container.update_view()
self.order_game_views()
def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]:
iw = self._icon_container.findChild(IconGameWidget, app_name)
lw = self._list_container.findChild(ListGameWidget, app_name)
return iw, lw
def __find_widget(self, app_name: str) -> Union[ViewWidget, None]:
return self._container.find_widget(app_name)

View file

@ -52,7 +52,7 @@ class ProgressLabel(QLabel):
origin_h = (image.height() - min_d) // 2
for x, y in zip(range(origin_w, min_d), range(origin_h, min_d)):
pixel = image.pixelColor(x, y).getRgb()
color = list(map(lambda t: sum(t) // 2, zip(pixel[0:3], color)))
color = list(map(lambda t: sum(t) // 2, zip(pixel[:3], color)))
# 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))

View file

@ -1,25 +1,28 @@
import platform as pf
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
from PyQt5.QtWidgets import (
QLabel,
QPushButton,
QWidget,
QHBoxLayout,
QComboBox, QToolButton, QMenu, QAction,
QComboBox,
QToolButton,
QMenu,
QAction,
)
from qtawesome import IconWidget
from rare.shared import RareCore
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
from rare.models.options import options, LibraryFilter, LibraryOrder
from rare.utils.extra_widgets import ButtonLineEdit
from rare.utils.misc import icon
class GameListHeadBar(QWidget):
filterChanged: pyqtSignal = pyqtSignal(str)
goto_import: pyqtSignal = pyqtSignal()
goto_egl_sync: pyqtSignal = pyqtSignal()
goto_eos_ubisoft: pyqtSignal = pyqtSignal()
filterChanged = pyqtSignal(object)
orderChanged = pyqtSignal(object)
viewChanged = pyqtSignal(object)
goto_import = pyqtSignal()
goto_egl_sync = pyqtSignal()
goto_eos_ubisoft = pyqtSignal()
def __init__(self, parent=None):
super(GameListHeadBar, self).__init__(parent=parent)
@ -27,43 +30,76 @@ class GameListHeadBar(QWidget):
self.settings = QSettings(self)
self.filter = QComboBox(self)
self.filter.addItem(self.tr("All games"), "all")
self.filter.addItem(self.tr("Installed"), "installed")
self.filter.addItem(self.tr("Offline"), "offline")
# self.filter.addItem(self.tr("Hidden"), "hidden")
filters = {
LibraryFilter.ALL: self.tr("All games"),
LibraryFilter.INSTALLED: self.tr("Installed"),
LibraryFilter.OFFLINE: self.tr("Offline"),
# int(LibraryFilter.HIDDEN): self.tr("Hidden"),
}
for data, text in filters.items():
self.filter.addItem(text, data)
if self.rcore.bit32_games:
self.filter.addItem(self.tr("32bit games"), "32bit")
self.filter.addItem(self.tr("32bit games"), LibraryFilter.WIN32)
if self.rcore.mac_games:
self.filter.addItem(self.tr("macOS games"), "mac")
self.filter.addItem(self.tr("macOS games"), LibraryFilter.MAC)
if self.rcore.origin_games:
self.filter.addItem(self.tr("Exclude Origin"), "installable")
self.filter.addItem(self.tr("Include Unreal"), "include_ue")
self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE)
filter_default = "mac" if pf.system() == "Darwin" else "all"
filter_index = i if (i := self.filter.findData(filter_default, Qt.UserRole)) >= 0 else 0
try:
self.filter.setCurrentIndex(self.settings.value("library_filter", filter_index, int))
except TypeError:
self.settings.setValue("library_filter", filter_index)
self.filter.setCurrentIndex(filter_index)
self.filter.currentIndexChanged.connect(self.filter_changed)
_filter = self.settings.value(*options.library_filter)
if (index := self.filter.findData(_filter, Qt.UserRole)) < 0:
raise ValueError
else:
self.filter.setCurrentIndex(index)
except (TypeError, ValueError):
self.settings.setValue(options.library_filter.key, options.library_filter.default)
_filter = LibraryFilter(options.library_filter.default)
self.filter.setCurrentIndex(self.filter.findData(_filter, Qt.UserRole))
self.filter.currentIndexChanged.connect(self.__filter_changed)
integrations_menu = QMenu(self)
import_action = QAction(icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu)
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"),
}
for data, text in sortings.items():
self.order.addItem(text, data)
try:
_order = LibraryOrder(self.settings.value(*options.library_order))
if (index := self.order.findData(_order, Qt.UserRole)) < 0:
raise ValueError
else:
self.order.setCurrentIndex(index)
except (TypeError, ValueError):
self.settings.setValue(options.library_order.key, options.library_order.default)
_order = LibraryOrder(options.library_order.default)
self.order.setCurrentIndex(self.order.findData(_order, Qt.UserRole))
self.order.currentIndexChanged.connect(self.__order_changed)
integrations_menu = QMenu(parent=self)
import_action = QAction(
icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu
)
import_action.triggered.connect(self.goto_import)
egl_sync_action = QAction(icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
egl_sync_action.triggered.connect(self.goto_egl_sync)
eos_ubisoft_action = QAction(icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"),
integrations_menu)
eos_ubisoft_action = QAction(
icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
)
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
integrations_menu.addAction(import_action)
integrations_menu.addAction(egl_sync_action)
integrations_menu.addAction(eos_ubisoft_action)
integrations = QToolButton(self)
integrations = QToolButton(parent=self)
integrations.setText(self.tr("Integrations"))
integrations.setMenu(integrations_menu)
integrations.setPopupMode(QToolButton.InstantPopup)
@ -71,13 +107,11 @@ class GameListHeadBar(QWidget):
self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Game"))
self.search_bar.setObjectName("SearchBar")
self.search_bar.setFrame(False)
self.search_bar.setMinimumWidth(200)
checked = QSettings().value("icon_view", True, bool)
self.search_bar.setMinimumWidth(250)
installed_tooltip = self.tr("Installed games")
self.installed_icon = IconWidget(parent=self)
self.installed_icon.setIcon(icon("ph.floppy-disk-back-fill"))
self.installed_icon = QLabel(parent=self)
self.installed_icon.setPixmap(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()
@ -85,45 +119,54 @@ class GameListHeadBar(QWidget):
self.installed_label.setFont(font)
self.installed_label.setToolTip(installed_tooltip)
available_tooltip = self.tr("Available games")
self.available_icon = IconWidget(parent=self)
self.available_icon.setIcon(icon("ph.floppy-disk-back-light"))
self.available_icon = QLabel(parent=self)
self.available_icon.setPixmap(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.view = SelectViewWidget(checked)
self.refresh_list = QPushButton()
self.refresh_list = QPushButton(parent=self)
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
self.refresh_list.clicked.connect(self.refresh_clicked)
self.refresh_list.clicked.connect(self.__refresh_clicked)
layout = QHBoxLayout()
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 5, 0, 5)
layout.addWidget(self.filter)
layout.addWidget(self.order)
layout.addStretch(0)
layout.addWidget(integrations)
layout.addStretch(5)
layout.addStretch(3)
layout.addWidget(self.search_bar)
layout.addStretch(2)
layout.addStretch(4)
layout.addWidget(self.installed_icon)
layout.addWidget(self.installed_label)
layout.addWidget(self.available_icon)
layout.addWidget(self.available_label)
layout.addStretch(2)
layout.addWidget(self.view)
layout.addStretch(2)
layout.addStretch(4)
layout.addWidget(self.refresh_list)
self.setLayout(layout)
def set_games_count(self, inst: int, avail: int) -> None:
self.installed_label.setText(str(inst))
self.available_label.setText(str(avail))
@pyqtSlot()
def refresh_clicked(self):
def __refresh_clicked(self):
self.rcore.fetch()
def current_filter(self) -> LibraryFilter:
return self.filter.currentData(Qt.UserRole)
@pyqtSlot(int)
def filter_changed(self, index: int):
self.filterChanged.emit(self.filter.itemData(index, Qt.UserRole))
self.settings.setValue("library_filter", index)
def __filter_changed(self, index: int):
data = self.filter.itemData(index, Qt.UserRole)
self.filterChanged.emit(data)
self.settings.setValue(options.library_filter.key, int(data))
def current_order(self) -> LibraryOrder:
return self.order.currentData(Qt.UserRole)
@pyqtSlot(int)
def __order_changed(self, index: int):
data = self.order.itemData(index, Qt.UserRole)
self.orderChanged.emit(data)
self.settings.setValue(options.library_order.key, int(data))

View file

@ -13,9 +13,10 @@ from legendary.models.game import InstalledGame
from rare.lgndr.glue.exception import LgndrException
from rare.models.pathspec import PathSpec
from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WineResolver
from rare.shared.workers.wine_resolver import WinePathResolver
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
from rare.utils.compat import utils as compat_utils
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
@ -87,11 +88,7 @@ class EGLSyncGroup(QGroupBox):
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver(
self.core,
PathSpec.egl_programdata,
"default"
)
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)
@ -122,14 +119,8 @@ class EGLSyncGroup(QGroupBox):
os.path.join(path, "dosdevices/c:")
):
# path is a wine prefix
path = os.path.join(
path,
"dosdevices/c:",
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
)
elif not path.rstrip("/").endswith(
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
):
path = PathSpec.prefix_egl_programdata(path)
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):
@ -311,7 +302,8 @@ class EGLSyncListGroup(QGroupBox):
def items(self) -> Iterable[EGLSyncListItem]:
# for i in range(self.list.count()):
# yield self.list.item(i)
return [self.ui.list.item(i) for i in range(self.ui.list.count())]
return map(self.ui.list.item, range(self.ui.list.count()))
# return [self.ui.list.item(i) for i in range(self.ui.list.count())]
class EGLSyncExportGroup(EGLSyncListGroup):

View file

@ -224,7 +224,7 @@ class EosGroup(QGroupBox):
if platform.system() != "Windows":
prefixes = config.get_prefixes()
prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)}
prefixes = {prefix for prefix, _ in prefixes if config.prefix_exists(prefix)}
if platform.system() == "Darwin":
# TODO: add crossover support
pass

View file

@ -263,13 +263,12 @@ class ImportGroup(QGroupBox):
self.app_name_edit.setText(app_name)
def path_edit_callback(self, path) -> Tuple[bool, str, int]:
if os.path.exists(path):
if os.path.exists(os.path.join(path, ".egstore")):
return True, path, IndicatorReasonsCommon.VALID
elif os.path.basename(path) in self.__install_dirs:
return True, path, IndicatorReasonsCommon.VALID
else:
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:
return True, path, IndicatorReasonsCommon.VALID
return False, path, IndicatorReasonsCommon.UNDEFINED
@pyqtSlot(str)

View file

@ -243,15 +243,14 @@ class UbisoftGroup(QGroupBox):
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."))
else:
if activated == len(uplay_games):
self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
else:
self.info_label.setText(
self.tr("You have <b>{}</b> games available to redeem.").format(
len(uplay_games) - activated
)
self.info_label.setText(
self.tr("You have <b>{}</b> games available to redeem.").format(
len(uplay_games) - activated
)
)
logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
self.loading_widget.stop()

View file

@ -1,9 +1,8 @@
from rare.components.tabs.settings.widgets.linux import LinuxSettings
from rare.shared import ArgumentsSingleton
from rare.widgets.side_tab import SideTabWidget
from .about import About
from .debug import DebugSettings
from .game_settings import DefaultGameSettings
from .settings import GameSettings
from .legendary import LegendarySettings
from .rare import RareSettings
@ -13,9 +12,14 @@ class SettingsTab(SideTabWidget):
super(SettingsTab, self).__init__(parent=parent)
self.args = ArgumentsSingleton()
self.rare_index = self.addTab(RareSettings(self), "Rare")
self.legendary_index = self.addTab(LegendarySettings(self), "Legendary")
self.settings_index = self.addTab(DefaultGameSettings(True, self), self.tr("Default Settings"))
rare_settings = RareSettings(self)
self.rare_index = self.addTab(rare_settings, "Rare")
legendary_settings = LegendarySettings(self)
self.legendary_index = self.addTab(legendary_settings, "Legendary")
game_settings = GameSettings(self)
self.settings_index = self.addTab(game_settings, self.tr("Defaults"))
self.about = About(self)
self.about_index = self.addTab(self.about, "About", "About")

View file

@ -16,7 +16,7 @@ def versiontuple(v):
try:
return tuple(map(int, (v.split("."))))
except Exception:
return tuple((9, 9, 9)) # It is a beta version and newer
return 9, 9, 9
class About(QWidget):
@ -63,7 +63,7 @@ class About(QWidget):
if self.update_available:
logger.info(f"Update available: {__version__} -> {latest_tag}")
self.ui.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
self.ui.update_lbl.setText(f"{__version__} -> {latest_tag}")
self.update_available_ready.emit()
else:
self.ui.update_lbl.setText(self.tr("You have the latest version"))

View file

@ -1,106 +0,0 @@
import platform
from logging import getLogger
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import (
QWidget,
QLabel
)
from rare.components.tabs.settings.widgets.env_vars import EnvVars
from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.game_settings import Ui_GameSettings
if platform.system() != "Windows":
from rare.components.tabs.settings.widgets.linux import LinuxSettings
if platform.system() != "Darwin":
from rare.components.tabs.settings.widgets.proton import ProtonSettings
logger = getLogger("GameSettings")
class DefaultGameSettings(QWidget):
# variable to no update when changing game
change = False
app_name: str
def __init__(self, is_default, parent=None):
super(DefaultGameSettings, self).__init__(parent=parent)
self.ui = Ui_GameSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.settings = QSettings()
self.wrapper_settings = WrapperSettings()
self.ui.launch_settings_group.layout().addRow(
QLabel("Wrapper"), self.wrapper_settings
)
self.env_vars = EnvVars(self)
self.ui.game_settings_layout.addWidget(self.env_vars)
if platform.system() != "Windows":
self.linux_settings = LinuxAppSettings()
if platform.system() != "Darwin":
self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
self.ui.proton_layout.addWidget(self.proton_settings)
self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
# FIXME: Remove the spacerItem and margins from the linux settings
# FIXME: This should be handled differently at soem point in the future
# NOTE: specerItem has been removed
self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
# FIXME: End of FIXME
self.ui.linux_settings_layout.addWidget(self.linux_settings)
self.ui.linux_settings_layout.setAlignment(Qt.AlignTop)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
self.linux_settings.mangohud.set_wrapper_activated.connect(
lambda active: self.wrapper_settings.add_wrapper("mangohud")
if active else self.wrapper_settings.delete_wrapper("mangohud"))
self.linux_settings.environ_changed.connect(self.env_vars.reset_model)
else:
self.ui.linux_settings_widget.setVisible(False)
if is_default:
self.ui.launch_settings_layout.removeRow(self.ui.skip_update)
self.ui.launch_settings_layout.removeRow(self.ui.offline)
self.ui.launch_settings_layout.removeRow(self.ui.launch_params)
self.load_settings("default")
def load_settings(self, app_name):
self.app_name = app_name
self.wrapper_settings.load_settings(app_name)
if platform.system() != "Windows":
self.linux_settings.update_game(app_name)
proton = self.wrapper_settings.wrappers.get("proton", "")
if proton:
proton = proton.text
if platform.system() != "Darwin":
self.proton_settings.load_settings(app_name, proton)
else:
proton = ""
if proton:
self.linux_settings.ui.wine_groupbox.setEnabled(False)
else:
self.linux_settings.ui.wine_groupbox.setEnabled(True)
self.env_vars.update_game(app_name)
if platform.system() != "Windows":
class LinuxAppSettings(LinuxSettings):
def __init__(self):
super(LinuxAppSettings, self).__init__()
def update_game(self, app_name):
self.name = app_name
self.wine_prefix.setText(self.load_prefix())
self.wine_exec.setText(self.load_setting(self.name, "wine_executable"))
self.dxvk.load_settings(self.name)
self.mangohud.load_settings(self.name)

View file

@ -1,11 +1,13 @@
import platform as pf
import re
from logging import getLogger
from typing import Tuple, List
from typing import Tuple, Set
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings
from PyQt5.QtGui import QShowEvent, QHideEvent
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton
from rare.shared.workers.worker import Worker
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
@ -19,14 +21,11 @@ class RefreshGameMetaWorker(Worker):
class Signals(QObject):
finished = pyqtSignal()
def __init__(self, platforms: List[str], include_unreal: bool):
def __init__(self, platforms: Set[str], include_unreal: bool):
super(RefreshGameMetaWorker, self).__init__()
self.signals = RefreshGameMetaWorker.Signals()
self.core = LegendaryCoreSingleton()
if platforms:
self.platforms = platforms
else:
self.platforms = ["Windows"]
self.platforms = platforms if platforms else {"Windows"}
self.skip_ue = not include_unreal
def run_real(self) -> None:
@ -37,10 +36,11 @@ class RefreshGameMetaWorker(Worker):
self.signals.finished.emit()
class LegendarySettings(QWidget, Ui_LegendarySettings):
class LegendarySettings(QWidget):
def __init__(self, parent=None):
super(LegendarySettings, self).__init__(parent=parent)
self.setupUi(self)
self.ui = Ui_LegendarySettings()
self.ui.setupUi(self)
self.settings = QSettings(self)
self.core = LegendaryCoreSingleton()
@ -53,7 +53,7 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
file_mode=QFileDialog.DirectoryOnly,
save_func=self.__mac_path_save,
)
self.install_dir_layout.addWidget(self.mac_install_dir)
self.ui.install_dir_layout.addWidget(self.mac_install_dir)
# Platform-independent installation directory
self.install_dir = PathEdit(
@ -62,34 +62,34 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
file_mode=QFileDialog.DirectoryOnly,
save_func=self.__win_path_save,
)
self.install_dir_layout.addWidget(self.install_dir)
self.ui.install_dir_layout.addWidget(self.install_dir)
# Max Workers
max_workers = self.core.lgd.config["Legendary"].getint(
"max_workers", fallback=0
)
self.max_worker_spin.setValue(max_workers)
self.max_worker_spin.valueChanged.connect(self.max_worker_save)
self.ui.max_worker_spin.setValue(max_workers)
self.ui.max_worker_spin.valueChanged.connect(self.max_worker_save)
# Max memory
max_memory = self.core.lgd.config["Legendary"].getint("max_memory", fallback=0)
self.max_memory_spin.setValue(max_memory)
self.max_memory_spin.valueChanged.connect(self.max_memory_save)
self.ui.max_memory_spin.setValue(max_memory)
self.ui.max_memory_spin.valueChanged.connect(self.max_memory_save)
# Preferred CDN
preferred_cdn = self.core.lgd.config["Legendary"].get(
"preferred_cdn", fallback=""
)
self.preferred_cdn_line.setText(preferred_cdn)
self.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save)
self.ui.preferred_cdn_line.setText(preferred_cdn)
self.ui.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save)
# Disable HTTPS
disable_https = self.core.lgd.config["Legendary"].getboolean(
"disable_https", fallback=False
)
self.disable_https_check.setChecked(disable_https)
self.disable_https_check.stateChanged.connect(self.disable_https_save)
self.ui.disable_https_check.setChecked(disable_https)
self.ui.disable_https_check.stateChanged.connect(self.disable_https_save)
# Cleanup
self.clean_button.clicked.connect(lambda: self.cleanup(False))
self.clean_keep_manifests_button.clicked.connect(lambda: self.cleanup(True))
self.ui.clean_button.clicked.connect(lambda: self.cleanup(False))
self.ui.clean_keep_manifests_button.clicked.connect(lambda: self.cleanup(True))
self.locale_edit = IndicatorLineEdit(
f"{self.core.language_code}-{self.core.country_code}",
@ -98,51 +98,59 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
horiz_policy=QSizePolicy.Minimum,
parent=self,
)
self.locale_layout.addWidget(self.locale_edit)
self.ui.locale_layout.addWidget(self.locale_edit)
self.fetch_win32_check.setChecked(self.settings.value("win32_meta", False, bool))
self.fetch_win32_check.stateChanged.connect(
lambda: self.settings.setValue("win32_meta", self.fetch_win32_check.isChecked())
self.ui.fetch_win32_check.setChecked(self.settings.value(*options.win32_meta))
self.ui.fetch_win32_check.stateChanged.connect(
lambda: self.settings.setValue(options.win32_meta.key, self.ui.fetch_win32_check.isChecked())
)
self.fetch_macos_check.setChecked(self.settings.value("macos_meta", pf.system() == "Darwin", bool))
self.fetch_macos_check.stateChanged.connect(
lambda: self.settings.setValue("macos_meta", self.fetch_macos_check.isChecked())
self.ui.fetch_macos_check.setChecked(self.settings.value(*options.macos_meta))
self.ui.fetch_macos_check.stateChanged.connect(
lambda: self.settings.setValue(options.macos_meta.key, self.ui.fetch_macos_check.isChecked())
)
self.fetch_macos_check.setDisabled(pf.system() == "Darwin")
self.ui.fetch_macos_check.setDisabled(pf.system() == "Darwin")
self.fetch_unreal_check.setChecked(self.settings.value("unreal_meta", False, bool))
self.fetch_unreal_check.stateChanged.connect(
lambda: self.settings.setValue("unreal_meta", self.fetch_unreal_check.isChecked())
self.ui.fetch_unreal_check.setChecked(self.settings.value(*options.unreal_meta))
self.ui.fetch_unreal_check.stateChanged.connect(
lambda: self.settings.setValue(options.unreal_meta.key, self.ui.fetch_unreal_check.isChecked())
)
self.exclude_non_asset_check.setChecked(
self.settings.value("exclude_non_asset", False, bool)
)
self.exclude_non_asset_check.stateChanged.connect(
lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked())
)
self.exclude_entitlements_check.setChecked(
self.settings.value("exclude_entitlements", False, bool)
)
self.exclude_entitlements_check.stateChanged.connect(
lambda: self.settings.setValue("exclude_entitlements", self.exclude_entitlements_check.isChecked())
self.ui.exclude_non_asset_check.setChecked(self.settings.value(*options.exclude_non_asset))
self.ui.exclude_non_asset_check.stateChanged.connect(
lambda: self.settings.setValue(options.exclude_non_asset.key, self.ui.exclude_non_asset_check.isChecked())
)
self.refresh_metadata_button.clicked.connect(self.refresh_metadata)
self.ui.exclude_entitlements_check.setChecked(self.settings.value(*options.exclude_entitlements))
self.ui.exclude_entitlements_check.stateChanged.connect(
lambda: self.settings.setValue(options.exclude_entitlements.key, self.ui.exclude_entitlements_check.isChecked())
)
self.ui.refresh_metadata_button.clicked.connect(self.refresh_metadata)
# FIXME: Disable the button for now because it interferes with RareCore
self.refresh_metadata_button.setEnabled(False)
self.refresh_metadata_button.setVisible(False)
self.ui.refresh_metadata_button.setEnabled(False)
self.ui.refresh_metadata_button.setVisible(False)
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
return super().showEvent(a0)
def hideEvent(self, a0: QHideEvent):
if a0.spontaneous():
return super().hideEvent(a0)
self.core.lgd.save_config()
return super().hideEvent(a0)
def refresh_metadata(self):
self.refresh_metadata_button.setDisabled(True)
platforms = []
if self.fetch_win32_check.isChecked():
platforms.append("Win32")
if self.fetch_macos_check.isChecked():
platforms.append("Mac")
worker = RefreshGameMetaWorker(platforms, self.fetch_unreal_check.isChecked())
worker.signals.finished.connect(lambda: self.refresh_metadata_button.setDisabled(False))
self.ui.refresh_metadata_button.setDisabled(True)
platforms = set()
if self.ui.fetch_win32_check.isChecked():
platforms.add("Win32")
if self.ui.fetch_macos_check.isChecked():
platforms.add("Mac")
worker = RefreshGameMetaWorker(platforms, self.ui.fetch_unreal_check.isChecked())
worker.signals.finished.connect(lambda: self.ui.refresh_metadata_button.setDisabled(False))
QThreadPool.globalInstance().start(worker)
@staticmethod
@ -162,10 +170,8 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
if text:
self.core.egs.language_code, self.core.egs.country_code = text.split("-")
self.core.lgd.config.set("Legendary", "locale", text)
else:
if self.core.lgd.config.has_option("Legendary", "locale"):
self.core.lgd.config.remove_option("Legendary", "locale")
self.core.lgd.save_config()
elif self.core.lgd.config.has_option("Legendary", "locale"):
self.core.lgd.config.remove_option("Legendary", "locale")
def __mac_path_save(self, text: str) -> None:
self.__path_save(text, "mac_install_dir")
@ -180,40 +186,35 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
if not text and option in self.core.lgd.config["Legendary"].keys():
self.core.lgd.config["Legendary"].pop(option)
else:
logger.debug(f"Set %s option in config to %s", option, text)
self.core.lgd.save_config()
logger.debug("Set %s option in config to %s", option, text)
def max_worker_save(self, workers: str):
if workers := int(workers):
self.core.lgd.config.set("Legendary", "max_workers", str(workers))
else:
self.core.lgd.config.remove_option("Legendary", "max_workers")
self.core.lgd.save_config()
def max_memory_save(self, memory: str):
if memory := int(memory):
self.core.lgd.config.set("Legendary", "max_memory", str(memory))
else:
self.core.lgd.config.remove_option("Legendary", "max_memory")
self.core.lgd.save_config()
def preferred_cdn_save(self, cdn: str):
if cdn:
self.core.lgd.config.set("Legendary", "preferred_cdn", cdn.strip())
else:
self.core.lgd.config.remove_option("Legendary", "preferred_cdn")
self.core.lgd.save_config()
def disable_https_save(self, checked: int):
self.core.lgd.config.set(
"Legendary", "disable_https", str(bool(checked)).lower()
)
self.core.lgd.save_config()
def cleanup(self, keep_manifests: bool):
before = self.core.lgd.get_dir_size()
logger.debug("Removing app metadata...")
app_names = set(g.app_name for g in self.core.get_assets(update_assets=False))
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:

View file

@ -1,13 +1,13 @@
import os
import platform
import subprocess
import sys
import locale
from logging import getLogger
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtCore import QSettings, Qt, pyqtSlot, QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.components.tabs.settings.widgets.rpc import RPCSettings
from rare.models.options import options, LibraryView
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils.misc import (
@ -22,116 +22,117 @@ from rare.utils.paths import create_desktop_link, desktop_link_path, log_dir, de
logger = getLogger("RareSettings")
languages = [("en", "English"),
("de", "Deutsch"),
("fr", "Français"),
("zh-Hans", "Simplified Chinese"),
("zh_TW", "Chinese Taiwan"),
("pt_BR", "Portuguese (Brazil)"),
("ca", "Catalan"),
("ru", "Russian"),
("tr", "Turkish"),
("uk", "Ukrainian")]
class RareSettings(QWidget, Ui_RareSettings):
class RareSettings(QWidget):
def __init__(self, parent=None):
super(RareSettings, self).__init__(parent=parent)
self.setupUi(self)
self.ui = Ui_RareSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
# (widget_name, option_name, default)
self.checkboxes = [
(self.sys_tray, "sys_tray", True),
(self.auto_update, "auto_update", False),
(self.confirm_start, "confirm_start", False),
(self.auto_sync_cloud, "auto_sync_cloud", False),
(self.notification, "notification", True),
(self.save_size, "save_size", False),
(self.log_games, "show_console", False),
]
self.settings = QSettings()
language = self.settings.value("language", self.core.language_code, type=str)
self.settings = QSettings(self)
# Select lang
self.lang_select.addItems([i[1] for i in languages])
if language in get_translations():
index = [lang[0] for lang in languages].index(language)
self.lang_select.setCurrentIndex(index)
self.ui.lang_select.addItem(self.tr("System default"), options.language.default)
for lang_code, title in get_translations():
self.ui.lang_select.addItem(title, lang_code)
language = self.settings.value(*options.language)
if (index := self.ui.lang_select.findData(language, Qt.UserRole)) > 0:
self.ui.lang_select.setCurrentIndex(index)
else:
self.lang_select.setCurrentIndex(0)
self.lang_select.currentIndexChanged.connect(self.update_lang)
self.ui.lang_select.setCurrentIndex(0)
self.ui.lang_select.currentIndexChanged.connect(self.on_lang_changed)
colors = get_color_schemes()
self.color_select.addItems(colors)
if (color := self.settings.value("color_scheme")) in colors:
self.color_select.setCurrentIndex(self.color_select.findText(color))
self.color_select.setDisabled(False)
self.style_select.setDisabled(True)
self.ui.color_select.addItem(self.tr("None"), "")
for item in get_color_schemes():
self.ui.color_select.addItem(item, item)
color = self.settings.value(*options.color_scheme)
if (index := self.ui.color_select.findData(color, Qt.UserRole)) > 0:
self.ui.color_select.setCurrentIndex(index)
self.ui.color_select.setDisabled(False)
self.ui.style_select.setDisabled(True)
else:
self.color_select.setCurrentIndex(0)
self.color_select.currentIndexChanged.connect(self.on_color_select_changed)
self.ui.color_select.setCurrentIndex(0)
self.ui.color_select.currentIndexChanged.connect(self.on_color_select_changed)
styles = get_style_sheets()
self.style_select.addItems(styles)
if (style := self.settings.value("style_sheet")) in styles:
self.style_select.setCurrentIndex(self.style_select.findText(style))
self.style_select.setDisabled(False)
self.color_select.setDisabled(True)
self.ui.style_select.addItem(self.tr("None"), "")
for item in get_style_sheets():
self.ui.style_select.addItem(item, item)
style = self.settings.value(*options.style_sheet)
if (index := self.ui.style_select.findData(style, Qt.UserRole)) > 0:
self.ui.style_select.setCurrentIndex(index)
self.ui.style_select.setDisabled(False)
self.ui.color_select.setDisabled(True)
else:
self.style_select.setCurrentIndex(0)
self.style_select.currentIndexChanged.connect(self.on_style_select_changed)
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)
view = LibraryView(self.settings.value(*options.library_view))
if (index := self.ui.view_combo.findData(view)) > -1:
self.ui.view_combo.setCurrentIndex(index)
else:
self.ui.view_combo.setCurrentIndex(0)
self.ui.view_combo.currentIndexChanged.connect(self.on_view_combo_changed)
self.rpc = RPCSettings(self)
self.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
self.ui.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
self.init_checkboxes(self.checkboxes)
self.sys_tray.stateChanged.connect(
lambda: self.settings.setValue("sys_tray", self.sys_tray.isChecked())
self.ui.sys_tray.setChecked(self.settings.value(*options.sys_tray))
self.ui.sys_tray.stateChanged.connect(
lambda: self.settings.setValue(options.sys_tray.key, self.ui.sys_tray.isChecked())
)
self.auto_update.stateChanged.connect(
lambda: self.settings.setValue("auto_update", self.auto_update.isChecked())
self.ui.auto_update.setChecked(self.settings.value(*options.auto_update))
self.ui.auto_update.stateChanged.connect(
lambda: self.settings.setValue(options.auto_update.key, self.ui.auto_update.isChecked())
)
self.confirm_start.stateChanged.connect(
lambda: self.settings.setValue(
"confirm_start", self.confirm_start.isChecked()
)
self.ui.confirm_start.setChecked(self.settings.value(*options.confirm_start))
self.ui.confirm_start.stateChanged.connect(
lambda: self.settings.setValue(options.confirm_start.key, self.ui.confirm_start.isChecked())
)
self.auto_sync_cloud.stateChanged.connect(
lambda: self.settings.setValue(
"auto_sync_cloud", self.auto_sync_cloud.isChecked()
)
self.ui.auto_sync_cloud.setChecked(self.settings.value(*options.auto_sync_cloud))
self.ui.auto_sync_cloud.stateChanged.connect(
lambda: self.settings.setValue(options.auto_sync_cloud.key, self.ui.auto_sync_cloud.isChecked())
)
self.notification.stateChanged.connect(
lambda: self.settings.setValue("notification", self.notification.isChecked())
self.ui.notification.setChecked(self.settings.value(*options.notification))
self.ui.notification.stateChanged.connect(
lambda: self.settings.setValue(options.notification.key, self.ui.notification.isChecked())
)
self.save_size.stateChanged.connect(self.save_window_size)
self.log_games.stateChanged.connect(
lambda: self.settings.setValue("show_console", self.log_games.isChecked())
self.ui.save_size.setChecked(self.settings.value(*options.save_size))
self.ui.save_size.stateChanged.connect(self.save_window_size)
self.ui.log_games.setChecked(self.settings.value(*options.log_games))
self.ui.log_games.stateChanged.connect(
lambda: self.settings.setValue(options.log_games.key, self.ui.log_games.isChecked())
)
if desktop_links_supported():
self.desktop_link = desktop_link_path("Rare", "desktop")
self.start_menu_link = desktop_link_path("Rare", "start_menu")
else:
self.desktop_link_btn.setToolTip(self.tr("Not supported"))
self.desktop_link_btn.setDisabled(True)
self.startmenu_link_btn.setToolTip(self.tr("Not supported"))
self.startmenu_link_btn.setDisabled(True)
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.setDisabled(True)
self.desktop_link = ""
self.start_menu_link = ""
if self.desktop_link and self.desktop_link.exists():
self.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.startmenu_link_btn.setText(self.tr("Remove start menu link"))
self.ui.startmenu_link_btn.setText(self.tr("Remove start menu link"))
self.desktop_link_btn.clicked.connect(self.create_desktop_link)
self.startmenu_link_btn.clicked.connect(self.create_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)
self.log_dir_open_button.clicked.connect(self.open_dir)
self.log_dir_clean_button.clicked.connect(self.clean_logdir)
self.ui.log_dir_open_button.clicked.connect(self.open_directory)
self.ui.log_dir_clean_button.clicked.connect(self.clean_logdir)
# get size of logdir
size = sum(
@ -139,10 +140,11 @@ class RareSettings(QWidget, Ui_RareSettings):
for f in log_dir().iterdir()
if log_dir().joinpath(f).is_file()
)
self.log_dir_size_label.setText(format_size(size))
self.ui.log_dir_size_label.setText(format_size(size))
# self.log_dir_clean_button.setVisible(False)
# self.log_dir_size_label.setVisible(False)
@pyqtSlot()
def clean_logdir(self):
for f in log_dir().iterdir():
try:
@ -155,17 +157,18 @@ class RareSettings(QWidget, Ui_RareSettings):
for f in log_dir().iterdir()
if log_dir().joinpath(f).is_file()
)
self.log_dir_size_label.setText(format_size(size))
self.ui.log_dir_size_label.setText(format_size(size))
@pyqtSlot()
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"):
return
self.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.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))
QMessageBox.warning(
@ -174,15 +177,16 @@ class RareSettings(QWidget, Ui_RareSettings):
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
)
@pyqtSlot()
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"):
return
self.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.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(
@ -191,43 +195,46 @@ class RareSettings(QWidget, Ui_RareSettings):
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
)
def on_color_select_changed(self, scheme):
@pyqtSlot(int)
def on_color_select_changed(self, index: int):
scheme = self.ui.color_select.itemData(index, Qt.UserRole)
if scheme:
self.style_select.setCurrentIndex(0)
self.style_select.setDisabled(True)
self.settings.setValue("color_scheme", self.color_select.currentText())
set_color_pallete(self.color_select.currentText())
self.ui.style_select.setCurrentIndex(0)
self.ui.style_select.setDisabled(True)
else:
self.settings.setValue("color_scheme", "")
self.style_select.setDisabled(False)
set_color_pallete("")
self.ui.style_select.setDisabled(False)
self.settings.setValue("color_scheme", scheme)
set_color_pallete(scheme)
def on_style_select_changed(self, style):
@pyqtSlot(int)
def on_style_select_changed(self, index: int):
style = self.ui.style_select.itemData(index, Qt.UserRole)
if style:
self.color_select.setCurrentIndex(0)
self.color_select.setDisabled(True)
self.settings.setValue("style_sheet", self.style_select.currentText())
set_style_sheet(self.style_select.currentText())
self.ui.color_select.setCurrentIndex(0)
self.ui.color_select.setDisabled(True)
else:
self.settings.setValue("style_sheet", "")
self.color_select.setDisabled(False)
set_style_sheet("")
self.ui.color_select.setDisabled(False)
self.settings.setValue("style_sheet", style)
set_style_sheet(style)
def open_dir(self):
if platform.system() == "Windows":
os.startfile(log_dir()) # pylint: disable=E1101
else:
opener = "open" if sys.platform == "darwin" else "xdg-open"
subprocess.Popen([opener, log_dir()])
@pyqtSlot(int)
def on_view_combo_changed(self, index: int):
view = LibraryView(self.ui.view_combo.itemData(index, Qt.UserRole))
self.settings.setValue(options.library_view.key, int(view))
@pyqtSlot()
def open_directory(self):
QDesktopServices.openUrl(QUrl(f"file://{log_dir()}"))
@pyqtSlot()
def save_window_size(self):
self.settings.setValue("save_size", self.save_size.isChecked())
self.settings.remove("window_size")
self.settings.setValue(options.save_size.key, self.ui.save_size.isChecked())
self.settings.remove(options.window_size.key)
def update_lang(self, i: int):
self.settings.setValue("language", languages[i][0])
def init_checkboxes(self, checkboxes):
for cb in checkboxes:
widget, option, default = cb
widget.setChecked(self.settings.value(option, default, bool))
@pyqtSlot(int)
def on_lang_changed(self, index: int):
lang_code = self.ui.lang_select.itemData(index, Qt.UserRole)
if lang_code == locale.getlocale()[0]:
self.settings.remove(options.language.key)
else:
self.settings.setValue(options.language.key, lang_code)

View file

@ -0,0 +1,43 @@
import platform as pf
from logging import getLogger
from .widgets.env_vars import EnvVars
from .widgets.game import GameSettingsBase
from .widgets.launch import LaunchSettingsBase
from .widgets.overlay import DxvkSettings
from .widgets.wrappers import WrapperSettings
if pf.system() != "Windows":
from .widgets.wine import WineSettings
if pf.system() in {"Linux", "FreeBSD"}:
from .widgets.proton import ProtonSettings
from .widgets.overlay import MangoHudSettings
logger = getLogger("GameSettings")
class LaunchSettings(LaunchSettingsBase):
def __init__(self, parent=None):
super(LaunchSettings, self).__init__(WrapperSettings, parent=parent)
class GameSettings(GameSettingsBase):
def __init__(self, parent=None):
if pf.system() in {"Linux", "FreeBSD"}:
super(GameSettings, self).__init__(
LaunchSettings, DxvkSettings, EnvVars,
WineSettings, ProtonSettings, MangoHudSettings,
parent=parent
)
elif pf.system() != "Windows":
super(GameSettings, self).__init__(
LaunchSettings, DxvkSettings, EnvVars,
WineSettings,
parent=parent
)
else:
super(GameSettings, self).__init__(
LaunchSettings, DxvkSettings, EnvVars,
parent=parent
)

View file

@ -1,26 +0,0 @@
from PyQt5.QtCore import QCoreApplication
from .overlay_settings import OverlaySettings, CustomOption
class DxvkSettings(OverlaySettings):
def __init__(self):
super(DxvkSettings, self).__init__(
[
("fps", QCoreApplication.translate("DxvkSettings", "FPS")),
("frametime", QCoreApplication.translate("DxvkSettings", "Frametime")),
("memory", QCoreApplication.translate("DxvkSettings", "Memory usage")),
("gpuload", QCoreApplication.translate("DxvkSettings", "GPU usage")),
("devinfo", QCoreApplication.translate("DxvkSettings", "Show Device info")),
("version", QCoreApplication.translate("DxvkSettings", "DXVK Version")),
("api", QCoreApplication.translate("DxvkSettings", "D3D feature level")),
("compiler", QCoreApplication.translate("DxvkSettings", "Compiler activity")),
],
[
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
],
"DXVK_HUD", "0"
)
self.setTitle(self.tr("DXVK Settings"))
self.gb_options.setTitle(self.tr("Custom options"))

View file

@ -1,6 +1,7 @@
from logging import getLogger
from PyQt5.QtCore import QFileSystemWatcher, Qt
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import (
QGroupBox,
QHeaderView,
@ -20,6 +21,7 @@ class EnvVars(QGroupBox):
self.setTitle(self.tr("Environment variables"))
self.core = LegendaryCoreSingleton()
self.app_name: str = "default"
self.table_model = EnvVarsTableModel(self.core)
self.table_view = QTableView(self)
@ -44,8 +46,14 @@ class EnvVars(QGroupBox):
layout = QVBoxLayout(self)
layout.addWidget(self.table_view)
def keyPressEvent(self, e):
if e.key() in {Qt.Key_Delete, Qt.Key_Backspace}:
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_Delete, Qt.Key_Backspace}:
indexes = self.table_view.selectedIndexes()
if not len(indexes):
return
@ -54,11 +62,8 @@ class EnvVars(QGroupBox):
self.table_view.model().removeRow(idx.row())
elif idx.column() == 1:
self.table_view.model().setData(idx, "", Qt.EditRole)
elif e.key() == Qt.Key_Escape:
e.ignore()
elif a0.key() == Qt.Key_Escape:
a0.ignore()
def reset_model(self):
self.table_model.reset()
def update_game(self, app_name):
self.table_model.load(app_name)

View file

@ -11,12 +11,14 @@ from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
if platform.system() != "Windows":
if platform.system() != "Darwin":
from rare.utils import proton
from rare.utils.compat.wine import get_wine_environment
if platform.system() in {"Linux", "FreeBSD"}:
from rare.utils.compat.steam import get_steam_environment
class EnvVarsTableModel(QAbstractTableModel):
def __init__(self, core: LegendaryCore, parent = None):
def __init__(self, core: LegendaryCore, parent=None):
super(EnvVarsTableModel, self).__init__(parent=parent)
self.core = core
@ -26,15 +28,15 @@ class EnvVarsTableModel(QAbstractTableModel):
self.__validator = re.compile(r"(^[A-Za-z_][A-Za-z0-9_]*)")
self.__data_map: ChainMap = ChainMap()
self.__readonly = [
"STEAM_COMPAT_DATA_PATH",
"WINEPREFIX",
self.__readonly = {
"DXVK_HUD",
"MANGOHUD",
"MANGOHUD_CONFIG",
]
}
if platform.system() != "Windows":
if platform.system() != "Darwin":
self.__readonly.extend(proton.get_steam_environment(None).keys())
self.__readonly.update(get_wine_environment().keys())
if platform.system() in {"Linux", "FreeBSD"}:
self.__readonly.update(get_steam_environment().keys())
self.__default: str = "default"
self.__appname: str = None
@ -256,8 +258,6 @@ class EnvVarsTableModel(QAbstractTableModel):
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QTableView, QHeaderView
from rare.resources import static_css
from rare.resources.stylesheets import RareStyle
from rare.utils.misc import set_style_sheet
from legendary.core import LegendaryCore

View file

@ -0,0 +1,81 @@
import platform as pf
from typing import Type
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtGui import QHideEvent
from PyQt5.QtWidgets import (
QWidget,
QVBoxLayout
)
from rare.shared import LegendaryCoreSingleton
from rare.utils import config_helper as config
from rare.widgets.side_tab import SideTabContents
from .env_vars import EnvVars
from .launch import LaunchSettingsType
from .overlay import DxvkSettings
if pf.system() != "Windows":
from .wine import WineSettings
if pf.system() in {"Linux", "FreeBSD"}:
from .proton import ProtonSettings
from .overlay import MangoHudSettings
class GameSettingsBase(QWidget, SideTabContents):
def __init__(
self,
launch_widget: Type[LaunchSettingsType],
dxvk_widget: Type[DxvkSettings],
envvar_widget: Type[EnvVars],
wine_widget: Type['WineSettings'] = None,
proton_widget: Type['ProtonSettings'] = None,
mangohud_widget: Type['MangoHudSettings'] = None,
parent=None
):
super(GameSettingsBase, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton()
self.settings = QSettings(self)
self.app_name: str = "default"
self.launch = launch_widget(self)
self.env_vars = envvar_widget(self)
if pf.system() != "Windows":
self.wine = wine_widget(self)
self.wine.environ_changed.connect(self.env_vars.reset_model)
if pf.system() in {"Linux", "FreeBSD"}:
self.proton_tool = proton_widget(self)
self.proton_tool.environ_changed.connect(self.env_vars.reset_model)
self.proton_tool.tool_enabled.connect(self.wine.tool_enabled)
self.proton_tool.tool_enabled.connect(self.launch.tool_enabled)
self.dxvk = dxvk_widget(self)
self.dxvk.environ_changed.connect(self.env_vars.reset_model)
if pf.system() in {"Linux", "FreeBSD"}:
self.mangohud = mangohud_widget(self)
self.mangohud.environ_changed.connect(self.env_vars.reset_model)
self.main_layout = QVBoxLayout(self)
self.main_layout.addWidget(self.launch)
if pf.system() != "Windows":
self.main_layout.addWidget(self.wine)
if pf.system() in {"Linux", "FreeBSD"}:
self.main_layout.addWidget(self.proton_tool)
self.main_layout.addWidget(self.dxvk)
if pf.system() in {"Linux", "FreeBSD"}:
self.main_layout.addWidget(self.mangohud)
self.main_layout.addWidget(self.env_vars)
self.main_layout.setAlignment(Qt.AlignTop)
def hideEvent(self, a0: QHideEvent):
if a0.spontaneous():
return super().hideEvent(a0)
config.save_config()
return super().hideEvent(a0)

View file

@ -0,0 +1,91 @@
import os
import shutil
from typing import Tuple, Type, TypeVar
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QCheckBox, QFileDialog, QFormLayout, QVBoxLayout, QGroupBox
from rare.shared import LegendaryCoreSingleton
import rare.utils.config_helper as config
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from .wrappers import WrapperSettings
class LaunchSettingsBase(QGroupBox):
def __init__(
self,
wrapper_widget: Type[WrapperSettings],
parent=None
):
super(LaunchSettingsBase, self).__init__(parent=parent)
self.setTitle(self.tr("Launch settings"))
self.core = LegendaryCoreSingleton()
self.app_name: str = "default"
self.prelaunch_edit = PathEdit(
path="",
placeholder=self.tr("Path to script or program to run before the game launches"),
file_mode=QFileDialog.ExistingFile,
edit_func=self.__prelaunch_edit_callback,
save_func=self.__prelaunch_save_callback,
)
self.wrappers_widget = wrapper_widget(self)
self.prelaunch_check = QCheckBox(self.tr("Wait for command to finish before starting the game"))
font = self.font()
font.setItalic(True)
self.prelaunch_check.setFont(font)
self.prelaunch_check.stateChanged.connect(self.__prelauch_check_changed)
prelaunch_layout = QVBoxLayout()
prelaunch_layout.addWidget(self.prelaunch_edit)
prelaunch_layout.addWidget(self.prelaunch_check)
self.main_layout = QFormLayout(self)
self.main_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
self.main_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.main_layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
self.main_layout.addRow(self.tr("Wrappers"), self.wrappers_widget)
self.main_layout.addRow(self.tr("Prelaunch"), prelaunch_layout)
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
command = config.get_option(self.app_name, "pre_launch_command", fallback="")
wait = config.get_boolean(self.app_name, "pre_launch_wait", fallback=False)
self.prelaunch_edit.setText(command)
self.prelaunch_check.setChecked(wait)
self.prelaunch_check.setEnabled(bool(command))
return super().showEvent(a0)
@pyqtSlot()
def tool_enabled(self):
self.wrappers_widget.update_state()
@staticmethod
def __prelaunch_edit_callback(text: str) -> Tuple[bool, str, int]:
if not text.strip():
return True, text, IndicatorReasonsCommon.VALID
if not os.path.isfile(text.split()[0]) and not shutil.which(text.split()[0]):
return False, text, IndicatorReasonsCommon.FILE_NOT_EXISTS
else:
return True, text, IndicatorReasonsCommon.VALID
def __prelaunch_save_callback(self, text):
config.save_option(self.app_name, "pre_launch_command", text)
self.prelaunch_check.setEnabled(bool(text))
if not text:
config.remove_option(self.app_name, "pre_launch_wait")
def __prelauch_check_changed(self):
config.set_boolean(self.app_name, "pre_launch_wait", self.prelaunch_check.isChecked())
LaunchSettingsType = TypeVar("LaunchSettingsType", bound=LaunchSettingsBase)

View file

@ -1,88 +0,0 @@
import os
import shutil
from logging import getLogger
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QFileDialog, QWidget
from rare.components.tabs.settings.widgets.dxvk import DxvkSettings
from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.utils import config_helper
logger = getLogger("LinuxSettings")
class LinuxSettings(QWidget):
# str: option key
environ_changed = pyqtSignal(str)
def __init__(self, name=None, parent=None):
super(LinuxSettings, self).__init__(parent=parent)
self.ui = Ui_LinuxSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.name = name if name is not None else "default"
# Wine prefix
self.wine_prefix = PathEdit(
self.load_prefix(),
file_mode=QFileDialog.DirectoryOnly,
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_prefix,
)
self.ui.prefix_layout.addWidget(self.wine_prefix)
# Wine executable
self.wine_exec = PathEdit(
self.load_setting(self.name, "wine_executable"),
file_mode=QFileDialog.ExistingFile,
name_filters=["wine", "wine64"],
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=lambda text: self.save_setting(
text, section=self.name, setting="wine_executable"
),
)
self.ui.exec_layout.addWidget(self.wine_exec)
# dxvk
self.dxvk = DxvkSettings()
self.dxvk.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.dxvk)
self.dxvk.load_settings(self.name)
self.mangohud = MangoHudSettings()
self.mangohud.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.mangohud)
self.mangohud.load_settings(self.name)
def load_prefix(self) -> str:
return self.load_setting(
f"{self.name}.env",
"WINEPREFIX",
fallback=self.load_setting(self.name, "wine_prefix"),
)
def save_prefix(self, text: str):
self.save_setting(text, f"{self.name}.env", "WINEPREFIX")
self.environ_changed.emit("WINEPREFIX")
self.save_setting(text, self.name, "wine_prefix")
self.signals.application.prefix_updated.emit()
def load_setting(self, section: str, setting: str, fallback: str = ""):
return self.core.lgd.config.get(section, setting, fallback=fallback)
def save_setting(self, text: str, section: str, setting: str):
if text:
config_helper.add_option(section, setting, text)
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")
else:
config_helper.remove_option(section, setting)
logger.debug(f"Unset {setting} from {f'[{section}]'}")
config_helper.save_config()

View file

@ -1,108 +0,0 @@
import shutil
from enum import Enum
from PyQt5.QtCore import QCoreApplication, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from rare.shared import LegendaryCoreSingleton
from .overlay_settings import OverlaySettings, CustomOption, ActivationStates
from rare.utils import config_helper
position_values = ["default", "top-left", "top-right", "middle-left", "middle-right", "bottom-left",
"bottom-right", "top-center"]
class MangoHudSettings(OverlaySettings):
set_wrapper_activated = pyqtSignal(bool)
def __init__(self):
super(MangoHudSettings, self).__init__(
[
("fps", QCoreApplication.translate("MangoSettings", "FPS")),
("frame_timing", QCoreApplication.translate("MangoSettings", "Frame Time")),
("cpu_stats", QCoreApplication.translate("MangoSettings", "CPU Load")),
("gpu_stats", QCoreApplication.translate("MangoSettings", "GPU Load")),
("cpu_temp", QCoreApplication.translate("MangoSettings", "CPU Temp")),
("gpu_temp", QCoreApplication.translate("MangoSettings", "GPU Temp")),
("ram", QCoreApplication.translate("MangoSettings", "Memory usage")),
("vram", QCoreApplication.translate("MangoSettings", "VRAM usage")),
("time", QCoreApplication.translate("MangoSettings", "Local Time")),
("version", QCoreApplication.translate("MangoSettings", "MangoHud Version")),
("arch", QCoreApplication.translate("MangoSettings", "System architecture")),
("histogram", QCoreApplication.translate("MangoSettings", "FPS Graph")),
("gpu_name", QCoreApplication.translate("MangoSettings", "GPU Name")),
("cpu_power", QCoreApplication.translate("MangoSettings", "CPU Power consumption")),
("gpu_power", QCoreApplication.translate("MangoSettings", "GPU Power consumption")),
],
[
(
CustomOption.number_input("font_size", 24, is_float=False),
QCoreApplication.translate("MangoSettings", "Font size")
),
(
CustomOption.select_input("position", position_values),
QCoreApplication.translate("MangoSettings", "Position")
)
],
"MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state
)
self.core = LegendaryCoreSingleton()
self.setTitle(self.tr("MangoHud Settings"))
self.gb_options.setTitle(self.tr("Custom options"))
def load_settings(self, name: str):
self.settings_updatable = False
self.name = name
# override
cfg = self.core.lgd.config.get(f"{name}.env", "MANGOHUD_CONFIG", fallback="")
activated = "mangohud" in self.core.lgd.config.get(name, "wrapper", fallback="")
if not activated:
self.settings_updatable = False
self.gb_options.setDisabled(True)
for i, checkbox in enumerate(list(self.checkboxes.values())):
checkbox.setChecked(i < 4)
self.show_overlay_combo.setCurrentIndex(0)
self.settings_updatable = True
return
super(MangoHudSettings, self).load_settings(name)
self.settings_updatable = False
self.show_overlay_combo.setCurrentIndex(2)
self.gb_options.setDisabled(False)
for var_name, checkbox in list(self.checkboxes.items())[:4]:
checkbox.setChecked(f"{var_name}=0" not in cfg)
self.settings_updatable = True
def set_activation_state(self, state: Enum): # pylint: disable=E0202
if state in [ActivationStates.DEFAULT, ActivationStates.HIDDEN]:
self.set_wrapper_activated.emit(False)
self.gb_options.setDisabled(True)
elif state == ActivationStates.ACTIVATED:
if not shutil.which("mangohud"):
self.show_overlay_combo.setCurrentIndex(0)
QMessageBox.warning(self, "Error", self.tr("Mangohud is not installed or not in path"))
return
cfg = self.core.lgd.config.get(f"{self.name}.env", "MANGOHUD_CONFIG", fallback="")
split_config = cfg.split(",")
for name in list(self.checkboxes.keys())[:4]:
if name in split_config:
split_config.remove(name)
cfg = ",".join(split_config)
for var_name, checkbox in list(self.checkboxes.items())[:4]: # first three are by default activated
if not checkbox.isChecked():
if cfg:
cfg += f",{var_name}=0"
else:
cfg = f"{var_name}=0"
if cfg:
config_helper.add_option(f"{self.name}.env", "MANGOHUD_CONFIG", cfg)
self.environ_changed.emit(self.config_env_var_name)
else:
config_helper.remove_option(f"{self.name}.env", "MANGOHUD_CONFIG")
self.environ_changed.emit(self.config_env_var_name)
self.set_wrapper_activated.emit(True)

View file

@ -0,0 +1,337 @@
from abc import abstractmethod
from enum import IntEnum
from logging import getLogger
from typing import List, Dict, Tuple, Union, Optional
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QShowEvent
from PyQt5.QtWidgets import QGroupBox, QCheckBox, QLineEdit, QComboBox
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
from rare.utils import config_helper as config
logger = getLogger("GameOverlays")
class OverlayLineEdit(QLineEdit):
def __init__(self, option: str, placeholder: str, parent=None):
self.option = option
super(OverlayLineEdit, self).__init__(parent=parent)
self.valueChanged = self.textChanged
self.setPlaceholderText(placeholder)
def setDefault(self):
self.setText("")
def getValue(self) -> Optional[str]:
return f"{self.option}={text}" if (text := self.text()) else None
def setValue(self, options: Dict[str, str]):
if (value := options.get(self.option, None)) is not None:
self.setText(value)
options.pop(self.option)
else:
self.setDefault()
class OverlayComboBox(QComboBox):
def __init__(self, option: str, parent=None):
self.option = option
super(OverlayComboBox, self).__init__(parent=parent)
self.valueChanged = self.currentIndexChanged
def setDefault(self):
self.setCurrentIndex(0)
def getValue(self) -> Optional[str]:
return f"{self.option}={self.currentText()}" if self.currentIndex() > 0 else None
def setValue(self, options: Dict[str, str]):
if (value := options.get(self.option, None)) is not None:
self.setCurrentText(value)
options.pop(self.option)
else:
self.setDefault()
class OverlayCheckBox(QCheckBox):
def __init__(self, option: str, title: str, desc: str = "", default_enabled: bool = False, parent=None):
self.option = option
super().__init__(title, parent=parent)
self.setChecked(default_enabled)
self.default_enabled = default_enabled
self.setToolTip(desc)
def setDefault(self):
self.setChecked(self.default_enabled)
def getValue(self) -> Optional[str]:
# lk: return the check state in case of non-default, otherwise None
checked = self.isChecked()
value = f"{self.option}={int(checked)}" if self.default_enabled else self.option
return value if checked ^ self.default_enabled else None
def setValue(self, options: Dict[str, str]):
if options.get(self.option, None) is not None:
self.setChecked(not self.default_enabled)
options.pop(self.option)
else:
self.setChecked(self.default_enabled)
class OverlayStringInput(OverlayLineEdit):
def __init__(self, option: str, placeholder: str, parent=None):
super().__init__(option, placeholder, parent=parent)
class OverlayNumberInput(OverlayLineEdit):
def __init__(self, option: str, placeholder: Union[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: List, parent=None):
super().__init__(option, parent=parent)
# self.addItems([str(v) for v in values])
self.addItems(map(str, values))
class ActivationStates(IntEnum):
GLOBAL = -1
DISABLED = 0
DEFAULTS = 1
CUSTOM = 2
class OverlaySettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
def __init__(self, parent=None):
super(OverlaySettings, self).__init__(parent=parent)
self.ui = Ui_OverlaySettings()
self.ui.setupUi(self)
self.ui.show_overlay_combo.addItem(self.tr("Global"), ActivationStates.GLOBAL)
self.ui.show_overlay_combo.addItem(self.tr("Disabled"), ActivationStates.DISABLED)
self.ui.show_overlay_combo.addItem(self.tr("Enabled (defaults)"), ActivationStates.DEFAULTS)
self.ui.show_overlay_combo.addItem(self.tr("Enabled (custom)"), ActivationStates.CUSTOM)
self.envvar: str = None
self.force_disabled: str = None
self.force_defaults: str = None
self.app_name: str = "default"
self.option_widgets: List[Union[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.show_overlay_combo.currentIndexChanged.connect(self.update_settings)
def setupWidget(
self,
grid_map: List[OverlayCheckBox],
form_map: List[Tuple[Union[OverlayLineEdit, OverlayComboBox], str]],
envvar: str,
force_disabled: str,
force_defaults: str,
):
self.envvar = envvar
self.force_disabled = force_disabled
self.force_defaults = force_defaults
for i, widget in enumerate(grid_map):
widget.setParent(self.ui.options_group)
self.ui.options_grid.addWidget(widget, i // 4, i % 4)
# self.checkboxes[widget.option] = widget
self.option_widgets.append(widget)
widget.stateChanged.connect(self.update_settings)
for widget, label in form_map:
widget.setParent(self.ui.options_group)
self.ui.options_form.addRow(label, widget)
# self.values[widget.option] = widget
self.option_widgets.append(widget)
widget.valueChanged.connect(self.update_settings)
@abstractmethod
def update_settings_override(self, state: ActivationStates):
raise NotImplementedError
def update_settings(self):
current_state = self.ui.show_overlay_combo.currentData(Qt.UserRole)
self.ui.options_group.setEnabled(current_state == ActivationStates.CUSTOM)
if current_state == ActivationStates.GLOBAL:
# System default (don't add any env variables)
config.remove_envvar(self.app_name, self.envvar)
elif current_state == ActivationStates.DISABLED:
# hidden
config.set_envvar(self.app_name, self.envvar, self.force_disabled)
elif current_state == ActivationStates.DEFAULTS:
config.set_envvar(self.app_name, self.envvar, self.force_defaults)
elif current_state == ActivationStates.CUSTOM:
self.ui.options_group.setDisabled(False)
# custom options
options = (name for widget in self.option_widgets if (name := widget.getValue()) is not None)
config.set_envvar(self.app_name, self.envvar, ",".join(options))
self.environ_changed.emit(self.envvar)
self.update_settings_override(current_state)
def setCurrentState(self, state: ActivationStates):
self.ui.show_overlay_combo.setCurrentIndex(self.ui.show_overlay_combo.findData(state, Qt.UserRole))
self.ui.options_group.setEnabled(state == ActivationStates.CUSTOM)
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
self.ui.options_group.blockSignals(True)
config_options = config.get_envvar(self.app_name, self.envvar, fallback=None)
if config_options is None:
logger.debug("Overlay setting %s is not present", self.envvar)
self.setCurrentState(ActivationStates.GLOBAL)
elif config_options == self.force_disabled:
self.setCurrentState(ActivationStates.DISABLED)
elif config_options == self.force_defaults:
self.setCurrentState(ActivationStates.DEFAULTS)
else:
self.setCurrentState(ActivationStates.CUSTOM)
opts = {}
for o in config_options.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"
for widget in self.option_widgets:
widget.setValue(opts)
if opts:
logger.info("Remaining options without a gui switch: %s", ",".join(opts.keys()))
self.ui.options_group.blockSignals(False)
return super().showEvent(a0)
class DxvkSettings(OverlaySettings):
def __init__(self, parent=None):
super(DxvkSettings, self).__init__(parent=parent)
self.setTitle(self.tr("DXVK settings"))
grid = [
OverlayCheckBox("fps", self.tr("FPS")),
OverlayCheckBox("frametime", self.tr("Frametime")),
OverlayCheckBox("memory", self.tr("Memory usage")),
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")),
]
form = [(OverlayNumberInput("scale", 1.0), self.tr("Scale"))]
self.setupWidget(grid, form, "DXVK_HUD", "0", "1")
def update_settings_override(self, state: ActivationStates):
pass
mangohud_position = [
"default",
"top-left",
"top-right",
"middle-left",
"middle-right",
"bottom-left",
"bottom-right",
"top-center",
]
class MangoHudSettings(OverlaySettings):
def __init__(self, parent=None):
super(MangoHudSettings, self).__init__(parent=parent)
self.setTitle(self.tr("MangoHud settings"))
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")),
]
form = [
(OverlayNumberInput("font_size", 24), self.tr("Font size")),
(OverlaySelectInput("position", mangohud_position), self.tr("Position")),
]
self.setupWidget(grid, form, "MANGOHUD_CONFIG", "no_display", "read_cfg")
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
self.ui.options_group.blockSignals(True)
self.ui.options_group.blockSignals(False)
return super().showEvent(a0)
def update_settings_override(self, state: IntEnum): # pylint: disable=E0202
if state == ActivationStates.GLOBAL:
config.remove_envvar(self.app_name, "MANGOHUD")
elif state == ActivationStates.DISABLED:
config.set_envvar(self.app_name, "MANGOHUD", "0")
elif state == ActivationStates.DEFAULTS:
config.set_envvar(self.app_name, "MANGOHUD", "1")
elif state == ActivationStates.CUSTOM:
config.set_envvar(self.app_name, "MANGOHUD", "1")
self.environ_changed.emit("MANGOHUD")
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout
from legendary.core import LegendaryCore
core = LegendaryCore()
config.init_config_handler(core)
app = QApplication(sys.argv)
dlg = QDialog()
dxvk = DxvkSettings(dlg)
mangohud = MangoHudSettings(dlg)
layout = QVBoxLayout(dlg)
layout.addWidget(dxvk)
layout.addWidget(mangohud)
dlg.show()
ret = app.exec()
config.save_config()
sys.exit(ret)

View file

@ -1,190 +0,0 @@
from enum import Enum
from logging import getLogger
from typing import List, Dict, Tuple, Any, Callable
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QIntValidator, QDoubleValidator
from PyQt5.QtWidgets import QGroupBox, QCheckBox, QWidget, QLineEdit, QLabel, QComboBox
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
from rare.utils import config_helper
logger = getLogger("GameOverlays")
class TextInputField(QLineEdit):
def __init__(self):
super(TextInputField, self).__init__()
self.value_changed = self.textChanged
self.set_value = self.setText
self.set_default = lambda: self.setText("")
def get_value(self):
return self.text()
class ComboBox(QComboBox):
def __init__(self):
super(ComboBox, self).__init__()
self.value_changed = self.currentIndexChanged
self.get_value = self.currentText
self.set_value = self.setCurrentText
self.set_default = lambda: self.setCurrentIndex(0)
class CustomOption:
input_field: QWidget
var_name: str
@classmethod
def string_input(cls, var_name: str, placeholder: str):
tmp = cls()
tmp.input_field = TextInputField()
tmp.var_name = var_name
tmp.input_field.setPlaceholderText(placeholder)
return tmp
@classmethod
def number_input(cls, var_name: str, placeholder: Any, is_float: bool = False):
tmp = cls()
tmp.input_field = TextInputField()
tmp.var_name = var_name
tmp.input_field.setPlaceholderText(str(placeholder))
if is_float:
validator = QDoubleValidator()
else:
validator = QIntValidator()
tmp.input_field.setValidator(validator)
return tmp
@classmethod
def select_input(cls, var_name: str, options: List[str]):
"""options: default value in options[0]"""
tmp = cls()
tmp.input_field = ComboBox()
tmp.var_name = var_name
tmp.input_field.addItems(options)
return tmp
class ActivationStates(Enum):
DEFAULT = 0
HIDDEN = 1
ACTIVATED = 2
class OverlaySettings(QGroupBox, Ui_OverlaySettings):
# str: option key
environ_changed = pyqtSignal(str)
name: str = "default"
settings_updatable = True
def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]],
config_env_var_name: str, no_display_value: str,
set_activation_state: Callable[[Enum], None] = lambda x: None):
super(OverlaySettings, self).__init__()
self.setupUi(self)
self.core = LegendaryCoreSingleton()
self.config_env_var_name = config_env_var_name
self.no_display_value = no_display_value
self.set_activation_state = set_activation_state
self.checkboxes: Dict[str, QCheckBox] = {}
for i, (var_name, translated_text) in enumerate(checkboxes_map):
cb = QCheckBox(translated_text)
self.options_grid.addWidget(cb, i // 4, i % 4)
self.checkboxes[var_name] = cb
cb.stateChanged.connect(self.update_settings)
self.values: Dict[str, QWidget] = {}
num_rows = len(checkboxes_map) // 4
for custom_option, translated_text in value_map:
input_field = custom_option.input_field
self.options_form.addRow(QLabel(translated_text), input_field)
self.values[custom_option.var_name] = input_field
input_field.value_changed.connect(self.update_settings)
num_rows += 1
self.show_overlay_combo.currentIndexChanged.connect(self.update_settings)
def update_settings(self):
if not self.settings_updatable:
return
if self.show_overlay_combo.currentIndex() == 0:
# System default
config_helper.remove_option(f"{self.name}.env", self.config_env_var_name)
self.environ_changed.emit(self.config_env_var_name)
self.gb_options.setDisabled(True)
self.set_activation_state(ActivationStates.DEFAULT)
return
elif self.show_overlay_combo.currentIndex() == 1:
# hidden
config_helper.add_option(f"{self.name}.env", self.config_env_var_name, self.no_display_value)
self.environ_changed.emit(self.config_env_var_name)
self.gb_options.setDisabled(True)
self.set_activation_state(ActivationStates.HIDDEN)
return
elif self.show_overlay_combo.currentIndex() == 2:
self.gb_options.setDisabled(False)
# custom options
var_names = []
for var_name, cb in self.checkboxes.items():
if cb.isChecked():
var_names.append(var_name)
for var_name, input_field in self.values.items():
text = input_field.get_value()
if text not in ["default", ""]:
var_names.append(f"{var_name}={text}")
if not var_names:
list(self.checkboxes.values())[0].setChecked(True)
var_names.append(list(self.checkboxes.keys())[0])
config_helper.add_option(f"{self.name}.env", self.config_env_var_name, ",".join(var_names))
self.environ_changed.emit(self.config_env_var_name)
self.set_activation_state(ActivationStates.ACTIVATED)
def load_settings(self, name: str):
self.settings_updatable = False
# load game specific
self.name = name
for checkbox in self.checkboxes.values():
checkbox.setChecked(False)
for input_field in self.values.values():
input_field.set_default()
options = self.core.lgd.config.get(f"{self.name}.env", self.config_env_var_name, fallback=None)
if options is None:
logger.debug(f"No Overlay settings found {self.config_env_var_name}")
self.show_overlay_combo.setCurrentIndex(0)
self.gb_options.setDisabled(True)
elif options == self.no_display_value:
# not visible
self.gb_options.setDisabled(True)
self.show_overlay_combo.setCurrentIndex(1)
else:
self.show_overlay_combo.setCurrentIndex(2)
for option in options.split(","):
try:
if "=" in option:
var_name, value = option.split("=")
if var_name in self.checkboxes.keys():
self.checkboxes[var_name].setChecked(False)
else:
self.values[var_name].set_value(value)
else:
self.checkboxes[option].setChecked(True)
except Exception as e:
logger.warning(e)
self.gb_options.setDisabled(False)
self.settings_updatable = True

View file

@ -1,61 +0,0 @@
import os
import shutil
from typing import Tuple
from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog
from rare.shared import LegendaryCoreSingleton
from rare.utils import config_helper
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
class PreLaunchSettings(QHBoxLayout):
app_name: str
def __init__(self):
super(PreLaunchSettings, self).__init__()
self.core = LegendaryCoreSingleton()
self.edit = PathEdit(
path="",
placeholder=self.tr("Path to script"),
file_mode=QFileDialog.ExistingFile,
edit_func=self.edit_command,
save_func=self.save_pre_launch_command,
)
self.layout().addWidget(self.edit)
self.wait_check = QCheckBox(self.tr("Wait for finish"))
self.layout().addWidget(self.wait_check)
self.wait_check.stateChanged.connect(self.save_wait_finish)
def edit_command(self, text: str) -> Tuple[bool, str, int]:
if not text.strip():
return True, text, IndicatorReasonsCommon.VALID
if not os.path.isfile(text.split()[0]) and not shutil.which(text.split()[0]):
return False, text, IndicatorReasonsCommon.FILE_NOT_EXISTS
else:
return True, text, IndicatorReasonsCommon.VALID
def save_pre_launch_command(self, text):
if text:
config_helper.add_option(self.app_name, "pre_launch_command", text)
self.wait_check.setDisabled(False)
else:
config_helper.remove_option(self.app_name, "pre_launch_command")
self.wait_check.setDisabled(True)
config_helper.remove_option(self.app_name, "pre_launch_wait")
def save_wait_finish(self):
config_helper.add_option(self.app_name, "pre_launch_wait", str(self.wait_check.isChecked()).lower())
def load_settings(self, app_name):
self.app_name = app_name
command = self.core.lgd.config.get(app_name, "pre_launch_command", fallback="")
self.edit.setText(command)
wait = self.core.lgd.config.getboolean(app_name, "pre_launch_wait", fallback=False)
self.wait_check.setChecked(wait)
self.wait_check.setEnabled(bool(command))

View file

@ -1,85 +1,122 @@
import os
from logging import getLogger
from pathlib import Path
from typing import Tuple
from typing import Tuple, Union, Optional
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QGroupBox, QFileDialog
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QFileDialog, QFormLayout, QComboBox
from rare.components.tabs.settings import LinuxSettings
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
from rare.utils import config_helper, proton
from rare.models.wrapper import Wrapper, WrapperType
from rare.shared import RareCore
from rare.shared.wrappers import Wrappers
from rare.utils import config_helper as config
from rare.utils.compat import steam
from rare.utils.paths import proton_compat_dir
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from .wrapper import WrapperSettings
logger = getLogger("Proton")
logger = getLogger("ProtonSettings")
class ProtonSettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
app_name: str
changeable = True
environ_changed: pyqtSignal = pyqtSignal(str)
# bool: state
tool_enabled: pyqtSignal = pyqtSignal(bool)
def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSettings):
super(ProtonSettings, self).__init__()
self.ui = Ui_ProtonSettings()
self.ui.setupUi(self)
self._linux_settings = linux_settings
self._wrapper_settings = wrapper_settings
self.core = LegendaryCoreSingleton()
self.possible_proton_combos = proton.find_proton_combos()
def __init__(self, parent=None):
super(ProtonSettings, self).__init__(parent=parent)
self.setTitle(self.tr("Proton settings"))
self.ui.proton_combo.addItems(self.possible_proton_combos)
self.ui.proton_combo.currentIndexChanged.connect(self.change_proton)
self.tool_combo = QComboBox(self)
self.tool_combo.currentIndexChanged.connect(self.__on_proton_changed)
self.proton_prefix = PathEdit(
self.tool_prefix = PathEdit(
file_mode=QFileDialog.DirectoryOnly,
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.ui.prefix_layout.addWidget(self.proton_prefix)
def change_proton(self, i):
if not self.changeable:
return
# First combo box entry: Don't use Proton
if i == 0:
self._wrapper_settings.delete_wrapper("proton")
config_helper.remove_option(self.app_name, "no_wine")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH")
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
layout = QFormLayout(self)
layout.addRow(self.tr("Proton tool"), self.tool_combo)
layout.addRow(self.tr("Compat data"), self.tool_prefix)
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
self.proton_prefix.setEnabled(False)
self.proton_prefix.setText("")
self.app_name: str = "default"
self.core = RareCore.instance().core()
self.wrappers: Wrappers = RareCore.instance().wrappers()
self.tool_wrapper: Optional[Wrapper] = None
self._linux_settings.ui.wine_groupbox.setEnabled(True)
else:
self.proton_prefix.setEnabled(True)
self._linux_settings.ui.wine_groupbox.setEnabled(False)
wrapper = self.possible_proton_combos[i - 1]
self._wrapper_settings.add_wrapper(wrapper)
config_helper.add_option(self.app_name, "no_wine", "true")
config_helper.add_option(
f"{self.app_name}.env",
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
str(Path.home().joinpath(".steam", "steam"))
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
self.tool_combo.blockSignals(True)
self.tool_combo.clear()
self.tool_combo.addItem(self.tr("Don't use a compatibility tool"), None)
tools = steam.find_tools()
for tool in tools:
self.tool_combo.addItem(tool.name, tool)
try:
wrapper = next(
filter(lambda w: w.is_compat_tool, self.wrappers.get_game_wrapper_list(self.app_name))
)
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.tool_wrapper = wrapper
tool = next(filter(lambda t: t.checksum == wrapper.checksum, tools))
index = self.tool_combo.findData(tool)
except StopIteration:
index = 0
self.tool_combo.setCurrentIndex(index)
self.tool_combo.blockSignals(False)
self.proton_prefix.setText(os.path.expanduser("~/.proton"))
enabled = bool(self.tool_combo.currentData(Qt.UserRole))
self.tool_prefix.blockSignals(True)
self.tool_prefix.setText(config.get_proton_compatdata(self.app_name, fallback=""))
self.tool_prefix.setEnabled(enabled)
self.tool_prefix.blockSignals(False)
# Don't use Wine
self._linux_settings.wine_exec.setText("")
self._linux_settings.wine_prefix.setText("")
super().showEvent(a0)
config_helper.save_config()
def __on_proton_changed(self, index):
steam_tool: Union[steam.ProtonTool, steam.CompatibilityTool] = self.tool_combo.itemData(index)
def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
steam_environ = steam.get_steam_environment(steam_tool, self.tool_prefix.text())
for key, value in steam_environ.items():
config.save_envvar(self.app_name, key, value)
self.environ_changed.emit(key)
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if self.tool_wrapper and self.tool_wrapper in wrappers:
wrappers.remove(self.tool_wrapper)
if steam_tool is None:
self.tool_wrapper = None
else:
wrapper = Wrapper(
command=steam_tool.command(), name=steam_tool.name, wtype=WrapperType.COMPAT_TOOL
)
wrappers.append(wrapper)
self.tool_wrapper = wrapper
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.tool_prefix.setEnabled(steam_tool is not None)
if steam_tool:
if not (compatdata_path := config.get_proton_compatdata(self.app_name, fallback="")):
compatdata_path = proton_compat_dir(self.app_name)
config.save_proton_compatdata(self.app_name, str(compatdata_path))
target = compatdata_path.joinpath("pfx")
if not target.is_dir():
os.makedirs(target, exist_ok=True)
self.tool_prefix.setText(str(compatdata_path))
else:
self.tool_prefix.setText("")
self.tool_enabled.emit(steam_tool is not None)
@staticmethod
def proton_prefix_edit(text: str) -> Tuple[bool, str, int]:
if not text:
return False, text, IndicatorReasonsCommon.EMPTY
parent_dir = os.path.dirname(text)
@ -88,28 +125,6 @@ class ProtonSettings(QGroupBox):
def proton_prefix_save(self, text: str):
if not text:
return
config_helper.add_option(
f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH", text
)
config.save_proton_compatdata(self.app_name, text)
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.save_config()
def load_settings(self, app_name: str, proton: str):
self.changeable = False
self.app_name = app_name
proton = proton.replace('"', "")
self.proton_prefix.setEnabled(bool(proton))
if proton:
self.ui.proton_combo.setCurrentText(
f'"{proton.replace(" run", "")}" run'
)
else:
self.ui.proton_combo.setCurrentIndex(0)
proton_prefix = self.core.lgd.config.get(
f"{app_name}.env",
"STEAM_COMPAT_DATA_PATH",
fallback="",
)
self.proton_prefix.setText(proton_prefix)
self.changeable = True

View file

@ -2,33 +2,35 @@ from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QGroupBox
from rare.shared import GlobalSignalsSingleton
from rare.models.options import options
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
class RPCSettings(QGroupBox, Ui_RPCSettings):
class RPCSettings(QGroupBox):
def __init__(self, parent):
super(RPCSettings, self).__init__(parent=parent)
self.setupUi(self)
self.ui = Ui_RPCSettings()
self.ui.setupUi(self)
self.signals = GlobalSignalsSingleton()
self.settings = QSettings()
self.enable.setCurrentIndex(self.settings.value("rpc_enable", 0, int))
self.enable.currentIndexChanged.connect(self.changed)
self.ui.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
self.ui.enable.currentIndexChanged.connect(self.__enable_changed)
self.show_game.setChecked((self.settings.value("rpc_name", True, bool)))
self.show_game.stateChanged.connect(
lambda: self.settings.setValue("rpc_game", self.show_game.isChecked())
self.ui.show_game.setChecked((self.settings.value(*options.rpc_name)))
self.ui.show_game.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_name.key, self.ui.show_game.isChecked())
)
self.show_os.setChecked((self.settings.value("rpc_os", True, bool)))
self.show_os.stateChanged.connect(
lambda: self.settings.setValue("rpc_os", self.show_os.isChecked())
self.ui.show_os.setChecked((self.settings.value(*options.rpc_os)))
self.ui.show_os.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_os.key, self.ui.show_os.isChecked())
)
self.show_time.setChecked((self.settings.value("rpc_time", True, bool)))
self.show_time.stateChanged.connect(
lambda: self.settings.setValue("rpc_time", self.show_time.isChecked())
self.ui.show_time.setChecked((self.settings.value(*options.rpc_time)))
self.ui.show_time.stateChanged.connect(
lambda: self.settings.setValue(options.rpc_time.key, self.ui.show_time.isChecked())
)
try:
@ -37,6 +39,6 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.setDisabled(True)
self.setToolTip(self.tr("Pypresence is not installed"))
def changed(self, i):
self.settings.setValue("rpc_enable", i)
def __enable_changed(self, i):
self.settings.setValue(options.rpc_enable.key, i)
self.signals.discord_rpc.apply_settings.emit()

View file

@ -0,0 +1,91 @@
import os
from logging import getLogger
from typing import Optional
from PyQt5.QtCore import pyqtSignal, Qt, QSignalBlocker
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QFileDialog, QFormLayout, QGroupBox
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.utils import config_helper as config
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
logger = getLogger("WineSettings")
class WineSettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
def __init__(self, parent=None):
super(WineSettings, self).__init__(parent=parent)
self.setTitle(self.tr("Wine settings"))
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.app_name: Optional[str] = "default"
# Wine prefix
self.wine_prefix = PathEdit(
path="",
file_mode=QFileDialog.DirectoryOnly,
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_prefix,
)
# Wine executable
self.wine_exec = PathEdit(
path="",
file_mode=QFileDialog.ExistingFile,
name_filters=["wine", "wine64"],
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_exec,
)
layout = QFormLayout(self)
layout.addRow(self.tr("Executable"), self.wine_exec)
layout.addRow(self.tr("Prefix"), self.wine_prefix)
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
layout.setFormAlignment(Qt.AlignLeading | Qt.AlignTop)
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
_ = QSignalBlocker(self.wine_prefix)
self.wine_prefix.setText(self.load_prefix())
_ = QSignalBlocker(self.wine_exec)
self.wine_exec.setText(self.load_exec())
self.setDisabled(config.get_boolean(self.app_name, "no_wine", fallback=False))
return super().showEvent(a0)
def tool_enabled(self, enabled: bool):
if enabled:
config.set_boolean(self.app_name, "no_wine", True)
else:
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 config.get_wine_prefix(self.app_name, "")
def save_prefix(self, path: str) -> None:
if self.app_name is None:
raise RuntimeError
config.save_wine_prefix(self.app_name, path)
self.environ_changed.emit("WINEPREFIX")
def load_exec(self) -> str:
if self.app_name is None:
raise RuntimeError
return config.get_option(self.app_name, "wine_executable", "")
def save_exec(self, text: str) -> None:
if self.app_name is None:
raise RuntimeError
config.save_option(self.app_name, "wine_executable", text)

View file

@ -1,356 +0,0 @@
import re
import shutil
from logging import getLogger
from typing import Dict, Optional
from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
from PyQt5.QtWidgets import (
QHBoxLayout,
QLabel,
QInputDialog,
QFrame,
QMessageBox,
QSizePolicy,
QWidget,
QScrollArea,
QAction,
QToolButton,
QMenu,
)
from rare.shared import RareCore
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
from rare.utils import config_helper
from rare.utils.misc import icon
logger = getLogger("WrapperSettings")
extra_wrapper_regex = {
"proton": "\".*proton\" run", # proton
"mangohud": "mangohud" # mangohud
}
class Wrapper:
pass
class WrapperWidget(QFrame):
update_wrapper = pyqtSignal(str, str)
delete_wrapper = pyqtSignal(str)
def __init__(self, text: str, show_text=None, parent=None):
super(WrapperWidget, self).__init__(parent=parent)
if not show_text:
show_text = text.split()[0]
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.text = text
self.setToolTip(text)
unmanaged = show_text in extra_wrapper_regex.keys()
text_lbl = QLabel(show_text, parent=self)
text_lbl.setFont(QFont("monospace"))
text_lbl.setDisabled(unmanaged)
image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
edit_action = QAction("Edit", parent=self)
edit_action.triggered.connect(self.__edit)
delete_action = QAction("Delete", parent=self)
delete_action.triggered.connect(self.__delete)
manage_menu = QMenu(parent=self)
manage_menu.addActions([edit_action, delete_action])
manage_button = QToolButton(parent=self)
manage_button.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup)
manage_button.setDisabled(unmanaged)
if unmanaged:
manage_button.setToolTip(self.tr("Manage through settings"))
else:
manage_button.setToolTip(self.tr("Manage"))
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(image_lbl)
layout.addWidget(text_lbl)
layout.addWidget(manage_button)
self.setLayout(layout)
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
manage_button.setObjectName(f"{self.objectName()}Button")
@pyqtSlot()
def __delete(self):
self.delete_wrapper.emit(self.text)
def __edit(self) -> None:
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Edit wrapper command"))
dialog.setTextValue(self.text)
accepted = dialog.exec()
wrapper = dialog.textValue()
dialog.deleteLater()
if accepted and wrapper:
self.update_wrapper.emit(self.text, wrapper)
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton:
a0.accept()
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
class WrapperSettings(QWidget):
def __init__(self):
super(WrapperSettings, self).__init__()
self.ui = Ui_WrapperSettings()
self.ui.setupUi(self)
self.wrappers: Dict[str, WrapperWidget] = {}
self.app_name: str = "default"
self.wrapper_scroll = QScrollArea(self.ui.widget_stack)
self.wrapper_scroll.setWidgetResizable(True)
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
self.scroll_content = WrapperContainer(
save_cb=self.save, parent=self.wrapper_scroll
)
self.wrapper_scroll.setWidget(self.scroll_content)
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
self.core = RareCore.instance().core()
self.ui.add_button.clicked.connect(self.add_button_pressed)
self.settings = QSettings()
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
self.ui.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll")
self.wrapper_scroll.horizontalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
@pyqtSlot(int, int)
def adjust_scrollarea(self, min: int, max: int):
wrapper_widget = self.scroll_content.findChild(WrapperWidget)
if not wrapper_widget:
return
# lk: when the scrollbar is not visible, min and max are 0
if max > min:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height() // 2
- self.wrapper_scroll.contentsRect().height() // 2
+ self.scroll_content.layout().spacing()
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
)
else:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height()
- self.wrapper_scroll.contentsRect().height()
)
def get_wrapper_string(self):
return " ".join(self.get_wrapper_list())
def get_wrapper_list(self):
wrappers = list(self.wrappers.values())
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
return [w.text for w in wrappers]
def add_button_pressed(self):
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Enter wrapper command"))
accepted = dialog.exec()
wrapper = dialog.textValue()
dialog.deleteLater()
if accepted:
self.add_wrapper(wrapper)
def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
if text == "mangohud" and self.wrappers.get("mangohud"):
return
show_text = ""
for key, extra_wrapper in extra_wrapper_regex.items():
if re.match(extra_wrapper, text):
show_text = key
if not show_text:
show_text = text.split()[0]
# validate
if not text.strip(): # is empty
return
if not from_load:
if self.wrappers.get(text):
QMessageBox.warning(
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(text)
)
return
if show_text != "proton" and not shutil.which(text.split()[0]):
if (
QMessageBox.question(
self,
self.tr("Warning"),
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
== QMessageBox.No
):
return
if text == "proton":
QMessageBox.warning(
self,
self.tr("Warning"),
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
)
return
self.ui.widget_stack.setCurrentIndex(0)
if widget := self.wrappers.get(show_text, None):
widget.deleteLater()
widget = WrapperWidget(text, show_text, self.scroll_content)
if position < 0:
self.scroll_content.layout().addWidget(widget)
else:
self.scroll_content.layout().insertWidget(position, widget)
self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum(),
)
widget.update_wrapper.connect(self.update_wrapper)
widget.delete_wrapper.connect(self.delete_wrapper)
self.wrappers[show_text] = widget
if not from_load:
self.save()
@pyqtSlot(str)
def delete_wrapper(self, text: str):
text = text.split()[0]
widget = self.wrappers.get(text, None)
if widget:
self.wrappers.pop(text)
widget.deleteLater()
if not self.wrappers:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
self.save()
@pyqtSlot(str, str)
def update_wrapper(self, old: str, new: str):
key = old.split()[0]
idx = self.scroll_content.layout().indexOf(self.wrappers[key])
self.delete_wrapper(key)
self.add_wrapper(new, position=idx)
def save(self):
# save wrappers twice, to support wrappers with spaces
if len(self.wrappers) == 0:
config_helper.remove_option(self.app_name, "wrapper")
self.settings.remove(f"{self.app_name}/wrapper")
else:
config_helper.add_option(self.app_name, "wrapper", self.get_wrapper_string())
self.settings.setValue(f"{self.app_name}/wrapper", self.get_wrapper_list())
def load_settings(self, app_name: str):
self.app_name = app_name
for i in self.wrappers.values():
i.deleteLater()
self.wrappers.clear()
wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str)
if not wrappers and (cfg := self.core.lgd.config.get(self.app_name, "wrapper", fallback="")):
logger.info("Loading wrappers from legendary config")
# no qt wrapper, but legendary wrapper, to have backward compatibility
pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
wrappers = pattern.split(cfg)[1::2]
for wrapper in wrappers:
self.add_wrapper(wrapper, from_load=True)
if not self.wrappers:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
else:
self.ui.widget_stack.setCurrentIndex(0)
self.save()
class WrapperContainer(QWidget):
def __init__(self, save_cb, parent=None):
super(WrapperContainer, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.save = save_cb
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.setLayout(layout)
self.drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
def dragEnterEvent(self, e: QDragEnterEvent):
widget = e.source()
self.drag_widget = widget
e.accept()
def _get_drop_index(self, x):
drag_idx = self.layout().indexOf(self.drag_widget)
if drag_idx > 0:
prev_widget = self.layout().itemAt(drag_idx - 1).widget()
if x < self.drag_widget.x() - prev_widget.width() // 2:
return drag_idx - 1
if drag_idx < self.layout().count() - 1:
next_widget = self.layout().itemAt(drag_idx + 1).widget()
if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2:
return drag_idx + 1
return drag_idx
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
i = self._get_drop_index(e.pos().x())
self.layout().insertWidget(i, self.drag_widget)
def dropEvent(self, e: QDropEvent):
pos = e.pos()
widget = e.source()
index = self._get_drop_index(pos.x())
self.layout().insertWidget(index, widget)
self.drag_widget = None
e.accept()
self.save()

View file

@ -0,0 +1,417 @@
import platform as pf
import shlex
import shutil
from logging import getLogger
from typing import Optional, Tuple, Iterable
from PyQt5.QtCore import pyqtSignal, QSize, Qt, QMimeData, pyqtSlot
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent, QShowEvent
from PyQt5.QtWidgets import (
QHBoxLayout,
QLabel,
QFrame,
QMessageBox,
QSizePolicy,
QWidget,
QScrollArea,
QAction,
QToolButton,
QMenu, QStackedWidget, QPushButton, QLineEdit, QVBoxLayout, QComboBox,
)
from rare.models.wrapper import Wrapper
from rare.shared import RareCore
from rare.utils.misc import icon
from rare.widgets.dialogs import ButtonDialog, game_title
if pf.system() in {"Linux", "FreeBSD"}:
from rare.utils.compat import steam
logger = getLogger("WrapperSettings")
class WrapperEditDialog(ButtonDialog):
result_ready = pyqtSignal(bool, str)
def __init__(self, parent=None):
super(WrapperEditDialog, self).__init__(parent=parent)
self.line_edit = QLineEdit(self)
self.line_edit.textChanged.connect(self.__on_text_changed)
self.widget_layout = QVBoxLayout()
self.widget_layout.addWidget(self.line_edit)
self.setCentralLayout(self.widget_layout)
self.accept_button.setText(self.tr("Save"))
self.accept_button.setIcon(icon("fa.edit"))
self.accept_button.setEnabled(False)
self.result: Tuple = ()
def setup(self, wrapper: Wrapper):
header = self.tr("Edit wrapper")
self.setWindowTitle(header)
self.setSubtitle(game_title(header, wrapper.name))
self.line_edit.setText(wrapper.as_str)
@pyqtSlot(str)
def __on_text_changed(self, text: str):
self.accept_button.setEnabled(bool(text))
def done_handler(self):
self.result_ready.emit(*self.result)
def accept_handler(self):
self.result = (True, self.line_edit.text())
def reject_handler(self):
self.result = (False, self.line_edit.text())
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.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")
self.setWindowTitle(header)
self.setSubtitle(header)
for wrapper in wrappers:
self.combo_box.addItem(f"{wrapper.name} ({wrapper.as_str})", wrapper.as_str)
@pyqtSlot(int)
def __on_index_changed(self, index: int):
command = self.combo_box.itemData(index, Qt.UserRole)
self.line_edit.setText(command)
class WrapperWidget(QFrame):
# object: current, object: new
update_wrapper = pyqtSignal(object, object)
# object: current
delete_wrapper = pyqtSignal(object)
def __init__(self, wrapper: Wrapper, parent=None):
super(WrapperWidget, self).__init__(parent=parent)
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.setToolTip(wrapper.as_str)
text_lbl = QLabel(wrapper.name, parent=self)
text_lbl.setFont(QFont("monospace"))
text_lbl.setEnabled(wrapper.is_editable)
image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
edit_action = QAction("Edit", parent=self)
edit_action.triggered.connect(self.__on_edit)
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 = QToolButton(parent=self)
manage_button.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup)
manage_button.setEnabled(wrapper.is_editable)
if not wrapper.is_editable:
manage_button.setToolTip(self.tr("Manage through settings"))
else:
manage_button.setToolTip(self.tr("Manage"))
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(image_lbl)
layout.addWidget(text_lbl)
layout.addWidget(manage_button)
self.setLayout(layout)
self.wrapper = wrapper
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
manage_button.setObjectName(f"{self.objectName()}Button")
def data(self) -> Wrapper:
return self.wrapper
@pyqtSlot()
def __on_delete(self) -> None:
self.delete_wrapper.emit(self.wrapper)
self.deleteLater()
@pyqtSlot()
def __on_edit(self) -> None:
dialog = WrapperEditDialog(self)
dialog.setup(self.wrapper)
dialog.result_ready.connect(self.__on_edit_result)
dialog.show()
@pyqtSlot(bool, str)
def __on_edit_result(self, accepted: bool, command: str):
if accepted and command:
new_wrapper = Wrapper(command=shlex.split(command))
self.update_wrapper.emit(self.wrapper, new_wrapper)
self.deleteLater()
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton:
a0.accept()
if self.wrapper.is_compat_tool:
return
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
class WrapperSettings(QWidget):
def __init__(self, parent=None):
super(WrapperSettings, self).__init__(parent=parent)
self.widget_stack = QStackedWidget(self)
self.wrapper_scroll = QScrollArea(self.widget_stack)
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.wrapper_scroll.setWidgetResizable(True)
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
self.wrapper_container = WrapperContainer(parent=self.wrapper_scroll)
self.wrapper_container.orderChanged.connect(self.__on_order_changed)
self.wrapper_scroll.setWidget(self.wrapper_container)
self.no_wrapper_label = QLabel(self.tr("No wrappers defined"), self.widget_stack)
self.widget_stack.addWidget(self.wrapper_scroll)
self.widget_stack.addWidget(self.no_wrapper_label)
self.add_button = QPushButton(self.tr("Add wrapper"), self)
self.add_button.clicked.connect(self.__on_add)
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet
self.setObjectName("WrapperSettings")
self.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll")
self.wrapper_scroll.horizontalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
main_layout = QHBoxLayout(self)
main_layout.addWidget(self.widget_stack)
main_layout.addWidget(self.add_button, alignment=Qt.AlignTop)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setAlignment(Qt.AlignTop)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.app_name: str = "default"
self.core = RareCore.instance().core()
self.wrappers = RareCore.instance().wrappers()
def showEvent(self, a0: QShowEvent):
if a0.spontaneous():
return super().showEvent(a0)
self.update_state()
return super().showEvent(a0)
@pyqtSlot(int, int)
def adjust_scrollarea(self, minh: int, maxh: int):
wrapper_widget = self.wrapper_container.findChild(WrapperWidget)
if not wrapper_widget:
return
# lk: when the scrollbar is not visible, min and max are 0
if maxh > minh:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height() // 2
- self.wrapper_scroll.contentsRect().height() // 2
+ self.wrapper_container.layout().spacing()
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
)
else:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height()
- self.wrapper_scroll.contentsRect().height()
)
@pyqtSlot(QWidget, int)
def __on_order_changed(self, widget: WrapperWidget, new_index: int):
wrapper = widget.data()
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
wrappers.remove(wrapper)
wrappers.insert(new_index, wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
@pyqtSlot()
def __on_add(self) -> None:
dialog = WrapperAddDialog(self)
dialog.setup(self.wrappers.user_wrappers)
dialog.result_ready.connect(self.__on_add_result)
dialog.show()
@pyqtSlot(bool, str)
def __on_add_result(self, accepted: bool, command: str):
if accepted and command:
wrapper = Wrapper(shlex.split(command))
self.add_user_wrapper(wrapper)
def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
self.widget_stack.setCurrentWidget(self.wrapper_scroll)
widget = WrapperWidget(wrapper, self.wrapper_container)
if position < 0:
self.wrapper_container.addWidget(widget)
else:
self.wrapper_container.insertWidget(position, widget)
self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum(),
)
widget.update_wrapper.connect(self.__update_wrapper)
widget.delete_wrapper.connect(self.__delete_wrapper)
def add_wrapper(self, wrapper: Wrapper, position: int = -1):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if position < 0 or wrapper.is_compat_tool:
wrappers.append(wrapper)
else:
wrappers.insert(position, wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.__add_wrapper(wrapper, position)
def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
if not wrapper:
return
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"),
)
return
if wrapper.checksum in self.wrappers.get_game_md5sum_list(self.app_name):
QMessageBox.warning(
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> 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 <b>{0}</b> is not in $PATH. Add it anyway?").format(wrapper.executable),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if ans == QMessageBox.No:
return
self.add_wrapper(wrapper, position)
@pyqtSlot(object)
def __delete_wrapper(self, wrapper: Wrapper):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
wrappers.remove(wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
if not wrappers:
self.wrapper_scroll.setMaximumHeight(self.no_wrapper_label.sizeHint().height())
self.widget_stack.setCurrentWidget(self.no_wrapper_label)
@pyqtSlot(object, object)
def __update_wrapper(self, old: Wrapper, new: Wrapper):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
index = wrappers.index(old)
wrappers.remove(old)
wrappers.insert(index, new)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.__add_wrapper(new, index)
@pyqtSlot()
def update_state(self):
for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindDirectChildrenOnly):
w.deleteLater()
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if not wrappers:
self.wrapper_scroll.setMaximumHeight(self.no_wrapper_label.sizeHint().height())
self.widget_stack.setCurrentWidget(self.no_wrapper_label)
else:
self.widget_stack.setCurrentWidget(self.wrapper_scroll)
for wrapper in wrappers:
self.__add_wrapper(wrapper)
class WrapperContainer(QWidget):
# QWidget: moving widget, int: new index
orderChanged: pyqtSignal = pyqtSignal(QWidget, int)
def __init__(self, parent=None):
super(WrapperContainer, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.__layout = QHBoxLayout(self)
self.__layout.setContentsMargins(0, 0, 0, 0)
self.__layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.__drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
# def count(self) -> int:
# return self.__layout.count()
#
# def itemData(self, index: int) -> Any:
# widget: WrapperWidget = self.__layout.itemAt(index).widget()
# return widget.data()
def addWidget(self, widget: WrapperWidget):
self.__layout.addWidget(widget)
def insertWidget(self, index: int, widget: WrapperWidget):
self.__layout.insertWidget(index, widget)
def dragEnterEvent(self, e: QDragEnterEvent):
widget = e.source()
self.__drag_widget = widget
e.accept()
def __get_drop_index(self, x) -> int:
drag_idx = self.__layout.indexOf(self.__drag_widget)
if drag_idx > 0:
prev_widget = self.__layout.itemAt(drag_idx - 1).widget()
if x < self.__drag_widget.x() - prev_widget.width() // 2:
return drag_idx - 1
if drag_idx < self.__layout.count() - 1:
next_widget = self.__layout.itemAt(drag_idx + 1).widget()
if x > self.__drag_widget.x() + self.__drag_widget.width() + next_widget.width() // 2:
return drag_idx + 1
return drag_idx
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
new_x = self.__get_drop_index(e.pos().x())
self.__layout.insertWidget(new_x, self.__drag_widget)
def dropEvent(self, e: QDropEvent):
pos = e.pos()
widget = e.source()
new_x = self.__get_drop_index(pos.x())
self.__layout.insertWidget(new_x, widget)
self.__drag_widget = None
self.orderChanged.emit(widget, new_x)
e.accept()

View file

@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication
from rare.models.options import options
from rare.shared import RareCore
logger = getLogger("TrayIcon")
@ -57,12 +58,12 @@ class TrayIcon(QSystemTrayIcon):
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[0:5]
return last_played[:5]
@pyqtSlot(str, str)
def notify(self, title: str, body: str):
if self.settings.value("notification", True, bool):
self.showMessage(f"{QApplication.applicationName()} - {title}", body, QSystemTrayIcon.Information, 4000)
if self.settings.value(*options.notification):
self.showMessage(f"{title} - {QApplication.applicationName()}", body, QSystemTrayIcon.Information, 4000)
@pyqtSlot()
def update_actions(self):

View file

@ -90,10 +90,7 @@ class LgndrIndirectStatus:
message: str = ""
def __len__(self):
if self.message:
return 2
else:
return 0
return 2 if self.message else 0
def __bool__(self):
return self.success
@ -128,7 +125,7 @@ class LgndrIndirectLogger:
self.level = level
def _log(self, level: int, msg: str):
self.status.success = True if level < self.level else False
self.status.success = level < self.level
self.status.message = msg
if self.logger:
self.logger.log(level, msg)

View file

@ -18,6 +18,8 @@ def main() -> int:
sys.stderr = open(os.devnull, 'w')
os.environ["QT_QPA_PLATFORMTHEME"] = ""
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "Floor"
if "LEGENDARY_CONFIG_PATH" in os.environ:
os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"])
@ -57,22 +59,26 @@ def main() -> int:
)
subparsers = parser.add_subparsers(title="Commands", dest="subparser")
launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"])
launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
metavar="<App Name>", action="store")
launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
launch_minimal_parser.add_argument("--offline", help="Launch game offline",
action="store_true")
launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
default=os.environ.get('LGDRY_WINE_BINARY', None),
help='Set WINE binary to use to launch the app')
launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
default=os.environ.get('LGDRY_WINE_PREFIX', None),
help='Set WINE prefix to use')
launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
action="store_true")
launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
action="store_true")
launch_parser = subparsers.add_parser("launch", aliases=["start"])
launch_parser.add_argument("app_name", help="AppName of the game to launch",
metavar="<App Name>", action="store")
launch_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
launch_parser.add_argument("--offline", help="Launch game offline",
action="store_true")
launch_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
default=os.environ.get('LGDRY_WINE_BINARY', None),
help='Set WINE binary to use to launch the app')
launch_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
default=os.environ.get('LGDRY_WINE_PREFIX', None),
help='Set WINE prefix to use')
launch_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
action="store_true")
launch_parser.add_argument("--skip-update-check", help="Do not check for updates",
action="store_true")
login_parser = subparsers.add_parser("login", aliases=["auth"])
login_parser.add_argument("egl_version", help="Epic Games Launcher User Agent version",
metavar="<EGL Version>", action="store")
args = parser.parse_args()
@ -93,8 +99,12 @@ def main() -> int:
print(f"Rare {__version__} Codename: {__codename__}")
return 0
if args.subparser in {"start", "launch"}:
from rare.launcher import launch
if args.subparser in {"login", "auth"}:
from rare.commands.webview import launch
return launch(args)
if args.subparser in {"launch", "start"}:
from rare.commands.launcher import launch
return launch(args)
from rare.utils import singleton

View file

@ -10,6 +10,7 @@ from legendary.lfs import eos
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
from legendary.utils.selective_dl import get_sdl_appname
from rare.models.options import options
from rare.lgndr.core import LegendaryCore
from rare.models.install import UninstallOptionsModel, InstallOptionsModel
@ -222,11 +223,13 @@ class RareGameSlim(RareGameBase):
@property
def auto_sync_saves(self):
return self.supports_cloud_saves and QSettings().value(
f"{self.app_name}/auto_sync_cloud",
QSettings().value("auto_sync_cloud", False, bool),
bool
auto_sync_cloud = QSettings(self).value(
f"{self.app_name}/{options.auto_sync_cloud.key}",
options.auto_sync_cloud.default,
options.auto_sync_cloud.dtype
)
auto_sync_cloud = auto_sync_cloud or QSettings(self).value(*options.auto_sync_cloud)
return self.supports_cloud_saves and auto_sync_cloud
@property
def save_path(self) -> Optional[str]:

View file

@ -2,7 +2,7 @@ import json
import os
import platform
from dataclasses import dataclass, field
from datetime import datetime
from datetime import datetime, UTC
from logging import getLogger
from threading import Lock
from typing import List, Optional, Dict, Set
@ -19,6 +19,7 @@ from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager
from rare.utils.paths import data_dir, get_rare_executable
from rare.utils.steam_grades import get_rating
from rare.utils.config_helper import set_envvar
logger = getLogger("RareGame")
@ -26,11 +27,11 @@ logger = getLogger("RareGame")
class RareGame(RareGameSlim):
@dataclass
class Metadata:
auto_update: bool = False
queued: bool = False
queue_pos: Optional[int] = None
last_played: datetime = datetime.min
grant_date: Optional[datetime] = None
grant_date: datetime = datetime.min
steam_appid: Optional[int] = None
steam_grade: Optional[str] = None
steam_date: datetime = datetime.min
tags: List[str] = field(default_factory=list)
@ -38,23 +39,24 @@ class RareGame(RareGameSlim):
@classmethod
def from_dict(cls, data: Dict):
return cls(
auto_update=data.get("auto_update", False),
queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None),
last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min,
grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None,
last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min,
grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min,
steam_appid=data.get("steam_appid", None),
steam_grade=data.get("steam_grade", None),
steam_date=datetime.fromisoformat(data["steam_date"]) if data.get("steam_date", None) else datetime.min,
steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min,
tags=data.get("tags", []),
)
def as_dict(self):
@property
def __dict__(self):
return dict(
auto_update=self.auto_update,
queued=self.queued,
queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
grant_date=self.grant_date.isoformat() if self.grant_date else None,
grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min,
steam_appid=self.steam_appid,
steam_grade=self.steam_grade,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
tags=self.tags,
@ -77,6 +79,7 @@ class RareGame(RareGameSlim):
self.pixmap: QPixmap = QPixmap()
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
self.grant_date()
self.owned_dlcs: Set[RareGame] = set()
@ -137,13 +140,14 @@ class RareGame(RareGameSlim):
def __load_metadata_json() -> Dict:
if RareGame.__metadata_json is None:
metadata = {}
file = os.path.join(data_dir(), "game_meta.json")
try:
with open(os.path.join(data_dir(), "game_meta.json"), "r") as metadata_fh:
metadata = json.load(metadata_fh)
with open(file, "r") as f:
metadata = json.load(f)
except FileNotFoundError:
logger.info("Game metadata json file does not exist.")
logger.info("%s does not exist", file)
except json.JSONDecodeError:
logger.warning("Game metadata json file is corrupt.")
logger.warning("%s is corrupt", file)
finally:
RareGame.__metadata_json = metadata
return RareGame.__metadata_json
@ -160,9 +164,9 @@ class RareGame(RareGameSlim):
with RareGame.__metadata_lock:
metadata: Dict = self.__load_metadata_json()
# pylint: disable=unsupported-assignment-operation
metadata[self.app_name] = self.metadata.as_dict()
with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json:
json.dump(metadata, metadata_json, indent=2)
metadata[self.app_name] = vars(self.metadata)
with open(os.path.join(data_dir(), "game_meta.json"), "w+") as file:
json.dump(metadata, file, indent=2)
def update_game(self):
self.game = self.core.get_game(
@ -337,8 +341,7 @@ class RareGame(RareGameSlim):
"""
if self.igame is not None:
return self.igame.needs_verification
else:
return False
return False
@needs_verification.setter
def needs_verification(self, needs: bool) -> None:
@ -426,19 +429,28 @@ class RareGame(RareGameSlim):
def steam_grade(self) -> str:
if platform.system() == "Windows" or self.is_unreal:
return "na"
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
if self.metadata.steam_grade is not None and elapsed_time.days < 3:
return self.metadata.steam_grade
if self.metadata.steam_grade != "pending":
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
def _set_steam_grade():
rating = get_rating(self.core, self.app_name)
self.set_steam_grade(rating)
if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
def _set_steam_grade():
appid, rating = get_rating(self.core, self.app_name)
self.set_steam_grade(appid, rating)
worker = QRunnable.create(_set_steam_grade)
QThreadPool.globalInstance().start(worker)
self.metadata.steam_grade = "pending"
return self.metadata.steam_grade
worker = QRunnable.create(_set_steam_grade)
QThreadPool.globalInstance().start(worker)
return "pending"
@property
def steam_appid(self) -> Optional[int]:
return self.metadata.steam_appid
def set_steam_grade(self, grade: str) -> None:
def set_steam_grade(self, appid: int, grade: str) -> None:
if appid and self.steam_appid is None:
set_envvar(self.app_name, "SteamAppId", str(appid))
set_envvar(self.app_name, "SteamGameId", str(appid))
set_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid))
self.metadata.steam_appid = appid
self.metadata.steam_grade = grade
self.metadata.steam_date = datetime.utcnow()
self.__save_metadata()
@ -446,17 +458,17 @@ class RareGame(RareGameSlim):
def grant_date(self, force=False) -> datetime:
if (entitlements := self.core.lgd.entitlements) is None:
return self.metadata.grant_date
if self.metadata.grant_date is None or force:
return self.metadata.grant_date.replace(tzinfo=UTC)
if self.metadata.grant_date == datetime.min.replace(tzinfo=UTC) or force:
logger.debug("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)
grant_date = datetime.fromisoformat(
entitlement["grantDate"].replace("Z", "+00:00")
) if entitlement else None
) if entitlement else datetime.min.replace(tzinfo=UTC)
self.metadata.grant_date = grant_date
self.__save_metadata()
return self.metadata.grant_date
return self.metadata.grant_date.replace(tzinfo=UTC)
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path
@ -474,9 +486,7 @@ class RareGame(RareGameSlim):
if self.is_installed:
if (not self.is_idle) or self.needs_verification:
return False
if self.is_foreign and not self.can_run_offline:
return False
return True
return bool(not self.is_foreign or self.can_run_offline)
return False
def get_pixmap(self, color=True) -> QPixmap:
@ -622,7 +632,7 @@ class RareEosOverlay(RareGameBase):
reg_paths = eos.query_registry_entries(prefix)
if old_path := reg_paths["overlay_path"]:
if os.path.normpath(old_path) == path:
logger.info(f"Overlay already enabled, nothing to do.")
logger.info("Overlay already enabled, nothing to do.")
return True
else:
logger.info(f'Updating overlay registry entries from "{old_path}" to "{path}"')

View file

@ -21,14 +21,9 @@ class ImageSize:
self.__img_factor = 17
self.__size = QSize(self.__img_factor * 16, self.__img_factor * 9) * pixel_ratio / divisor
# lk: for prettier images set this to true
self.__smooth_transform: bool = True
if divisor > 2:
self.__smooth_transform = False
if base is not None:
self.__base = base
else:
self.__base = self
# self.__smooth_transform: bool = True
self.__smooth_transform = divisor <= 2
self.__base = base if base is not None else self
def __eq__(self, other: 'ImageSize.Preset'):
return (

24
rare/models/library.py Normal file
View file

@ -0,0 +1,24 @@
from enum import IntEnum
class LibraryView(IntEnum):
COVER = 1
VLIST = 2
class LibraryFilter(IntEnum):
ALL = 1
INSTALLED = 2
OFFLINE = 3
HIDDEN = 4
WIN32 = 5
MAC = 6
INSTALLABLE = 7
INCLUDE_UE = 8
class LibraryOrder(IntEnum):
TITLE = 1
RECENT = 2
NEWEST = 3
OLDEST = 4

56
rare/models/options.py Normal file
View file

@ -0,0 +1,56 @@
import locale
import platform as pf
from argparse import Namespace
from typing import Any, Type
from .library import LibraryFilter, LibraryOrder, LibraryView
class Value(Namespace):
key: str
default: Any
dtype: Type
def __len__(self):
return len(self.__dict__)
def __iter__(self):
yield from self.__dict__.values()
# They key names are set to the existing option name
class Defaults(Namespace):
win32_meta = Value(key="win32_meta", default=False, dtype=bool)
macos_meta = Value(key="macos_meta", default=pf.system() == "Darwin", dtype=bool)
unreal_meta = Value(key="unreal_meta", default=False, dtype=bool)
exclude_non_asset = Value(key="exclude_non_asset", default=False, dtype=bool)
exclude_entitlements = Value(key="exclude_entitlements", default=False, dtype=bool)
language = Value(key="language", default=locale.getlocale()[0], dtype=str)
sys_tray = Value(key="sys_tray", default=True, dtype=bool)
auto_update = Value(key="auto_update", default=False, dtype=bool)
auto_sync_cloud = Value(key="auto_sync_cloud", default=False, dtype=bool)
confirm_start = Value(key="confirm_start", default=False, dtype=bool)
save_size = Value(key="save_size", default=False, dtype=bool)
window_size = Value(key="window_size", default=(1280, 720), dtype=tuple)
notification = Value(key="notification", default=True, dtype=bool)
log_games = Value(key="show_console", default=False, dtype=bool)
color_scheme = Value(key="color_scheme", default="", dtype=str)
style_sheet = Value(key="style_sheet", default="RareStyle", dtype=str)
library_view = Value(key="library_view", default=int(LibraryView.COVER), dtype=int)
library_filter = Value(
key="library_filter",
default=int(LibraryFilter.MAC if pf.system() == "Darwin" else LibraryFilter.ALL), dtype=int
)
library_order = Value(key="library_order", default=int(LibraryOrder.TITLE), dtype=int)
rpc_enable = Value(key="rpc_enable", default=0, dtype=int)
rpc_name = Value(key="rpc_game", default=True, dtype=bool)
rpc_time = Value(key="rpc_time", default=True, dtype=bool)
rpc_os = Value(key="rpc_os", default=True, dtype=bool)
options = Defaults()
__all__ = ['options', 'LibraryFilter', 'LibraryOrder', 'LibraryView']

View file

@ -1,47 +1,70 @@
import os
from typing import Union, List
from typing import Union, List, LiteralString
from legendary.core import LegendaryCore
from legendary.models.game import InstalledGame
from rare.utils.config_helper import get_prefixes
class PathSpec:
__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"),
}
egl_appdata: str = r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
egl_programdata: str = r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
wine_programdata: str = r"dosdevices/c:/ProgramData"
def __init__(self, core: LegendaryCore = None, app_name: str = "default"):
if core is not None:
self.__egl_path_vars.update({"{epicid}": core.lgd.userdata["account_id"]})
self.app_name = app_name
@staticmethod
def egl_appdata() -> str:
return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
def cook(self, path: str) -> str:
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
return os.path.join(*cooked_path)
@staticmethod
def egl_programdata() -> str:
return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
@property
def wine_egl_programdata(self):
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
@staticmethod
def wine_programdata() -> str:
return r"ProgramData"
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
possible_prefixes = [
os.path.expanduser("~/.wine"),
os.path.expanduser("~/Games/epic-games-store"),
@staticmethod
def wine_egl_programdata() -> str:
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())
@staticmethod
def wine_egl_prefixes(results: int = 0) -> Union[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()))
]
prefixes = []
for prefix in possible_prefixes:
if os.path.exists(os.path.join(prefix, self.wine_egl_programdata)):
prefixes.append(prefix)
if not prefixes:
return str()
return ""
if not results:
return prefixes
elif results == 1:
return prefixes[0]
else:
return prefixes[:results]
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"),
}
if core is not None:
self.__egl_path_vars["{epicid}"] = core.lgd.userdata["account_id"]
if igame is not None:
self.__egl_path_vars["{installdir}"] = igame.install_path
def resolve_egl_path_vars(self, path: str) -> Union[LiteralString, str, bytes]:
cooked_path = (self.__egl_path_vars.get(p.lower(), p) for p in path.split("/"))
return os.path.join(*cooked_path)

View file

@ -14,11 +14,11 @@ class GlobalSignals:
# str: title, str: body
notify = pyqtSignal(str, str)
# none
prefix_updated = pyqtSignal()
# none
update_tray = pyqtSignal()
# none
update_statusbar = pyqtSignal()
# str: locale
# change_translation = pyqtSignal(str)
class GameSignals(QObject):
# model

77
rare/models/wrapper.py Normal file
View file

@ -0,0 +1,77 @@
import os
import shlex
from hashlib import md5
from enum import IntEnum
from typing import Dict, List, Union
class WrapperType(IntEnum):
NONE = 0
COMPAT_TOOL = 1
LEGENDARY_IMPORT = 8
USER_DEFINED = 9
class Wrapper:
def __init__(self, command: Union[str, List[str]], name: str = None, wtype: WrapperType = None):
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
@property
def is_compat_tool(self) -> bool:
return self.__wtype == WrapperType.COMPAT_TOOL
@property
def is_editable(self) -> bool:
return self.__wtype in {WrapperType.USER_DEFINED, WrapperType.LEGENDARY_IMPORT}
@property
def checksum(self) -> str:
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]:
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))
@property
def name(self) -> str:
return self.__name
@property
def type(self) -> WrapperType:
return self.__wtype
def __eq__(self, other) -> bool:
return self.as_str == other.as_str
def __hash__(self):
return hash(self.__command)
def __bool__(self) -> bool:
return True if not self.is_editable else bool(self.as_str.strip())
@classmethod
def from_dict(cls, data: Dict):
return cls(
command=data.get("command"),
name=data.get("name"),
wtype=WrapperType(data.get("wtype", WrapperType.USER_DEFINED))
)
@property
def __dict__(self):
return dict(
command=self.__command,
name=self.__name,
wtype=int(self.__wtype)
)

Binary file not shown.

Binary file not shown.

View file

@ -73,6 +73,7 @@ class GameProcess(QObject):
).format(self.game.app_name)
)
self.timer.stop()
self.tried_connections = 0
self.finished.emit(GameProcess.Code.TIMEOUT)
@pyqtSlot()

View file

@ -29,7 +29,7 @@ from .workers import (
)
from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState
from rare.utils import config_helper
from .wrappers import Wrappers
logger = getLogger("RareCore")
@ -53,6 +53,8 @@ class RareCore(QObject):
self.__signals: Optional[GlobalSignals] = None
self.__core: Optional[LegendaryCore] = None
self.__image_manager: Optional[ImageManager] = None
self.__settings: Optional[QSettings] = None
self.__wrappers: Optional[Wrappers] = None
self.__start_time = time.perf_counter()
@ -61,8 +63,8 @@ class RareCore(QObject):
self.core(init=True)
config_helper.init_config_handler(self.__core)
self.image_manager(init=True)
self.settings = QSettings(self)
self.__settings = QSettings(self)
self.__wrappers = Wrappers()
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
@ -100,13 +102,17 @@ class RareCore(QObject):
self.__signals.application.update_statusbar.emit()
def active_workers(self) -> Iterable[QueueWorker]:
return list(filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers))
# return list(filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers))
yield from filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers)
def queued_workers(self) -> Iterable[QueueWorker]:
return list(filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers))
# return list(filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers))
yield from filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers)
def queue_info(self) -> List[QueueWorkerInfo]:
return [w.worker_info() for w in self.queue_workers]
def queue_info(self) -> Iterable[QueueWorkerInfo]:
# return (w.worker_info() for w in self.queue_workers)
for w in self.queue_workers:
yield w.worker_info()
@staticmethod
def instance() -> 'RareCore':
@ -205,6 +211,12 @@ class RareCore(QObject):
self.__image_manager = ImageManager(self.signals(), self.core())
return self.__image_manager
def wrappers(self) -> Wrappers:
return self.__wrappers
def settings(self) -> QSettings:
return self.__settings
def deleteLater(self) -> None:
self.__image_manager.deleteLater()
del self.__image_manager
@ -328,10 +340,13 @@ class RareCore(QObject):
self.__core.lgd.entitlements = result
self.__fetched_entitlements = True
logger.info(f"Acquired data for {FetchWorker.Result(result_type).name}")
logger.info("Acquired data from %s worker", FetchWorker.Result(result_type).name)
if all([self.__fetched_games_dlcs, self.__fetched_entitlements]):
logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds")
logger.debug("Fetch time %s seconds", time.perf_counter() - self.__start_time)
self.__wrappers.import_wrappers(
self.__core, self.__settings, [rgame.app_name for rgame in self.games]
)
self.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit()
QTimer.singleShot(100, self.__post_init)
@ -366,7 +381,7 @@ class RareCore(QObject):
continue
self.__library[app_name].load_saves(saves)
except (HTTPError, ConnectionError) as e:
logger.error(f"Exception while fetching saves from EGS.")
logger.error("Exception while fetching saves from EGS.")
logger.error(e)
return
logger.info(f"Saves: {len(saves_dict)}")

View file

@ -8,6 +8,7 @@ from requests.exceptions import HTTPError, ConnectionError
from rare.lgndr.core import LegendaryCore
from rare.utils.metrics import timelogger
from rare.models.options import options
from .worker import Worker
logger = getLogger("FetchWorker")
@ -32,13 +33,13 @@ class FetchWorker(Worker):
class EntitlementsWorker(FetchWorker):
def __init__(self, core: LegendaryCore, args: Namespace):
super(EntitlementsWorker, self).__init__(core, args)
def run_real(self):
want_entitlements = not self.settings.value(*options.exclude_entitlements)
entitlements = ()
want_entitlements = not self.settings.value("exclude_entitlements", False, bool)
if want_entitlements:
if want_entitlements and not self.args.offline:
# Get entitlements, Ubisoft integration also uses them
self.signals.progress.emit(0, self.signals.tr("Updating entitlements"))
with timelogger(logger, "Request entitlements"):
@ -51,20 +52,20 @@ class EntitlementsWorker(FetchWorker):
class GamesDlcsWorker(FetchWorker):
def __init__(self, core: LegendaryCore, args: Namespace):
super(GamesDlcsWorker, self).__init__(core, args)
self.exclude_non_asset = QSettings().value("exclude_non_asset", False, bool)
def run_real(self):
# Fetch regular EGL games with assets
want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug
want_win32 = self.settings.value("win32_meta", False, bool)
want_macos = self.settings.value("macos_meta", False, bool)
# want_unreal = self.settings.value(*options.unreal_meta) or self.args.debug
# want_win32 = self.settings.value(*options.win32_meta) or self.args.debug
# want_macos = self.settings.value(*options.macos_meta) or self.args.debug
want_unreal = self.settings.value(*options.unreal_meta)
want_win32 = self.settings.value(*options.win32_meta)
want_macos = self.settings.value(*options.macos_meta)
want_non_asset = not self.settings.value(*options.exclude_non_asset)
need_macos = platform.system() == "Darwin"
need_windows = not any([want_win32, want_macos, need_macos, self.args.debug]) and not self.args.offline
need_windows = not any([want_win32, want_macos, need_macos]) and not self.args.offline
if want_win32 or self.args.debug:
if want_win32:
logger.info(
"Requesting Win32 metadata due to %s, %s Unreal engine",
"settings" if want_win32 else "debug",
@ -76,7 +77,7 @@ class GamesDlcsWorker(FetchWorker):
update_assets=not self.args.offline, platform="Win32", skip_ue=not want_unreal
)
if need_macos or want_macos or self.args.debug:
if need_macos or want_macos:
logger.info(
"Requesting macOS metadata due to %s, %s Unreal engine",
"platform" if need_macos else "settings" if want_macos else "debug",
@ -101,7 +102,6 @@ class GamesDlcsWorker(FetchWorker):
logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict))
# Fetch non-asset games
want_non_asset = not self.settings.value("exclude_non_asset", False, bool)
if want_non_asset:
self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata"))
try:

View file

@ -58,7 +58,8 @@ class InstallInfoWorker(Worker):
if not download.res or not download.res.failures:
self.signals.result.emit(download)
else:
self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
# self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
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:

View file

@ -11,7 +11,7 @@ from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
from rare.utils import config_helper
from rare.utils import config_helper as config
from rare.utils.paths import desktop_links_supported, desktop_link_types, desktop_link_path
from .worker import Worker
@ -31,12 +31,12 @@ def uninstall_game(
logger.info('Removing registry entries...')
if platform.system() != "Window":
prefixes = config_helper.get_wine_prefixes()
prefixes = config.get_prefixes()
if platform.system() == "Darwin":
# TODO: add crossover support
pass
if prefixes is not None:
for prefix in prefixes:
if len(prefixes):
for prefix, _ in prefixes:
remove_registry_entries(prefix)
logger.debug("Removed registry entries for prefix %s", prefix)
else:
@ -65,10 +65,10 @@ def uninstall_game(
)
if not keep_config:
logger.info("Removing sections in config file")
config_helper.remove_section(rgame.app_name)
config_helper.remove_section(f"{rgame.app_name}.env")
config.remove_section(rgame.app_name)
config.remove_section(f"{rgame.app_name}.env")
config_helper.save_config()
config.save_config()
return status.success, status.message

View file

@ -3,63 +3,113 @@ import platform
import time
from configparser import ConfigParser
from logging import getLogger
from typing import Union, Iterable
from typing import Union, Iterable, List, Tuple, Dict
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable
from PyQt5.QtCore import pyqtSignal, QObject
import rare.utils.wine as wine
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.pathspec import PathSpec
from rare.shared.wrappers import Wrappers
from rare.utils import config_helper as config
from rare.utils.misc import path_size, format_size
from .worker import Worker
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
import winreg # pylint: disable=E0401
import winreg # pylint: disable=E0401
from legendary.lfs import windows_helpers
else:
from rare.utils.compat import utils as compat_utils, steam
logger = getLogger("WineResolver")
class WineResolver(Worker):
class WinePathResolver(Worker):
class Signals(QObject):
result_ready = pyqtSignal(str)
result_ready = pyqtSignal(str, str)
def __init__(self, core: LegendaryCore, path: str, app_name: str):
super(WineResolver, self).__init__()
self.signals = WineResolver.Signals()
self.wine_env = wine.environ(core, app_name)
self.wine_exec = wine.wine(core, app_name)
self.path = PathSpec(core, app_name).cook(path)
def __init__(self, core: LegendaryCore, app_name: str, path: str):
super(WinePathResolver, self). __init__()
self.signals = WinePathResolver.Signals()
self.core = core
self.app_name = app_name
self.path = path
@staticmethod
def _configure_process(core: LegendaryCore, app_name: str) -> Tuple[List, Dict]:
tool: steam.CompatibilityTool = None
if config.get_boolean(app_name, "no_wine"):
wrappers = Wrappers()
for w in wrappers.get_game_wrapper_list(app_name):
if w.is_compat_tool:
for t in steam.find_tools():
if t.checksum == w.checksum:
tool = t
break
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")
)
env = core.get_app_environment(app_name, disable_wine=config.get_boolean(app_name, "no_wine"))
env = compat_utils.get_host_environment(env, silent=True)
return cmd, env
@staticmethod
def _resolve_unix_path(cmd, env, path: str) -> str:
logger.info("Resolving path '%s'", path)
wine_path = compat_utils.resolve_path(cmd, env, path)
logger.info("Resolved Wine path '%s'", wine_path)
unix_path = compat_utils.convert_to_unix_path(cmd, env, wine_path)
logger.info("Resolved Unix path '%s'", unix_path)
return unix_path
def run_real(self):
if "WINEPREFIX" not in self.wine_env or not os.path.exists(self.wine_env["WINEPREFIX"]):
# pylint: disable=E1136
self.signals.result_ready[str].emit("")
return
if not os.path.exists(self.wine_exec):
# pylint: disable=E1136
self.signals.result_ready[str].emit("")
return
path = wine.resolve_path(self.wine_exec, self.wine_env, self.path)
# Clean wine output
real_path = wine.convert_to_unix_path(self.wine_exec, self.wine_env, path)
# pylint: disable=E1136
self.signals.result_ready[str].emit(real_path)
command, environ = self._configure_process(self.core, self.app_name)
if not (command and environ):
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)
return
class OriginWineWorker(QRunnable):
class WineSavePathResolver(WinePathResolver):
def __init__(self, core: LegendaryCore, rgame: RareGame):
path = PathSpec(core, rgame.igame).resolve_egl_path_vars(rgame.raw_save_path)
super(WineSavePathResolver, self).__init__(rgame.core, rgame.app_name, str(path))
self.rgame = rgame
def run_real(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):
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
# pylint: disable=E1136
if os.path.exists(path):
self.rgame.save_path = path
self.signals.result_ready.emit(path, self.rgame.app_name)
return
class OriginWineWorker(WinePathResolver):
def __init__(self, core: LegendaryCore, games: Union[Iterable[RareGame], RareGame]):
super(OriginWineWorker, self).__init__()
super(OriginWineWorker, self).__init__(core, "", "")
self.__cache: dict[str, ConfigParser] = {}
self.core = core
if isinstance(games, RareGame):
games = [games]
self.games = games
self.games = [games] if isinstance(games, RareGame) else games
def run(self) -> None:
def run_real(self) -> None:
t = time.time()
for rgame in self.games:
@ -79,15 +129,17 @@ class OriginWineWorker(QRunnable):
if platform.system() == "Windows":
install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key)
else:
wine_env = wine.environ(self.core, rgame.app_name)
wine_exec = wine.wine(self.core, rgame.app_name)
command, environ = self._configure_process(self.core, rgame.app_name)
use_wine = False
prefix = config.get_prefix(rgame.app_name)
if not prefix:
return
use_wine = True
if not use_wine:
# lk: this is the original way of getting the path by parsing "system.reg"
wine_prefix = wine.prefix(self.core, rgame.app_name)
reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
self.__cache[wine_prefix] = reg
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")
# lk: split and rejoin the registry path to avoid slash expansion
@ -96,11 +148,11 @@ class OriginWineWorker(QRunnable):
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 = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
install_dir = compat_utils.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key)
if install_dir:
logger.debug("Found Wine install directory %s", install_dir)
install_dir = wine.convert_to_unix_path(wine_exec, wine_env, install_dir)
install_dir = compat_utils.convert_to_unix_path(command, environ, install_dir)
if install_dir:
logger.debug("Found Unix install directory %s", install_dir)
else:

165
rare/shared/wrappers.py Normal file
View file

@ -0,0 +1,165 @@
import json
import os
from logging import getLogger
import shlex
from typing import List, Dict, Iterable
from rare.utils import config_helper as config
from PyQt5.QtCore import QSettings
from rare.lgndr.core import LegendaryCore
from rare.models.wrapper import Wrapper, WrapperType
from rare.utils.paths import config_dir
logger = getLogger("Wrappers")
class Wrappers:
def __init__(self):
self.__file = os.path.join(config_dir(), "wrappers.json")
self.__wrappers_dict = {}
try:
with open(self.__file) as f:
self.__wrappers_dict = json.load(f)
except FileNotFoundError:
logger.info("%s does not exist", self.__file)
except json.JSONDecodeError:
logger.warning("%s is corrupt", self.__file)
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[str]] = {}
for app_name, wrapper_list in self.__wrappers_dict.get("applists", {}).items():
self.__applists.update({app_name: wrapper_list})
def import_wrappers(self, core: LegendaryCore, settings: QSettings, app_names: List):
for app_name in app_names:
wrappers = self.get_game_wrapper_list(app_name)
if not wrappers and (commands := settings.value(f"{app_name}/wrapper", [], list)):
logger.info("Importing wrappers from Rare's config")
settings.remove(f"{app_name}/wrapper")
for command in commands:
wrapper = Wrapper(command=shlex.split(command))
wrappers.append(wrapper)
self.set_game_wrapper_list(app_name, wrappers)
logger.debug("Imported previous wrappers in %s Rare: %s", app_name, wrapper.name)
# NOTE: compatibility with Legendary
if not wrappers and (command := core.lgd.config.get(app_name, "wrapper", fallback="")):
logger.info("Importing wrappers from legendary's config")
# no qt wrapper, but legendary wrapper, to have backward compatibility
# pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
# wrappers = pattern.split(command)[1::2]
wrapper = Wrapper(
command=shlex.split(command),
name="Imported from Legendary",
wtype=WrapperType.LEGENDARY_IMPORT
)
wrappers = [wrapper]
self.set_game_wrapper_list(app_name, wrappers)
logger.debug("Imported existing wrappers in %s legendary: %s", app_name, wrapper.name)
@property
def user_wrappers(self) -> Iterable[Wrapper]:
return filter(lambda w: w.is_editable, self.__wrappers.values())
# for wrap in self.__wrappers.values():
# if wrap.is_user_defined:
# yield wrap
def get_game_wrapper_string(self, app_name: str) -> str:
commands = [wrapper.as_str for wrapper in self.get_game_wrapper_list(app_name)]
return " ".join(commands)
def get_game_wrapper_list(self, app_name: str) -> List[Wrapper]:
_wrappers = []
for wrap_id in self.__applists.get(app_name, []):
if wrap := self.__wrappers.get(wrap_id, None):
_wrappers.append(wrap)
return _wrappers
def get_game_md5sum_list(self, app_name: str) -> List[str]:
return self.__applists.get(app_name, [])
def set_game_wrapper_list(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 w != self.__wrappers[md5sum]:
logger.error(
"Non-unique md5sum for different wrappers %s, %s",
w.name,
self.__wrappers[md5sum].name,
)
if w.is_compat_tool:
self.__wrappers.update({md5sum: w})
else:
self.__wrappers.update({md5sum: w})
self.__applists[app_name] = [w.checksum for w in _wrappers]
self.__save_config(app_name)
self.__save_wrappers()
def __save_config(self, app_name: str):
command_string = self.get_game_wrapper_string(app_name)
config.save_option(app_name, "wrapper", command_string)
def __save_wrappers(self):
existing = {wrap_id for wrap_id in self.__wrappers.keys()}
in_use = {wrap_id for wrappers in self.__applists.values() for wrap_id in wrappers}
for redudant in existing.difference(in_use):
del self.__wrappers[redudant]
self.__wrappers_dict["wrappers"] = self.__wrappers
self.__wrappers_dict["applists"] = self.__applists
with open(os.path.join(self.__file), "w+") as f:
json.dump(self.__wrappers_dict, f, default=lambda o: vars(o), indent=2)
if __name__ == "__main__":
from pprint import pprint
from argparse import Namespace
from rare.utils.compat import steam
global config_dir
config_dir = os.getcwd
global config
config = Namespace()
config.set_option = lambda x, y, z: print(x, y, z)
config.remove_option = lambda x, y: print(x, y)
config.save_config = lambda: print()
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)
w4 = Wrapper(command=["/usr/bin/w4"], wtype=WrapperType.USER_DEFINED)
wr.set_game_wrapper_list("testgame", [w1, w2, w3, w4])
w5 = Wrapper(command=["/usr/bin/w5"], wtype=WrapperType.COMPAT_TOOL)
wr.set_game_wrapper_list("testgame2", [w2, w1, w5])
w6 = Wrapper(command=["/usr/bin/w 6", "-w", "-t"], wtype=WrapperType.USER_DEFINED)
wr.set_game_wrapper_list("testgame", [w1, w2, w3, w6])
w7 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL)
wrs = wr.get_game_wrapper_list("testgame")
wrs.remove(w7)
wr.set_game_wrapper_list("testgame", wrs)
game_wrappers = wr.get_game_wrapper_list("testgame")
pprint(game_wrappers)
game_wrappers = wr.get_game_wrapper_list("testgame2")
pprint(game_wrappers)
for i, tool in enumerate(steam.find_tools()):
wt = Wrapper(command=tool.command(), name=tool.name, wtype=WrapperType.COMPAT_TOOL)
wr.set_game_wrapper_list(f"compat_game_{i}", [wt])
print(wt.as_str)
for wrp in wr.user_wrappers:
pprint(wrp)

View file

@ -133,6 +133,7 @@ class Ui_GameInfo(object):
self.info_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_grade)
self.grade = QtWidgets.QLabel(GameInfo)
self.grade.setText("error")
self.grade.setOpenExternalLinks(True)
self.grade.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse)
self.grade.setObjectName("grade")
self.info_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.grade)

View file

@ -232,6 +232,9 @@
<property name="text">
<string notr="true">error</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>

View file

@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/game_settings.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameSettings(object):
def setupUi(self, GameSettings):
GameSettings.setObjectName("GameSettings")
GameSettings.resize(505, 261)
self.game_settings_layout = QtWidgets.QVBoxLayout(GameSettings)
self.game_settings_layout.setObjectName("game_settings_layout")
self.launch_settings_group = QtWidgets.QGroupBox(GameSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.launch_settings_group.sizePolicy().hasHeightForWidth())
self.launch_settings_group.setSizePolicy(sizePolicy)
self.launch_settings_group.setObjectName("launch_settings_group")
self.launch_settings_layout = QtWidgets.QFormLayout(self.launch_settings_group)
self.launch_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.launch_settings_layout.setObjectName("launch_settings_layout")
self.skip_update_label = QtWidgets.QLabel(self.launch_settings_group)
self.skip_update_label.setObjectName("skip_update_label")
self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.skip_update_label)
self.skip_update = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.skip_update.sizePolicy().hasHeightForWidth())
self.skip_update.setSizePolicy(sizePolicy)
self.skip_update.setObjectName("skip_update")
self.skip_update.addItem("")
self.skip_update.addItem("")
self.skip_update.addItem("")
self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.skip_update)
self.offline_label = QtWidgets.QLabel(self.launch_settings_group)
self.offline_label.setObjectName("offline_label")
self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.offline_label)
self.offline = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.offline.sizePolicy().hasHeightForWidth())
self.offline.setSizePolicy(sizePolicy)
self.offline.setObjectName("offline")
self.offline.addItem("")
self.offline.addItem("")
self.offline.addItem("")
self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.offline)
self.launch_params_label = QtWidgets.QLabel(self.launch_settings_group)
self.launch_params_label.setObjectName("launch_params_label")
self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.launch_params_label)
self.launch_params = QtWidgets.QLineEdit(self.launch_settings_group)
self.launch_params.setObjectName("launch_params")
self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.launch_params)
self.game_settings_layout.addWidget(self.launch_settings_group)
self.proton_layout = QtWidgets.QVBoxLayout()
self.proton_layout.setObjectName("proton_layout")
self.game_settings_layout.addLayout(self.proton_layout)
self.linux_settings_widget = QtWidgets.QWidget(GameSettings)
self.linux_settings_widget.setObjectName("linux_settings_widget")
self.linux_settings_layout = QtWidgets.QVBoxLayout(self.linux_settings_widget)
self.linux_settings_layout.setContentsMargins(0, 0, 0, 0)
self.linux_settings_layout.setObjectName("linux_settings_layout")
self.game_settings_layout.addWidget(self.linux_settings_widget)
self.retranslateUi(GameSettings)
def retranslateUi(self, GameSettings):
_translate = QtCore.QCoreApplication.translate
GameSettings.setWindowTitle(_translate("GameSettings", "GameSettings"))
self.launch_settings_group.setTitle(_translate("GameSettings", "Launch Settings"))
self.skip_update_label.setText(_translate("GameSettings", "Skip update check"))
self.skip_update.setItemText(0, _translate("GameSettings", "Default"))
self.skip_update.setItemText(1, _translate("GameSettings", "Yes"))
self.skip_update.setItemText(2, _translate("GameSettings", "No"))
self.offline_label.setText(_translate("GameSettings", "Offline mode"))
self.offline.setItemText(0, _translate("GameSettings", "Default"))
self.offline.setItemText(1, _translate("GameSettings", "Yes"))
self.offline.setItemText(2, _translate("GameSettings", "No"))
self.launch_params_label.setText(_translate("GameSettings", "Launch parameters"))
self.launch_params.setPlaceholderText(_translate("GameSettings", "parameters"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
GameSettings = QtWidgets.QWidget()
ui = Ui_GameSettings()
ui.setupUi(GameSettings)
GameSettings.show()
sys.exit(app.exec_())

View file

@ -1,117 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameSettings</class>
<widget class="QWidget" name="GameSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>483</width>
<height>154</height>
</rect>
</property>
<property name="windowTitle">
<string>GameSettings</string>
</property>
<layout class="QVBoxLayout" name="game_settings_layout">
<item>
<widget class="QGroupBox" name="launch_settings_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Launch Settings</string>
</property>
<layout class="QFormLayout" name="launch_settings_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="skip_update_label">
<property name="text">
<string>Skip update check</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="skip_update">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Yes</string>
</property>
</item>
<item>
<property name="text">
<string>No</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="offline_label">
<property name="text">
<string>Offline mode</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="offline">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Yes</string>
</property>
</item>
<item>
<property name="text">
<string>No</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="launch_params_label">
<property name="text">
<string>Launch parameters</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="launch_params">
<property name="placeholderText">
<string>parameters</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/linux.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LinuxSettings(object):
def setupUi(self, LinuxSettings):
LinuxSettings.setObjectName("LinuxSettings")
LinuxSettings.resize(394, 84)
self.linux_layout = QtWidgets.QVBoxLayout(LinuxSettings)
self.linux_layout.setObjectName("linux_layout")
self.wine_groupbox = QtWidgets.QGroupBox(LinuxSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.wine_groupbox.sizePolicy().hasHeightForWidth())
self.wine_groupbox.setSizePolicy(sizePolicy)
self.wine_groupbox.setObjectName("wine_groupbox")
self.wine_layout = QtWidgets.QFormLayout(self.wine_groupbox)
self.wine_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.wine_layout.setObjectName("wine_layout")
self.prefix_label = QtWidgets.QLabel(self.wine_groupbox)
self.prefix_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.prefix_label.setObjectName("prefix_label")
self.wine_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.prefix_label)
self.prefix_layout = QtWidgets.QVBoxLayout()
self.prefix_layout.setObjectName("prefix_layout")
self.wine_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
self.exec_label = QtWidgets.QLabel(self.wine_groupbox)
self.exec_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.exec_label.setObjectName("exec_label")
self.wine_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.exec_label)
self.exec_layout = QtWidgets.QVBoxLayout()
self.exec_layout.setObjectName("exec_layout")
self.wine_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.exec_layout)
self.linux_layout.addWidget(self.wine_groupbox)
self.retranslateUi(LinuxSettings)
def retranslateUi(self, LinuxSettings):
_translate = QtCore.QCoreApplication.translate
LinuxSettings.setWindowTitle(_translate("LinuxSettings", "LinuxSettings"))
self.wine_groupbox.setTitle(_translate("LinuxSettings", "Wine Settings"))
self.prefix_label.setText(_translate("LinuxSettings", "Prefix"))
self.exec_label.setText(_translate("LinuxSettings", "Executable"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
LinuxSettings = QtWidgets.QWidget()
ui = Ui_LinuxSettings()
ui.setupUi(LinuxSettings)
LinuxSettings.show()
sys.exit(app.exec_())

View file

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LinuxSettings</class>
<widget class="QWidget" name="LinuxSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>394</width>
<height>84</height>
</rect>
</property>
<property name="windowTitle">
<string>LinuxSettings</string>
</property>
<layout class="QVBoxLayout" name="linux_layout">
<item>
<widget class="QGroupBox" name="wine_groupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Wine Settings</string>
</property>
<layout class="QFormLayout" name="wine_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="prefix_label">
<property name="text">
<string>Prefix</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="prefix_layout"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="exec_label">
<property name="text">
<string>Executable</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QVBoxLayout" name="exec_layout"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/proton.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ProtonSettings(object):
def setupUi(self, ProtonSettings):
ProtonSettings.setObjectName("ProtonSettings")
ProtonSettings.resize(190, 86)
ProtonSettings.setWindowTitle("ProtonSettings")
self.proton_settings_layout = QtWidgets.QFormLayout(ProtonSettings)
self.proton_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.proton_settings_layout.setObjectName("proton_settings_layout")
self.proton_wrapper_label = QtWidgets.QLabel(ProtonSettings)
self.proton_wrapper_label.setObjectName("proton_wrapper_label")
self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.proton_wrapper_label)
self.proton_combo = QtWidgets.QComboBox(ProtonSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.proton_combo.sizePolicy().hasHeightForWidth())
self.proton_combo.setSizePolicy(sizePolicy)
self.proton_combo.setObjectName("proton_combo")
self.proton_combo.addItem("")
self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.proton_combo)
self.proton_prefix_label = QtWidgets.QLabel(ProtonSettings)
self.proton_prefix_label.setObjectName("proton_prefix_label")
self.proton_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label)
self.prefix_layout = QtWidgets.QHBoxLayout()
self.prefix_layout.setObjectName("prefix_layout")
self.proton_settings_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
self.retranslateUi(ProtonSettings)
def retranslateUi(self, ProtonSettings):
_translate = QtCore.QCoreApplication.translate
ProtonSettings.setTitle(_translate("ProtonSettings", "Proton Settings"))
self.proton_wrapper_label.setText(_translate("ProtonSettings", "Proton"))
self.proton_combo.setItemText(0, _translate("ProtonSettings", "Don\'t use Proton"))
self.proton_prefix_label.setText(_translate("ProtonSettings", "Prefix"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ProtonSettings = QtWidgets.QGroupBox()
ui = Ui_ProtonSettings()
ui.setupUi(ProtonSettings)
ProtonSettings.show()
sys.exit(app.exec_())

View file

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ProtonSettings</class>
<widget class="QGroupBox" name="ProtonSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>190</width>
<height>86</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">ProtonSettings</string>
</property>
<property name="title">
<string>Proton Settings</string>
</property>
<layout class="QFormLayout" name="proton_settings_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="proton_wrapper_label">
<property name="text">
<string>Proton</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="proton_combo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Don't use Proton</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="proton_prefix_label">
<property name="text">
<string>Prefix</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="prefix_layout"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

Some files were not shown because too many files have changed in this diff Show more