2026-08-03 22:15:55 +08:00
|
|
|
|
import shutil
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from PySide6.QtCore import Qt, QUrl, Signal
|
|
|
|
|
|
from PySide6.QtGui import QDesktopServices, QIcon
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QFileDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit,
|
|
|
|
|
|
QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpinBox,
|
2026-08-04 17:39:38 +08:00
|
|
|
|
QVBoxLayout, QWidget, QMenu, QComboBox,
|
2026-08-03 22:15:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from manager.profile import ProfileDetail, ProfileManager, ProfileSummary, UpdateProfileRequest
|
2026-08-04 17:39:38 +08:00
|
|
|
|
from manager.game import AvailableVersion
|
|
|
|
|
|
from pages.add_modded_version import AddModdedVersionDialog
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ManageProfileItem(QPushButton):
|
|
|
|
|
|
selected = Signal(object)
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
def __init__(self, app, profile: ProfileSummary,
|
|
|
|
|
|
parent=None,
|
|
|
|
|
|
delete_callback=None,
|
|
|
|
|
|
duplicate_callback=None):
|
2026-08-03 22:15:55 +08:00
|
|
|
|
super().__init__(parent)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.app = app
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.profile = profile
|
|
|
|
|
|
self.setObjectName("manageProfileItem")
|
|
|
|
|
|
self.setCheckable(True)
|
|
|
|
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
|
|
self.setMinimumHeight(64)
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
# Callback
|
|
|
|
|
|
self.delete_callback = delete_callback
|
|
|
|
|
|
self.duplicate_callback = duplicate_callback
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
layout = QHBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(12, 8, 12, 8)
|
|
|
|
|
|
layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
icon_path = Path(profile.icon)
|
|
|
|
|
|
if not icon_path.is_file():
|
|
|
|
|
|
icon_path = Path(app.icon_dir, "profile_default.png")
|
|
|
|
|
|
icon = QLabel()
|
|
|
|
|
|
icon.setFixedSize(40, 40)
|
|
|
|
|
|
icon.setPixmap(QIcon(icon_path.as_posix()).pixmap(36, 36))
|
|
|
|
|
|
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
|
|
|
|
|
|
|
texts = QVBoxLayout()
|
|
|
|
|
|
texts.setSpacing(1)
|
|
|
|
|
|
name = QLabel(profile.display_name)
|
|
|
|
|
|
name.setObjectName("itemName")
|
|
|
|
|
|
version = QLabel(profile.version_id)
|
|
|
|
|
|
version.setObjectName("itemVersion")
|
|
|
|
|
|
texts.addWidget(name)
|
|
|
|
|
|
texts.addWidget(version)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(icon)
|
|
|
|
|
|
layout.addLayout(texts, 1)
|
|
|
|
|
|
self.clicked.connect(lambda: self.selected.emit(self.profile))
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
class ManageProfilePage(QWidget):
|
2026-08-04 17:39:38 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Profile manage page
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
VERSION_CATEGORIES = (
|
|
|
|
|
|
("Vanilla", "vanilla"),
|
|
|
|
|
|
("Modded", "modded"),
|
|
|
|
|
|
("Custom", "custom"),
|
|
|
|
|
|
)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, app, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.app = app
|
|
|
|
|
|
self.profile_manager: ProfileManager = app.profile_manager
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.manifest_manager = app.game_manager.manifest
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.selected_profile_id: str | None = None
|
|
|
|
|
|
self.profile_items: dict[str, ManageProfileItem] = {}
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.version_options: list[AvailableVersion] = []
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
root = QVBoxLayout(self)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
title = QLabel(self.app.tr_text("Manage Profiles"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
title.setObjectName("manageTitle")
|
|
|
|
|
|
root.addWidget(title)
|
|
|
|
|
|
|
|
|
|
|
|
columns = QHBoxLayout()
|
|
|
|
|
|
columns.setSpacing(12)
|
|
|
|
|
|
root.addLayout(columns, 1)
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
# Widgets
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.list_scroll = self._scroll_area("manageProfileListScroll")
|
|
|
|
|
|
self.list_content = QWidget()
|
|
|
|
|
|
self.list_layout = QVBoxLayout(self.list_content)
|
|
|
|
|
|
self.list_layout.setContentsMargins(6, 6, 6, 6)
|
|
|
|
|
|
self.list_layout.setSpacing(4)
|
|
|
|
|
|
self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
|
|
|
|
self.list_scroll.setWidget(self.list_content)
|
|
|
|
|
|
self.list_scroll.setMinimumWidth(260)
|
|
|
|
|
|
self.list_scroll.setMaximumWidth(380)
|
|
|
|
|
|
columns.addWidget(self.list_scroll, 2)
|
|
|
|
|
|
|
|
|
|
|
|
self.options_scroll = self._scroll_area("manageProfileOptionsScroll")
|
|
|
|
|
|
self.options_content = QWidget()
|
|
|
|
|
|
self.options_layout = QVBoxLayout(self.options_content)
|
|
|
|
|
|
self.options_layout.setContentsMargins(20, 16, 20, 20)
|
|
|
|
|
|
self.options_layout.setSpacing(14)
|
|
|
|
|
|
self.options_scroll.setWidget(self.options_content)
|
|
|
|
|
|
columns.addWidget(self.options_scroll, 3)
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.empty_label = QLabel(self.app.tr_text("Select a profile from the list to edit its settings."))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
|
self.empty_label.setWordWrap(True)
|
|
|
|
|
|
self.options_layout.addWidget(self.empty_label, 1)
|
|
|
|
|
|
|
|
|
|
|
|
self.editor = QFrame()
|
|
|
|
|
|
self.editor.setObjectName("profileEditor")
|
|
|
|
|
|
editor_layout = QVBoxLayout(self.editor)
|
|
|
|
|
|
editor_layout.setSpacing(14)
|
|
|
|
|
|
form = QFormLayout()
|
|
|
|
|
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
|
|
|
|
|
form.setVerticalSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
self.name_edit = QLineEdit()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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")
|
2026-08-03 22:15:55 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
resolution = QHBoxLayout()
|
|
|
|
|
|
resolution.addWidget(self.width_spin)
|
|
|
|
|
|
resolution.addWidget(QLabel("×"))
|
|
|
|
|
|
resolution.addWidget(self.height_spin)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
form.addRow(self.app.tr_text("Resolution"), resolution)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
editor_layout.addLayout(form)
|
|
|
|
|
|
|
|
|
|
|
|
file_actions = QHBoxLayout()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
open_button.clicked.connect(self.open_profile_folder)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
import_button = QPushButton(self.app.tr_text("Import Mods…"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
import_button.clicked.connect(self.import_mods)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
file_actions.addWidget(add_version_button)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
file_actions.addWidget(open_button)
|
|
|
|
|
|
file_actions.addWidget(import_button)
|
|
|
|
|
|
file_actions.addStretch()
|
|
|
|
|
|
editor_layout.addLayout(file_actions)
|
|
|
|
|
|
|
|
|
|
|
|
actions = QHBoxLayout()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
duplicate_button = QPushButton(self.app.tr_text("Duplicate"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
duplicate_button.clicked.connect(self.duplicate_profile)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
delete_button = QPushButton(self.app.tr_text("Delete"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
delete_button.setObjectName("dangerButton")
|
|
|
|
|
|
delete_button.clicked.connect(self.delete_profile)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
save_button = QPushButton(self.app.tr_text("Save Changes"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
save_button.setObjectName("saveProfileButton")
|
|
|
|
|
|
save_button.clicked.connect(self.save_profile)
|
|
|
|
|
|
actions.addWidget(duplicate_button)
|
|
|
|
|
|
actions.addWidget(delete_button)
|
|
|
|
|
|
actions.addStretch()
|
|
|
|
|
|
actions.addWidget(save_button)
|
|
|
|
|
|
editor_layout.addLayout(actions)
|
|
|
|
|
|
|
|
|
|
|
|
self.options_layout.addWidget(self.editor)
|
|
|
|
|
|
self.options_layout.addStretch()
|
|
|
|
|
|
self.editor.hide()
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.type_dropdown.currentIndexChanged.connect(self._filter_versions)
|
|
|
|
|
|
self.profile_manager.profiles_changed.connect(self.refresh_profiles)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.app.apply_qss(Path("manage_profile.qss"), self.setStyleSheet)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self._load_type_dropdown()
|
|
|
|
|
|
self.refresh_profiles()
|
2026-08-03 22:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _scroll_area(name: str) -> QScrollArea:
|
|
|
|
|
|
area = QScrollArea()
|
|
|
|
|
|
area.setObjectName(name)
|
|
|
|
|
|
area.setWidgetResizable(True)
|
|
|
|
|
|
area.setFrameShape(QFrame.Shape.NoFrame)
|
|
|
|
|
|
area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
|
|
return area
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
def _dimension_spin(self) -> QSpinBox:
|
2026-08-03 22:15:55 +08:00
|
|
|
|
spin = QSpinBox()
|
|
|
|
|
|
spin.setRange(0, 16384)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
spin.setSpecialValueText(self.app.tr_text("Default"))
|
2026-08-03 22:15:55 +08:00
|
|
|
|
spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
|
|
|
|
return spin
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
def load_profiles(self) -> None:
|
|
|
|
|
|
while self.list_layout.count():
|
|
|
|
|
|
item = self.list_layout.takeAt(0)
|
|
|
|
|
|
if item.widget():
|
|
|
|
|
|
item.widget().deleteLater()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.profile_items.clear()
|
|
|
|
|
|
|
|
|
|
|
|
profiles = self.profile_manager.list_profiles()
|
|
|
|
|
|
for profile in profiles:
|
2026-08-04 17:39:38 +08:00
|
|
|
|
item = ManageProfileItem(self.app, profile,
|
|
|
|
|
|
self.list_content,
|
|
|
|
|
|
duplicate_callback=self.duplicate_profile,
|
|
|
|
|
|
delete_callback=self.delete_profile)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
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}
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if self.selected_profile_id not in available_ids:
|
|
|
|
|
|
self.selected_profile_id = None
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
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])
|
|
|
|
|
|
self.select_profile(selected)
|
|
|
|
|
|
elif self.selected_profile_id:
|
|
|
|
|
|
profile = next(p for p in profiles if p.id == self.selected_profile_id)
|
|
|
|
|
|
self.select_profile(profile)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.editor.hide()
|
|
|
|
|
|
self.empty_label.show()
|
|
|
|
|
|
|
|
|
|
|
|
def select_profile(self, profile: ProfileSummary) -> None:
|
|
|
|
|
|
detail = self.profile_manager.get_profile(profile.id)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if detail is None:
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.selected_profile_id = profile.id
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
for profile_id, item in self.profile_items.items():
|
|
|
|
|
|
item.setChecked(profile_id == profile.id)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self._set_detail(detail)
|
|
|
|
|
|
self.empty_label.hide()
|
|
|
|
|
|
self.editor.show()
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
def _set_detail(self, profile: ProfileDetail) -> None:
|
|
|
|
|
|
self.name_edit.setText(profile.name)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.game_dir_edit.setText(str(profile.game_dir or ""))
|
|
|
|
|
|
self.java_args_edit.setText(profile.java_args)
|
|
|
|
|
|
self.icon_edit.setText(profile.icon)
|
|
|
|
|
|
width, height = profile.resolution or (0, 0)
|
|
|
|
|
|
self.width_spin.setValue(width)
|
|
|
|
|
|
self.height_spin.setValue(height)
|
|
|
|
|
|
|
|
|
|
|
|
def _profile_dir(self) -> Path | None:
|
|
|
|
|
|
if self.selected_profile_id is None:
|
|
|
|
|
|
return None
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
detail = self.profile_manager.get_profile(self.selected_profile_id)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if detail is None:
|
|
|
|
|
|
return None
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
return detail.game_dir or Path(self.app.default_game_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def save_profile(self) -> None:
|
|
|
|
|
|
if self.selected_profile_id is None:
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
width, height = self.width_spin.value(), self.height_spin.value()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if (width == 0) != (height == 0):
|
|
|
|
|
|
self.app.widget_manager.signal.show_warning_message.emit(
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.app.tr_text("Set both resolution values, or set both to Default.")
|
2026-08-03 22:15:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
game_dir_text = self.game_dir_edit.text().strip()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
request = UpdateProfileRequest(
|
|
|
|
|
|
name=self.name_edit.text().strip(),
|
2026-08-04 17:39:38 +08:00
|
|
|
|
version_id=version.id,
|
|
|
|
|
|
version_type=version.version_type,
|
2026-08-03 22:15:55 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if self.profile_manager.validate_update(request).is_valid and self.profile_manager.update_profile(
|
|
|
|
|
|
self.selected_profile_id, request
|
|
|
|
|
|
):
|
|
|
|
|
|
self.profile_manager.save()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.app.widget_manager.signal.show_info_message.emit(
|
|
|
|
|
|
self.app.tr_text("Profile settings saved.")
|
|
|
|
|
|
)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
else:
|
|
|
|
|
|
errors = self.profile_manager.validate_update(request).errors
|
|
|
|
|
|
if errors:
|
|
|
|
|
|
self.app.widget_manager.signal.show_warning_message.emit("\n".join(errors.values()))
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
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)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
def open_profile_folder(self) -> None:
|
|
|
|
|
|
profile_dir = self._profile_dir()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if profile_dir is None:
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
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()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if profile_dir is None:
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
files, _ = QFileDialog.getOpenFileNames(
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self, self.app.tr_text("Import Mods"), "", "Minecraft mods (*.jar *.zip);;All files (*)"
|
2026-08-03 22:15:55 +08:00
|
|
|
|
)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if not files:
|
|
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
mods_dir = profile_dir / "mods"
|
|
|
|
|
|
mods_dir.mkdir(parents=True, exist_ok=True)
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
try:
|
|
|
|
|
|
for source in files:
|
|
|
|
|
|
shutil.copy2(source, mods_dir / Path(source).name)
|
|
|
|
|
|
except OSError as error:
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.app.widget_manager.signal.show_error_message.emit(
|
|
|
|
|
|
self.app.tr_text("Unable to import mod: {error}", error=error)
|
|
|
|
|
|
)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.app.widget_manager.signal.show_info_message.emit(
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.app.tr_text(
|
|
|
|
|
|
"Imported {count} mod(s) into {directory}.",
|
|
|
|
|
|
count=len(files), directory=mods_dir,
|
|
|
|
|
|
)
|
2026-08-03 22:15:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
def duplicate_profile(self, profile: ProfileSummary | None=None) -> None:
|
|
|
|
|
|
if self.selected_profile_id is None and profile is None:
|
2026-08-03 22:15:55 +08:00
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
|
|
|
|
|
profile_id = self.selected_profile_id
|
|
|
|
|
|
|
|
|
|
|
|
new_id = self.profile_manager.duplicate_profile(profile_id)
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
if new_id:
|
|
|
|
|
|
self.selected_profile_id = new_id
|
|
|
|
|
|
self.load_profiles()
|
|
|
|
|
|
self.profile_manager.save()
|
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
|
def delete_profile(self, profile: ProfileSummary | None=None) -> None:
|
|
|
|
|
|
if self.selected_profile_id is None and profile is None:
|
2026-08-03 22:15:55 +08:00
|
|
|
|
return
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
|
|
|
|
|
profile_id = self.selected_profile_id
|
|
|
|
|
|
|
2026-08-03 22:15:55 +08:00
|
|
|
|
answer = QMessageBox.question(
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self,
|
|
|
|
|
|
self.app.tr_text("Delete Profile"),
|
|
|
|
|
|
self.app.tr_text("Delete this profile? Game files will not be removed."),
|
2026-08-03 22:15:55 +08:00
|
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
|
|
|
QMessageBox.StandardButton.No,
|
|
|
|
|
|
)
|
|
|
|
|
|
if answer == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
if self.profile_manager.remove_profile(profile_id):
|
2026-08-04 17:39:38 +08:00
|
|
|
|
self.selected_profile_id = None
|
2026-08-03 22:15:55 +08:00
|
|
|
|
self.profile_manager.save()
|