Code some part of the create profile page. Now it can load versions and types.

This commit is contained in:
wei
2026-07-30 21:43:19 +08:00
parent 3a4c1f42b2
commit 8d865c92a9
9 changed files with 468 additions and 183 deletions
+257 -14
View File
@@ -1,14 +1,39 @@
import threading
from collections.abc import Callable
from pathlib import Path
from PySide6.QtCore import QSize
from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout
from PySide6.QtCore import QSize, Qt, QObject, Signal, Slot
from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout, \
QSizePolicy
class CreateProfile(QWidget):
from core_lib.game.version_info import get_all_versions, get_available_version_types
from manager.game import ManifestManager, GameManager
from manager.widgets import SignalTransferItem, WidgetManager
LOADER_LIST = {
"fabric": {
"type": "modded",
"loadVersionMethod": None,
},
"Don't Install": {
"type": "vanilla",
"loadVersionMethod": "official",
}
}
class CreateProfilePage(QWidget):
def __init__(self, app, parent=None):
super(CreateProfile, self).__init__(parent)
super(CreateProfilePage, self).__init__(parent)
self.app = app
self.layout = QHBoxLayout()
# Manager
self.widget_mgr: WidgetManager = app.widget_manager
self.game_mgr: GameManager = app.game_manager
self.manifest_mgr: ManifestManager = self.game_mgr.manifest
# Layout
self.layout = QVBoxLayout()
self.content_layout = QHBoxLayout()
self.main_layout = QVBoxLayout()
self.right_layout = QVBoxLayout()
@@ -19,6 +44,7 @@ class CreateProfile(QWidget):
self.version_type_dropdown = QComboBox()
self.version_type_dropdown.setObjectName("Dropdown")
# Mod loader
self.mod_loader_icon_label = QLabel()
icon = self.app.get_icon(Path(self.app.icon_dir, "mod_loader_temp.png"))
pixmap = icon.pixmap(QSize(100, 100))
@@ -27,17 +53,33 @@ class CreateProfile(QWidget):
self.mod_loader_dropdown = QComboBox()
self.mod_loader_dropdown.setObjectName("Dropdown")
# Mod loader layout
self.mod_config_layout = QHBoxLayout()
self.mod_config_fields_layout = QVBoxLayout()
self.mod_loader_version_label = QLabel(
"Mod Loader Version"
)
self.mod_loader_version_list = QListWidget()
self.mod_loader_version_list.setObjectName(
"ListWidget"
)
# Version list
self.version_list_type_label = QLabel("Version: Vanilla")
self.version_list_label = QLabel("Select a version to continue:")
self.version_list = QListWidget()
self.version_list.setObjectName("ListWidget")
# Button
self.create_profile = QPushButton("Create Profile")
self.create_profile.setObjectName("Button")
self.refresh_button = QPushButton("Refresh")
self.refresh_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)
@@ -45,17 +87,218 @@ class CreateProfile(QWidget):
self.operate_layout.addWidget(self.refresh_button)
self.operate_layout.addWidget(self.create_profile)
self.main_layout.addLayout(self.operate_layout)
self.mod_config_fields_layout.addWidget(
self.mod_loader_label
)
self.mod_config_fields_layout.addWidget(
self.mod_loader_dropdown
)
self.mod_config_fields_layout.addWidget(
self.version_type_label
)
self.mod_config_fields_layout.addWidget(
self.version_type_dropdown
)
self.mod_config_fields_layout.addStretch()
self.mod_config_layout.addWidget(
self.mod_loader_icon_label,
alignment=Qt.AlignmentFlag.AlignTop,
)
self.mod_config_layout.addLayout(
self.mod_config_fields_layout
)
self.right_layout.addLayout(
self.mod_config_layout
)
self.right_layout.addWidget(self.mod_loader_icon_label)
self.right_layout.addWidget(self.mod_loader_label)
self.right_layout.addWidget(self.mod_loader_dropdown)
self.right_layout.addWidget(self.version_type_label)
self.right_layout.addWidget(self.version_type_dropdown)
self.right_layout.addStretch()
self.right_layout.addWidget(self.mod_loader_version_label)
self.right_layout.addWidget(
self.mod_loader_version_list,
1,
)
self.mod_loader_version_list.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Expanding,
)
self.right_layout.setContentsMargins(0, 20, 0, 0)
self.setLayout(self.layout)
self.layout.addLayout(self.main_layout)
self.layout.addLayout(self.right_layout)
self.content_layout.addLayout(
self.main_layout,
stretch=2,
)
self.content_layout.addLayout(
self.right_layout,
stretch=1,
)
self.layout.addLayout(
self.content_layout,
stretch=1,
)
self.layout.addLayout(
self.operate_layout,
)
# Signals
self.signals = self.PageSignal()
self.signals.get_selected_type.connect(self.get_selected_type)
self.signals.get_selected_version.connect(self.get_selected_version)
self.signals.update_type_dropdown.connect(self.update_type_dropdown)
self.signals.update_version_dropdown.connect(self.update_version_dropdown)
# events
self.type_list_loading = threading.Event()
self.version_list_loading = threading.Event()
self.init_all()
class PageSignal(QObject):
mod_loader_dropdown_changed = Signal(str)
load_version_list = Signal()
# widget
clean_list = Signal(object)
update_version_dropdown = Signal(list)
update_type_dropdown = Signal(list)
update_mod_loader_icon_label = Signal(object)
update_mod_loader_dropdown = Signal(list)
update_mod_loader_version_list = Signal(list)
get_selected_version = Signal(object)
get_selected_type = Signal(object)
#
# Signal Handler
#
@staticmethod
def clean_list(target_list: QListWidget | QComboBox):
if not isinstance(target_list, QListWidget | QComboBox):
raise TypeError("Target list is not a QListWidget")
target_list.clear()
@Slot(dict)
def get_selected_type(self, item: SignalTransferItem):
if not isinstance(item, SignalTransferItem):
raise TypeError("Item is not a SignalTransferItem")
selected_type = self.version_type_dropdown.currentText()
if not selected_type:
item.result_queue.put(None)
else:
item.result_queue.put(selected_type)
item.wait_event.set()
@Slot(object)
def get_selected_version(self, item: SignalTransferItem):
if not isinstance(item, SignalTransferItem):
raise TypeError("Item is not a SignalTransferItem")
selected_item = self.version_list.currentItem()
if selected_item is None:
item.result_queue.put(None)
else:
item.result_queue.put(selected_item.text())
item.wait_event.set()
def update_type_dropdown(self, types: list):
# Move release to top
if "release" in types:
types.pop(types.index("release"))
types.insert(0, "release")
self.version_type_dropdown.addItems(types)
if len(self.version_type_dropdown.currentText()) == 0:
self.version_type_dropdown.setCurrentIndex(0)
def update_version_dropdown(self, versions: list):
ids = []
for version in versions:
ver_id = version.get("id", None)
if ver_id is not None:
ids.append(ver_id)
self.version_list.addItems(ids)
selected_item = self.version_list.currentItem()
if selected_item is None and len(ids) > 0:
self.version_list.setCurrentRow(0)
#
# Thread Starter
#
def init_all(self):
self.start_load_type_list(callback=self.start_load_version_list)
def start_load_type_list(self, callback=None):
thread = threading.Thread(target=self.load_type_list, args=(callback,))
thread.start()
def start_load_version_list(self):
thread = threading.Thread(target=self.load_version_list)
thread.start()
def start_load_mod_loader_list_and_version(self):
pass
#
# Background Thread
#
def load_type_list(self, callback: Callable[None]=None):
# Cleanup
self.signals.clean_list.emit(self.version_type_dropdown)
manifest = self.manifest_mgr.get_cached_version_manifest()
if not manifest:
return
ver_types = get_available_version_types(manifest)
self.signals.update_type_dropdown.emit(ver_types)
if callable(callback):
callback()
def load_version_list(self, callback: Callable[Callable[None] | None]=None):
# Cleanup
self.version_list.clear()
# Get selected version type
request = SignalTransferItem()
self.signals.get_selected_type.emit(request)
if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit(
"Timeout while waiting for selected type.",
)
return
selected_type = request.result()
if not selected_type:
self.widget_mgr.signal.show_warning_message.emit(
"No type selected.",
)
return
# Fetch (or read) manifest
manifest = self.manifest_mgr.get_cached_version_manifest()
versions = get_all_versions(
manifest,
specified_type=selected_type
)
# Update list
self.update_version_dropdown(versions)
if callable(callback):
callback(None)
+7
View File
@@ -0,0 +1,7 @@
from PySide6.QtWidgets import QWidget
class LaunchProfilePage(QWidget):
def __init__(self, app, parent=None):
super(LaunchProfilePage, self).__init__(parent)
self.app = app
+17 -141
View File
@@ -2,10 +2,8 @@ import json
import subprocess
import textwrap
import threading
import time
import traceback
from pathlib import Path
from uuid import uuid4
from PySide6.QtCore import Qt, QObject, Signal, Slot
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QPlainTextEdit, \
@@ -13,8 +11,7 @@ from PySide6.QtWidgets import QPushButton, QVBoxLayout, QPlainTextEdit, \
from core_lib.common.common import FileObject, download_multiple_files
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
NativeResolveException, DownloadException, DependencyException, VersionManifestFetchException, \
VersionManifestSaveException, NoSpecifiedVersionKeyException, AssetManifestFetchException, \
NativeResolveException, DownloadException, DependencyException, AssetManifestFetchException, \
AssetManifestSaveException, AssetDataKeyNotFoundException, VersionDataKeyNotFoundException
from core_lib.game.argument import ArgumentMappings, parse_and_generate_arguments, find_game_and_jvm_args, \
generate_launch_command
@@ -22,12 +19,11 @@ from core_lib.game.asset import download_assets, check_assets_exist, find_correc
from core_lib.game.library_core import generate_classpath, download_full_libraries, check_full_libraries_exists
from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest, \
get_all_versions, get_available_version_types, \
fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash,
get_all_versions, get_available_version_types,
download_core_file)
from core_lib.qt.hack import is_main_thread
from core_lib.qt.progress_dialog import ProgressDialog
from core_lib.runtime.java import find_java_installations
from manager.game import ManifestManager
from manager.widgets import SelectPathRequest, AskForRequest
MAX_DOWNLOAD_ATTEMPTS = 3
@@ -40,6 +36,7 @@ class TestPage(QWidget):
self.app = app
self.parent = parent
self.widget_mgr = widget_manager
self.manifest_mgr: ManifestManager = app.game_manager.manifest
self.resize(800, 600)
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
@@ -262,7 +259,7 @@ class TestPage(QWidget):
if not ver:
return
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
arguments, asset_index, downloads, java_version, libraries, main_class, legacy_game_args = (
find_useful_part_in_specific_version_manifest(ver_data))
@@ -408,7 +405,7 @@ class TestPage(QWidget):
def _load_all_types(self):
self.signals.clean_ver_types_dropdown.emit()
data = self.get_cached_version_manifest()
data = self.manifest_mgr.get_cached_version_manifest()
if not data:
return
@@ -418,7 +415,7 @@ class TestPage(QWidget):
def _load_all_version(self, ver_type):
self.signals.clean_vers_dropdown.emit()
data = self.get_cached_version_manifest()
data = self.manifest_mgr.get_cached_version_manifest()
if not data:
return
@@ -428,135 +425,14 @@ class TestPage(QWidget):
)
self.signals.update_vers_dropdown.emit(vers)
#
# ============ Version Data Cache ============
#
@property
def version_manifest_path(self) -> Path:
return Path(self.app.work_dir, "versions", "version_manifest_v2.json")
def get_specific_version_manifest_path(self, target_version: str) -> Path | None:
return Path(self.app.work_dir, "versions", target_version, f"{target_version}.json")
def switch_between_specific_path(self, specified_ver: str | None = None):
if specified_ver is None:
path = self.version_manifest_path
else:
path = self.get_specific_version_manifest_path(specified_ver)
return path
def _cache_version_manifest(self, specified_ver: str | None = None, root_manifest: dict | None = None) \
-> tuple[bool, dict | None]:
data = None
path = self.switch_between_specific_path(specified_ver)
try:
data = fetch_and_save_version_manifest(path, target_version=specified_ver, root_manifest=root_manifest)
except VersionManifestFetchException as e:
self.widget_mgr.signal.show_error_message.emit(
e.user_message,
)
except VersionManifestSaveException as e:
self.widget_mgr.signal.show_error_message.emit(
e.user_message,
)
except Exception as e:
self.widget_mgr.signal.show_and_print_error_message.emit(
{
"error": traceback.format_exception(e),
"title": "Cache Error",
"message": "An unexpected error occurred while caching version manifest."
}
)
return True if data else False, data
def _read_cached_version_manifest(self, specified_ver: str = None) -> dict | None:
path = self.switch_between_specific_path(specified_ver)
if not path.exists():
self.widget_mgr.signal.show_error_message.emit(
"Version manifest missing after check. Please try again later.",
)
return None
try:
version_manifest = json.loads(path.read_text())
return version_manifest
except Exception as e:
self.widget_mgr.signal.show_and_print_error_message.emit(
{
"error": traceback.format_exception(e),
"title": "Cache Error",
"message": "Unable to read version manifest."
}
)
return None
def get_cached_version_manifest(self, specified_ver: str = None, max_age: int = 3600) -> dict | None:
path = self.switch_between_specific_path(specified_ver)
file = FileObject(path)
sha1 = None
root_manifest = None
if specified_ver:
root_manifest = self.get_cached_version_manifest()
if root_manifest is None:
return None
try:
_, sha1 = get_specific_version_manifest_url_and_hash(root_manifest, specified_ver)
except NoSpecifiedVersionKeyException as e:
self.widget_mgr.signal.show_error_message.emit(
f"Version {specified_ver} does not exist in version manifest.\n{e.user_message}"
)
return None
if file.exists:
age = time.time() - path.stat().st_mtime
# For root manifest
if not specified_ver and age < max_age:
return self._read_cached_version_manifest()
# Below are all for specific ver
if specified_ver and sha1 is None:
self.app.logger.warning("Specified version '{}' hash doesn't exist.".format(specified_ver))
# Also check sha1
if specified_ver and age < max_age and file.verify("sha1", sha1):
return self._read_cached_version_manifest(specified_ver)
result, data = self._cache_version_manifest(specified_ver, root_manifest=root_manifest)
if not result:
return None
return data
def create_download_progress_callback(self, progress_title: str):
task_id = uuid4().hex
signals = self.widget_mgr.download_signal
signals.started.emit(task_id, progress_title)
def callback(name, percent):
signals.progress.emit(
task_id,
f"Downloading {name}",
percent,
)
return callback, task_id
#
# ============ Game Files ============
#
# INFO: All are not in main thread
def _download_client(self, ver: str):
callback, task_id = self.create_download_progress_callback(f"Downloading {ver} client")
callback, task_id = self.manifest_mgr.create_download_progress_callback(f"Downloading {ver} client")
try:
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
if not ver_data:
self.widget_mgr.download_signal.failed.emit(
@@ -611,11 +487,11 @@ class TestPage(QWidget):
self.widget_mgr.signal.enable_widget.emit(self.button_2)
def _download_libraries(self, ver: str):
callback, task_id = self.create_download_progress_callback(f"Downloading libraries for version {ver}")
callback, task_id = self.manifest_mgr.create_download_progress_callback(f"Downloading libraries for version {ver}")
try:
self.signals.clean_output.emit()
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
if not ver_data:
self.widget_mgr.download_signal.failed.emit(
@@ -694,11 +570,11 @@ class TestPage(QWidget):
self.widget_mgr.signal.enable_widget.emit(self.button_3)
def _download_assets(self, ver: str):
callback, task_id = self.create_download_progress_callback(f"Downloading assets for version {ver}")
callback, task_id = self.manifest_mgr.create_download_progress_callback(f"Downloading assets for version {ver}")
try:
self.signals.clean_output.emit()
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
if not ver_data:
self.widget_mgr.download_signal.failed.emit(
@@ -766,7 +642,7 @@ class TestPage(QWidget):
def _parse_argument(self, ver):
try:
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
if not ver_data:
return
@@ -815,7 +691,7 @@ class TestPage(QWidget):
# , 3 = check assets
# Version manifest
ver_data = self.get_cached_version_manifest(ver)
ver_data = self.manifest_mgr.get_cached_version_manifest(ver)
if not ver_data:
return
@@ -849,7 +725,7 @@ class TestPage(QWidget):
current_status = 3
default_assets_dir = Path(self.app.work_dir, "assets")
if not check_assets_exist(ver_data, default_assets_dir, launcher_root=self.app.work_dir):
callback, task_id = self.create_download_progress_callback(
callback, task_id = self.manifest_mgr.create_download_progress_callback(
"Downloading missing assets"
)
@@ -996,7 +872,7 @@ class TestPage(QWidget):
# copy
pending = list(files)
callback, task_id = self.create_download_progress_callback(f"Redownloading {len(files)} files.")
callback, task_id = self.manifest_mgr.create_download_progress_callback(f"Redownloading {len(files)} files.")
for retry_time in range(1, MAX_DOWNLOAD_ATTEMPTS + 1):
if not pending: