Add multiple languages (i18n) support.
Now you can add a version in profile manage page. Add install java button and dialog.
This commit is contained in:
+30
-23
@@ -25,9 +25,10 @@ from manager.settings_models import AccountSettings
|
||||
|
||||
|
||||
class AccountWidget(QWidget):
|
||||
def __init__(self, account_name: str, account_head: QPixmap, login_status: str, parent=None,
|
||||
def __init__(self, app, account_name: str, account_head: QPixmap, login_status: str, parent=None,
|
||||
/, delete_account_callback=None, check_account_callback=None, switch_account_callback=None,):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
|
||||
# Layout
|
||||
self.layout = QHBoxLayout(self)
|
||||
@@ -57,10 +58,10 @@ class AccountWidget(QWidget):
|
||||
|
||||
def show_right_click_menu(self, position):
|
||||
menu = QMenu(self)
|
||||
switch_action = menu.addAction("Switch to this account")
|
||||
check_action = menu.addAction("Check login status")
|
||||
switch_action = menu.addAction(self.app.tr_text("Switch to this account"))
|
||||
check_action = menu.addAction(self.app.tr_text("Check login status"))
|
||||
menu.addSeparator()
|
||||
delete_action = menu.addAction("Delete account")
|
||||
delete_action = menu.addAction(self.app.tr_text("Delete account"))
|
||||
selected = menu.exec(self.mapToGlobal(position))
|
||||
|
||||
if selected == delete_action and callable(self.delete_account_callback):
|
||||
@@ -74,7 +75,7 @@ class LoginDialog(QDialog):
|
||||
def __init__(self, app, parent=None, code="", error_message=""):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.setWindowTitle("Microsoft Login")
|
||||
self.setWindowTitle(self.app.tr_text("Microsoft Login"))
|
||||
self.setMinimumWidth(440)
|
||||
self.setMinimumHeight(380)
|
||||
self.resize(440, 420)
|
||||
@@ -86,9 +87,9 @@ class LoginDialog(QDialog):
|
||||
self.layout.setSpacing(14)
|
||||
|
||||
# Top widgets
|
||||
self.title_label = QLabel("Login Minecraft Account")
|
||||
self.title_label = QLabel(self.app.tr_text("Login Minecraft Account"))
|
||||
self.title_label.setObjectName("dialogTitle")
|
||||
self.help_label = QLabel("Copy the code below, then continue in your browser.")
|
||||
self.help_label = QLabel(self.app.tr_text("Copy the code below, then continue in your browser."))
|
||||
self.help_label.setObjectName("helpText")
|
||||
self.help_label.setWordWrap(True)
|
||||
|
||||
@@ -106,9 +107,9 @@ class LoginDialog(QDialog):
|
||||
self.error_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
# Button
|
||||
self.copy_code_button = QPushButton("Copy code")
|
||||
self.copy_code_button = QPushButton(self.app.tr_text("Copy code"))
|
||||
self.copy_code_button.setObjectName("copyCodeButton")
|
||||
self.open_browser_button = QPushButton("Open browser")
|
||||
self.open_browser_button = QPushButton(self.app.tr_text("Open browser"))
|
||||
self.open_browser_button.setObjectName("openBrowserButton")
|
||||
|
||||
self.center_layout = QVBoxLayout()
|
||||
@@ -142,12 +143,12 @@ class LoginDialog(QDialog):
|
||||
|
||||
def copy_code(self):
|
||||
QApplication.clipboard().setText(self.code)
|
||||
self.copy_code_button.setText("Copied!")
|
||||
self.copy_code_button.setText(self.app.tr_text("Copied!"))
|
||||
self.copy_code_button.setEnabled(False)
|
||||
QTimer.singleShot(1200, self._reset_copy_button)
|
||||
|
||||
def _reset_copy_button(self):
|
||||
self.copy_code_button.setText("Copy code")
|
||||
self.copy_code_button.setText(self.app.tr_text("Copy code"))
|
||||
self.copy_code_button.setEnabled(True)
|
||||
|
||||
def set_error_message(self, message):
|
||||
@@ -173,9 +174,9 @@ class AccountPage(QWidget):
|
||||
|
||||
# Widgets
|
||||
self.account_list = QListWidget(self)
|
||||
self.add_account_button = QPushButton("Add Account")
|
||||
self.refresh_button = QPushButton("Refresh all")
|
||||
self.delete_all_button = QPushButton("Delete All")
|
||||
self.add_account_button = QPushButton(self.app.tr_text("Add Account"))
|
||||
self.refresh_button = QPushButton(self.app.tr_text("Refresh all"))
|
||||
self.delete_all_button = QPushButton(self.app.tr_text("Delete All"))
|
||||
|
||||
# Button click handle
|
||||
self.add_account_button.clicked.connect(self.start_login_page)
|
||||
@@ -244,7 +245,7 @@ class AccountPage(QWidget):
|
||||
|
||||
# Add suggestion to account list if no account available
|
||||
if not accounts:
|
||||
empty_item = QListWidgetItem("No accounts yet. Use Add Account to sign in.")
|
||||
empty_item = QListWidgetItem(self.app.tr_text("No accounts yet. Use Add Account to sign in."))
|
||||
empty_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
empty_item.setFlags(Qt.ItemFlag.NoItemFlags)
|
||||
self.account_list.addItem(empty_item)
|
||||
@@ -258,9 +259,10 @@ class AccountPage(QWidget):
|
||||
|
||||
head_size = QSize(32, 32)
|
||||
widget = AccountWidget(
|
||||
self.app,
|
||||
account.display_name,
|
||||
self.account_manager.get_head_pixmap(account.avatar, head_size),
|
||||
"Current" if is_current else "Available",
|
||||
self.app.tr_text("Current" if is_current else "Available"),
|
||||
self,
|
||||
delete_account_callback=lambda: self._delete_account(account.id),
|
||||
check_account_callback=lambda: self._show_account_status(account.id),
|
||||
@@ -310,9 +312,14 @@ class AccountPage(QWidget):
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Account status",
|
||||
f"{account.display_name} — Minecraft profile "
|
||||
f"{'available' if account.minecraft_uuid else 'not available'}",
|
||||
self.app.tr_text("Account status"),
|
||||
self.app.tr_text(
|
||||
"{account} — Minecraft profile {status}",
|
||||
account=account.display_name,
|
||||
status=self.app.tr_text(
|
||||
"available" if account.minecraft_uuid else "not available"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def _switch_account(self, account_id: str):
|
||||
@@ -336,7 +343,7 @@ class AccountPage(QWidget):
|
||||
# Create login dialog
|
||||
self.add_account_button.setEnabled(False)
|
||||
self.login_dialog = LoginDialog(parent=self, app=self.app)
|
||||
self.login_dialog.help_label.setText("Requesting a Microsoft sign-in code…")
|
||||
self.login_dialog.help_label.setText(self.app.tr_text("Requesting a Microsoft sign-in code…"))
|
||||
self.login_dialog.copy_code_button.setEnabled(False)
|
||||
self.login_dialog.open_browser_button.setEnabled(False)
|
||||
self.login_dialog.open_browser_button.clicked.connect(self.open_sign_in_page)
|
||||
@@ -351,7 +358,7 @@ class AccountPage(QWidget):
|
||||
|
||||
if self.login_dialog is not None:
|
||||
self.login_dialog.help_label.setText(
|
||||
device.get("message") or "Copy the code below, then continue in your browser."
|
||||
device.get("message") or self.app.tr_text("Copy the code below, then continue in your browser.")
|
||||
)
|
||||
self.login_dialog.set_code(device.get("user_code", ""))
|
||||
self.login_dialog.copy_code_button.setEnabled(True)
|
||||
@@ -378,11 +385,11 @@ class AccountPage(QWidget):
|
||||
def _login_failed(self, message: str):
|
||||
if self.login_dialog is not None:
|
||||
self.login_dialog.set_error_message(message)
|
||||
self.login_dialog.help_label.setText("Microsoft sign-in could not be completed.")
|
||||
self.login_dialog.help_label.setText(self.app.tr_text("Microsoft sign-in could not be completed."))
|
||||
self.login_dialog.copy_code_button.setEnabled(False)
|
||||
self.login_dialog.open_browser_button.setEnabled(False)
|
||||
else:
|
||||
QMessageBox.critical(self, "Microsoft sign-in failed", message)
|
||||
QMessageBox.critical(self, self.app.tr_text("Microsoft sign-in failed"), message)
|
||||
|
||||
def _login_finished(self):
|
||||
self.add_account_button.setEnabled(True)
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import threading
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from core_lib.game.mod import fabric
|
||||
from core_lib.game.mod.mod_core import find_useful_part_in_mod_version_manifest
|
||||
from core_lib.game.version_info import get_available_version_types
|
||||
|
||||
|
||||
class AddModdedVersionDialog(QDialog):
|
||||
"""
|
||||
Create a local mod-loader manifest without creating a profile.
|
||||
|
||||
This dialog is vibed.
|
||||
"""
|
||||
|
||||
version_created = Signal(str)
|
||||
|
||||
class WorkerSignals(QObject):
|
||||
manifest_loaded = Signal(object, object)
|
||||
loader_versions_loaded = Signal(str, object, object)
|
||||
version_installed = Signal(object, object)
|
||||
|
||||
def __init__(self, app, parent=None):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.manifest_manager = app.game_manager.manifest
|
||||
self.root_manifest: dict = {}
|
||||
self.worker_signals = self.WorkerSignals(self)
|
||||
|
||||
self.setWindowTitle(self.app.tr_text("Add Modded Version"))
|
||||
self.setMinimumWidth(480)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(20, 18, 20, 18)
|
||||
root.setSpacing(14)
|
||||
|
||||
title = QLabel(self.app.tr_text("Add Modded Version"))
|
||||
title.setObjectName("dialogTitle")
|
||||
description = QLabel(self.app.tr_text(
|
||||
"Create a local mod-loader version manifest without creating a profile."
|
||||
))
|
||||
description.setWordWrap(True)
|
||||
|
||||
form = QFormLayout()
|
||||
form.setVerticalSpacing(10)
|
||||
|
||||
self.loader_dropdown = QComboBox()
|
||||
self.loader_dropdown.setObjectName("Dropdown")
|
||||
self.loader_dropdown.addItem(self.app.tr_text("Fabric"), "fabric")
|
||||
|
||||
self.minecraft_type_dropdown = QComboBox()
|
||||
self.minecraft_type_dropdown.setObjectName("Dropdown")
|
||||
self.minecraft_version_dropdown = QComboBox()
|
||||
self.minecraft_version_dropdown.setObjectName("Dropdown")
|
||||
self.loader_version_dropdown = QComboBox()
|
||||
self.loader_version_dropdown.setObjectName("Dropdown")
|
||||
|
||||
form.addRow(self.app.tr_text("Mod loader"), self.loader_dropdown)
|
||||
form.addRow(self.app.tr_text("Minecraft type"), self.minecraft_type_dropdown)
|
||||
form.addRow(self.app.tr_text("Minecraft version"), self.minecraft_version_dropdown)
|
||||
form.addRow(self.app.tr_text("Loader version"), self.loader_version_dropdown)
|
||||
|
||||
self.status_label = QLabel()
|
||||
self.status_label.setWordWrap(True)
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
self.refresh_button = QPushButton(self.app.tr_text("Refresh"))
|
||||
self.cancel_button = QPushButton(self.app.tr_text("Cancel"))
|
||||
self.add_button = QPushButton(self.app.tr_text("Add Version"))
|
||||
self.add_button.setObjectName("saveProfileButton")
|
||||
buttons.addWidget(self.refresh_button)
|
||||
buttons.addStretch()
|
||||
buttons.addWidget(self.cancel_button)
|
||||
buttons.addWidget(self.add_button)
|
||||
|
||||
root.addWidget(title)
|
||||
root.addWidget(description)
|
||||
root.addLayout(form)
|
||||
root.addWidget(self.status_label)
|
||||
root.addLayout(buttons)
|
||||
|
||||
self.minecraft_type_dropdown.currentIndexChanged.connect(
|
||||
self._update_minecraft_versions
|
||||
)
|
||||
self.minecraft_version_dropdown.currentIndexChanged.connect(
|
||||
self._start_load_loader_versions
|
||||
)
|
||||
self.refresh_button.clicked.connect(self.load_versions)
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
self.add_button.clicked.connect(self.install_selected_version)
|
||||
|
||||
self.worker_signals.manifest_loaded.connect(self._manifest_loaded)
|
||||
self.worker_signals.loader_versions_loaded.connect(
|
||||
self._loader_versions_loaded
|
||||
)
|
||||
self.worker_signals.version_installed.connect(self._version_installed)
|
||||
|
||||
self.load_versions()
|
||||
|
||||
@staticmethod
|
||||
def _error_text(error: Exception) -> str:
|
||||
return str(getattr(error, "user_message", None) or error)
|
||||
|
||||
def _set_loading(self, loading: bool, status: str = "") -> None:
|
||||
self.refresh_button.setEnabled(not loading)
|
||||
self.add_button.setEnabled(not loading)
|
||||
self.status_label.setText(status)
|
||||
|
||||
def load_versions(self) -> None:
|
||||
self._set_loading(True, self.app.tr_text("Loading Minecraft versions..."))
|
||||
self.minecraft_type_dropdown.setEnabled(False)
|
||||
self.minecraft_version_dropdown.setEnabled(False)
|
||||
self.loader_version_dropdown.setEnabled(False)
|
||||
|
||||
def worker():
|
||||
try:
|
||||
manifest = self.manifest_manager.get_cached_version_manifest()
|
||||
if not manifest:
|
||||
raise RuntimeError("Unable to load the Minecraft version manifest.")
|
||||
self.worker_signals.manifest_loaded.emit(manifest, None)
|
||||
except Exception as error:
|
||||
self.worker_signals.manifest_loaded.emit(None, error)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _manifest_loaded(self, manifest, error) -> None:
|
||||
if error is not None:
|
||||
self._set_loading(False, self.app.tr_text(
|
||||
"Unable to load versions: {error}", error=self._error_text(error)
|
||||
))
|
||||
return
|
||||
|
||||
self.root_manifest = manifest
|
||||
try:
|
||||
types = get_available_version_types(manifest)
|
||||
except Exception as exc:
|
||||
self._set_loading(False, self.app.tr_text(
|
||||
"Unable to read version types: {error}", error=self._error_text(exc)
|
||||
))
|
||||
return
|
||||
|
||||
types = [item for item in types if isinstance(item, str) and item]
|
||||
if "release" in types:
|
||||
types.remove("release")
|
||||
types.insert(0, "release")
|
||||
|
||||
self.minecraft_type_dropdown.blockSignals(True)
|
||||
self.minecraft_type_dropdown.clear()
|
||||
self.minecraft_type_dropdown.addItems(types)
|
||||
self.minecraft_type_dropdown.blockSignals(False)
|
||||
self.minecraft_type_dropdown.setEnabled(True)
|
||||
self.minecraft_version_dropdown.setEnabled(True)
|
||||
self._update_minecraft_versions()
|
||||
|
||||
def _update_minecraft_versions(self, _index: int = -1) -> None:
|
||||
version_type = self.minecraft_type_dropdown.currentText()
|
||||
self.minecraft_version_dropdown.blockSignals(True)
|
||||
self.minecraft_version_dropdown.clear()
|
||||
|
||||
for version in self.root_manifest.get("versions", []):
|
||||
if version.get("type") != version_type:
|
||||
continue
|
||||
version_id = version.get("id")
|
||||
if version_id:
|
||||
self.minecraft_version_dropdown.addItem(version_id, version_id)
|
||||
|
||||
self.minecraft_version_dropdown.blockSignals(False)
|
||||
self._start_load_loader_versions()
|
||||
|
||||
def _start_load_loader_versions(self, _index: int = -1) -> None:
|
||||
minecraft_version = self.minecraft_version_dropdown.currentData()
|
||||
self.loader_version_dropdown.clear()
|
||||
self.loader_version_dropdown.setEnabled(False)
|
||||
|
||||
if not minecraft_version:
|
||||
self._set_loading(False, self.app.tr_text("No Minecraft version is available for this type."))
|
||||
return
|
||||
|
||||
self._set_loading(True, self.app.tr_text(
|
||||
"Loading Fabric versions for {version}...", version=minecraft_version
|
||||
))
|
||||
|
||||
def worker():
|
||||
try:
|
||||
loaders = fabric.get_version_support_loader_list(minecraft_version)
|
||||
self.worker_signals.loader_versions_loaded.emit(
|
||||
minecraft_version, loaders, None
|
||||
)
|
||||
except Exception as error:
|
||||
self.worker_signals.loader_versions_loaded.emit(
|
||||
minecraft_version, None, error
|
||||
)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _loader_versions_loaded(self, minecraft_version, loaders, error) -> None:
|
||||
# Ignore an obsolete response after the user selected another version.
|
||||
if minecraft_version != self.minecraft_version_dropdown.currentData():
|
||||
return
|
||||
|
||||
if error is not None:
|
||||
self._set_loading(
|
||||
False,
|
||||
self.app.tr_text(
|
||||
"Unable to load Fabric versions: {error}",
|
||||
error=self._error_text(error),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
self.loader_version_dropdown.clear()
|
||||
for loader in loaders:
|
||||
loader_version = loader.get("version")
|
||||
if not loader_version:
|
||||
continue
|
||||
display_name = loader_version
|
||||
if loader.get("stable") is True:
|
||||
display_name += " (stable)"
|
||||
self.loader_version_dropdown.addItem(display_name, loader)
|
||||
|
||||
self.loader_version_dropdown.setEnabled(
|
||||
self.loader_version_dropdown.count() > 0
|
||||
)
|
||||
self._set_loading(False, self.app.tr_text("Select a loader version to continue."))
|
||||
|
||||
def install_selected_version(self) -> None:
|
||||
minecraft_version = self.minecraft_version_dropdown.currentData()
|
||||
loader_data = self.loader_version_dropdown.currentData()
|
||||
|
||||
if not minecraft_version or not isinstance(loader_data, dict):
|
||||
self.status_label.setText(self.app.tr_text("Select a Minecraft and loader version."))
|
||||
return
|
||||
|
||||
loader_version = loader_data.get("version")
|
||||
if not loader_version:
|
||||
self.status_label.setText(self.app.tr_text("The selected loader version is invalid."))
|
||||
return
|
||||
|
||||
self._set_loading(True, self.app.tr_text("Creating the Fabric version manifest..."))
|
||||
self.loader_dropdown.setEnabled(False)
|
||||
self.minecraft_type_dropdown.setEnabled(False)
|
||||
self.minecraft_version_dropdown.setEnabled(False)
|
||||
self.loader_version_dropdown.setEnabled(False)
|
||||
|
||||
def worker():
|
||||
try:
|
||||
# Cache the parent manifest as well, so the new modded version
|
||||
# can be resolved immediately when it is selected for launch.
|
||||
parent = self.manifest_manager.get_cached_version_manifest(
|
||||
minecraft_version
|
||||
)
|
||||
if not parent:
|
||||
raise RuntimeError(
|
||||
f"Unable to cache Minecraft {minecraft_version}."
|
||||
)
|
||||
|
||||
version_id = fabric.get_full_version_string(
|
||||
version=minecraft_version,
|
||||
loader_version=loader_version,
|
||||
)
|
||||
|
||||
def fetch_manifest(_version_id: str):
|
||||
return (
|
||||
fabric.get_loader_manifest_from_loader_data(
|
||||
minecraft_version, loader_data
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
manifest = self.manifest_manager.get_cached_custom_version_manifest(
|
||||
version_id,
|
||||
fetch_manifest_handler=fetch_manifest,
|
||||
)
|
||||
if not manifest:
|
||||
raise RuntimeError("Unable to create the Fabric version manifest.")
|
||||
|
||||
loader_info = find_useful_part_in_mod_version_manifest(manifest)
|
||||
if loader_info["id"] != version_id:
|
||||
raise ValueError(
|
||||
f"Manifest ID mismatch: expected {version_id}, "
|
||||
f"got {loader_info['id']}."
|
||||
)
|
||||
if loader_info["inheritsFrom"] != minecraft_version:
|
||||
raise ValueError(
|
||||
"The created manifest inherits from an unexpected "
|
||||
"Minecraft version."
|
||||
)
|
||||
|
||||
self.worker_signals.version_installed.emit(version_id, None)
|
||||
except Exception as error:
|
||||
self.worker_signals.version_installed.emit(None, error)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _version_installed(self, version_id, error) -> None:
|
||||
if error is not None:
|
||||
self.loader_dropdown.setEnabled(True)
|
||||
self.minecraft_type_dropdown.setEnabled(True)
|
||||
self.minecraft_version_dropdown.setEnabled(True)
|
||||
self.loader_version_dropdown.setEnabled(True)
|
||||
self._set_loading(False, self.app.tr_text(
|
||||
"Unable to add version: {error}", error=self._error_text(error)
|
||||
))
|
||||
return
|
||||
|
||||
self.status_label.setText(self.app.tr_text(
|
||||
"Added version {version_id}.", version_id=version_id
|
||||
))
|
||||
self.version_created.emit(version_id)
|
||||
self.accept()
|
||||
+74
-38
@@ -13,6 +13,7 @@ from core_lib.game.version_info import get_all_versions, get_available_version_t
|
||||
from manager.game import ManifestManager, GameManager
|
||||
from manager.profile import CreateProfileRequest
|
||||
from manager.widgets import SignalTransferItem, WidgetManager
|
||||
from pages.install_java_dialog import InstallJavaDialog
|
||||
|
||||
class CreateProfilePage(QWidget):
|
||||
def __init__(self, app, parent=None):
|
||||
@@ -33,7 +34,7 @@ class CreateProfilePage(QWidget):
|
||||
self.operate_layout = QHBoxLayout()
|
||||
|
||||
# Some items
|
||||
self.version_type_label = QLabel("Version Type")
|
||||
self.version_type_label = QLabel(self.app.tr_text("Version Type"))
|
||||
self.version_type_dropdown = QComboBox()
|
||||
self.version_type_dropdown.setObjectName("Dropdown")
|
||||
|
||||
@@ -42,7 +43,7 @@ class CreateProfilePage(QWidget):
|
||||
icon = self.app.get_icon(Path(self.app.icon_dir, "mod_loader_temp.png"))
|
||||
pixmap = icon.pixmap(QSize(100, 100))
|
||||
self.mod_loader_icon_label.setPixmap(pixmap)
|
||||
self.mod_loader_label = QLabel("Mod Loader")
|
||||
self.mod_loader_label = QLabel(self.app.tr_text("Mod Loader"))
|
||||
self.mod_loader_dropdown = QComboBox()
|
||||
self.mod_loader_dropdown.setObjectName("Dropdown")
|
||||
|
||||
@@ -50,9 +51,7 @@ class CreateProfilePage(QWidget):
|
||||
self.mod_config_layout = QHBoxLayout()
|
||||
self.mod_config_fields_layout = QVBoxLayout()
|
||||
|
||||
self.mod_loader_version_label = QLabel(
|
||||
"Mod Loader Version"
|
||||
)
|
||||
self.mod_loader_version_label = QLabel(self.app.tr_text("Mod Loader Version"))
|
||||
|
||||
self.mod_loader_version_list = QListWidget()
|
||||
self.mod_loader_version_list.setObjectName(
|
||||
@@ -60,23 +59,27 @@ class CreateProfilePage(QWidget):
|
||||
)
|
||||
|
||||
# Version list
|
||||
self.version_list_type_label = QLabel("Version: Vanilla")
|
||||
self.version_list_label = QLabel("Select a version to continue:")
|
||||
self.version_list_type_label = QLabel(self.app.tr_text("Version: Vanilla"))
|
||||
self.version_list_label = QLabel(self.app.tr_text("Select a version to continue:"))
|
||||
self.version_list = QListWidget()
|
||||
self.version_list.setObjectName("ListWidget")
|
||||
|
||||
# Button
|
||||
self.create_profile_button = QPushButton("Create Profile")
|
||||
self.create_profile_button = QPushButton(self.app.tr_text("Create Profile"))
|
||||
self.create_profile_button.setObjectName("Button")
|
||||
|
||||
self.refresh_button = QPushButton("Refresh")
|
||||
self.refresh_button = QPushButton(self.app.tr_text("Refresh"))
|
||||
self.refresh_button.setObjectName("Button")
|
||||
|
||||
self.install_java_button = QPushButton(self.app.tr_text("Install Java"))
|
||||
self.install_java_button.setObjectName("Button")
|
||||
|
||||
# Bind widget
|
||||
self.main_layout.addWidget(self.version_list_label)
|
||||
self.main_layout.addWidget(self.version_list)
|
||||
self.operate_layout.addWidget(self.version_list_type_label)
|
||||
self.operate_layout.addStretch()
|
||||
self.operate_layout.addWidget(self.install_java_button)
|
||||
self.operate_layout.addWidget(self.refresh_button)
|
||||
self.operate_layout.addWidget(self.create_profile_button)
|
||||
|
||||
@@ -142,6 +145,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
# Button event
|
||||
self.create_profile_button.clicked.connect(self.create_profile)
|
||||
self.install_java_button.clicked.connect(self.create_install_jvm_dialog)
|
||||
|
||||
# Update version list while current type is changed
|
||||
self.version_type_dropdown.currentTextChanged.connect(
|
||||
@@ -406,7 +410,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected version.",
|
||||
self.app.tr_text("Timeout while waiting for selected version."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -414,7 +418,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not minecraft_version:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Selected a version before creating profile.",
|
||||
self.app.tr_text("Selected a version before creating profile."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -425,7 +429,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not loader_request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected mod loader.",
|
||||
self.app.tr_text("Timeout while waiting for selected mod loader."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -433,7 +437,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if loader_name not in self.loader_list:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Loader {} not found.".format(loader_name),
|
||||
self.app.tr_text("Loader {loader} not found.", loader=loader_name),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -442,7 +446,10 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if profile_create_handler is None:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Profile creation for loader {} is not implemented.".format(loader_name),
|
||||
self.app.tr_text(
|
||||
"Profile creation for loader {loader} is not implemented.",
|
||||
loader=loader_name,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -453,7 +460,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not loader_version_request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected mod loader version.",
|
||||
self.app.tr_text("Timeout while waiting for selected mod loader version."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -464,8 +471,8 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
profile_name, accepted = QInputDialog.getText(
|
||||
self,
|
||||
"Create Profile",
|
||||
"Profile name:",
|
||||
self.app.tr_text("Create Profile"),
|
||||
self.app.tr_text("Profile name:"),
|
||||
text=f"Minecraft {minecraft_version}",
|
||||
)
|
||||
|
||||
@@ -475,7 +482,7 @@ class CreateProfilePage(QWidget):
|
||||
profile_name = profile_name.strip()
|
||||
if not profile_name:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Profile name cannot be empty.",
|
||||
self.app.tr_text("Profile name cannot be empty."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -496,7 +503,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not manifest:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Unable to create profile due to manifest fetch failure.",
|
||||
self.app.tr_text("Unable to create profile due to manifest fetch failure."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -508,7 +515,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if version_id is None:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Unable to create profile due to version ID is missing in manifest.",
|
||||
self.app.tr_text("Unable to create profile due to version ID is missing in manifest."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -531,7 +538,7 @@ class CreateProfilePage(QWidget):
|
||||
return
|
||||
|
||||
self.widget_mgr.signal.show_info_message.emit(
|
||||
f"Profile {profile_id} successfully created.",
|
||||
self.app.tr_text("Profile {profile_id} successfully created.", profile_id=profile_id),
|
||||
)
|
||||
|
||||
def handle_fabric_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None, switch_to_current=False):
|
||||
@@ -545,7 +552,7 @@ class CreateProfilePage(QWidget):
|
||||
"""
|
||||
if not loader_version:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Selected a fabric loader version before creating profile.",
|
||||
self.app.tr_text("Selected a fabric loader version before creating profile."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -555,7 +562,7 @@ class CreateProfilePage(QWidget):
|
||||
)
|
||||
if not vanilla_manifest:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Unable to create profile due to vanilla manifest fetch failure.",
|
||||
self.app.tr_text("Unable to create profile due to vanilla manifest fetch failure."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -573,7 +580,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not loader_manifest:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Unable to create profile due to mod loader manifest fetch failure.",
|
||||
self.app.tr_text("Unable to create profile due to mod loader manifest fetch failure."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -585,7 +592,7 @@ class CreateProfilePage(QWidget):
|
||||
if loader_info["id"] != version_id:
|
||||
self.app.logger.error(f"Fabric manifest ID mismatch: expected {version_id}, got {loader_info["id"]}.")
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Unable to create profile. Exising loader version ID mismatch.",
|
||||
self.app.tr_text("Unable to create profile. Exising loader version ID mismatch."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -595,7 +602,7 @@ class CreateProfilePage(QWidget):
|
||||
f'{loader_info["inheritsFrom"]}, not {minecraft_version}.'
|
||||
)
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Unable to create profile. Inherits version is mismatched as expected.",
|
||||
self.app.tr_text("Unable to create profile. Inherits version is mismatched as expected."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -620,20 +627,23 @@ class CreateProfilePage(QWidget):
|
||||
return
|
||||
|
||||
self.widget_mgr.signal.show_info_message.emit(
|
||||
f"Profile {profile_id} with fabric loader successfully created.",
|
||||
self.app.tr_text(
|
||||
"Profile {profile_id} with fabric loader successfully created.",
|
||||
profile_id=profile_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle_forge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None):
|
||||
if not loader_version:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Selected a forge loader version before creating profile.",
|
||||
self.app.tr_text("Selected a forge loader version before creating profile."),
|
||||
)
|
||||
|
||||
def handle_neoforge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None):
|
||||
if not loader_version:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Selected a neoforge loader version before creating profile.",
|
||||
self.app.tr_text("Selected a neoforge loader version before creating profile."),
|
||||
)
|
||||
|
||||
#
|
||||
@@ -666,7 +676,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected type.",
|
||||
self.app.tr_text("Timeout while waiting for selected type."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -674,7 +684,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not selected_type:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"No type selected.",
|
||||
self.app.tr_text("No type selected."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -715,14 +725,14 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected version.",
|
||||
self.app.tr_text("Timeout while waiting for selected version."),
|
||||
)
|
||||
|
||||
selected_version = request.result()
|
||||
|
||||
if not selected_version:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"No version selected.",
|
||||
self.app.tr_text("No version selected."),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -731,12 +741,17 @@ class CreateProfilePage(QWidget):
|
||||
)
|
||||
except (VersionManifestFetchException, VersionManifestException) as e:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Failed to fetch version manifest: {}".format(e.user_message),
|
||||
self.app.tr_text(
|
||||
"Failed to fetch version manifest: {error}", error=e.user_message
|
||||
),
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"An unexpected error occurred while fetching version manifest: {}".format(e),
|
||||
self.app.tr_text(
|
||||
"An unexpected error occurred while fetching version manifest: {error}",
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -768,7 +783,7 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not request.wait(10):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Timeout while waiting for selected loader.",
|
||||
self.app.tr_text("Timeout while waiting for selected loader."),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -782,7 +797,10 @@ class CreateProfilePage(QWidget):
|
||||
|
||||
if not version_handler:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"Selected loader \"{}\" is not implemented".format(loader_name),
|
||||
self.app.tr_text(
|
||||
'Selected loader "{loader}" is not implemented',
|
||||
loader=loader_name,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -791,4 +809,22 @@ class CreateProfilePage(QWidget):
|
||||
else:
|
||||
self.app.logger.warning(
|
||||
"Select mod loader \"loadVersionMethod\" is not callable."
|
||||
)
|
||||
)
|
||||
|
||||
def create_install_jvm_dialog(self):
|
||||
manifest = None
|
||||
selected = self.version_list.currentItem()
|
||||
if selected:
|
||||
version_id = (
|
||||
selected.raw_value
|
||||
if isinstance(selected, self.DisplayListItem)
|
||||
else selected.text()
|
||||
)
|
||||
try:
|
||||
resolved = self.manifest_mgr.resolve_manifest(version_id)
|
||||
manifest = resolved.manifest if resolved else None
|
||||
except Exception as exc:
|
||||
self.app.logger.warning(f"Unable to resolve Java requirement for {version_id}: {exc}")
|
||||
self.dialog = InstallJavaDialog(self.app, manifest=manifest, parent=self)
|
||||
self.dialog.setModal(False)
|
||||
self.dialog.show()
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import threading
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from manager.game import InstallableJavaRuntime, JavaManager, JavaRuntime
|
||||
|
||||
|
||||
class InstallJavaDialog(QDialog):
|
||||
"""Install and register launcher-managed Mojang Java runtimes."""
|
||||
|
||||
class WorkerSignals(QObject):
|
||||
components_loaded = Signal(object, object)
|
||||
installed = Signal(object, object)
|
||||
|
||||
runtime_installed = Signal(object)
|
||||
|
||||
def __init__(self, app, manifest: dict | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.java_manager: JavaManager = app.game_manager.java
|
||||
self.manifest = manifest or {}
|
||||
self.worker_signals = self.WorkerSignals(self)
|
||||
self.worker_signals.components_loaded.connect(self._components_loaded)
|
||||
self.worker_signals.installed.connect(self._installed)
|
||||
|
||||
self.setWindowTitle(self.app.tr_text("Install Java"))
|
||||
self.setMinimumWidth(480)
|
||||
layout = QVBoxLayout(self)
|
||||
self.requirement_label = QLabel(self._requirement_text())
|
||||
self.runtime_list = QListWidget()
|
||||
self.component_dropdown = QComboBox()
|
||||
self.component_dropdown.setEditable(False)
|
||||
self.status_label = QLabel(self.app.tr_text("Loading available Mojang runtimes..."))
|
||||
self.install_button = QPushButton(self.app.tr_text("Install"))
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
|
||||
layout.addWidget(self.requirement_label)
|
||||
layout.addWidget(QLabel(self.app.tr_text("Detected Java runtimes")))
|
||||
layout.addWidget(self.runtime_list)
|
||||
layout.addWidget(QLabel(self.app.tr_text("Mojang runtime component")))
|
||||
layout.addWidget(self.component_dropdown)
|
||||
layout.addWidget(self.status_label)
|
||||
layout.addWidget(self.install_button)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
buttons.rejected.connect(self.reject)
|
||||
self.install_button.clicked.connect(self.install_selected)
|
||||
self._refresh_runtime_list()
|
||||
threading.Thread(target=self._load_components, daemon=True).start()
|
||||
|
||||
def _requirement_text(self) -> str:
|
||||
requirement = self.manifest.get("javaVersion") or {}
|
||||
major = requirement.get("majorVersion")
|
||||
component = requirement.get("component")
|
||||
if major:
|
||||
return self.app.tr_text(
|
||||
"This version requires Java {version}.", version=major
|
||||
) + f" ({component or 'component unknown'})"
|
||||
return self.app.tr_text("Choose a Mojang Java runtime component to install.")
|
||||
|
||||
def _refresh_runtime_list(self) -> None:
|
||||
self.runtime_list.clear()
|
||||
for runtime in self.java_manager.list_runtimes():
|
||||
state = "available" if runtime.valid else "missing"
|
||||
self.runtime_list.addItem(
|
||||
f"Java {runtime.major_version} | {runtime.source} | {state}\n{runtime.executable}"
|
||||
)
|
||||
|
||||
def _load_components(self) -> None:
|
||||
try:
|
||||
runtimes = self.java_manager.list_installable_runtimes()
|
||||
self.worker_signals.components_loaded.emit(runtimes, None)
|
||||
except Exception as exc:
|
||||
self.worker_signals.components_loaded.emit([], exc)
|
||||
|
||||
def _components_loaded(self, runtimes: list[InstallableJavaRuntime], error) -> None:
|
||||
requirement = self.manifest.get("javaVersion") or {}
|
||||
required = requirement.get("component")
|
||||
required_major = requirement.get("majorVersion")
|
||||
self.component_dropdown.clear()
|
||||
for runtime in runtimes:
|
||||
major = runtime.major_version
|
||||
if runtime.component == required and required_major is not None:
|
||||
# The Minecraft version manifest is authoritative for its own
|
||||
# runtime requirement if Mojang's version name is unusual.
|
||||
major = int(required_major)
|
||||
label = f"Java {major} | {runtime.component}" if major is not None else runtime.component
|
||||
if runtime.raw_version:
|
||||
label += f" ({runtime.raw_version})"
|
||||
self.component_dropdown.addItem(label, runtime.component)
|
||||
if required:
|
||||
index = self.component_dropdown.findData(required)
|
||||
if index < 0:
|
||||
label = f"Java {required_major} | {required}" if required_major else required
|
||||
self.component_dropdown.addItem(label, required)
|
||||
index = self.component_dropdown.count() - 1
|
||||
self.component_dropdown.setCurrentIndex(index)
|
||||
self.install_button.setEnabled(self.component_dropdown.count() > 0)
|
||||
self.status_label.setText(
|
||||
self.app.tr_text("Unable to load component list: {error}", error=error)
|
||||
if error else self.app.tr_text("Ready to install.")
|
||||
)
|
||||
|
||||
def install_selected(self) -> None:
|
||||
component = self.component_dropdown.currentData()
|
||||
if not component:
|
||||
self.status_label.setText(self.app.tr_text("Select a runtime component."))
|
||||
return
|
||||
self.install_button.setEnabled(False)
|
||||
self.component_dropdown.setEnabled(False)
|
||||
self.status_label.setText(self.app.tr_text("Installing {component}...", component=component))
|
||||
callback, task_id = self.app.game_manager.manifest.create_download_progress_callback(
|
||||
self.app.tr_text("Installing Java ({component})", component=component)
|
||||
)
|
||||
|
||||
def worker():
|
||||
try:
|
||||
runtime = self.java_manager.install_runtime(
|
||||
component,
|
||||
progress_callback=callback,
|
||||
redownload_callback=self.app.game_manager.game_file.redownload_files,
|
||||
)
|
||||
self.app.widget_manager.download_signal.finished.emit(task_id)
|
||||
self.worker_signals.installed.emit(runtime, None)
|
||||
except Exception as exc:
|
||||
self.app.widget_manager.download_signal.failed.emit(task_id, str(exc))
|
||||
self.worker_signals.installed.emit(None, exc)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _installed(self, runtime: JavaRuntime | None, error) -> None:
|
||||
self.install_button.setEnabled(True)
|
||||
self.component_dropdown.setEnabled(True)
|
||||
if error:
|
||||
self.status_label.setText(self.app.tr_text("Installation failed: {error}", error=error))
|
||||
return
|
||||
self._refresh_runtime_list()
|
||||
self.status_label.setText(self.app.tr_text(
|
||||
"Java {version} installed successfully.", version=runtime.major_version
|
||||
))
|
||||
self.runtime_installed.emit(runtime)
|
||||
+27
-18
@@ -208,7 +208,7 @@ class LaunchProfilePage(QWidget):
|
||||
self.bottom_layout = QHBoxLayout()
|
||||
|
||||
# Widgets
|
||||
self.status_label = QLabel("Select a profile to launch")
|
||||
self.status_label = QLabel(self.app.tr_text("Select a profile to launch"))
|
||||
self.status_label.setStyleSheet("font-size: 12px; font-weight: 600;")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
@@ -221,7 +221,7 @@ class LaunchProfilePage(QWidget):
|
||||
self.profile_popup.profile_selected.connect(self.select_profile)
|
||||
self.profile_popup.closed.connect(self.refresh_profile_button)
|
||||
|
||||
launch_button = QPushButton("Launch Game")
|
||||
launch_button = QPushButton(self.app.tr_text("Launch Game"))
|
||||
launch_button.setObjectName("launchButton")
|
||||
launch_button.setMinimumWidth(120)
|
||||
launch_button.setMinimumHeight(42)
|
||||
@@ -258,7 +258,7 @@ class LaunchProfilePage(QWidget):
|
||||
# self.profile_frame.setGraphicsEffect(shadow)
|
||||
|
||||
# Header
|
||||
self.header_label = QLabel("Launch Profile")
|
||||
self.header_label = QLabel(self.app.tr_text("Launch Profile"))
|
||||
self.header_label.setObjectName("headerLabel")
|
||||
|
||||
self.header_layout = QHBoxLayout(self.profile_overlay)
|
||||
@@ -322,16 +322,20 @@ class LaunchProfilePage(QWidget):
|
||||
:return:
|
||||
"""
|
||||
if not result.success:
|
||||
self.status_label.setText("Launch Failed")
|
||||
self.status_label.setText(self.app.tr_text("Launch Failed"))
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
f"Unable to launch profile "
|
||||
f"{launch_profile.display_name}: "
|
||||
f"{result.error_message}"
|
||||
self.app.tr_text(
|
||||
"Unable to launch profile {profile}: {error}",
|
||||
profile=launch_profile.display_name,
|
||||
error=result.error_message,
|
||||
)
|
||||
)
|
||||
self.status_label_cleanup()
|
||||
return
|
||||
|
||||
# Create log view
|
||||
log_window = ProcessLogWindow(
|
||||
self.app,
|
||||
result.process,
|
||||
launch_profile.profile_id,
|
||||
title=(
|
||||
@@ -350,12 +354,17 @@ class LaunchProfilePage(QWidget):
|
||||
self.process_log_windows.append(log_window)
|
||||
log_window.show()
|
||||
|
||||
self.status_label.setText("Launched!")
|
||||
self.status_label.setText(self.app.tr_text("Launched!"))
|
||||
|
||||
# Cleanup for status label
|
||||
self.status_label_cleanup()
|
||||
|
||||
def status_label_cleanup(self):
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(lambda: self.status_label.setText("Select a profile to launch"))
|
||||
timer.timeout.connect(lambda: self.status_label.setText(
|
||||
self.app.tr_text("Select a profile to launch")
|
||||
))
|
||||
timer.start(3000)
|
||||
|
||||
def load_profiles(self):
|
||||
@@ -364,7 +373,7 @@ class LaunchProfilePage(QWidget):
|
||||
|
||||
if not profiles:
|
||||
self.profile_button.setProperty("profile_id", None)
|
||||
self.profile_button.name_label.setText("No profiles available")
|
||||
self.profile_button.name_label.setText(self.app.tr_text("No profiles available"))
|
||||
self.profile_button.version_label.clear()
|
||||
return
|
||||
|
||||
@@ -441,30 +450,30 @@ class LaunchProfilePage(QWidget):
|
||||
|
||||
if profile_id is None:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"There is no profile is selected."
|
||||
" Please create one first."
|
||||
self.app.tr_text("There is no profile is selected. Please create one first.")
|
||||
)
|
||||
return
|
||||
|
||||
self.status_label.setText("Preparing to launch...")
|
||||
self.status_label.setText(self.app.tr_text("Preparing to launch..."))
|
||||
launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
|
||||
|
||||
if launch_profile is None:
|
||||
self.status_label.setText("Unable to load the selected profile.")
|
||||
self.status_label.setText(self.app.tr_text("Unable to load the selected profile."))
|
||||
return
|
||||
|
||||
if self.launch_manager.is_profile_running(launch_profile.profile_id):
|
||||
request = AskForRequest(
|
||||
"Launch Request",
|
||||
"Target profile {} is running. Are you sure you want to launch it again?".format(
|
||||
launch_profile.display_name
|
||||
self.app.tr_text("Launch Request"),
|
||||
self.app.tr_text(
|
||||
"Target profile {profile} is running. Are you sure you want to launch it again?",
|
||||
profile=launch_profile.display_name,
|
||||
),
|
||||
)
|
||||
self.widget_mgr.signal.askyesno.emit(request)
|
||||
|
||||
if not request.wait(60):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Time out waiting for request for launch."
|
||||
self.app.tr_text("Time out waiting for request for launch.")
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
+229
-42
@@ -6,23 +6,33 @@ from PySide6.QtGui import QDesktopServices, QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit,
|
||||
QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpinBox,
|
||||
QVBoxLayout, QWidget,
|
||||
QVBoxLayout, QWidget, QMenu, QComboBox,
|
||||
)
|
||||
|
||||
from manager.profile import ProfileDetail, ProfileManager, ProfileSummary, UpdateProfileRequest
|
||||
from manager.game import AvailableVersion
|
||||
from pages.add_modded_version import AddModdedVersionDialog
|
||||
|
||||
|
||||
class ManageProfileItem(QPushButton):
|
||||
selected = Signal(object)
|
||||
|
||||
def __init__(self, app, profile: ProfileSummary, parent=None):
|
||||
def __init__(self, app, profile: ProfileSummary,
|
||||
parent=None,
|
||||
delete_callback=None,
|
||||
duplicate_callback=None):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.profile = profile
|
||||
self.setObjectName("manageProfileItem")
|
||||
self.setCheckable(True)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setMinimumHeight(64)
|
||||
|
||||
# Callback
|
||||
self.delete_callback = delete_callback
|
||||
self.duplicate_callback = duplicate_callback
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
@@ -48,19 +58,57 @@ class ManageProfileItem(QPushButton):
|
||||
layout.addLayout(texts, 1)
|
||||
self.clicked.connect(lambda: self.selected.emit(self.profile))
|
||||
|
||||
# Enable right click menu
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.show_right_click_menu)
|
||||
|
||||
def show_right_click_menu(self, position):
|
||||
menu = QMenu(self)
|
||||
|
||||
duplicate_action = menu.addAction(self.app.tr_text("Duplicate"))
|
||||
remove_action = menu.addAction(self.app.tr_text("Delete"))
|
||||
|
||||
menu.addAction(duplicate_action)
|
||||
menu.addAction(remove_action)
|
||||
|
||||
def duplicate_callback():
|
||||
self.duplicate_callback(self.profile)
|
||||
|
||||
def delete_callback():
|
||||
self.delete_callback(self.profile)
|
||||
|
||||
if callable(self.duplicate_callback):
|
||||
duplicate_action.triggered.connect(duplicate_callback)
|
||||
|
||||
if callable(self.delete_callback):
|
||||
remove_action.triggered.connect(delete_callback)
|
||||
|
||||
global_position = self.mapToGlobal(position)
|
||||
menu.exec(global_position)
|
||||
|
||||
|
||||
class ManageProfilePage(QWidget):
|
||||
"""Select a profile on the left and edit its launcher settings on the right."""
|
||||
"""
|
||||
Profile manage page
|
||||
"""
|
||||
|
||||
VERSION_CATEGORIES = (
|
||||
("Vanilla", "vanilla"),
|
||||
("Modded", "modded"),
|
||||
("Custom", "custom"),
|
||||
)
|
||||
|
||||
def __init__(self, app, parent=None):
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.profile_manager: ProfileManager = app.profile_manager
|
||||
self.manifest_manager = app.game_manager.manifest
|
||||
self.selected_profile_id: str | None = None
|
||||
self.profile_items: dict[str, ManageProfileItem] = {}
|
||||
self.version_options: list[AvailableVersion] = []
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
title = QLabel("Manage Profiles (Experimental)")
|
||||
title = QLabel(self.app.tr_text("Manage Profiles"))
|
||||
title.setObjectName("manageTitle")
|
||||
root.addWidget(title)
|
||||
|
||||
@@ -68,6 +116,7 @@ class ManageProfilePage(QWidget):
|
||||
columns.setSpacing(12)
|
||||
root.addLayout(columns, 1)
|
||||
|
||||
# Widgets
|
||||
self.list_scroll = self._scroll_area("manageProfileListScroll")
|
||||
self.list_content = QWidget()
|
||||
self.list_layout = QVBoxLayout(self.list_content)
|
||||
@@ -87,7 +136,7 @@ class ManageProfilePage(QWidget):
|
||||
self.options_scroll.setWidget(self.options_content)
|
||||
columns.addWidget(self.options_scroll, 3)
|
||||
|
||||
self.empty_label = QLabel("Select a profile from the list to edit its settings.")
|
||||
self.empty_label = QLabel(self.app.tr_text("Select a profile from the list to edit its settings."))
|
||||
self.empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.empty_label.setWordWrap(True)
|
||||
self.options_layout.addWidget(self.empty_label, 1)
|
||||
@@ -101,45 +150,58 @@ class ManageProfilePage(QWidget):
|
||||
form.setVerticalSpacing(12)
|
||||
|
||||
self.name_edit = QLineEdit()
|
||||
self.version_edit = QLineEdit()
|
||||
self.type_edit = QLineEdit()
|
||||
self.version_dropdown = QComboBox()
|
||||
self.version_dropdown.setObjectName("Dropdown")
|
||||
self.version_dropdown.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
self.version_dropdown.setSizeAdjustPolicy(
|
||||
QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon
|
||||
)
|
||||
self.version_dropdown.setMinimumContentsLength(24)
|
||||
self.type_dropdown = QComboBox()
|
||||
self.type_dropdown.setObjectName("Dropdown")
|
||||
self.game_dir_edit = QLineEdit()
|
||||
self.java_args_edit = QLineEdit()
|
||||
self.icon_edit = QLineEdit()
|
||||
self.width_spin = self._dimension_spin()
|
||||
self.height_spin = self._dimension_spin()
|
||||
|
||||
form.addRow("Profile name", self.name_edit)
|
||||
form.addRow("Version ID", self.version_edit)
|
||||
form.addRow("Profile type", self.type_edit)
|
||||
form.addRow("Game directory", self.game_dir_edit)
|
||||
form.addRow("Java arguments", self.java_args_edit)
|
||||
form.addRow("Icon", self.icon_edit)
|
||||
form.addRow(self.app.tr_text("Profile name"), self.name_edit)
|
||||
form.addRow(self.app.tr_text("Type"), self.type_dropdown)
|
||||
form.addRow(self.app.tr_text("Version"), self.version_dropdown)
|
||||
form.addRow(self.app.tr_text("Game directory"), self.game_dir_edit)
|
||||
form.addRow(self.app.tr_text("Java arguments"), self.java_args_edit)
|
||||
form.addRow(self.app.tr_text("Icon"), self.icon_edit)
|
||||
|
||||
resolution = QHBoxLayout()
|
||||
resolution.addWidget(self.width_spin)
|
||||
resolution.addWidget(QLabel("×"))
|
||||
resolution.addWidget(self.height_spin)
|
||||
form.addRow("Resolution", resolution)
|
||||
form.addRow(self.app.tr_text("Resolution"), resolution)
|
||||
editor_layout.addLayout(form)
|
||||
|
||||
file_actions = QHBoxLayout()
|
||||
open_button = QPushButton("Open Profile Folder")
|
||||
add_version_button = QPushButton(self.app.tr_text("Add Modded Version"))
|
||||
add_version_button.clicked.connect(self.open_add_modded_version_dialog)
|
||||
open_button = QPushButton(self.app.tr_text("Open Profile Folder"))
|
||||
open_button.clicked.connect(self.open_profile_folder)
|
||||
import_button = QPushButton("Import Mods…")
|
||||
import_button = QPushButton(self.app.tr_text("Import Mods…"))
|
||||
import_button.clicked.connect(self.import_mods)
|
||||
file_actions.addWidget(add_version_button)
|
||||
file_actions.addWidget(open_button)
|
||||
file_actions.addWidget(import_button)
|
||||
file_actions.addStretch()
|
||||
editor_layout.addLayout(file_actions)
|
||||
|
||||
actions = QHBoxLayout()
|
||||
duplicate_button = QPushButton("Duplicate")
|
||||
duplicate_button = QPushButton(self.app.tr_text("Duplicate"))
|
||||
duplicate_button.clicked.connect(self.duplicate_profile)
|
||||
delete_button = QPushButton("Delete")
|
||||
delete_button = QPushButton(self.app.tr_text("Delete"))
|
||||
delete_button.setObjectName("dangerButton")
|
||||
delete_button.clicked.connect(self.delete_profile)
|
||||
save_button = QPushButton("Save Changes")
|
||||
save_button = QPushButton(self.app.tr_text("Save Changes"))
|
||||
save_button.setObjectName("saveProfileButton")
|
||||
save_button.clicked.connect(self.save_profile)
|
||||
actions.addWidget(duplicate_button)
|
||||
@@ -152,9 +214,11 @@ class ManageProfilePage(QWidget):
|
||||
self.options_layout.addStretch()
|
||||
self.editor.hide()
|
||||
|
||||
self.profile_manager.profiles_changed.connect(self.load_profiles)
|
||||
self.type_dropdown.currentIndexChanged.connect(self._filter_versions)
|
||||
self.profile_manager.profiles_changed.connect(self.refresh_profiles)
|
||||
self.app.apply_qss(Path("manage_profile.qss"), self.setStyleSheet)
|
||||
self.load_profiles()
|
||||
self._load_type_dropdown()
|
||||
self.refresh_profiles()
|
||||
|
||||
@staticmethod
|
||||
def _scroll_area(name: str) -> QScrollArea:
|
||||
@@ -165,31 +229,68 @@ class ManageProfilePage(QWidget):
|
||||
area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
return area
|
||||
|
||||
@staticmethod
|
||||
def _dimension_spin() -> QSpinBox:
|
||||
def _dimension_spin(self) -> QSpinBox:
|
||||
spin = QSpinBox()
|
||||
spin.setRange(0, 16384)
|
||||
spin.setSpecialValueText("Default")
|
||||
spin.setSpecialValueText(self.app.tr_text("Default"))
|
||||
spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
return spin
|
||||
|
||||
def _load_type_dropdown(self) -> None:
|
||||
self.type_dropdown.blockSignals(True)
|
||||
self.type_dropdown.clear()
|
||||
for display_name, category in self.VERSION_CATEGORIES:
|
||||
self.type_dropdown.addItem(self.app.tr_text(display_name), category)
|
||||
self.type_dropdown.blockSignals(False)
|
||||
|
||||
def refresh_profiles(self) -> None:
|
||||
self.version_options = self.manifest_manager.list_available_profile_versions()
|
||||
self.load_profiles()
|
||||
|
||||
def _filter_versions(self, _index: int = -1) -> None:
|
||||
selected_category = self.type_dropdown.currentData()
|
||||
selected_option = self.version_dropdown.currentData()
|
||||
selected_id = (
|
||||
selected_option.id
|
||||
if isinstance(selected_option, AvailableVersion)
|
||||
else None
|
||||
)
|
||||
|
||||
self.version_dropdown.blockSignals(True)
|
||||
self.version_dropdown.clear()
|
||||
|
||||
for option in self.version_options:
|
||||
if option.source == selected_category:
|
||||
self.version_dropdown.addItem(option.display_name, option)
|
||||
|
||||
if selected_id is not None:
|
||||
self._select_version(selected_id)
|
||||
|
||||
self.version_dropdown.blockSignals(False)
|
||||
|
||||
def load_profiles(self) -> None:
|
||||
while self.list_layout.count():
|
||||
item = self.list_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
self.profile_items.clear()
|
||||
|
||||
profiles = self.profile_manager.list_profiles()
|
||||
for profile in profiles:
|
||||
item = ManageProfileItem(self.app, profile, self.list_content)
|
||||
item = ManageProfileItem(self.app, profile,
|
||||
self.list_content,
|
||||
duplicate_callback=self.duplicate_profile,
|
||||
delete_callback=self.delete_profile)
|
||||
item.selected.connect(self.select_profile)
|
||||
self.profile_items[profile.id] = item
|
||||
self.list_layout.addWidget(item)
|
||||
|
||||
available_ids = {profile.id for profile in profiles}
|
||||
|
||||
if self.selected_profile_id not in available_ids:
|
||||
self.selected_profile_id = None
|
||||
|
||||
if self.selected_profile_id is None and profiles:
|
||||
current = self.profile_manager.get_current_profile_id()
|
||||
selected = next((p for p in profiles if p.id == current), profiles[0])
|
||||
@@ -203,19 +304,53 @@ class ManageProfilePage(QWidget):
|
||||
|
||||
def select_profile(self, profile: ProfileSummary) -> None:
|
||||
detail = self.profile_manager.get_profile(profile.id)
|
||||
|
||||
if detail is None:
|
||||
return
|
||||
|
||||
self.selected_profile_id = profile.id
|
||||
|
||||
for profile_id, item in self.profile_items.items():
|
||||
item.setChecked(profile_id == profile.id)
|
||||
|
||||
self._set_detail(detail)
|
||||
self.empty_label.hide()
|
||||
self.editor.show()
|
||||
|
||||
def _select_version(self, version_id: str) -> None:
|
||||
for index in range(self.version_dropdown.count()):
|
||||
data = self.version_dropdown.itemData(index)
|
||||
|
||||
if isinstance(data, AvailableVersion) and data.id == version_id:
|
||||
self.version_dropdown.setCurrentIndex(index)
|
||||
return
|
||||
|
||||
def _select_type(self, category: str) -> None:
|
||||
index = self.type_dropdown.findData(category)
|
||||
if index >= 0:
|
||||
self.type_dropdown.setCurrentIndex(index)
|
||||
|
||||
def _set_detail(self, profile: ProfileDetail) -> None:
|
||||
self.name_edit.setText(profile.name)
|
||||
self.version_edit.setText(profile.version_id)
|
||||
self.type_edit.setText(profile.version_type)
|
||||
option = next(
|
||||
(item for item in self.version_options if item.id == profile.version_id),
|
||||
None,
|
||||
)
|
||||
|
||||
# Preserve profiles imported from another launcher, even when their
|
||||
# version manifest is not available locally anymore.
|
||||
if option is None:
|
||||
option = AvailableVersion(
|
||||
id=profile.version_id,
|
||||
display_name=f"{profile.version_id} (Unavailable)",
|
||||
version_type=profile.version_type or "custom",
|
||||
source="custom",
|
||||
)
|
||||
self.version_options.append(option)
|
||||
|
||||
self._select_type(option.source)
|
||||
self._filter_versions()
|
||||
self._select_version(profile.version_id)
|
||||
self.game_dir_edit.setText(str(profile.game_dir or ""))
|
||||
self.java_args_edit.setText(profile.java_args)
|
||||
self.icon_edit.setText(profile.icon)
|
||||
@@ -226,87 +361,139 @@ class ManageProfilePage(QWidget):
|
||||
def _profile_dir(self) -> Path | None:
|
||||
if self.selected_profile_id is None:
|
||||
return None
|
||||
|
||||
detail = self.profile_manager.get_profile(self.selected_profile_id)
|
||||
|
||||
if detail is None:
|
||||
return None
|
||||
|
||||
return detail.game_dir or Path(self.app.default_game_dir)
|
||||
|
||||
def save_profile(self) -> None:
|
||||
if self.selected_profile_id is None:
|
||||
return
|
||||
|
||||
width, height = self.width_spin.value(), self.height_spin.value()
|
||||
|
||||
if (width == 0) != (height == 0):
|
||||
self.app.widget_manager.signal.show_warning_message.emit(
|
||||
"Set both resolution values, or set both to Default."
|
||||
self.app.tr_text("Set both resolution values, or set both to Default.")
|
||||
)
|
||||
return
|
||||
|
||||
game_dir_text = self.game_dir_edit.text().strip()
|
||||
version = self.version_dropdown.currentData()
|
||||
|
||||
if not isinstance(version, AvailableVersion):
|
||||
self.app.widget_manager.signal.show_warning_message.emit(
|
||||
self.app.tr_text("Select a version.")
|
||||
)
|
||||
return
|
||||
|
||||
request = UpdateProfileRequest(
|
||||
name=self.name_edit.text().strip(),
|
||||
version_id=self.version_edit.text().strip(),
|
||||
version_type=self.type_edit.text().strip(),
|
||||
version_id=version.id,
|
||||
version_type=version.version_type,
|
||||
game_dir=Path(game_dir_text) if game_dir_text else None,
|
||||
java_args=self.java_args_edit.text(),
|
||||
icon=self.icon_edit.text().strip(),
|
||||
resolution=(width, height) if width and height else None,
|
||||
)
|
||||
|
||||
if self.profile_manager.validate_update(request).is_valid and self.profile_manager.update_profile(
|
||||
self.selected_profile_id, request
|
||||
):
|
||||
self.profile_manager.save()
|
||||
self.app.widget_manager.signal.show_info_message.emit("Profile settings saved.")
|
||||
self.app.widget_manager.signal.show_info_message.emit(
|
||||
self.app.tr_text("Profile settings saved.")
|
||||
)
|
||||
else:
|
||||
errors = self.profile_manager.validate_update(request).errors
|
||||
if errors:
|
||||
self.app.widget_manager.signal.show_warning_message.emit("\n".join(errors.values()))
|
||||
|
||||
def open_add_modded_version_dialog(self) -> None:
|
||||
dialog = AddModdedVersionDialog(self.app, self)
|
||||
dialog.version_created.connect(self._modded_version_created)
|
||||
dialog.exec()
|
||||
|
||||
def _modded_version_created(self, version_id: str) -> None:
|
||||
self.version_options = self.manifest_manager.list_available_profile_versions()
|
||||
# self._select_type("modded")
|
||||
# self._filter_versions()
|
||||
# self._select_version(version_id)
|
||||
self.app.widget_manager.signal.show_info_message.emit(
|
||||
self.app.tr_text("Modded version {version_id} added.", version_id=version_id)
|
||||
)
|
||||
|
||||
def open_profile_folder(self) -> None:
|
||||
profile_dir = self._profile_dir()
|
||||
|
||||
if profile_dir is None:
|
||||
return
|
||||
|
||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(str(profile_dir.resolve())))
|
||||
|
||||
def import_mods(self) -> None:
|
||||
profile_dir = self._profile_dir()
|
||||
|
||||
if profile_dir is None:
|
||||
return
|
||||
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self, "Import Mods", "", "Minecraft mods (*.jar *.zip);;All files (*)"
|
||||
self, self.app.tr_text("Import Mods"), "", "Minecraft mods (*.jar *.zip);;All files (*)"
|
||||
)
|
||||
|
||||
if not files:
|
||||
return
|
||||
|
||||
mods_dir = profile_dir / "mods"
|
||||
mods_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
for source in files:
|
||||
shutil.copy2(source, mods_dir / Path(source).name)
|
||||
except OSError as error:
|
||||
self.app.widget_manager.signal.show_error_message.emit(f"Unable to import mod: {error}")
|
||||
self.app.widget_manager.signal.show_error_message.emit(
|
||||
self.app.tr_text("Unable to import mod: {error}", error=error)
|
||||
)
|
||||
return
|
||||
|
||||
self.app.widget_manager.signal.show_info_message.emit(
|
||||
f"Imported {len(files)} mod(s) into {mods_dir}."
|
||||
self.app.tr_text(
|
||||
"Imported {count} mod(s) into {directory}.",
|
||||
count=len(files), directory=mods_dir,
|
||||
)
|
||||
)
|
||||
|
||||
def duplicate_profile(self) -> None:
|
||||
if self.selected_profile_id is None:
|
||||
def duplicate_profile(self, profile: ProfileSummary | None=None) -> None:
|
||||
if self.selected_profile_id is None and profile is None:
|
||||
return
|
||||
new_id = self.profile_manager.duplicate_profile(self.selected_profile_id)
|
||||
|
||||
profile_id = self.selected_profile_id
|
||||
|
||||
new_id = self.profile_manager.duplicate_profile(profile_id)
|
||||
|
||||
if new_id:
|
||||
self.selected_profile_id = new_id
|
||||
self.load_profiles()
|
||||
self.profile_manager.save()
|
||||
|
||||
def delete_profile(self) -> None:
|
||||
if self.selected_profile_id is None:
|
||||
def delete_profile(self, profile: ProfileSummary | None=None) -> None:
|
||||
if self.selected_profile_id is None and profile is None:
|
||||
return
|
||||
|
||||
profile_id = self.selected_profile_id
|
||||
|
||||
answer = QMessageBox.question(
|
||||
self, "Delete Profile", "Delete this profile? Game files will not be removed.",
|
||||
self,
|
||||
self.app.tr_text("Delete Profile"),
|
||||
self.app.tr_text("Delete this profile? Game files will not be removed."),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Yes:
|
||||
profile_id = self.selected_profile_id
|
||||
self.selected_profile_id = None
|
||||
if self.profile_manager.remove_profile(profile_id):
|
||||
self.selected_profile_id = None
|
||||
self.profile_manager.save()
|
||||
|
||||
+14
-9
@@ -31,24 +31,25 @@ class ProcessLogWindow(QMainWindow):
|
||||
A Simple client log view
|
||||
"""
|
||||
|
||||
def __init__(self, process: subprocess.Popen, profile_id: str, title: str = "Game Log",
|
||||
def __init__(self, app, process: subprocess.Popen, profile_id: str, title: str = "Game Log",
|
||||
parent: QWidget | None = None, finished_callback: Callable[[str, int], None]=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.process = process
|
||||
self.profile_id = profile_id
|
||||
self._signals = _ProcessSignals(self)
|
||||
self._last_error_message: str | None = None
|
||||
self._stderr_lines: deque[str] = deque(maxlen=500)
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.setWindowTitle(self.app.tr_text(title))
|
||||
self.resize(700, 720)
|
||||
|
||||
container = QWidget(self)
|
||||
layout = QVBoxLayout(container)
|
||||
controls = QHBoxLayout()
|
||||
|
||||
self.status_label = QLabel(f"Running (PID {process.pid})")
|
||||
self.kill_button = QPushButton("Kill Process")
|
||||
self.status_label = QLabel(self.app.tr_text("Running (PID {pid})", pid=process.pid))
|
||||
self.kill_button = QPushButton(self.app.tr_text("Kill Process"))
|
||||
self.kill_button.clicked.connect(self.kill_process)
|
||||
|
||||
self.log_output = QPlainTextEdit()
|
||||
@@ -135,18 +136,20 @@ class ProcessLogWindow(QMainWindow):
|
||||
return
|
||||
|
||||
self.kill_button.setEnabled(False)
|
||||
self.status_label.setText("Killing process...")
|
||||
self.status_label.setText(self.app.tr_text("Killing process..."))
|
||||
|
||||
try:
|
||||
self.process.kill()
|
||||
except Exception as exc:
|
||||
self.kill_button.setEnabled(True)
|
||||
self.status_label.setText("Unable to kill process")
|
||||
self.status_label.setText(self.app.tr_text("Unable to kill process"))
|
||||
self.append_message(f"[launcher] Unable to kill process: {exc}")
|
||||
|
||||
def _process_finished(self, exit_code: int) -> None:
|
||||
self.kill_button.setEnabled(False)
|
||||
self.status_label.setText(f"Process exited with code {exit_code}")
|
||||
self.status_label.setText(self.app.tr_text(
|
||||
"Process exited with code {exit_code}", exit_code=exit_code
|
||||
))
|
||||
self.append_message(f"[launcher] Process exited with code {exit_code}.")
|
||||
|
||||
if exit_code != 0 and self._last_error_message:
|
||||
@@ -164,8 +167,10 @@ class ProcessLogWindow(QMainWindow):
|
||||
|
||||
messagebox.error_with_plain_text(
|
||||
self,
|
||||
title="Minecraft Client Error",
|
||||
message=f"Minecraft exited with code {exit_code}.",
|
||||
title=self.app.tr_text("Minecraft Client Error"),
|
||||
message=self.app.tr_text(
|
||||
"Minecraft exited with code {exit_code}.", exit_code=exit_code
|
||||
),
|
||||
content=error_content,
|
||||
)
|
||||
|
||||
|
||||
+65
-20
@@ -111,9 +111,44 @@ class SettingsPage(QWidget):
|
||||
root_layout.addWidget(self.stack)
|
||||
self.app.apply_qss(Path("settings.qss"), self.setStyleSheet)
|
||||
self.language_combo.currentIndexChanged.connect(self._mark_settings_dirty)
|
||||
self.theme_combo.currentIndexChanged.connect(self._mark_settings_dirty)
|
||||
self.client_id_edit.textEdited.connect(self._mark_settings_dirty)
|
||||
|
||||
self.theme_combo.currentIndexChanged.connect(
|
||||
self._apply_selected_theme
|
||||
)
|
||||
self.app.i18n.language_changed.connect(self.retranslate_ui)
|
||||
|
||||
def _apply_selected_theme(self) -> None:
|
||||
theme = self.theme_combo.currentData()
|
||||
self.app.apply_theme(theme)
|
||||
self._mark_settings_dirty()
|
||||
|
||||
def retranslate_ui(self, *_args) -> None:
|
||||
"""Rebuild this page so every label uses the active catalog."""
|
||||
current_page = self.stack.currentIndex()
|
||||
while self.stack.count():
|
||||
old_page = self.stack.widget(0)
|
||||
self.stack.removeWidget(old_page)
|
||||
old_page.deleteLater()
|
||||
|
||||
self.main_page = self._create_main_page()
|
||||
self.general_page = self._create_general_page()
|
||||
self.appearance_page = self._create_appearance_page()
|
||||
self.account_page = self._create_account_page()
|
||||
self.about_page = self._create_about_page()
|
||||
for page in (
|
||||
self.main_page,
|
||||
self.general_page,
|
||||
self.appearance_page,
|
||||
self.account_page,
|
||||
self.about_page,
|
||||
):
|
||||
self.stack.addWidget(page)
|
||||
self.stack.setCurrentIndex(min(current_page, self.stack.count() - 1))
|
||||
self.language_combo.currentIndexChanged.connect(self._mark_settings_dirty)
|
||||
self.client_id_edit.textEdited.connect(self._mark_settings_dirty)
|
||||
self.theme_combo.currentIndexChanged.connect(self._apply_selected_theme)
|
||||
|
||||
def _create_main_page(self) -> QWidget:
|
||||
page = QWidget()
|
||||
page.setObjectName("settingsMainPage")
|
||||
@@ -132,16 +167,17 @@ class SettingsPage(QWidget):
|
||||
scroll_layout.setContentsMargins(24, 20, 24, 24)
|
||||
scroll_layout.setSpacing(12)
|
||||
|
||||
title = QLabel("Settings")
|
||||
tr = self.app.tr_text
|
||||
title = QLabel(tr("Settings"))
|
||||
title.setObjectName("settingsPageTitle")
|
||||
scroll_layout.addWidget(title)
|
||||
scroll_layout.addSpacing(8)
|
||||
|
||||
pages = (
|
||||
("General", "Language and launcher directories", "general_page"),
|
||||
("Appearance", "Launcher theme", "appearance_page"),
|
||||
("Account", "Microsoft authentication settings", "account_page"),
|
||||
("About", "Version and project information", "about_page"),
|
||||
(tr("General"), tr("Language and launcher directories"), "general_page"),
|
||||
(tr("Appearance"), tr("Launcher theme"), "appearance_page"),
|
||||
(tr("Account"), tr("Microsoft authentication settings"), "account_page"),
|
||||
(tr("About"), tr("Version and project information"), "about_page"),
|
||||
)
|
||||
for title_text, description, page_name in pages:
|
||||
button = SettingsItem(title_text, description)
|
||||
@@ -157,21 +193,22 @@ class SettingsPage(QWidget):
|
||||
return page
|
||||
|
||||
def _create_general_page(self) -> QWidget:
|
||||
page = SettingsSubPage("General", self._go_back)
|
||||
tr = self.app.tr_text
|
||||
page = SettingsSubPage(tr("General"), self._go_back)
|
||||
current = self.settings.get("general", GeneralSettings)
|
||||
|
||||
page.content_layout.addWidget(QLabel("Current launcher working directory"))
|
||||
page.content_layout.addWidget(QLabel(tr("Current launcher working directory")))
|
||||
launcher_dir = QLineEdit(self.app.work_dir.as_posix())
|
||||
launcher_dir.setReadOnly(True)
|
||||
page.content_layout.addWidget(launcher_dir)
|
||||
|
||||
page.content_layout.addWidget(QLabel("Current program directory"))
|
||||
page.content_layout.addWidget(QLabel(tr("Current program directory")))
|
||||
program_dir = QLineEdit(self.app.program_dir.as_posix())
|
||||
program_dir.setReadOnly(True)
|
||||
page.content_layout.addWidget(program_dir)
|
||||
|
||||
page.content_layout.addSpacing(8)
|
||||
page.content_layout.addWidget(QLabel("Language"))
|
||||
page.content_layout.addWidget(QLabel(tr("Language")))
|
||||
self.language_combo = self._choice_combo(
|
||||
"general", "language", LANGUAGE_LABELS, current.language
|
||||
)
|
||||
@@ -179,10 +216,11 @@ class SettingsPage(QWidget):
|
||||
return page
|
||||
|
||||
def _create_appearance_page(self) -> QWidget:
|
||||
page = SettingsSubPage("Appearance", self._go_back)
|
||||
tr = self.app.tr_text
|
||||
page = SettingsSubPage(tr("Appearance"), self._go_back)
|
||||
current = self.settings.get("appearance", AppearanceSettings)
|
||||
|
||||
page.content_layout.addWidget(QLabel("Theme"))
|
||||
page.content_layout.addWidget(QLabel(tr("Theme")))
|
||||
self.theme_combo = self._choice_combo(
|
||||
"appearance", "theme", THEME_LABELS, current.theme
|
||||
)
|
||||
@@ -190,30 +228,32 @@ class SettingsPage(QWidget):
|
||||
return page
|
||||
|
||||
def _create_account_page(self) -> QWidget:
|
||||
page = SettingsSubPage("Account", self._go_back)
|
||||
tr = self.app.tr_text
|
||||
page = SettingsSubPage(tr("Account"), self._go_back)
|
||||
current = self.settings.get("account", AccountSettings)
|
||||
|
||||
page.content_layout.addWidget(QLabel("Microsoft OAuth Client ID"))
|
||||
page.content_layout.addWidget(QLabel(tr("Microsoft OAuth Client ID")))
|
||||
self.client_id_edit = QLineEdit(str(current.client_id))
|
||||
self.client_id_edit.setPlaceholderText(
|
||||
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
)
|
||||
page.content_layout.addWidget(self.client_id_edit)
|
||||
|
||||
help_label = QLabel(
|
||||
help_label = QLabel(tr(
|
||||
"The Client ID must belong to an Azure application configured "
|
||||
"for Microsoft device-code authentication."
|
||||
)
|
||||
))
|
||||
help_label.setObjectName("settingsHelpText")
|
||||
help_label.setWordWrap(True)
|
||||
page.content_layout.addWidget(help_label)
|
||||
return page
|
||||
|
||||
def _create_about_page(self) -> QWidget:
|
||||
page = SettingsSubPage("About", self._go_back)
|
||||
tr = self.app.tr_text
|
||||
page = SettingsSubPage(tr("About"), self._go_back)
|
||||
brand_label = QLabel(LAUNCHER_NAME)
|
||||
brand_label.setObjectName("settingsBrand")
|
||||
version_label = QLabel(f"Version: {LAUNCHER_VERSION}")
|
||||
version_label = QLabel(tr("Version: {version}", version=LAUNCHER_VERSION))
|
||||
version_label.setObjectName("settingsVersion")
|
||||
description = QPlainTextEdit(LAUNCHER_DESCRIPTION)
|
||||
description.setObjectName("settingsDescription")
|
||||
@@ -233,7 +273,10 @@ class SettingsPage(QWidget):
|
||||
) -> QComboBox:
|
||||
combo = QComboBox()
|
||||
for value in self.settings.choices(section, field):
|
||||
combo.addItem(labels.get(value, str(value)), value)
|
||||
combo.addItem(
|
||||
self.app.tr_text(labels.get(value, str(value))),
|
||||
value,
|
||||
)
|
||||
index = combo.findData(current)
|
||||
combo.setCurrentIndex(max(index, 0))
|
||||
return combo
|
||||
@@ -257,10 +300,12 @@ class SettingsPage(QWidget):
|
||||
self.settings.save()
|
||||
except Exception as error:
|
||||
if show_error:
|
||||
QMessageBox.critical(self, "Settings error", str(error))
|
||||
QMessageBox.critical(self, self.app.tr_text("Settings error"), str(error))
|
||||
return False
|
||||
|
||||
self._settings_dirty = False
|
||||
selected_language = data["general"]["language"]
|
||||
self.app.apply_language(selected_language)
|
||||
return True
|
||||
|
||||
def _open_page(self, page: QWidget) -> None:
|
||||
|
||||
+7
-7
@@ -43,31 +43,31 @@ class TestPage(QWidget):
|
||||
self.resize(800, 600)
|
||||
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
|
||||
|
||||
self.button = QPushButton("Fetch Version Data")
|
||||
self.button = QPushButton(self.app.tr_text("Fetch Version Data"))
|
||||
self.button.setObjectName("ActionButton")
|
||||
self.button.clicked.connect(self.run_test)
|
||||
|
||||
self.button_2 = QPushButton("Download client")
|
||||
self.button_2 = QPushButton(self.app.tr_text("Download client"))
|
||||
self.button_2.setObjectName("ActionButton")
|
||||
self.button_2.clicked.connect(self.run_download_client)
|
||||
|
||||
self.button_3 = QPushButton("Download libraries")
|
||||
self.button_3 = QPushButton(self.app.tr_text("Download libraries"))
|
||||
self.button_3.setObjectName("ActionButton")
|
||||
self.button_3.clicked.connect(self.run_download_libraries)
|
||||
|
||||
self.button_4 = QPushButton("Refresh versions")
|
||||
self.button_4 = QPushButton(self.app.tr_text("Refresh versions"))
|
||||
self.button_4.setObjectName("ActionButton")
|
||||
self.button_4.clicked.connect(self.load_all_version)
|
||||
|
||||
self.button_5 = QPushButton("Download assets")
|
||||
self.button_5 = QPushButton(self.app.tr_text("Download assets"))
|
||||
self.button_5.setObjectName("ActionButton")
|
||||
self.button_5.clicked.connect(self.download_assets)
|
||||
|
||||
self.button_6 = QPushButton("Parse Arguments")
|
||||
self.button_6 = QPushButton(self.app.tr_text("Parse Arguments"))
|
||||
self.button_6.setObjectName("ActionButton")
|
||||
self.button_6.clicked.connect(self.parse_argument)
|
||||
|
||||
self.button_7 = QPushButton("Launch Game! (Offline)")
|
||||
self.button_7 = QPushButton(self.app.tr_text("Launch Game! (Offline)"))
|
||||
self.button_7.setObjectName("ActionButton")
|
||||
self.button_7.clicked.connect(self.launch_offline_game)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user