Code launch profile page ui/ux.

This commit is contained in:
wei
2026-08-01 00:47:41 +08:00
parent b5df1ec1ca
commit 338a5efe89
11 changed files with 465 additions and 107 deletions
-79
View File
@@ -19,85 +19,6 @@ from manager.profile import ProfileManager
from manager.widgets import WidgetManager
from window import LauncherMainWindow
class ToolBar(QtWidgets.QToolBar):
def __init__(self, app: QApplication, parent=None):
super(ToolBar, self).__init__(parent)
self.app = app
self.spacer = QWidget()
self.orientationChanged.connect(self._update_widget_size)
def _update_widget_size(self, orientation):
vertical = orientation == Qt.Orientation.Vertical
if vertical:
self.spacer.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Expanding,
)
else:
self.spacer.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Preferred,
)
for button in self.findChildren(QToolButton):
if vertical:
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonIconOnly
)
button.setFixedSize(60, 60)
continue
button.setMinimumSize(0, 0)
button.setMaximumSize(16777215, 16777215)
if not button.icon().isNull() and not button.text():
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonIconOnly
)
button.setFixedSize(60, 60)
elif button.icon().isNull():
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonTextOnly
)
button.setFixedWidth(150)
else:
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
)
button.setMinimumWidth(60)
def add_button(self, text=None, icon: QIcon=None) -> QToolButton:
button = QtWidgets.QToolButton(self)
if text:
text = " "+text
button.setText(text)
# else:
# button.setText("\u00a0")
if icon:
button.setIcon(icon)
button.setIconSize(QSize(32, 32))
if text:
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
)
else:
button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonIconOnly
)
button.setAutoRaise(True)
button.setFixedHeight(60)
return button
def update_all(self):
self._update_widget_size(self.orientation())
self.update()
class Launcher(QApplication, LauncherObject):
def __init__(self, logger):
super().__init__()
+13
View File
@@ -89,4 +89,17 @@ def is_light_theme(widget=None) -> bool:
return luminance >= 0.5
def add_icon_padding(icon: QIcon, icon_size=32, padding=6) -> QIcon:
source = icon.pixmap(icon_size, icon_size)
canvas_size = icon_size + padding * 2
canvas = QPixmap(canvas_size, canvas_size)
canvas.fill(Qt.GlobalColor.transparent)
painter = QPainter(canvas)
painter.drawPixmap(padding, padding, source)
painter.end()
return QIcon(canvas)
# ======================== AI generated END ========================
+2
View File
@@ -101,6 +101,8 @@ class ProfileManager(QObject):
self.load,
)
self.load()
@property
def default_profile_path(self):
return Path(self.app.work_dir, DEFAuLT_PROFILE_RELATIVE_PATH)
+1 -25
View File
@@ -53,6 +53,7 @@ class AccountWidget(QWidget):
elif selected_action == check_log_stat_action and callable(self.check_account_callback): self.check_account_callback()
elif selected_action == switch_acc_action and callable(self.switch_account_callback): self.switch_account_callback()
class AccountPage(QWidget):
def __init__(self, app, parent=None):
QWidget.__init__(self, parent)
@@ -60,31 +61,6 @@ class AccountPage(QWidget):
self.layout = QHBoxLayout()
# center message (will be deleted if launcher finished development)
# content = """Still digging!"""
# message = "If you found that something might be a issue. You can report it to the main repo! Any suggestion are welcome."
# self.icon_label = QLabel()
# self.icon_label.setObjectName("icon_label")
# self.dev_info_box = QLabel(content)
# self.dev_info_box.setObjectName("dev_info_box")
# self.dev_info_2 = QLabel(message)
# self.dev_info_2.setObjectName("dev_info_message")
#
# self.layout.addStretch()
# self.layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
# self.layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter)
# self.layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter)
# self.layout.addStretch()
#
# if Path(self.app.resources_dir, "pictures", "in_progress.png").exists():
# 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.setLayout(self.layout)
# self.app.apply_qss(Path("main.qss"), self.setStyleSheet)
self.main_layout = QVBoxLayout()
self.right_layout = QVBoxLayout()
+40
View File
@@ -156,16 +156,28 @@ class CreateProfilePage(QWidget):
self.start_load_mod_loader_version
)
self.mod_loader_dropdown.currentTextChanged.connect(
self.update_mod_loader_icon
)
self.loader_list = {
"Don't Install": {
"type": "vanilla",
"icon": Path(self.app.icon_dir, "vanilla.png"),
"loadVersionMethod": self.load_vanilla_version_list,
"profileCreateHandler": self.handle_vanilla_profile_create,
},
"Fabric": {
"type": "modded",
"icon": Path(self.app.icon_dir, "modLoader", "fabric.png"),
"loadVersionMethod": getattr(self, "load_fabric_version_list", None),
"profileCreateHandler": self.handle_fabric_profile_create,
},
"NeoForge": {
"type": "modded",
"icon": Path(self.app.icon_dir, "modLoader", "neoforged.ico"),
"loadVersionMethod": getattr(self, "load_neoforge_version_list", None),
"profileCreateHandler": self.handle_neoforge_profile_create,
}
}
self.init_all()
@@ -243,6 +255,34 @@ class CreateProfilePage(QWidget):
if callable(request.complete_callback):
request.complete_callback()
@Slot(str)
def update_mod_loader_icon(self, loader_name: str):
loader = self.loader_list.get(loader_name)
if not loader:
self.app.logger.warning(
"Mod Loader {} not found".format(loader_name)
)
return
icon_path = loader.get("icon")
if not isinstance(icon_path, Path):
self.app.logger.warning(
"Mod Loader {} icon path not found or is not Path type".format(loader_name)
)
return
pixmap = self.app.get_picture(
icon_path,
QSize(100, 100),
)
pixmap = pixmap.scaled(
QSize(100, 100),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.mod_loader_icon_label.setPixmap(pixmap)
@staticmethod
def clean_list(target_list: QListWidget | QComboBox):
+351 -1
View File
@@ -1,7 +1,357 @@
from PySide6.QtWidgets import QWidget
from pathlib import Path
from PySide6.QtCore import Qt, Signal, QPointF
from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter, QColor
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QGridLayout, \
QComboBox, QFrame, QSizePolicy, QApplication, QGraphicsDropShadowEffect
from manager.profile import ProfileSummary
class ProfileButton(QFrame):
clicked = Signal()
"""
ProfileButton
This object is coded by codex!
"""
def __init__(self, app, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.app = app
self.setObjectName("profileButton")
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setMinimumHeight(64)
self.setMaximumHeight(64)
# Layout
self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(12, 8, 12, 8)
self.layout.setSpacing(12)
self.text_layout = QVBoxLayout()
self.text_layout.setSpacing(1)
# Widget
self.icon_label = QLabel()
self.icon_label.setFixedSize(40, 40)
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.name_label = QLabel()
self.name_label.setObjectName("profileName")
self.version_label = QLabel()
self.version_label.setObjectName("profileVersion")
self.arrow_label = QLabel("")
self.arrow_label.setObjectName("profileArrow")
self.arrow_label.setStyleSheet("border-style:solid; background-color: transparent; ")
self.arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.arrow_label.setFixedWidth(24)
# Bind
self.text_layout.addWidget(self.name_label)
self.text_layout.addWidget(self.version_label)
self.layout.addWidget(self.icon_label)
self.layout.addLayout(self.text_layout, 1)
self.layout.addWidget(self.arrow_label)
def set_profile(self, profile: ProfileSummary) -> None:
"""
Set current profile id
:param profile:
:return:
"""
self.setProperty("profile_id", profile.id)
self.name_label.setText(profile.name)
self.version_label.setText(profile.version_id)
icon_path = profile.icon
if not isinstance(icon_path, Path):
icon_path = Path(icon_path)
if not icon_path.exists() or not icon_path.is_file():
# Use default
icon_path = Path(self.app.icon_dir, "profile_default.png")
self.icon_label.setPixmap(QIcon(icon_path.as_posix()).pixmap(36, 36))
def set_popup_open(self, opened: bool) -> None:
self.arrow_label.setText("" if opened else "")
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit()
super().mousePressEvent(event)
class ProfileListItem(QPushButton):
selected = Signal(object)
def __init__(self, profile: ProfileSummary, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.profile = profile
self.setObjectName("profileItem")
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setMinimumHeight(62)
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 7, 12, 7)
layout.setSpacing(12)
self.icon_label = QLabel()
self.icon_label.setFixedSize(40, 40)
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
text_layout = QVBoxLayout()
text_layout.setSpacing(1)
name_label = QLabel(profile.name)
name_label.setObjectName("itemName")
version_label = QLabel(profile.version_id)
version_label.setObjectName("itemVersion")
text_layout.addWidget(name_label)
text_layout.addWidget(version_label)
layout.addWidget(self.icon_label)
layout.addLayout(text_layout, 1)
self.clicked.connect(lambda: self.selected.emit(self.profile))
def set_icon(self, icon_path: Path) -> None:
self.icon_label.setPixmap(QIcon(icon_path.as_posix()).pixmap(36, 36))
class ProfilePopup(QFrame):
profile_selected = Signal(object)
closed = Signal()
def __init__(self, app, parent: QWidget | None = None) -> None:
super().__init__(parent, Qt.WindowType.Popup)
self.app = app
self.setFixedHeight(210)
self.setObjectName("profilePopup")
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(4, 4, 4, 4)
self.layout.setSpacing(0)
self.scroll_area = QScrollArea(self)
self.scroll_area.setObjectName("profileScrollArea")
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
)
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
self.content = QWidget()
self.content.setObjectName("profileListContent")
self.content_layout = QVBoxLayout(self.content)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(2)
self.scroll_area.setWidget(self.content)
self.layout.addWidget(self.scroll_area)
self.content_layout.setAlignment(
Qt.AlignmentFlag.AlignTop
)
def load_profiles(self, profiles: list[ProfileSummary]) -> None:
for profile in profiles:
icon_path = profile.icon
if not isinstance(icon_path, Path):
icon_path = Path(icon_path)
if not icon_path.exists() or not icon_path.is_file():
# Use default
icon_path = Path(self.app.icon_dir, "profile_default.png")
item = ProfileListItem(profile, self)
item.selected.connect(self.profile_selected)
item.set_icon(icon_path)
self.content_layout.addWidget(item)
def hideEvent(self, event: QHideEvent) -> None:
self.closed.emit()
super().hideEvent(event)
class LaunchProfilePage(QWidget):
def __init__(self, app, parent=None):
super(LaunchProfilePage, self).__init__(parent)
self.app = app
self.profile_manager = app.profile_manager
# Layout
self.layout = QVBoxLayout(self)
self.bottom_layout = QHBoxLayout()
# Widgets
self.status_label = QLabel("Select a version to launch")
self.status_label.setStyleSheet("font-size: 12px; font-weight: 600;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Profile button (just like the official launcher look like)
self.profile_button = ProfileButton(self.app)
self.profile_button.setMinimumWidth(260)
self.profile_button.setMaximumWidth(360)
self.profile_popup = ProfilePopup(parent=self, app=self.app)
self.profile_button.clicked.connect(self.toggle_profile_popup)
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.setObjectName("launchButton")
launch_button.setMinimumWidth(120)
launch_button.setMinimumHeight(42)
launch_button.setMaximumHeight(64)
launch_button.setSizePolicy(
QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Expanding,
)
launch_button.clicked.connect(self.launch_profile)
# Profile overlay
self.profile_frame = self.ProfileBackgroundFrame(
parent=self,
image_path=Path(
self.app.icon_dir,
"profile_default.png",
),
)
self.profile_frame.setObjectName("profileFrame")
# Frame layout and overlay
frame_layout = QVBoxLayout(self.profile_frame)
frame_layout.setContentsMargins(20, 20, 20, 20)
self.profile_overlay = QFrame(self.profile_frame)
self.profile_overlay.setObjectName("header")
# Add a nice shadow
# shadow = QGraphicsDropShadowEffect(self.profile_frame)
# shadow.setBlurRadius(24)
# shadow.setOffset(QPointF(0, 6))
# shadow.setColor(QColor(0, 0, 0, 150))
# self.profile_frame.setGraphicsEffect(shadow)
# Header
self.header_label = QLabel("Launch Profile")
self.header_label.setObjectName("headerLabel")
self.header_layout = QHBoxLayout(self.profile_overlay)
self.header_layout.addWidget(self.header_label)
self.header_layout.addStretch()
frame_layout.addWidget(self.profile_overlay)
frame_layout.addStretch()
# Bind button widget
self.bottom_layout.setContentsMargins(0, 0, 0, 0)
self.bottom_layout.setSpacing(10)
self.bottom_layout.addWidget(self.profile_button)
self.bottom_layout.addStretch()
self.bottom_layout.addWidget(self.status_label)
self.bottom_layout.addStretch()
self.bottom_layout.addWidget(launch_button)
self.layout.addWidget(self.profile_frame, 1)
self.layout.addLayout(self.bottom_layout)
self.app.apply_qss(Path("profile.qss"), self.setStyleSheet)
self.load_profiles()
class ProfileBackgroundFrame(QFrame):
def __init__(self, image_path, parent=None):
super().__init__(parent)
self.background = QPixmap(str(image_path))
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
if not self.background.isNull():
scaled = self.background.scaled(
self.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation,
)
x = (self.width() - scaled.width()) // 2
y = (self.height() - scaled.height()) // 2
painter.drawPixmap(x, y, scaled)
def load_profiles(self):
profiles: list[ProfileSummary] = self.profile_manager.list_profiles()
self.profile_popup.load_profiles(profiles)
if len(profiles) > 0:
self.select_profile(profiles[0])
def toggle_profile_popup(self) -> None:
if self.profile_popup.isVisible():
self.profile_popup.hide()
self.profile_button.set_popup_open(False)
return
self.profile_popup.setFixedWidth(self.profile_button.width())
button_top = self.profile_button.mapToGlobal(
self.profile_button.rect().topLeft()
)
button_bottom = self.profile_button.mapToGlobal(
self.profile_button.rect().bottomLeft()
)
screen = QApplication.screenAt(
self.profile_button.mapToGlobal(
self.profile_button.rect().center()
)
)
if screen is None:
screen = QApplication.primaryScreen()
# Show profile popup at bottom if screen space is enough to render (if not, render at top of the button)
available = screen.availableGeometry()
popup_width = self.profile_popup.width()
popup_height = self.profile_popup.height()
space_below = available.bottom() - button_bottom.y()
space_above = button_top.y() - available.top()
if space_below < popup_height and space_above > space_below:
popup_y = button_top.y() - popup_height
else:
popup_y = button_bottom.y() + 1
popup_x = max(
available.left(),
min(button_bottom.x(), available.right() - popup_width + 1),
)
popup_y = max(
available.top(),
min(popup_y, available.bottom() - popup_height + 1),
)
self.profile_popup.move(popup_x, popup_y)
self.profile_popup.show()
self.profile_button.set_popup_open(True)
def refresh_profile_button(self) -> None:
# Refresh profile button (Due to some unknown issue, the button arrow have a black
# background after profile menu profile_popup. Rerender the button can fix this issue)
self.profile_button.set_popup_open(False)
self.profile_button.updateGeometry()
self.profile_button.repaint()
def select_profile(self, profile: ProfileSummary) -> None:
# Update select profile id
self.profile_button.set_profile(profile)
self.profile_popup.hide()
self.profile_button.set_popup_open(False)
def launch_profile(self) -> None:
profile_id = self.profile_button.property("profile_id")
self.status_label.setText(f"Preparing to launch...")
+1 -1
View File
@@ -6,5 +6,5 @@
- profile_default.png : [Source](https://www.pixilart.com/art/minecraft-crafting-table-8aa9e1b9aee1a62)
- modLoader/fabric.png : [Source](https://fabricmc.net/assets/logo.png)
- modLoader/neoforged.png : [Source](https://raw.githubusercontent.com/neoforged/.github/refs/heads/main/art/neoforged.ico)
- vanilla.png : [Source](https://en.wikipedia.org/wiki/File:Updated_Minecraft_cube.svg)
## For the icon owner: If you want to take down icon that infringing upon your copyright. Contact us with this [link](https://repo.weispace.net/wei/Launcher)
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+56
View File
@@ -0,0 +1,56 @@
QFrame#profileButton {
background: palette(mid);
border: 1px solid palette(midlight);
border-radius: 5px;
}
QFrame#profileButton:hover {
background: palette(midlight);
}
QLabel#profileName, QLabel#itemName {
color: #f4f6f8;
font-size: 16px;
font-weight: 600;
}
QLabel#profileVersion, QLabel#itemVersion {
color: #aeb6c2;
font-size: 12px;
}
QLabel#profileArrow {
font-size: 18px;
}
QFrame#profilePopup {
background: palette(mid);
border: 1px solid palette(midlight);
border-radius: 5px;
}
QPushButton#profileItem {
background: transparent;
border: none;
text-align: left;
}
QPushButton#profileItem:hover {
background: #3d6fb4;
}
QPushButton#launchButton {
min-width: 150px;
font-size: 15px;
font-weight: 600;
}
QLabel#headerLabel {
font-size: 20px;
font-weight: bold
}
QFrame#profileFrame {
background-color: palette(base);
border: 1px solid palette(mid);
border-radius: 10px;
}