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:
@@ -1,15 +1,17 @@
|
||||
import sys
|
||||
from _colorize import Theme
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtCore import QSize
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from PySide6.QtWidgets import QApplication, QStyle
|
||||
from PySide6.QtGui import QIcon, QPixmap, QPalette, QColor
|
||||
from PySide6.QtWidgets import QApplication, QStyle, QToolButton
|
||||
|
||||
from constant import LAUNCHER_VERSION
|
||||
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
|
||||
recolor_icon
|
||||
from core_lib.qt import messagebox
|
||||
from core_lib.i18n import TranslationManager
|
||||
from core_lib.launcher.object import Launcher as LauncherObject
|
||||
from manager.account import AccountManager
|
||||
from manager.game import GameManager
|
||||
@@ -22,6 +24,8 @@ from window import LauncherMainWindow
|
||||
class Launcher(QApplication, LauncherObject):
|
||||
def __init__(self, logger, debug: bool = False):
|
||||
super().__init__()
|
||||
self._system_palette = QPalette(self.palette()) # theme
|
||||
|
||||
# logger
|
||||
self.logger = logger
|
||||
self.debug = debug
|
||||
@@ -41,6 +45,15 @@ class Launcher(QApplication, LauncherObject):
|
||||
self.settings_manager.load()
|
||||
else:
|
||||
self.settings_manager.load_defaults().save()
|
||||
|
||||
general = self.settings_manager.get("general", GeneralSettings)
|
||||
self.i18n = TranslationManager(
|
||||
self.resources_dir / "translations",
|
||||
general.language,
|
||||
)
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
self.profile_manager = ProfileManager(self, self.widget_manager)
|
||||
self.account_manager = AccountManager(self, self.widget_manager)
|
||||
self.game_manager = GameManager(self)
|
||||
@@ -73,8 +86,8 @@ class Launcher(QApplication, LauncherObject):
|
||||
if not path.exists() or not path.is_file():
|
||||
messagebox.error(
|
||||
self.main_window,
|
||||
"Resource Error",
|
||||
"Missing icon file: {}".format(path)
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text("Missing icon file: {path}", path=path)
|
||||
)
|
||||
|
||||
return icon
|
||||
@@ -89,19 +102,58 @@ class Launcher(QApplication, LauncherObject):
|
||||
except Exception as e:
|
||||
messagebox.error(
|
||||
self.main_window,
|
||||
"Resource Error",
|
||||
"Failed to load icon {}: {}".format(path, e)
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
|
||||
)
|
||||
|
||||
return icon
|
||||
|
||||
def tr_text(self, key: str, /, **values) -> str:
|
||||
"""Translate a stable English source string for the active language."""
|
||||
return self.i18n.translate(key, **values)
|
||||
|
||||
def apply_language(self, language: str) -> None:
|
||||
if language != self.i18n.language:
|
||||
self.i18n.set_language(language)
|
||||
messagebox.info(None,"Launcher",
|
||||
self.tr_text("To make all page apply new language."
|
||||
" Is recommend to restart the launcher after change it."))
|
||||
|
||||
def set_reloadable_toolbutton_icon(
|
||||
self,
|
||||
button: QToolButton,
|
||||
path: Path,
|
||||
no_color_change: bool = False,
|
||||
) -> None:
|
||||
"""Set an icon and retain its source so theme changes can reload it."""
|
||||
button.setProperty("themeIconPath", str(path))
|
||||
button.setProperty("themeIconNoColorChange", no_color_change)
|
||||
button.setIcon(self.get_icon(path, no_color_change))
|
||||
|
||||
def reload_toolbutton_icons(self) -> None:
|
||||
for button in self.allWidgets():
|
||||
if not isinstance(button, QToolButton):
|
||||
continue
|
||||
|
||||
path = button.property("themeIconPath")
|
||||
if not path:
|
||||
continue
|
||||
|
||||
no_color_change = bool(
|
||||
button.property("themeIconNoColorChange")
|
||||
)
|
||||
button.setIcon(
|
||||
self.get_icon(Path(path), no_color_change)
|
||||
)
|
||||
|
||||
def get_picture(self, path: Path, size: QSize, custom_error: str=None) -> QPixmap:
|
||||
picture = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(size)
|
||||
if not path.exists() or not path.is_file():
|
||||
messagebox.error(
|
||||
self.main_window,
|
||||
"Resource Error",
|
||||
"Missing icon file: {}".format(path) if not custom_error else custom_error
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text("Missing icon file: {path}", path=path)
|
||||
if not custom_error else custom_error
|
||||
)
|
||||
|
||||
return picture
|
||||
@@ -111,8 +163,8 @@ class Launcher(QApplication, LauncherObject):
|
||||
except Exception as e:
|
||||
messagebox.error(
|
||||
self.main_window,
|
||||
"Resource Error",
|
||||
"Failed to load icon {}: {}".format(path, e)
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
|
||||
)
|
||||
|
||||
return picture
|
||||
@@ -167,8 +219,8 @@ class Launcher(QApplication, LauncherObject):
|
||||
if not full_path.exists():
|
||||
messagebox.error(
|
||||
None,
|
||||
"Resource Error",
|
||||
"Missing stylesheet file: {}".format(full_path),
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text("Missing stylesheet file: {path}", path=full_path),
|
||||
)
|
||||
return ""
|
||||
|
||||
@@ -178,11 +230,86 @@ class Launcher(QApplication, LauncherObject):
|
||||
except Exception as e:
|
||||
messagebox.error(
|
||||
self.main_window,
|
||||
"Resource Error",
|
||||
"Unable to apply QSS stylesheet {}: {}".format(full_path, e),
|
||||
self.tr_text("Resource Error"),
|
||||
self.tr_text(
|
||||
"Unable to apply QSS stylesheet {path}: {error}",
|
||||
path=full_path,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
def get_main_window(self):
|
||||
return self.main_window
|
||||
|
||||
def apply_theme(self, theme: Theme | None=None):
|
||||
if not theme:
|
||||
appearance = self.settings_manager.get("appearance", AppearanceSettings)
|
||||
|
||||
theme = appearance.theme
|
||||
|
||||
if theme == "system":
|
||||
palette = self._system_palette
|
||||
elif theme == "dark":
|
||||
palette = self._dark_palette()
|
||||
elif theme == "light":
|
||||
palette = self._light_palette()
|
||||
else:
|
||||
raise ValueError(f"Unsupported theme: {theme}")
|
||||
|
||||
self.setPalette(palette)
|
||||
|
||||
for widget in self.allWidgets():
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
widget.update()
|
||||
|
||||
if hasattr(self, "main_window"):
|
||||
self.reload_toolbutton_icons()
|
||||
self.main_window.toolbar.update_all()
|
||||
|
||||
@staticmethod
|
||||
def _light_palette() -> QPalette:
|
||||
palette = QPalette()
|
||||
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor("#f5f5f5"))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor("#202020"))
|
||||
palette.setColor(QPalette.ColorRole.Base, QColor("#ffffff"))
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#eeeeee"))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor("#202020"))
|
||||
palette.setColor(QPalette.ColorRole.Button, QColor("#ffffff"))
|
||||
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#202020"))
|
||||
palette.setColor(QPalette.ColorRole.Mid, QColor("#c5c5c5"))
|
||||
palette.setColor(QPalette.ColorRole.Midlight, QColor("#e2e2e2"))
|
||||
palette.setColor(QPalette.ColorRole.Dark, QColor("#aaaaaa"))
|
||||
palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
|
||||
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.PlaceholderText,
|
||||
QColor("#707070"),
|
||||
)
|
||||
|
||||
return palette
|
||||
@staticmethod
|
||||
def _dark_palette() -> QPalette:
|
||||
palette = QPalette()
|
||||
|
||||
palette.setColor(QPalette.ColorRole.Window, QColor("#202124"))
|
||||
palette.setColor(QPalette.ColorRole.WindowText, QColor("#f1f3f4"))
|
||||
palette.setColor(QPalette.ColorRole.Base, QColor("#17181a"))
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#292a2d"))
|
||||
palette.setColor(QPalette.ColorRole.Text, QColor("#f1f3f4"))
|
||||
palette.setColor(QPalette.ColorRole.Button, QColor("#292a2d"))
|
||||
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#f1f3f4"))
|
||||
palette.setColor(QPalette.ColorRole.Mid, QColor("#4a4c50"))
|
||||
palette.setColor(QPalette.ColorRole.Midlight, QColor("#383a3e"))
|
||||
palette.setColor(QPalette.ColorRole.Dark, QColor("#121315"))
|
||||
palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
|
||||
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.PlaceholderText,
|
||||
QColor("#a0a4aa"),
|
||||
)
|
||||
|
||||
return palette
|
||||
|
||||
+2
-2
@@ -739,8 +739,8 @@ class AccountManager(QObject):
|
||||
# Ask user before actually delete
|
||||
answer = QMessageBox.question(
|
||||
self.app.main_window,
|
||||
"Delete all accounts",
|
||||
"Delete every saved account? This cannot be undone.",
|
||||
self.app.tr_text("Delete all accounts"),
|
||||
self.app.tr_text("Delete every saved account? This cannot be undone."),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
+448
-69
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -15,7 +16,7 @@ from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from constant import CLIENT_ID, LAUNCHER_VERSION
|
||||
from core_lib.common.common import FileObject, download_multiple_files
|
||||
from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
|
||||
from core_lib.common.exception import JavaRuntimeException, VersionManifestFetchException, VersionManifestSaveException, \
|
||||
NoSpecifiedVersionKeyException
|
||||
from core_lib.game.argument import ArgumentMappings, generate_launch_command
|
||||
from core_lib.game.asset import check_assets_exist, download_assets, find_correct_assets_dir
|
||||
@@ -24,13 +25,22 @@ from core_lib.game.mod.mod_core import merge_manifest
|
||||
from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, \
|
||||
save_version_manifest, find_useful_part_in_specific_version_manifest, download_core_file, get_latest_version, \
|
||||
LATEST_VERSION_ALIASES
|
||||
from core_lib.runtime.java import find_java_installations
|
||||
from core_lib.runtime.java import find_java_installations, get_java_version
|
||||
from core_lib.runtime.mojang import download_java_runtimes, fetch_runtime_manifest, get_current_platform_runtime_key
|
||||
from manager.account import AccountManager, LaunchAccount
|
||||
from manager.profile import LaunchProfile
|
||||
from manager.widgets import SelectPathRequest, AskForRequest, SignalTransferItem
|
||||
|
||||
MAX_DOWNLOAD_ATTEMPTS = 3
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AvailableVersion:
|
||||
id: str
|
||||
display_name: str
|
||||
version_type: str
|
||||
source: str
|
||||
inherits_from: str | None = None
|
||||
|
||||
class ManifestManager:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
@@ -305,10 +315,112 @@ class ManifestManager:
|
||||
is_snapshot=version_id == "latest-snapshot",
|
||||
)
|
||||
|
||||
def list_available_profile_versions(self) -> list[AvailableVersion]:
|
||||
result: list[AvailableVersion] = []
|
||||
root_manifest = self.get_cached_version_manifest()
|
||||
vanilla_ids: set[str] = set()
|
||||
|
||||
# For vanilla version
|
||||
for version in (root_manifest or {}).get("versions", []):
|
||||
version_id = version.get("id")
|
||||
if not version_id:
|
||||
continue
|
||||
|
||||
vanilla_ids.add(version_id)
|
||||
result.append(
|
||||
AvailableVersion(
|
||||
id=version_id,
|
||||
display_name=f"Minecraft {version_id}",
|
||||
version_type=version.get("type") or "custom",
|
||||
source="vanilla",
|
||||
)
|
||||
)
|
||||
|
||||
# Official launcher aliases are valid vanilla profile versions too.
|
||||
for alias_id, alias in LATEST_VERSION_ALIASES.items():
|
||||
result.append(
|
||||
AvailableVersion(
|
||||
id=alias_id,
|
||||
display_name=alias.get("displayName", alias_id),
|
||||
version_type=(
|
||||
"snapshot" if alias_id == "latest-snapshot" else "release"
|
||||
),
|
||||
source="vanilla",
|
||||
)
|
||||
)
|
||||
|
||||
# Locally created modded and custom versions.
|
||||
versions_dir = Path(self.app.work_dir, "versions")
|
||||
|
||||
for manifest_path in versions_dir.glob("*/*.json"):
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
self.app.logger.warning(
|
||||
f"Unable to read local version manifest {manifest_path}: {exc}"
|
||||
)
|
||||
continue
|
||||
|
||||
version_id = manifest.get("id")
|
||||
if not version_id or version_id in vanilla_ids:
|
||||
continue
|
||||
|
||||
inherits_from = manifest.get("inheritsFrom")
|
||||
result.append(
|
||||
AvailableVersion(
|
||||
id=version_id,
|
||||
display_name=(
|
||||
f"{version_id} - Minecraft {inherits_from}"
|
||||
if inherits_from else version_id
|
||||
),
|
||||
version_type=manifest.get("type") or "custom",
|
||||
source="modded" if inherits_from else "custom",
|
||||
inherits_from=inherits_from,
|
||||
)
|
||||
)
|
||||
|
||||
# Keep the first entry for each ID. The root manifest takes precedence
|
||||
# over locally cached copies of vanilla manifests.
|
||||
unique: dict[str, AvailableVersion] = {}
|
||||
for version in result:
|
||||
unique.setdefault(version.id, version)
|
||||
|
||||
return list(unique.values())
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JavaRuntime:
|
||||
id: str
|
||||
major_version: int
|
||||
raw_version: str
|
||||
executable: Path
|
||||
home_dir: Path
|
||||
source: Literal["system", "managed", "custom"]
|
||||
component: str | None = None
|
||||
valid: bool = True
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"major": self.major_version,
|
||||
"raw": self.raw_version,
|
||||
"executable": self.executable.as_posix(),
|
||||
"homeDir": self.home_dir.as_posix(),
|
||||
"source": self.source,
|
||||
"component": self.component,
|
||||
}
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallableJavaRuntime:
|
||||
component: str
|
||||
major_version: int | None
|
||||
raw_version: str | None
|
||||
released: str | None = None
|
||||
|
||||
class JavaManager:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.widget_mgr = app.widget_manager
|
||||
self.runtimes: list[JavaRuntime] = self._read_runtimes()
|
||||
|
||||
#
|
||||
# JVM Management
|
||||
@@ -317,81 +429,210 @@ class JavaManager:
|
||||
def jvm_settings_path(self) -> Path:
|
||||
return Path(self.app.work_dir) / "jvms.json"
|
||||
|
||||
def _find_and_save_jvm_search_result(self):
|
||||
installations, errors = find_java_installations()
|
||||
if not installations:
|
||||
self.widget_mgr.signal.show_warning_message.emit(
|
||||
"There are no Java installations available in you system.\n"
|
||||
@staticmethod
|
||||
def _runtime_from_dict(data: dict) -> JavaRuntime:
|
||||
"""
|
||||
Convert runtime data to JavaRuntime object
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
executable = Path(data["executable"])
|
||||
home_dir = Path(data.get("homeDir") or data.get("home_dir") or executable.parent.parent)
|
||||
|
||||
return JavaRuntime(
|
||||
id=str(data.get("id") or uuid4()),
|
||||
major_version=int(data["major"]),
|
||||
raw_version=str(data.get("raw") or data["major"]),
|
||||
executable=executable,
|
||||
home_dir=home_dir,
|
||||
source=data.get("source") or "system",
|
||||
component=data.get("component"),
|
||||
valid=executable.is_file(),
|
||||
)
|
||||
|
||||
def _read_runtimes(self) -> list[JavaRuntime]:
|
||||
"""
|
||||
Read existing runtime data from jvms.json
|
||||
:return:
|
||||
"""
|
||||
if not self.jvm_settings_path.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(self.jvm_settings_path.read_text(encoding="utf-8"))
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise ValueError("JVM settings root must be a list")
|
||||
|
||||
return [self._runtime_from_dict(item) for item in data if isinstance(item, dict)]
|
||||
except (OSError, ValueError, KeyError, TypeError) as exc:
|
||||
self.app.logger.warning(f"Unable to read JVM settings: {exc}")
|
||||
return []
|
||||
|
||||
if errors:
|
||||
request = AskForRequest("Java Virtual Machine Finder", message = (
|
||||
"Unable to detect below java installation version:\n"
|
||||
))
|
||||
request.details = "\n".join(path.as_posix() for path in errors)
|
||||
request.use_plain_text = True
|
||||
|
||||
self.widget_mgr.signal.askyesno.emit(request)
|
||||
|
||||
def save_runtimes(self) -> None:
|
||||
"""
|
||||
Save memory runtimes to jvms.json
|
||||
:return:
|
||||
"""
|
||||
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
with self.jvm_settings_path.open("w") as file:
|
||||
json.dump(installations, file, indent=4)
|
||||
|
||||
return installations
|
||||
except Exception as e:
|
||||
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||
{
|
||||
"error": traceback.format_exception(e),
|
||||
"title": "Settings Error",
|
||||
"message": "An error occurred while attempting to save jvm settings. Please try again.",
|
||||
}
|
||||
temporary = self.jvm_settings_path.with_suffix(".json.tmp")
|
||||
temporary.write_text(
|
||||
json.dumps([runtime.to_dict() for runtime in self.runtimes], indent=4),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def _read_exist_jvm_settings(self):
|
||||
try:
|
||||
return json.loads(self.jvm_settings_path.read_text())
|
||||
except Exception as e:
|
||||
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||
{
|
||||
"error": traceback.format_exception(e),
|
||||
"title": "Settings Error",
|
||||
"message": "An error occurred while reading jvm settings.",
|
||||
}
|
||||
temporary.replace(self.jvm_settings_path)
|
||||
|
||||
@staticmethod
|
||||
def inspect_runtime(executable: str | Path,
|
||||
*,
|
||||
source: Literal["system", "managed", "custom"] = "custom",
|
||||
component: str | None = None,
|
||||
runtime_id: str | None = None,
|
||||
) -> JavaRuntime:
|
||||
"""
|
||||
Check runtime version
|
||||
:param executable:
|
||||
:param source:
|
||||
:param component:
|
||||
:param runtime_id:
|
||||
:return:
|
||||
runtime: JavaRuntime
|
||||
"""
|
||||
executable = Path(executable).expanduser().resolve()
|
||||
|
||||
if not executable.is_file():
|
||||
raise JavaRuntimeException(f"Java executable does not exist: {executable}")
|
||||
|
||||
major, raw = get_java_version(executable)
|
||||
return JavaRuntime(
|
||||
id=runtime_id or uuid4().hex,
|
||||
major_version=major,
|
||||
raw_version=raw,
|
||||
executable=executable,
|
||||
home_dir=executable.parent.parent,
|
||||
source=source,
|
||||
component=component,
|
||||
)
|
||||
|
||||
def discover_runtimes(self, extra_install_patterns: list | None = None) -> tuple[list[JavaRuntime], list[Path]]:
|
||||
"""
|
||||
Find available runtimes
|
||||
:param extra_install_patterns:
|
||||
:return:
|
||||
discovered: list[JavaRuntime]
|
||||
errors: list[Path]
|
||||
"""
|
||||
installations, errors = find_java_installations(extra_install_patterns)
|
||||
known_ids = {runtime.executable.resolve(): runtime.id for runtime in self.runtimes}
|
||||
|
||||
discovered = []
|
||||
for item in installations:
|
||||
executable = Path(item["executable"]).resolve()
|
||||
|
||||
discovered.append(self._runtime_from_dict({
|
||||
**item,
|
||||
"id": known_ids.get(executable, uuid4().hex),
|
||||
"source": "system",
|
||||
}))
|
||||
|
||||
return discovered, errors
|
||||
|
||||
def refresh_runtimes(self, extra_install_patterns: list | None = None) -> tuple[list[JavaRuntime], list[Path]]:
|
||||
"""
|
||||
Discover available runtimes and save
|
||||
:param extra_install_patterns:
|
||||
:return:
|
||||
runtimes: list[JavaRuntime]
|
||||
errors: list[Path]
|
||||
"""
|
||||
discovered, errors = self.discover_runtimes(extra_install_patterns)
|
||||
retained = [runtime for runtime in self.runtimes if runtime.source != "system"]
|
||||
by_executable = {runtime.executable.resolve(): runtime for runtime in retained}
|
||||
|
||||
for runtime in discovered:
|
||||
by_executable.setdefault(runtime.executable.resolve(), runtime)
|
||||
|
||||
self.runtimes = list(by_executable.values())
|
||||
self.save_runtimes()
|
||||
return list(self.runtimes), errors
|
||||
|
||||
def register_runtime(self, executable: str | Path) -> JavaRuntime:
|
||||
"""
|
||||
Register runtime to manage's runtimes
|
||||
:param executable:
|
||||
:return:
|
||||
"""
|
||||
candidate = Path(executable).expanduser().resolve()
|
||||
|
||||
for runtime in self.runtimes:
|
||||
if runtime.executable.resolve() == candidate:
|
||||
return runtime
|
||||
|
||||
runtime = self.inspect_runtime(candidate, source="custom")
|
||||
self.runtimes.append(runtime)
|
||||
self.save_runtimes()
|
||||
return runtime
|
||||
|
||||
@staticmethod
|
||||
def _runtime_major_version(raw_version: str | None) -> int | None:
|
||||
if not raw_version:
|
||||
return None
|
||||
match = re.match(r"^(?:1\.)?(\d+)", raw_version)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
def get_or_cache_jvm_settings(self) -> list:
|
||||
if self.jvm_settings_path.exists():
|
||||
exists = self._read_exist_jvm_settings()
|
||||
def list_installable_runtimes(self) -> list[InstallableJavaRuntime]:
|
||||
"""Return Mojang components with the Java version carried by each one."""
|
||||
manifest = fetch_runtime_manifest()
|
||||
platform_key = get_current_platform_runtime_key()
|
||||
if not platform_key:
|
||||
return []
|
||||
platform_runtimes = manifest.get(platform_key, {})
|
||||
result = []
|
||||
for component, versions in platform_runtimes.items():
|
||||
if not isinstance(versions, list) or not versions:
|
||||
continue
|
||||
latest = versions[0] if isinstance(versions[0], dict) else {}
|
||||
version = latest.get("version") or {}
|
||||
raw_version = version.get("name")
|
||||
result.append(InstallableJavaRuntime(
|
||||
component=component,
|
||||
major_version=self._runtime_major_version(raw_version),
|
||||
raw_version=raw_version,
|
||||
released=version.get("released"),
|
||||
))
|
||||
return sorted(result, key=lambda item: (item.major_version or -1, item.component), reverse=True)
|
||||
|
||||
if exists:
|
||||
return exists
|
||||
def list_installable_components(self) -> list[str]:
|
||||
"""Compatibility API returning component names only."""
|
||||
return [runtime.component for runtime in self.list_installable_runtimes()]
|
||||
|
||||
return self._find_and_save_jvm_search_result()
|
||||
def unregister_runtime(self, runtime_id: str) -> bool:
|
||||
"""
|
||||
Remove runtime from manage's runtimes
|
||||
:param runtime_id:
|
||||
:return:
|
||||
"""
|
||||
original_count = len(self.runtimes)
|
||||
self.runtimes = [runtime for runtime in self.runtimes if runtime.id != runtime_id]
|
||||
|
||||
changed = len(self.runtimes) != original_count
|
||||
|
||||
if changed:
|
||||
self.save_runtimes()
|
||||
|
||||
return changed
|
||||
|
||||
def get_or_cache_jvm_settings(self) -> list[dict]:
|
||||
if not self.runtimes:
|
||||
self.refresh_runtimes()
|
||||
|
||||
return [runtime.to_dict() for runtime in self.runtimes]
|
||||
|
||||
def get_support_runtime_jvm_executable(self, major_version: int | str, no_error: bool=False) -> str | None:
|
||||
result = self.get_or_cache_jvm_settings()
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
version = item.get("major", None)
|
||||
executable = item.get("executable", None)
|
||||
|
||||
try:
|
||||
version = str(version)
|
||||
major_version = str(major_version)
|
||||
except Exception as e:
|
||||
self.app.logger.warning(
|
||||
f"Unable to convert version {version} to string: {e}"
|
||||
)
|
||||
|
||||
if major_version == version and executable:
|
||||
return executable
|
||||
runtime = self.resolve_runtime(major_version=int(major_version))
|
||||
if runtime:
|
||||
return runtime.executable.as_posix()
|
||||
|
||||
if not no_error:
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
@@ -400,6 +641,81 @@ class JavaManager:
|
||||
|
||||
return None
|
||||
|
||||
def list_runtimes(self, *, refresh: bool = False) -> list[JavaRuntime]:
|
||||
if refresh:
|
||||
self.refresh_runtimes()
|
||||
return list(self.runtimes)
|
||||
|
||||
def resolve_runtime(self, *, manifest: dict | None = None, major_version: int | None = None,
|
||||
preferred_runtime_id: str | None = None) -> JavaRuntime | None:
|
||||
java_requirement = (manifest or {}).get("javaVersion", {})
|
||||
required_major = major_version or java_requirement.get("majorVersion")
|
||||
|
||||
if required_major is None:
|
||||
return None
|
||||
|
||||
runtimes = self.list_runtimes()
|
||||
if not runtimes:
|
||||
runtimes, _ = self.refresh_runtimes()
|
||||
|
||||
if preferred_runtime_id:
|
||||
preferred = next((item for item in runtimes if item.id == preferred_runtime_id), None)
|
||||
if preferred and preferred.valid and preferred.major_version == int(required_major):
|
||||
return preferred
|
||||
|
||||
source_order = {"managed": 0, "custom": 1, "system": 2}
|
||||
matches = [item for item in runtimes if item.valid and item.major_version == int(required_major)]
|
||||
return min(matches, key=lambda item: source_order[item.source], default=None)
|
||||
|
||||
@property
|
||||
def launcher_runtime_dir(self) -> Path:
|
||||
return Path(self.app.work_dir) / "runtimes"
|
||||
|
||||
def install_runtime(self, component: str, *, progress_callback=None, redownload_callback=None) -> JavaRuntime:
|
||||
destination = self.launcher_runtime_dir / component
|
||||
download_java_runtimes(
|
||||
component,
|
||||
destination,
|
||||
progress_callback=progress_callback,
|
||||
redownload_callback=redownload_callback,
|
||||
)
|
||||
executable_name = "java.exe" if sys.platform == "win32" else "java"
|
||||
candidates = sorted(destination.rglob(executable_name))
|
||||
if not candidates:
|
||||
raise JavaRuntimeException(f"Installed runtime has no {executable_name}: {destination}")
|
||||
runtime = self.inspect_runtime(candidates[0], source="managed", component=component)
|
||||
self.runtimes = [item for item in self.runtimes if item.executable.resolve() != runtime.executable.resolve()]
|
||||
self.runtimes.append(runtime)
|
||||
self.save_runtimes()
|
||||
return runtime
|
||||
|
||||
def ensure_runtime(self, manifest: dict, *, auto_install: bool = False, progress_callback=None,
|
||||
redownload_callback=None) -> JavaRuntime:
|
||||
runtime = self.resolve_runtime(manifest=manifest)
|
||||
|
||||
if runtime:
|
||||
return runtime
|
||||
|
||||
requirement = manifest.get("javaVersion") or {}
|
||||
component = requirement.get("component")
|
||||
major = requirement.get("majorVersion")
|
||||
|
||||
if not auto_install or not component:
|
||||
raise JavaRuntimeException(f"No compatible Java runtime found (required major version: {major})")
|
||||
|
||||
runtime = self.install_runtime(
|
||||
component,
|
||||
progress_callback=progress_callback,
|
||||
redownload_callback=redownload_callback,
|
||||
)
|
||||
if major is not None and runtime.major_version != int(major):
|
||||
raise JavaRuntimeException(
|
||||
f"Installed Java {runtime.major_version}, but Minecraft requires Java {major}"
|
||||
)
|
||||
|
||||
return runtime
|
||||
|
||||
|
||||
class GameFileDownloadRequest(SignalTransferItem):
|
||||
def __init__(self, job_name: Literal["download_client", "download_libraries", "download_assets"],
|
||||
args: tuple):
|
||||
@@ -650,6 +966,7 @@ class LaunchResult:
|
||||
process: subprocess.Popen | None = None
|
||||
error_message: str | None = None
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
fail_callback: Callable | None = None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
@@ -657,6 +974,8 @@ class LaunchResult:
|
||||
|
||||
def fail(self, message: str) -> "LaunchResult":
|
||||
self.error_message = message
|
||||
if callable(self.fail_callback):
|
||||
self.fail_callback()
|
||||
return self
|
||||
|
||||
def add_warning(self, message: str) -> None:
|
||||
@@ -680,6 +999,11 @@ class LaunchManager:
|
||||
|
||||
def launch(self, profile: LaunchProfile) -> LaunchResult:
|
||||
result = LaunchResult()
|
||||
|
||||
def fail_callback():
|
||||
self.profile_failed(profile.profile_id)
|
||||
|
||||
result.fail_callback = fail_callback
|
||||
try:
|
||||
version_id = profile.version_id
|
||||
version_id = self.manifest_mgr.resolve_version_alias(version_id)
|
||||
@@ -815,11 +1139,37 @@ class LaunchManager:
|
||||
"Unable to parse arguments because data are invalid."
|
||||
)
|
||||
|
||||
# Use found java runtime (if available)
|
||||
java_path = self.java_mgr.get_support_runtime_jvm_executable(
|
||||
java_major_version,
|
||||
no_error=True
|
||||
# Resolve a compatible runtime, then offer the Mojang runtime from
|
||||
# the version manifest before falling back to a custom executable.
|
||||
runtime = self.java_mgr.resolve_runtime(manifest=version_manifest)
|
||||
java_path = runtime.executable.as_posix() if runtime else None
|
||||
|
||||
if not java_path:
|
||||
component = java_version.get("component")
|
||||
if component:
|
||||
install_request = AskForRequest(
|
||||
"Java Runtime Required",
|
||||
f"Minecraft {version_id} requires Java {java_major_version}. "
|
||||
f"Download the Mojang runtime now?",
|
||||
)
|
||||
self.widget_mgr.signal.askyesno.emit(install_request)
|
||||
if install_request.wait(120) and install_request.result():
|
||||
callback, task_id = self.manifest_mgr.create_download_progress_callback(
|
||||
f"Installing Java {java_major_version}"
|
||||
)
|
||||
try:
|
||||
runtime = self.java_mgr.ensure_runtime(
|
||||
version_manifest,
|
||||
auto_install=True,
|
||||
progress_callback=callback,
|
||||
redownload_callback=self.game_file_mgr.redownload_files,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.widget_mgr.download_signal.failed.emit(task_id, str(exc))
|
||||
self.app.logger.warning(f"Automatic Java installation failed: {exc}")
|
||||
else:
|
||||
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||
java_path = runtime.executable.as_posix()
|
||||
|
||||
if not java_path:
|
||||
# Ask for a support java version. If not, use system environment java
|
||||
@@ -840,10 +1190,19 @@ class LaunchManager:
|
||||
java_path = request.result()
|
||||
|
||||
if java_path:
|
||||
try:
|
||||
runtime = self.java_mgr.register_runtime(java_path)
|
||||
except JavaRuntimeException as exc:
|
||||
return result.fail(f"The selected Java executable is invalid: {exc}")
|
||||
if runtime.major_version != int(java_major_version):
|
||||
return result.fail(
|
||||
f"Minecraft requires Java {java_major_version}, but the selected executable is "
|
||||
f"Java {runtime.major_version}."
|
||||
)
|
||||
java_path = runtime.executable.as_posix()
|
||||
self.app.logger.debug(f"Use specified java executable path: {java_path}")
|
||||
else:
|
||||
self.app.logger.debug("Use system environment java.")
|
||||
java_path = "java"
|
||||
return result.fail(f"Java {java_major_version} is required to launch this version.")
|
||||
|
||||
self.app.logger.debug(f"Java executable path is: {java_path}")
|
||||
|
||||
@@ -913,12 +1272,32 @@ class LaunchManager:
|
||||
or profile_id in self.launched_profiles
|
||||
)
|
||||
|
||||
def profile_failed(self, profile_id: str) -> None:
|
||||
try:
|
||||
self.launched_profiles.remove(profile_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.launching_profiles.remove(profile_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.app.logger.debug(
|
||||
f"Profile {profile_id} exited."
|
||||
)
|
||||
|
||||
def profile_finished(self, profile_id: str, exit_code: int) -> None:
|
||||
try:
|
||||
self.launched_profiles.remove(profile_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.launching_profiles.remove(profile_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.app.logger.debug(
|
||||
f"Profile {profile_id} exited with code {exit_code}."
|
||||
)
|
||||
|
||||
+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()
|
||||
+73
-37
@@ -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
|
||||
|
||||
@@ -792,3 +810,21 @@ class CreateProfilePage(QWidget):
|
||||
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)
|
||||
|
||||
|
||||
@@ -82,3 +82,38 @@ QLabel#settingsVersion {
|
||||
QPlainTextEdit#settingsDescription {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
QWidget#settingsPage QComboBox {
|
||||
min-height: 22px;
|
||||
padding: 6px 32px 6px 10px;
|
||||
border: 1px solid palette(mid);
|
||||
border-radius: 6px;
|
||||
background-color: palette(base);
|
||||
color: palette(text);
|
||||
selection-background-color: palette(highlight);
|
||||
selection-color: palette(highlighted-text);
|
||||
}
|
||||
|
||||
QWidget#settingsPage QComboBox:hover {
|
||||
border-color: palette(highlight);
|
||||
}
|
||||
|
||||
QWidget#settingsPage QComboBox:focus {
|
||||
border: 2px solid palette(highlight);
|
||||
padding: 5px 31px 5px 9px;
|
||||
}
|
||||
|
||||
/*QWidget#settingsPage QComboBox::drop-down {*/
|
||||
/* width: 28px;*/
|
||||
/* border: none;*/
|
||||
/* border-left: 1px solid palette(mid);*/
|
||||
/*}*/
|
||||
|
||||
QWidget#settingsPage QComboBox QAbstractItemView {
|
||||
border: 1px solid palette(mid);
|
||||
background-color: palette(base);
|
||||
color: palette(text);
|
||||
selection-background-color: palette(highlight);
|
||||
selection-color: palette(highlighted-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"About": "About",
|
||||
"Account": "Account",
|
||||
"Account status": "Account status",
|
||||
"Accounts": "Accounts",
|
||||
"Add Account": "Add Account",
|
||||
"Add Modded Version": "Add Modded Version",
|
||||
"Add Version": "Add Version",
|
||||
"Added version {version_id}.": "Added version {version_id}.",
|
||||
"Added {version_id}.": "Added {version_id}.",
|
||||
"All assets downloaded successfully.": "All assets downloaded successfully.",
|
||||
"All libraries downloaded successfully.": "All libraries downloaded successfully.",
|
||||
"Appearance": "Appearance",
|
||||
"Authentication failed.": "Authentication failed.",
|
||||
"Authentication failed. Session may expire.": "Authentication failed. Session may expire.",
|
||||
"Available": "Available",
|
||||
"Cancel": "Cancel",
|
||||
"Check login status": "Check login status",
|
||||
"Checking Minecraft ownership": "Checking Minecraft ownership",
|
||||
"Choose a Mojang Java runtime component to install.": "Choose a Mojang Java runtime component to install.",
|
||||
"Close": "Close",
|
||||
"Completed": "Completed",
|
||||
"Copied!": "Copied!",
|
||||
"Copy code": "Copy code",
|
||||
"Copy the code below, then continue in your browser.": "Copy the code below, then continue in your browser.",
|
||||
"Create a local mod-loader version manifest without creating a profile.": "Create a local mod-loader version manifest without creating a profile.",
|
||||
"Create Profile": "Create Profile",
|
||||
"Creating the Fabric version manifest...": "Creating the Fabric version manifest...",
|
||||
"Current": "Current",
|
||||
"Current launcher working directory": "Current launcher working directory",
|
||||
"Current program directory": "Current program directory",
|
||||
"Current version: {version}": "Current version: {version}",
|
||||
"Dark": "Dark",
|
||||
"Default": "Default",
|
||||
"Delete": "Delete",
|
||||
"Delete account": "Delete account",
|
||||
"Delete All": "Delete All",
|
||||
"Delete Profile": "Delete Profile",
|
||||
"Delete this profile? Game files will not be removed.": "Delete this profile? Game files will not be removed.",
|
||||
"Detected Java runtimes": "Detected Java runtimes",
|
||||
"Don't Install": "Don't Install",
|
||||
"Download assets": "Download assets",
|
||||
"Download client": "Download client",
|
||||
"Download Error": "Download Error",
|
||||
"Download libraries": "Download libraries",
|
||||
"Downloaded": "Downloaded",
|
||||
"Downloading": "Downloading",
|
||||
"Downloading assets for version {version}": "Downloading assets for version {version}",
|
||||
"Downloading libraries for version {version}": "Downloading libraries for version {version}",
|
||||
"Downloading missing assets": "Downloading missing assets",
|
||||
"Downloads": "Downloads",
|
||||
"Duplicate": "Duplicate",
|
||||
"English": "English",
|
||||
"Error": "Error",
|
||||
"Fabric": "Fabric",
|
||||
"Failed: {error}": "Failed: {error}",
|
||||
"Fetch Error": "Fetch Error",
|
||||
"Fetch Version Data": "Fetch Version Data",
|
||||
"Follow system settings": "Follow system settings",
|
||||
"Game directory": "Game directory",
|
||||
"Game Log": "Game Log",
|
||||
"General": "General",
|
||||
"Home": "Home",
|
||||
"Icon": "Icon",
|
||||
"Import Mods": "Import Mods",
|
||||
"Import Mods…": "Import Mods…",
|
||||
"Imported {count} mod(s).": "Imported {count} mod(s).",
|
||||
"Info": "Info",
|
||||
"Install": "Install",
|
||||
"Install Java": "Install Java",
|
||||
"Installation failed: {error}": "Installation failed: {error}",
|
||||
"Installing {component}...": "Installing {component}...",
|
||||
"Java {version} installed successfully.": "Java {version} installed successfully.",
|
||||
"Java arguments": "Java arguments",
|
||||
"Java Version:": "Java Version:",
|
||||
"Kill Process": "Kill Process",
|
||||
"Killing process...": "Killing process...",
|
||||
"Language": "Language",
|
||||
"Language and launcher directories": "Language and launcher directories",
|
||||
"Launch Failed": "Launch Failed",
|
||||
"Launch Game": "Launch Game",
|
||||
"Launch Game! (Offline)": "Launch Game! (Offline)",
|
||||
"Launch Profile": "Launch Profile",
|
||||
"Launch Request": "Launch Request",
|
||||
"Launched!": "Launched!",
|
||||
"Launcher error occurred.": "Launcher error occurred.",
|
||||
"Launcher theme": "Launcher theme",
|
||||
"Light": "Light",
|
||||
"Loading all types...": "Loading all types...",
|
||||
"Loading all versions...": "Loading all versions...",
|
||||
"Loading available Mojang runtimes...": "Loading available Mojang runtimes...",
|
||||
"Loading Fabric versions for {version}...": "Loading Fabric versions for {version}...",
|
||||
"Loading Minecraft versions...": "Loading Minecraft versions...",
|
||||
"Login Minecraft Account": "Login Minecraft Account",
|
||||
"Main repo": "Main repo",
|
||||
"Manage Profiles": "Manage Profiles",
|
||||
"Microsoft authentication settings": "Microsoft authentication settings",
|
||||
"Microsoft Client ID": "Microsoft Client ID",
|
||||
"Microsoft Login": "Microsoft Login",
|
||||
"Microsoft OAuth Client ID": "Microsoft OAuth Client ID",
|
||||
"Microsoft sign-in could not be completed.": "Microsoft sign-in could not be completed.",
|
||||
"Microsoft sign-in failed": "Microsoft sign-in failed",
|
||||
"Minecraft Client Error": "Minecraft Client Error",
|
||||
"Minecraft exited with code {exit_code}.": "Minecraft exited with code {exit_code}.",
|
||||
"Minecraft type": "Minecraft type",
|
||||
"Minecraft version": "Minecraft version",
|
||||
"Mod Loader": "Mod Loader",
|
||||
"Mod Loader Version": "Mod Loader Version",
|
||||
"Mod loader": "Mod loader",
|
||||
"Modded": "Modded",
|
||||
"Mojang runtime component": "Mojang runtime component",
|
||||
"No accounts yet. Use Add Account to sign in.": "No accounts yet. Use Add Account to sign in.",
|
||||
"No content": "No content",
|
||||
"No Minecraft version is available for this type.": "No Minecraft version is available for this type.",
|
||||
"No profiles available": "No profiles available",
|
||||
"No type selected.": "No type selected.",
|
||||
"No version selected.": "No version selected.",
|
||||
"Open browser": "Open browser",
|
||||
"Open Profile Folder": "Open Profile Folder",
|
||||
"Ouch. The icon is missing.": "Ouch. The icon is missing.",
|
||||
"Parse Arguments": "Parse Arguments",
|
||||
"Parse Error": "Parse Error",
|
||||
"Preparing...": "Preparing...",
|
||||
"Preparing to launch...": "Preparing to launch...",
|
||||
"Process exited with code {exit_code}": "Process exited with code {exit_code}",
|
||||
"Profile name": "Profile name",
|
||||
"Profile name:": "Profile name:",
|
||||
"Profile name cannot be empty.": "Profile name cannot be empty.",
|
||||
"Profile settings saved.": "Profile settings saved.",
|
||||
"Ready to install.": "Ready to install.",
|
||||
"Redownload": "Redownload",
|
||||
"Refresh": "Refresh",
|
||||
"Refresh all": "Refresh all",
|
||||
"Refresh versions": "Refresh versions",
|
||||
"Requesting a Microsoft sign-in code…": "Requesting a Microsoft sign-in code…",
|
||||
"Resolution": "Resolution",
|
||||
"Resource Error": "Resource Error",
|
||||
"Running (PID {pid})": "Running (PID {pid})",
|
||||
"Save Changes": "Save Changes",
|
||||
"Select a loader version to continue.": "Select a loader version to continue.",
|
||||
"Select a Minecraft and loader version.": "Select a Minecraft and loader version.",
|
||||
"Select a profile from the list to edit its settings.": "Select a profile from the list to edit its settings.",
|
||||
"Select a profile to launch": "Select a profile to launch",
|
||||
"Select a runtime component.": "Select a runtime component.",
|
||||
"Select a version.": "Select a version.",
|
||||
"Select a version before continuing.": "Select a version before continuing.",
|
||||
"Select a version to continue:": "Select a version to continue:",
|
||||
"Set both resolution values, or set both to Default.": "Set both resolution values, or set both to Default.",
|
||||
"Settings": "Settings",
|
||||
"Settings error": "Settings error",
|
||||
"Signing in to Minecraft services": "Signing in to Minecraft services",
|
||||
"Signing in to Xbox Live": "Signing in to Xbox Live",
|
||||
"Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.": "Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.",
|
||||
"Some files failed to download.": "Some files failed to download.",
|
||||
"Some files failed to download. Would you like to retry downloading them?": "Some files failed to download. Would you like to retry downloading them?",
|
||||
"Still digging!": "Still digging!",
|
||||
"Switch to this account": "Switch to this account",
|
||||
"Test Error": "Test Error",
|
||||
"Tests": "Tests",
|
||||
"The Client ID must belong to an Azure application configured for Microsoft device-code authentication.": "The Client ID must belong to an Azure application configured for Microsoft device-code authentication.",
|
||||
"The selected loader version is invalid.": "The selected loader version is invalid.",
|
||||
"Theme": "Theme",
|
||||
"There is no profile is selected. Please create one first.": "There is no profile is selected. Please create one first.",
|
||||
"This version requires Java {version}.": "This version requires Java {version}.",
|
||||
"Type": "Type",
|
||||
"Unable to kill process": "Unable to kill process",
|
||||
"Unable to launch profile": "Unable to launch profile",
|
||||
"Unable to load the Minecraft version manifest.": "Unable to load the Minecraft version manifest.",
|
||||
"Unable to load the selected profile.": "Unable to load the selected profile.",
|
||||
"Version": "Version",
|
||||
"Version and project information": "Version and project information",
|
||||
"Version Type": "Version Type",
|
||||
"Version: Vanilla": "Version: Vanilla",
|
||||
"Version: {version}": "Version: {version}",
|
||||
"Waiting for Microsoft sign-in approval": "Waiting for Microsoft sign-in approval",
|
||||
"Warning": "Warning",
|
||||
"If you find an issue, you can report it in the main repository. Suggestions are always welcome!": "If you find an issue, you can report it in the main repository. Suggestions are always welcome!",
|
||||
"An error occurred while connecting to authentication server.": "An error occurred while connecting to authentication server.",
|
||||
"An error occurred while executing Java runtime.": "An error occurred while executing Java runtime.",
|
||||
"An error occurred while fetching third party API.": "An error occurred while fetching third party API.",
|
||||
"An error occurred while processing account data.": "An error occurred while processing account data.",
|
||||
"An error occurred while processing profile.": "An error occurred while processing profile.",
|
||||
"An settings error occurred.": "An settings error occurred.",
|
||||
"Current platform or architecture is not supported.": "Current platform or architecture is not supported.",
|
||||
"Delete all accounts": "Delete all accounts",
|
||||
"Delete every saved account? This cannot be undone.": "Delete every saved account? This cannot be undone.",
|
||||
"Microsoft approved the sign-in but did not return an access token. Please sign in again.": "Microsoft approved the sign-in but did not return an access token. Please sign in again.",
|
||||
"Microsoft could not refresh this account. Please sign in again.": "Microsoft could not refresh this account. Please sign in again.",
|
||||
"Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.": "Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.",
|
||||
"Microsoft did not return a sign-in code. Check the client ID or update the launcher.": "Microsoft did not return a sign-in code. Check the client ID or update the launcher.",
|
||||
"Microsoft sign-in was rejected (HTTP {status}): {description}": "Microsoft sign-in was rejected (HTTP {status}): {description}",
|
||||
"Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.": "Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.",
|
||||
"Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.": "Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.",
|
||||
"Minecraft services returned an invalid ownership response. Please sign in again or try later.": "Minecraft services returned an invalid ownership response. Please sign in again or try later.",
|
||||
"Modded version {version_id} added.": "Modded version {version_id} added.",
|
||||
"No compatible native library was found for this platform.": "No compatible native library was found for this platform.",
|
||||
"No version found. (This should never happen)": "No version found. (This should never happen)",
|
||||
"Runtime manifest is missing or corrupted.": "Runtime manifest is missing or corrupted.",
|
||||
"Settings section could not be registered.": "Settings section could not be registered.",
|
||||
"Settings section could not be validated.": "Settings section could not be validated.",
|
||||
"Specified account was not found.": "Specified account was not found.",
|
||||
"Specified asset data section (key) not found.": "Specified asset data section (key) not found.",
|
||||
"Specified profile key not found. Profile may corrupted.": "Specified profile key not found. Profile may corrupted.",
|
||||
"Specified version data section (key) not found.": "Specified version data section (key) not found.",
|
||||
"Specified version or type not found.": "Specified version or type not found.",
|
||||
"The authentication request or session was rejected.": "The authentication request or session was rejected.",
|
||||
"The authentication service received too many requests. Wait a moment and try again.": "The authentication service received too many requests. Wait a moment and try again.",
|
||||
"The authentication service returned an unexpected response. Try again later.": "The authentication service returned an unexpected response. Try again later.",
|
||||
"The login session is invalid or expired. Sign in again.": "The login session is invalid or expired. Sign in again.",
|
||||
"Unable to extract native libraries.": "Unable to extract native libraries.",
|
||||
"Unable to fetch asset manifest.": "Unable to fetch asset manifest.",
|
||||
"Unable to fetch runtime manifest.": "Unable to fetch runtime manifest.",
|
||||
"Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.": "Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.",
|
||||
"Unable to fetch third party API. Try updating the launcher.": "Unable to fetch third party API. Try updating the launcher.",
|
||||
"Unable to fetch version manifest.": "Unable to fetch version manifest.",
|
||||
"Unable to find dependency.": "Unable to find dependency.",
|
||||
"Unable to load account data. The account file may be corrupted.": "Unable to load account data. The account file may be corrupted.",
|
||||
"Unable to load asset manifest.": "Unable to load asset manifest.",
|
||||
"Unable to load profile. Profile may corrupted": "Unable to load profile. Profile may corrupted",
|
||||
"Unable to load version manifest.": "Unable to load version manifest.",
|
||||
"Unable to parse Java runtime details.": "Unable to parse Java runtime details.",
|
||||
"Unable to resolve native libraries.": "Unable to resolve native libraries.",
|
||||
"Unable to save account data. Try again later.": "Unable to save account data. Try again later.",
|
||||
"Unable to save asset manifest.": "Unable to save asset manifest.",
|
||||
"Unable to save profile. Try again later.": "Unable to save profile. Try again later.",
|
||||
"Unable to save version manifest.": "Unable to save version manifest.",
|
||||
"Unable to verify Minecraft ownership because the ownership list is missing or invalid.": "Unable to verify Minecraft ownership because the ownership list is missing or invalid.",
|
||||
"Unable to verify Minecraft ownership because the ownership response is invalid.": "Unable to verify Minecraft ownership because the ownership response is invalid.",
|
||||
"Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.": "Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.",
|
||||
"Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.": "Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.",
|
||||
"Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.": "Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.",
|
||||
"Chinese (Traditional)": "Chinese (Traditional)",
|
||||
"Installing Java ({component})": "Installing Java ({component})",
|
||||
"Unable to load component list: {error}": "Unable to load component list: {error}",
|
||||
"Target profile {profile} is running. Are you sure you want to launch it again?": "Target profile {profile} is running. Are you sure you want to launch it again?",
|
||||
"Time out waiting for request for launch.": "Time out waiting for request for launch.",
|
||||
"Imported {count} mod(s) into {directory}.": "Imported {count} mod(s) into {directory}.",
|
||||
"Unable to import mod: {error}": "Unable to import mod: {error}",
|
||||
"Loader version": "Loader version",
|
||||
"Unable to load versions: {error}": "Unable to load versions: {error}",
|
||||
"Unable to load Fabric versions: {error}": "Unable to load Fabric versions: {error}",
|
||||
"Unable to add version: {error}": "Unable to add version: {error}",
|
||||
"Unable to read version types: {error}": "Unable to read version types: {error}",
|
||||
"Missing icon file: {path}": "Missing icon file: {path}",
|
||||
"Missing stylesheet file: {path}": "Missing stylesheet file: {path}",
|
||||
"Failed to load icon {path}: {error}": "Failed to load icon {path}: {error}",
|
||||
"Unable to apply QSS stylesheet {path}: {error}": "Unable to apply QSS stylesheet {path}: {error}",
|
||||
"{account} — Minecraft profile {status}": "{account} — Minecraft profile {status}",
|
||||
"available": "available",
|
||||
"not available": "not available",
|
||||
"Profile {profile_id} successfully created.": "Profile {profile_id} successfully created.",
|
||||
"Profile {profile_id} with fabric loader successfully created.": "Profile {profile_id} with fabric loader successfully created.",
|
||||
"Timeout while waiting for selected version.": "Timeout while waiting for selected version.",
|
||||
"Selected a version before creating profile.": "Selected a version before creating profile.",
|
||||
"Timeout while waiting for selected mod loader.": "Timeout while waiting for selected mod loader.",
|
||||
"Loader {loader} not found.": "Loader {loader} not found.",
|
||||
"Profile creation for loader {loader} is not implemented.": "Profile creation for loader {loader} is not implemented.",
|
||||
"Timeout while waiting for selected mod loader version.": "Timeout while waiting for selected mod loader version.",
|
||||
"Unable to create profile due to manifest fetch failure.": "Unable to create profile due to manifest fetch failure.",
|
||||
"Unable to create profile due to version ID is missing in manifest.": "Unable to create profile due to version ID is missing in manifest.",
|
||||
"Selected a fabric loader version before creating profile.": "Selected a fabric loader version before creating profile.",
|
||||
"Unable to create profile due to vanilla manifest fetch failure.": "Unable to create profile due to vanilla manifest fetch failure.",
|
||||
"Unable to create profile due to mod loader manifest fetch failure.": "Unable to create profile due to mod loader manifest fetch failure.",
|
||||
"Unable to create profile. Exising loader version ID mismatch.": "Unable to create profile. Exising loader version ID mismatch.",
|
||||
"Unable to create profile. Inherits version is mismatched as expected.": "Unable to create profile. Inherits version is mismatched as expected.",
|
||||
"Selected a forge loader version before creating profile.": "Selected a forge loader version before creating profile.",
|
||||
"Selected a neoforge loader version before creating profile.": "Selected a neoforge loader version before creating profile.",
|
||||
"Timeout while waiting for selected type.": "Timeout while waiting for selected type.",
|
||||
"Timeout while waiting for selected loader.": "Timeout while waiting for selected loader.",
|
||||
"Selected loader \"{loader}\" is not implemented": "Selected loader \"{loader}\" is not implemented",
|
||||
"Failed to fetch version manifest: {error}": "Failed to fetch version manifest: {error}",
|
||||
"An unexpected error occurred while fetching version manifest: {error}": "An unexpected error occurred while fetching version manifest: {error}",
|
||||
"Unable to launch profile {profile}: {error}": "Unable to launch profile {profile}: {error}",
|
||||
"To make all page apply new language. Is recommend to restart the launcher after change it.": "To make all page apply new language. Is recommend to restart the launcher after change it."
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"About": "關於",
|
||||
"Account": "帳號",
|
||||
"Account status": "帳號狀態",
|
||||
"Accounts": "帳號",
|
||||
"Add Account": "新增帳號",
|
||||
"Add Modded Version": "新增模組載入器版本",
|
||||
"Add Version": "新增版本",
|
||||
"Added version {version_id}.": "新增版本 {version_id} 。",
|
||||
"All assets downloaded successfully.": "所有資源皆已成功下載。",
|
||||
"All libraries downloaded successfully.": "所有程式庫皆已成功下載。",
|
||||
"Appearance": "外觀",
|
||||
"Authentication failed.": "驗證失敗。",
|
||||
"Authentication failed. Session may expire.": "驗證失敗,登入工作階段可能已過期。",
|
||||
"Available": "可用",
|
||||
"Cancel": "取消",
|
||||
"Check login status": "檢查登入狀態",
|
||||
"Checking Minecraft ownership": "正在檢查 Minecraft 擁有權",
|
||||
"Choose a Mojang Java runtime component to install.": "選擇要安裝的 Mojang Java 執行環境元件。",
|
||||
"Close": "關閉",
|
||||
"Completed": "已完成",
|
||||
"Copied!": "已複製!",
|
||||
"Copy code": "複製代碼",
|
||||
"Copy the code below, then continue in your browser.": "複製下方代碼,然後在瀏覽器中繼續。",
|
||||
"Create a local mod-loader version manifest without creating a profile.": "建立本機模組載入器版本資訊,而不建立設定檔。",
|
||||
"Create Profile": "建立設定檔",
|
||||
"Creating the Fabric version manifest...": "正在建立 Fabric 版本資訊……",
|
||||
"Current": "目前使用",
|
||||
"Current launcher working directory": "目前啟動器工作目錄",
|
||||
"Current program directory": "目前程式目錄",
|
||||
"Current version: {version}": "目前版本:{version}",
|
||||
"Dark": "深色",
|
||||
"Default": "預設",
|
||||
"Delete": "刪除",
|
||||
"Delete account": "刪除帳號",
|
||||
"Delete All": "全部刪除",
|
||||
"Delete Profile": "刪除設定檔",
|
||||
"Delete this profile? Game files will not be removed.": "確定要刪除此設定檔嗎?遊戲檔案不會被移除。",
|
||||
"Detected Java runtimes": "偵測到的 Java 執行環境",
|
||||
"Don't Install": "不要安裝",
|
||||
"Download assets": "下載資源",
|
||||
"Download client": "下載用戶端",
|
||||
"Download Error": "下載錯誤",
|
||||
"Download libraries": "下載程式庫",
|
||||
"Downloaded": "已下載",
|
||||
"Downloading": "正在下載",
|
||||
"Downloading assets for version {version}": "正在下載版本 {version} 的資源",
|
||||
"Downloading libraries for version {version}": "正在下載版本 {version} 的程式庫",
|
||||
"Downloading missing assets": "正在下載缺少的資源",
|
||||
"Downloads": "下載項目",
|
||||
"Duplicate": "建立副本",
|
||||
"English": "English",
|
||||
"Error": "錯誤",
|
||||
"Fabric": "Fabric",
|
||||
"Failed: {error}": "失敗:{error}",
|
||||
"Fetch Error": "取得資料時發生錯誤",
|
||||
"Fetch Version Data": "取得版本資料",
|
||||
"Follow system settings": "跟隨系統設定",
|
||||
"Game directory": "遊戲目錄",
|
||||
"Game Log": "遊戲記錄",
|
||||
"General": "一般",
|
||||
"Home": "首頁",
|
||||
"Icon": "圖示",
|
||||
"Import Mods": "匯入模組",
|
||||
"Import Mods…": "匯入模組……",
|
||||
"Imported {count} mod(s).": "已匯入 {count} 個模組。",
|
||||
"Info": "資訊",
|
||||
"Install": "安裝",
|
||||
"Install Java": "安裝 Java",
|
||||
"Installation failed: {error}": "安裝失敗:{error}",
|
||||
"Installing {component}...": "正在安裝 {component}……",
|
||||
"Java {version} installed successfully.": "Java {version} 已成功安裝。",
|
||||
"Java arguments": "Java 引數",
|
||||
"Java Version:": "Java 版本:",
|
||||
"Kill Process": "終止處理程序",
|
||||
"Killing process...": "正在終止處理程序……",
|
||||
"Language": "語言",
|
||||
"Language and launcher directories": "語言與啟動器目錄",
|
||||
"Launch Failed": "啟動失敗",
|
||||
"Launch Game": "啟動遊戲",
|
||||
"Launch Game! (Offline)": "啟動遊戲!(離線)",
|
||||
"Launch Profile": "啟動設定檔",
|
||||
"Launch Request": "啟動要求",
|
||||
"Launched!": "已啟動!",
|
||||
"Launcher error occurred.": "啟動器發生錯誤。",
|
||||
"Launcher theme": "啟動器佈景主題",
|
||||
"Light": "淺色",
|
||||
"Loading all types...": "正在載入所有類型……",
|
||||
"Loading all versions...": "正在載入所有版本……",
|
||||
"Loading available Mojang runtimes...": "正在載入可用的 Mojang 執行環境……",
|
||||
"Loading Fabric versions for {version}...": "正在載入適用於 {version} 的 Fabric 版本……",
|
||||
"Loading Minecraft versions...": "正在載入 Minecraft 版本……",
|
||||
"Login Minecraft Account": "登入 Minecraft 帳號",
|
||||
"Main repo": "主要程式碼儲存庫",
|
||||
"Manage Profiles": "管理設定檔",
|
||||
"Microsoft authentication settings": "Microsoft 驗證設定",
|
||||
"Microsoft Client ID": "Microsoft 用戶端 ID",
|
||||
"Microsoft Login": "Microsoft 登入",
|
||||
"Microsoft OAuth Client ID": "Microsoft OAuth 用戶端 ID",
|
||||
"Microsoft sign-in could not be completed.": "無法完成 Microsoft 登入。",
|
||||
"Microsoft sign-in failed": "Microsoft 登入失敗",
|
||||
"Minecraft Client Error": "Minecraft 用戶端錯誤",
|
||||
"Minecraft exited with code {exit_code}.": "Minecraft 已結束,結束代碼為 {exit_code}。",
|
||||
"Minecraft type": "Minecraft 類型",
|
||||
"Minecraft version": "Minecraft 版本",
|
||||
"Mod Loader": "模組載入器",
|
||||
"Mod Loader Version": "模組載入器版本",
|
||||
"Mod loader": "模組載入器",
|
||||
"Modded": "模組版",
|
||||
"Mojang runtime component": "Mojang 執行環境元件",
|
||||
"No accounts yet. Use Add Account to sign in.": "尚未新增帳號。請使用「新增帳號」登入。",
|
||||
"No content": "沒有內容",
|
||||
"No Minecraft version is available for this type.": "此類型沒有可用的 Minecraft 版本。",
|
||||
"No profiles available": "沒有可用的設定檔",
|
||||
"No type selected.": "尚未選擇類型。",
|
||||
"No version selected.": "尚未選擇版本。",
|
||||
"Open browser": "開啟瀏覽器",
|
||||
"Open Profile Folder": "開啟設定檔資料夾",
|
||||
"Ouch. The icon is missing.": "糟糕,找不到圖示。",
|
||||
"Parse Arguments": "解析引數",
|
||||
"Parse Error": "解析錯誤",
|
||||
"Preparing...": "正在準備……",
|
||||
"Preparing to launch...": "正在準備啟動……",
|
||||
"Process exited with code {exit_code}": "處理程序已結束,結束代碼為 {exit_code}",
|
||||
"Profile name": "設定檔名稱",
|
||||
"Profile name:": "設定檔名稱:",
|
||||
"Profile name cannot be empty.": "設定檔名稱不可為空白。",
|
||||
"Profile settings saved.": "設定檔設定已儲存。",
|
||||
"Ready to install.": "已準備好安裝。",
|
||||
"Redownload": "重新下載",
|
||||
"Refresh": "重新整理",
|
||||
"Refresh all": "全部重新整理",
|
||||
"Refresh versions": "重新整理版本",
|
||||
"Requesting a Microsoft sign-in code…": "正在要求 Microsoft 登入代碼……",
|
||||
"Resolution": "解析度",
|
||||
"Resource Error": "資源錯誤",
|
||||
"Running (PID {pid})": "執行中(PID {pid})",
|
||||
"Save Changes": "儲存變更",
|
||||
"Select a loader version to continue.": "選擇載入器版本以繼續。",
|
||||
"Select a Minecraft and loader version.": "請選擇 Minecraft 與載入器版本。",
|
||||
"Select a profile from the list to edit its settings.": "從清單選擇設定檔以編輯其設定。",
|
||||
"Select a profile to launch": "選擇要啟動的設定檔",
|
||||
"Select a runtime component.": "請選擇執行環境元件。",
|
||||
"Select a version.": "請選擇版本。",
|
||||
"Select a version before continuing.": "請先選擇版本再繼續。",
|
||||
"Select a version to continue:": "選擇版本以繼續:",
|
||||
"Set both resolution values, or set both to Default.": "請同時設定寬度與高度,或將兩者都設為預設值。",
|
||||
"Settings": "設定",
|
||||
"Settings error": "設定錯誤",
|
||||
"Signing in to Minecraft services": "正在登入 Minecraft 服務",
|
||||
"Signing in to Xbox Live": "正在登入 Xbox Live",
|
||||
"Some assets could not be downloaded. They are not required to launch the game, but missing assets may cause texture issues while you play.": "部分資源無法下載。這些資源不是啟動遊戲的必要項目,但缺少資源可能造成遊戲中的材質問題。",
|
||||
"Some files failed to download.": "部分檔案下載失敗。",
|
||||
"Some files failed to download. Would you like to retry downloading them?": "部分檔案下載失敗。要重試下載嗎?",
|
||||
"Still digging!": "仍在努力開發中!",
|
||||
"Switch to this account": "切換至此帳號",
|
||||
"Test Error": "測試錯誤",
|
||||
"Tests": "測試",
|
||||
"The Client ID must belong to an Azure application configured for Microsoft device-code authentication.": "用戶端 ID 必須屬於已設定 Microsoft 裝置代碼驗證的 Azure 應用程式。",
|
||||
"The selected loader version is invalid.": "選取的載入器版本無效。",
|
||||
"Theme": "佈景主題",
|
||||
"There is no profile is selected. Please create one first.": "尚未選擇設定檔,請先建立一個設定檔。",
|
||||
"This version requires Java {version}.": "此版本需要 Java {version}。",
|
||||
"Type": "類型",
|
||||
"Unable to kill process": "無法終止處理程序",
|
||||
"Unable to launch profile": "無法啟動設定檔",
|
||||
"Unable to load the Minecraft version manifest.": "無法載入 Minecraft 版本資訊。",
|
||||
"Unable to load the selected profile.": "無法載入選取的設定檔。",
|
||||
"Version": "版本",
|
||||
"Version and project information": "版本與專案資訊",
|
||||
"Version Type": "版本類型",
|
||||
"Version: Vanilla": "版本:原版",
|
||||
"Version: {version}": "版本:{version}",
|
||||
"Waiting for Microsoft sign-in approval": "正在等待 Microsoft 登入核准",
|
||||
"Warning": "警告",
|
||||
"If you find an issue, you can report it in the main repository. Suggestions are always welcome!": "如果發現問題,可以在主要程式碼儲存庫中回報。也歡迎提供任何建議!",
|
||||
"An error occurred while connecting to authentication server.": "連線至驗證伺服器時發生錯誤。",
|
||||
"An error occurred while executing Java runtime.": "執行 Java 執行環境時發生錯誤。",
|
||||
"An error occurred while fetching third party API.": "取得第三方 API 資料時發生錯誤。",
|
||||
"An error occurred while processing account data.": "處理帳號資料時發生錯誤。",
|
||||
"An error occurred while processing profile.": "處理設定檔時發生錯誤。",
|
||||
"An settings error occurred.": "發生設定錯誤。",
|
||||
"Current platform or architecture is not supported.": "不支援目前的平台或處理器架構。",
|
||||
"Delete all accounts": "刪除所有帳號",
|
||||
"Delete every saved account? This cannot be undone.": "確定要刪除所有已儲存的帳號嗎?此操作無法復原。",
|
||||
"Microsoft approved the sign-in but did not return an access token. Please sign in again.": "Microsoft 已核准登入,但未傳回存取權杖。請重新登入。",
|
||||
"Microsoft could not refresh this account. Please sign in again.": "Microsoft 無法重新整理此帳號。請重新登入。",
|
||||
"Microsoft did not return a refresh token. Ensure the offline_access permission is enabled, then sign in again.": "Microsoft 未傳回重新整理權杖。請確認已啟用 offline_access 權限,然後重新登入。",
|
||||
"Microsoft did not return a sign-in code. Check the client ID or update the launcher.": "Microsoft 未傳回登入代碼。請檢查用戶端 ID 或更新啟動器。",
|
||||
"Microsoft sign-in was rejected (HTTP {status}): {description}": "Microsoft 登入遭拒絕(HTTP {status}):{description}",
|
||||
"Minecraft services accepted the Xbox session but did not return a game access token. Please sign in again.": "Minecraft 服務已接受 Xbox 工作階段,但未傳回遊戲存取權杖。請重新登入。",
|
||||
"Minecraft services did not return a valid player profile. Ensure this account owns Minecraft and has created a profile.": "Minecraft 服務未傳回有效的玩家資料。請確認此帳號擁有 Minecraft 且已建立玩家資料。",
|
||||
"Minecraft services returned an invalid ownership response. Please sign in again or try later.": "Minecraft 服務傳回無效的擁有權回應。請重新登入或稍後再試。",
|
||||
"Modded version {version_id} added.": "已新增模組版本 {version_id}。",
|
||||
"No compatible native library was found for this platform.": "找不到適用於此平台的原生程式庫。",
|
||||
"No version found. (This should never happen)": "找不到任何版本。(正常情況下不應發生)",
|
||||
"Runtime manifest is missing or corrupted.": "執行環境資訊遺失或損毀。",
|
||||
"Settings section could not be registered.": "無法註冊設定區段。",
|
||||
"Settings section could not be validated.": "無法驗證設定區段。",
|
||||
"Specified account was not found.": "找不到指定的帳號。",
|
||||
"Specified asset data section (key) not found.": "找不到指定的資源資料區段(鍵)。",
|
||||
"Specified profile key not found. Profile may corrupted.": "找不到指定的設定檔鍵,設定檔可能已損毀。",
|
||||
"Specified version data section (key) not found.": "找不到指定的版本資料區段(鍵)。",
|
||||
"Specified version or type not found.": "找不到指定的版本或類型。",
|
||||
"The authentication request or session was rejected.": "驗證要求或工作階段遭到拒絕。",
|
||||
"The authentication service received too many requests. Wait a moment and try again.": "驗證服務收到過多要求。請稍候再試。",
|
||||
"The authentication service returned an unexpected response. Try again later.": "驗證服務傳回非預期的回應。請稍後再試。",
|
||||
"The login session is invalid or expired. Sign in again.": "登入工作階段無效或已過期。請重新登入。",
|
||||
"Unable to extract native libraries.": "無法解壓縮原生程式庫。",
|
||||
"Unable to fetch asset manifest.": "無法取得資源資訊。",
|
||||
"Unable to fetch runtime manifest.": "無法取得執行環境資訊。",
|
||||
"Unable to fetch third party API. Provider may no longer exist. Try updating the launcher.": "無法取得第三方 API 資料。服務提供者可能已不存在,請嘗試更新啟動器。",
|
||||
"Unable to fetch third party API. Try updating the launcher.": "無法取得第三方 API 資料。請嘗試更新啟動器。",
|
||||
"Unable to fetch version manifest.": "無法取得版本資訊。",
|
||||
"Unable to find dependency.": "找不到相依項目。",
|
||||
"Unable to load account data. The account file may be corrupted.": "無法載入帳號資料,帳號檔案可能已損毀。",
|
||||
"Unable to load asset manifest.": "無法載入資源資訊。",
|
||||
"Unable to load profile. Profile may corrupted": "無法載入設定檔,設定檔可能已損毀。",
|
||||
"Unable to load version manifest.": "無法載入版本資訊。",
|
||||
"Unable to parse Java runtime details.": "無法解析 Java 執行環境詳細資料。",
|
||||
"Unable to resolve native libraries.": "無法解析原生程式庫。",
|
||||
"Unable to save account data. Try again later.": "無法儲存帳號資料,請稍後再試。",
|
||||
"Unable to save asset manifest.": "無法儲存資源資訊。",
|
||||
"Unable to save profile. Try again later.": "無法儲存設定檔,請稍後再試。",
|
||||
"Unable to save version manifest.": "無法儲存版本資訊。",
|
||||
"Unable to verify Minecraft ownership because the ownership list is missing or invalid.": "無法驗證 Minecraft 擁有權,因為擁有權清單遺失或無效。",
|
||||
"Unable to verify Minecraft ownership because the ownership response is invalid.": "無法驗證 Minecraft 擁有權,因為擁有權回應無效。",
|
||||
"Xbox Live accepted the sign-in but did not return an authentication token. Please sign in again.": "Xbox Live 已接受登入,但未傳回驗證權杖。請重新登入。",
|
||||
"Xbox Live did not return the account identifier. Check the account's Xbox profile and try again.": "Xbox Live 未傳回帳號識別碼。請檢查帳號的 Xbox 個人檔案後再試。",
|
||||
"Xbox security authorization did not return a token. The account may need an Xbox profile or may be restricted.": "Xbox 安全性授權未傳回權杖。此帳號可能需要建立 Xbox 個人檔案,或可能受到限制。",
|
||||
"Added {version_id}.": "已新增 {version_id}。",
|
||||
"Chinese (Traditional)": "繁體中文",
|
||||
"Installing Java ({component})": "正在安裝 Java({component})",
|
||||
"Unable to load component list: {error}": "無法載入元件清單:{error}",
|
||||
"Target profile {profile} is running. Are you sure you want to launch it again?": "設定檔 {profile} 正在執行。確定要再次啟動嗎?",
|
||||
"Time out waiting for request for launch.": "等待啟動確認逾時。",
|
||||
"Imported {count} mod(s) into {directory}.": "已將 {count} 個模組匯入 {directory}。",
|
||||
"Unable to import mod: {error}": "無法匯入模組:{error}",
|
||||
"Loader version": "載入器版本",
|
||||
"Unable to load versions: {error}": "無法載入版本:{error}",
|
||||
"Unable to load Fabric versions: {error}": "無法載入 Fabric 版本:{error}",
|
||||
"Unable to add version: {error}": "無法新增版本:{error}",
|
||||
"Unable to read version types: {error}": "無法讀取版本類型:{error}",
|
||||
"Missing icon file: {path}": "找不到圖示檔案:{path}",
|
||||
"Missing stylesheet file: {path}": "找不到樣式表檔案:{path}",
|
||||
"Failed to load icon {path}: {error}": "無法載入圖示 {path}:{error}",
|
||||
"Unable to apply QSS stylesheet {path}: {error}": "無法套用 QSS 樣式表 {path}:{error}",
|
||||
"{account} — Minecraft profile {status}": "{account} — Minecraft 玩家資料{status}",
|
||||
"available": "可用",
|
||||
"not available": "不可用",
|
||||
"Profile {profile_id} successfully created.": "設定檔 {profile_id} 已成功建立。",
|
||||
"Profile {profile_id} with fabric loader successfully created.": "已成功建立含 Fabric 載入器的設定檔 {profile_id}。",
|
||||
"Timeout while waiting for selected version.": "等待選取版本逾時。",
|
||||
"Selected a version before creating profile.": "請先選擇版本再建立設定檔。",
|
||||
"Timeout while waiting for selected mod loader.": "等待選取模組載入器逾時。",
|
||||
"Loader {loader} not found.": "找不到載入器 {loader}。",
|
||||
"Profile creation for loader {loader} is not implemented.": "尚未實作載入器 {loader} 的設定檔建立功能。",
|
||||
"Timeout while waiting for selected mod loader version.": "等待選取模組載入器版本逾時。",
|
||||
"Unable to create profile due to manifest fetch failure.": "因無法取得版本資訊,所以無法建立設定檔。",
|
||||
"Unable to create profile due to version ID is missing in manifest.": "版本資訊缺少版本 ID,無法建立設定檔。",
|
||||
"Selected a fabric loader version before creating profile.": "請先選擇 Fabric 載入器版本再建立設定檔。",
|
||||
"Unable to create profile due to vanilla manifest fetch failure.": "因無法取得原版版本資訊,所以無法建立設定檔。",
|
||||
"Unable to create profile due to mod loader manifest fetch failure.": "因無法取得模組載入器版本資訊,所以無法建立設定檔。",
|
||||
"Unable to create profile. Exising loader version ID mismatch.": "載入器版本 ID 不一致,無法建立設定檔。",
|
||||
"Unable to create profile. Inherits version is mismatched as expected.": "繼承的版本不符合預期,無法建立設定檔。",
|
||||
"Selected a forge loader version before creating profile.": "請先選擇 Forge 載入器版本再建立設定檔。",
|
||||
"Selected a neoforge loader version before creating profile.": "請先選擇 NeoForge 載入器版本再建立設定檔。",
|
||||
"Timeout while waiting for selected type.": "等待選取類型逾時。",
|
||||
"Timeout while waiting for selected loader.": "等待選取載入器逾時。",
|
||||
"Selected loader \"{loader}\" is not implemented": "尚未實作選取的載入器「{loader}」",
|
||||
"Failed to fetch version manifest: {error}": "無法取得版本資訊:{error}",
|
||||
"An unexpected error occurred while fetching version manifest: {error}": "取得版本資訊時發生非預期錯誤:{error}",
|
||||
"Unable to launch profile {profile}: {error}": "無法啟動設定檔 {profile}:{error}",
|
||||
"To make all page apply new language. Is recommend to restart the launcher after change it.": "為了讓所有頁面套用語言更新,建議在修改後重新啟動啟動器。"
|
||||
}
|
||||
@@ -116,13 +116,17 @@ class LauncherMainWindow(QMainWindow):
|
||||
"Suggestions are always welcome!")
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setObjectName("icon_label")
|
||||
self.dev_info_box = QLabel(content)
|
||||
self.dev_info_box = QLabel(self.app.tr_text(content))
|
||||
self.dev_info_box.setObjectName("dev_info_box")
|
||||
self.dev_info_2 = QLabel(message)
|
||||
self.dev_info_2 = QLabel(self.app.tr_text(message))
|
||||
self.dev_info_2.setObjectName("dev_info_message")
|
||||
self.version_label = QLabel("Current version: {}".format(self.app.launcher_version))
|
||||
self.version_label = QLabel(self.app.tr_text(
|
||||
"Current version: {version}", version=self.app.launcher_version
|
||||
))
|
||||
self.version_label.setObjectName("version_label")
|
||||
self.repo_url_label = QLabel(f"<a href=\"{MAIN_REPO_URL}\">Main repo</a>")
|
||||
self.repo_url_label = QLabel(
|
||||
f'<a href="{MAIN_REPO_URL}">{self.app.tr_text("Main repo")}</a>'
|
||||
)
|
||||
self.repo_url_label.setObjectName("repo_url_label")
|
||||
self.repo_url_label.setOpenExternalLinks(True)
|
||||
|
||||
@@ -130,7 +134,7 @@ class LauncherMainWindow(QMainWindow):
|
||||
image = QPixmap(Path(self.app.resources_dir, "pictures", "in_progress.png"))
|
||||
self.icon_label.setPixmap(image.scaled(300, 300))
|
||||
else:
|
||||
self.icon_label.setText("Ouch. The icon is missing.")
|
||||
self.icon_label.setText(self.app.tr_text("Ouch. The icon is missing."))
|
||||
|
||||
# Dialog
|
||||
self.download_dialog = ProgressDialog(self)
|
||||
@@ -146,6 +150,7 @@ class LauncherMainWindow(QMainWindow):
|
||||
self.home_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
self.home_layout.addStretch()
|
||||
|
||||
if self.app.debug:
|
||||
# test page
|
||||
self.test_page = TestPage(
|
||||
app=self.app,
|
||||
@@ -172,6 +177,7 @@ class LauncherMainWindow(QMainWindow):
|
||||
|
||||
# Add page
|
||||
self.central.addWidget(self.home_page)
|
||||
if self.app.debug:
|
||||
self.central.addWidget(self.test_page)
|
||||
self.central.addWidget(self.account_page)
|
||||
self.central.addWidget(self.settings_page)
|
||||
@@ -190,7 +196,12 @@ class LauncherMainWindow(QMainWindow):
|
||||
# Toolbar
|
||||
|
||||
# Test Window btn
|
||||
self.open_test_page = self.toolbar.add_button(" Tests", icon=self.app.get_icon(Path(self.app.icon_dir, "debug.png")))
|
||||
if self.app.debug:
|
||||
self.open_test_page = self.toolbar.add_button(" Tests")
|
||||
self.app.set_reloadable_toolbutton_icon(
|
||||
self.open_test_page,
|
||||
Path(self.app.icon_dir, "debug.png"),
|
||||
)
|
||||
self.open_test_page.setObjectName("open_test_page")
|
||||
|
||||
# Launch profile btn
|
||||
@@ -198,10 +209,18 @@ class LauncherMainWindow(QMainWindow):
|
||||
icon=self.app.get_icon(Path(self.app.icon_dir, "profile_default.png"), True))
|
||||
|
||||
# Home btn
|
||||
self.home_button = self.toolbar.add_button(" Home", icon=self.app.get_icon(Path(self.app.icon_dir, "home.png")))
|
||||
self.home_button = self.toolbar.add_button(" Home")
|
||||
self.app.set_reloadable_toolbutton_icon(
|
||||
self.home_button,
|
||||
Path(self.app.icon_dir, "home.png"),
|
||||
)
|
||||
|
||||
# Settings btn (will be moved to the bottom of the toolbar)
|
||||
self.open_settings_page = self.toolbar.add_button(" Settings", icon=self.app.get_icon(Path(self.app.icon_dir, "settings.png")))
|
||||
self.open_settings_page = self.toolbar.add_button(" Settings")
|
||||
self.app.set_reloadable_toolbutton_icon(
|
||||
self.open_settings_page,
|
||||
Path(self.app.icon_dir, "settings.png"),
|
||||
)
|
||||
|
||||
# Account btn (will be moved to the bottom (or last one item) of the toolbar
|
||||
self.open_account_page = self.toolbar.add_button(" Accounts", icon=self.app.get_icon(Path(self.app.icon_dir, "head.png"), True))
|
||||
@@ -209,8 +228,11 @@ class LauncherMainWindow(QMainWindow):
|
||||
# current account's avatar
|
||||
|
||||
# Profile btn
|
||||
self.open_create_profile_page = self.toolbar.add_button("Create Profile",
|
||||
icon=self.app.get_icon(Path(self.app.icon_dir, "add.png")))
|
||||
self.open_create_profile_page = self.toolbar.add_button("Create Profile")
|
||||
self.app.set_reloadable_toolbutton_icon(
|
||||
self.open_create_profile_page,
|
||||
Path(self.app.icon_dir, "add.png"),
|
||||
)
|
||||
|
||||
# manage profile
|
||||
self.open_manage_profile_page = self.toolbar.add_button(
|
||||
@@ -220,6 +242,7 @@ class LauncherMainWindow(QMainWindow):
|
||||
|
||||
# Bind tool buttons
|
||||
self.toolbar.addWidget(self.home_button)
|
||||
if self.app.debug:
|
||||
self.toolbar.addWidget(self.open_test_page)
|
||||
self.toolbar.addWidget(self.open_create_profile_page)
|
||||
self.toolbar.addWidget(self.open_launch_profile_page)
|
||||
@@ -238,6 +261,7 @@ class LauncherMainWindow(QMainWindow):
|
||||
lambda: self.set_current_page(self.home_page, self.home_button)
|
||||
)
|
||||
|
||||
if self.app.debug:
|
||||
self.open_test_page.clicked.connect(
|
||||
lambda: self.set_current_page(self.test_page, self.open_test_page)
|
||||
)
|
||||
@@ -266,6 +290,31 @@ class LauncherMainWindow(QMainWindow):
|
||||
self.bind_event()
|
||||
|
||||
self.set_current_page(self.home_page, self.home_button)
|
||||
self.app.i18n.language_changed.connect(self.retranslate_ui)
|
||||
self.retranslate_ui()
|
||||
|
||||
def retranslate_ui(self, *_args):
|
||||
tr = self.app.tr_text
|
||||
self.dev_info_box.setText(tr("Still digging!"))
|
||||
self.dev_info_2.setText(tr(
|
||||
"If you find an issue, you can report it in the main repository. "
|
||||
"Suggestions are always welcome!"
|
||||
))
|
||||
self.version_label.setText(
|
||||
tr("Current version: {version}", version=self.app.launcher_version)
|
||||
)
|
||||
self.repo_url_label.setText(
|
||||
f'<a href="{MAIN_REPO_URL}">{tr("Main repo")}</a>'
|
||||
)
|
||||
self.home_button.setText(" " + tr("Home"))
|
||||
self.open_launch_profile_page.setText(" " + tr("Launch Profile"))
|
||||
self.open_settings_page.setText(" " + tr("Settings"))
|
||||
self.open_account_page.setText(" " + tr("Accounts"))
|
||||
self.open_create_profile_page.setText(" " + tr("Create Profile"))
|
||||
self.open_manage_profile_page.setText(" " + tr("Manage Profiles"))
|
||||
if self.app.debug:
|
||||
self.open_test_page.setText(" " + tr("Tests"))
|
||||
self.toolbar.update_all()
|
||||
|
||||
@Slot(object)
|
||||
def ask_request(self, context: AskForRequest):
|
||||
@@ -309,8 +358,8 @@ class LauncherMainWindow(QMainWindow):
|
||||
@Slot(dict)
|
||||
def show_and_print_error(self, context: dict):
|
||||
error = context.get("error", None)
|
||||
title = context.get("title", "Error")
|
||||
message = context.get("message", "No content")
|
||||
title = context.get("title", self.app.tr_text("Error"))
|
||||
message = context.get("message", self.app.tr_text("No content"))
|
||||
|
||||
if isinstance(error, list):
|
||||
self.app.logger.error("".join(error))
|
||||
@@ -335,15 +384,15 @@ class LauncherMainWindow(QMainWindow):
|
||||
|
||||
@Slot(str)
|
||||
def show_error(self, message):
|
||||
messagebox.error(self, "Error", message=message)
|
||||
messagebox.error(self, self.app.tr_text("Error"), message=message)
|
||||
|
||||
@Slot(str)
|
||||
def show_warning(self, widget):
|
||||
messagebox.warning(self, "Warning", message=widget)
|
||||
messagebox.warning(self, self.app.tr_text("Warning"), message=widget)
|
||||
|
||||
@Slot(str)
|
||||
def show_info(self, widget):
|
||||
messagebox.info(self, "Info", message=widget)
|
||||
messagebox.info(self, self.app.tr_text("Info"), message=widget)
|
||||
|
||||
def set_current_page(self, page: QWidget, related_button: QToolButton):
|
||||
self.central.setCurrentWidget(page)
|
||||
|
||||
Reference in New Issue
Block a user