Files
Launcher/pages/add_modded_version.py
wei f6bf8f5c8b Add multiple languages (i18n) support.
Now you can add a version in profile manage page.

Add install java button and dialog.
2026-08-04 17:39:38 +08:00

316 lines
12 KiB
Python

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()