From 338a5efe89c19099a0512aaddd8d1846e38e7ee3 Mon Sep 17 00:00:00 2001 From: Wei Hong Date: Sat, 1 Aug 2026 00:47:41 +0800 Subject: [PATCH] Code launch profile page ui/ux. --- app.py | 79 ------ core_lib/qt/hack.py | 13 + manager/profile.py | 2 + pages/account.py | 26 +- pages/create_profile.py | 40 +++ pages/launch_profile.py | 354 +++++++++++++++++++++++- resources/icons/COPYRIGHT.md | 2 +- resources/icons/modLoader/fabric.png | Bin 0 -> 5867 bytes resources/icons/modLoader/neoforged.ico | Bin 0 -> 67646 bytes resources/icons/vanilla.png | Bin 0 -> 3623 bytes resources/styles/profile.qss | 56 ++++ 11 files changed, 465 insertions(+), 107 deletions(-) create mode 100644 resources/icons/modLoader/fabric.png create mode 100644 resources/icons/modLoader/neoforged.ico create mode 100644 resources/icons/vanilla.png create mode 100644 resources/styles/profile.qss diff --git a/app.py b/app.py index b4a708a..52e7126 100644 --- a/app.py +++ b/app.py @@ -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__() diff --git a/core_lib/qt/hack.py b/core_lib/qt/hack.py index 9274831..0046a7f 100644 --- a/core_lib/qt/hack.py +++ b/core_lib/qt/hack.py @@ -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 ======================== \ No newline at end of file diff --git a/manager/profile.py b/manager/profile.py index cb5c970..ec08602 100644 --- a/manager/profile.py +++ b/manager/profile.py @@ -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) diff --git a/pages/account.py b/pages/account.py index 94a0d4a..536a30c 100644 --- a/pages/account.py +++ b/pages/account.py @@ -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() diff --git a/pages/create_profile.py b/pages/create_profile.py index 0f17ed0..156aac3 100644 --- a/pages/create_profile.py +++ b/pages/create_profile.py @@ -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): diff --git a/pages/launch_profile.py b/pages/launch_profile.py index c7c78b7..638d019 100644 --- a/pages/launch_profile.py +++ b/pages/launch_profile.py @@ -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 \ No newline at end of file + 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...") \ No newline at end of file diff --git a/resources/icons/COPYRIGHT.md b/resources/icons/COPYRIGHT.md index 2425a47..32074c8 100644 --- a/resources/icons/COPYRIGHT.md +++ b/resources/icons/COPYRIGHT.md @@ -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) \ No newline at end of file diff --git a/resources/icons/modLoader/fabric.png b/resources/icons/modLoader/fabric.png new file mode 100644 index 0000000000000000000000000000000000000000..4ab8370ff4b4de7b4ab665a6628e07b3f94cb44d GIT binary patch literal 5867 zcmV zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*tmgG2cg#Y6dIs&l+$HDrZZlL4qGl02NRrYkx zNR=}g*$f5=xSN}sIm4{~`R{H1i+?fKU_#6#)s!Cog&L}B{866!@4nh6oZsi45AS>S z*W<>2e8_Rg>&*L~X}#w+j>oSL3q#*AFgJL8tO{^57fT|6uB#Wx+}xAWCK@87{ken($^ zj-6Ta=iq(*^;_(o_8#}YhQd7QJt6%TqxZOtza96Vw{gF$$bQ}Vmp4kc@%jGmTkQ6p zW$$%&*Fqvv$-JBT*vk2f6FZ^w=VOVV%>N^Q=krtfDQt0&u}L;NKf~B+9nsu2*lC;H zcAeK@2TKfYxy-9`!})Wp`0T11Cn-TccDwAWp8%5F46^1q>^|3G&wcB;->`){Ps^Fk zaxu#%{`fIJ9sEyzJ}vF<0%TVU@o3luW-EU0eiSPL_-*EGMOROh@ z@r1ds!S?ifi5~42w*1Mn=e+m?^W4&~dA=7QMBF>F80*QvldO+6*(W=fttF0y{4`eX zd~lr%*cjckW|JH56Uiw~_vSO%o^xGv_b}9Q3MBbVYN~G}D})GsawuS^MIVESF{YSf zi8Z$5Q%EtTl#@8BWuHTiIpv&7uDKOoLWw1nTuP~>RaZR}7Hg{6)KY70jhi+u-S~9l zjMlpAzK0%r>baL*dmF%Kgb_y?d6ZE{n{N6^`Oh@7nPt}5mR&$;g%wv?d6iXH+hA?` z9d_Jl=UsN)?K5lkE!$tRW`E1vzs{O{v!;xNb6)<+8i#ZKxP%p*MCFW(xnRk7Rt5mH zQ_h}ZLvYHRa`se5QA9`9Y*cclRmR9*Sk}dDKXdn$xqq2A6T`oiH}_YWGfLh6N9K%D z_h#h@Anwd<^Diy(eLg^!d_%}6Yt5dkar4d6=Ofz*2uH(k=mLz);Vj) zy`AH(b4Jg5n#bwD26vGT;~F0>$dqN1j<-ig!wCI+EXEu*`o(XdxjVmd+IFylZbIv{ zj>5LUOKfYD=trNeuSDNSi51UKHSedZRluYEje(vSg*(7^iH5OJkGW#tz7bBdPC>`_T zt9kzBgs;~hGnS~#x`>Q{zQ$Qe9Oo6nuWLMIQI`j+qx)6e?upd!;hD;}zgr`X)NuC{ z?aY}reUlm3&?L>BnyNeL=4r3h2}j>)A*wp36Y0SJ+Bm@|F|fn~NR~2c(7`oU7b>|> zAXcn?K?fdgcuuk>8`y;^>EsM*05B=9yz4ZJ+m8OsUPfYBEWFK3C}!a!LE(7d+Ozq# zBY@mC^ZE3gJ*4w{CoTjT!6Z-h-9D4*7(32&kdnYvt{`{Lo`KXDIggWS!LkA8fviYc zA)qx>Q-|oLb8CfuOSzTsk8}C)YCeLy*FQ4pjJnUG1OL8Z^#<<8DB|pt+w!R)^0mrsi{OWt5sW?IU_GdRCuzgPt@BA!3_Vp5D$?*W>!dMV{!o zm{_Z=c~EF7>D{#OC$4ca`8fDzkYs3Q+O7*Z)_*Qw9qjER_n<`~9ZAA&!3{Pz5tC#2 znk;usq0H_JvY!r+(T~teQH;)IXc|?o=p>XtANJhWIq)xGdEknS6tBlhMOXNt(p!!W zs;!!f7zi=X7Bfpif}}8`@2NmS z6-wlHdsc@!n}>-8H=Y}qz`vt+quoA!B|ArH%lC_l@1R)Z=F%Ua{Z4;yVJ`Ih{y^i# zA~>?6$o-{rYF^!I7nGxS6@k$p6-Q1BEZMtnD*#*-H1`!exuI`d$oDEKG6nV++$|Ce z$3J;pv`GEbHq3ew5&`6Ti?RqrAyXi8!fVKYAB%%H&$6Z(*yPCWi#VG`5`2XnLhogZ zD2ZfC6QO~zs09jtR2^xcCJ_P-BkZy(M%U1D_g9O3v69);vh}wZU3brrVB*Bk3!AGsos`LCE z^0|t;+6+xSW}^>r*^oKi4jOko?FxY*Ub$QtmAvG!h0jcb0_T7k$q;Qz(fx*Y^DEq` zeKx{7zI17W*bCicp+H=jD7ONvgI%bX2iWyXYXVv^&>H*(sR1a2NEyB6fV0YyEI*gS zzEbe(BD#k8x=5z2l#+%RY+F_g>88q?ZlaKIW$D;|DJ$1w33Q#&Tvj8=u^Uw@djw?C zYa3QCnTAjM<;s|UniCs21{+@Rz^=woyRGx|A58p}w&q98OBpY3-P8r?EzbgG3rd|3 z9mE{3?Aw7Tc`FRoZu%2xFKi!TI+)#-9!gA5O{2B?;|J*gn_UeD@4MB*Grf^jTRlof zBFe%VHiaWPpXE+d z!p4xPCavW04Z9%2NYtT{o}yHm8hs=y9W2!SkddQ3aoJRL-aW4ceX`(9&C(PBL6V!; zbu6juZdF;lC8D5NrL(3i3e^H4`iRaH) zK={bWHMD2Ii4zGNAx zlIE#u1Qk}|$dQMTJqcCoFsrs;L`ZVIE@{k&FTwMM#O2uYrQ~cjQAWli+sDkoM~Zsm zThey49`uXDIid+c9t@F*@Tju_$ECioiiC1gi!)hLpo+!Y=fQ8sK*<^I>>oODn|QYp zhzwd{OxG`1NYyey_YbFpUN~UAz&s!k|6=1{64mTwu5pFk2v4?WA zD|2o>wBbsQ13W3&WDXfk@t$x_HfYbe+gvtHiac71 zUAB(~S132=xdQI7oB^OY;q|;`y@-`8Dn%h@DTGR!Jwfg3UbvZ(Xvvm{Ks=3hhQ1CJFTYM6CGi zgw&!M0tltSIv+-{qiUWVB@FUIOZQ=sR7|R;i{hCjyp#Z4dS27Hke3&Ta?1#8ykwop zX*6c>k86!attU^vI8zxLuUUt)5O3#>%*zF5>`lx~$ z>*!e{!hGsPf2c}evXZZ_qpgH+`R<%y{^}G>S2E`6npO<)Uk#$LbY~Ftr6;=k763Wq|M-3osZN3)$PA&qCl!gl*h?J2v3vVpts5yly++&7DaTGinU4L{G8 zlM{Lp2ckjk(>~udf4~XFFCMu7An& zd!Fp{t4dSz2}!$n)@&^ci4qh9`AOlf=#IwbgWN{e)U~3Ol9xh8>wCGgbE4F$+7MJe z+%>OdoTne>^UYMe1=SMhJ!p!4a{*$3+@f91yX`(w4+*n&)^vr}LW-w?kB!p)Wni6Tw+BPleCZC!P=F3Vig+d;k)dF0Qep>#~E1^{q4 zt_=!jD^?|`>lQw$o0xYFVegQI>hMDYH%CL3#<{bs01zkSo+S0T5xhmK!e3#IhJP}t zH))L%N{=bZws-r^L8ryB!Dc7Smx~^0)Q-=GHCJ@=t zXRk{>V+=jlkZovEGu)!h8NGrZi51&VxFZckP=`x7b08x?!SkX$(#E6bM*HIw*7g0Q z_Y~pYd>~n4w|5FHg2OLHdN=a+9qkfJ90l6{ftvk*zoqWA9{T05*;oy6`$|bXNFtz? zvS_wwTd9F%0ozsB^SR|o1z$x1K?d3?1&O2pM_W992Fz_8Cx!xYk?N(F;FG~yq#N~Y zAIJswjQsA^5^Y-7Ag#FQc=&MsJA<7(N#a*l%Cv61xK2O(4}g@(`U4d1=lBC{fppdyGf}w!Ae;^G?O?s zODG546;Tn-*a4!x^jmtz73UmvnmN+Kn*F^j{b`b(CBcYBwI`Z#TQYr^rpD?4a~Vo;n= zBeCP7GUtip&ocwf^;u%oRH@d>NcceX9_yw$G>bUfJ#d&hyiB+c4LXkfIsmcEAYiIE zZb(37Ogm(ik&3c~?3*k)vO_vZG8N(%4YJnLswts&K=1z;I(o^~nff~(zUTtZ)PPmV z(He}dcY4D}M;0fIMiAn@;|q?6vMyB*q1iN1i_}MweD^~eQa>bHgum3XNQmilWxTL*zQug7%JfQrGoyRU@2y zxIvF+<-n3At!z^xpdrWUZaex?^GPzArPGr=S<=%AsA@Zn1gwhRcO;6foV4TZY?=#_ z0%s%v&)ulNnef|sR!GkwY~LXlH4;M%R9Ry^tW$H$ZUFzL4I*oZY1j0G^XMq(C`kq^ zUTQ-lOxu>8chUi-%I=*{(h+e1XlhNcGQG;;`-dzLVz^3o`(4I#Nso`LhNw~fzMYXT zLZIKLOXw)x*Eb?8!qZ{VcQ=FfRuwpQ)6(^|8gbqYZw-R{Z!_&)!!F{3%vzi56N<=W zKf+3@2AhhWc_9BH!UpuSIrQxG)5;XnG&}q#ZIy26Jlg7g!Zv5Z)S$uoRTV`-NE#O` z$cSjv6h5;5m3uupjQ z!gp(7{+%yl02}T>Vfy__T$DnMdV-9s5%&^~lJoLvvl5ZC_kN=CB6>(c-Z`KTv*U^~ z%n~j+r=$O&TIXNKh9Rp_+NWyjr$lctBA+i`L;h^GqH`c6bVCDPx? zRnd??#2RW<8mic|Rmo@#u(UK{Bg5u3@zPj7oo`W)O@s-Y9qSGhL#;JFu&?a#=}w`L zj-<8>tw*)VJh3&gfy=LfbJhdCTr@q#dbO=7zq(jVAr*Zy$T@sgb)lf_sE#tnvlgM% zlZxY2sM>eP(yq%cdZugkZH=0Oh;|e8#E_$ zz9dhV^>TgS=z45$l(sk(9F3}&qIjUK)70$=LpM{7W9nMvL@KTMJ_S*=P?s7fv%v(v z^stJaReCJil$|?Y8Phj0^cB4|Bg7a+3+PphW`MWl9@3qClG}IzThZdD#f9^jV23;I6VeikL82J~3RpU8RxeZap&i{#FlQY7op z@XETJxxUUL9N#^D+o2l4AICtM9<%RePIAMql<=Rtnm^w7i^oyDBCq;wTk8%d^dx0# z^r}P}e|3nyW|`vZ{c`r5@W)B>cAA04dLHl(bb-bV!Jfy(aB{8h@ZD(M@Na)fLMzycQ5eh*9<+J+nK z^WkYo7RRuV~}j&8DB#F1>wCX4_8002ovPDHLkV1j`O BTp|Df literal 0 HcmV?d00001 diff --git a/resources/icons/modLoader/neoforged.ico b/resources/icons/modLoader/neoforged.ico new file mode 100644 index 0000000000000000000000000000000000000000..3e78a3f6c0ffc9819da57e21bedda0e9f43f7ebd GIT binary patch literal 67646 zcmeI*zprIu6vpwR(+h=4C8AM~_$z4q1sc&xs7>!jjO=0FZ{D@ndY-l3ea@Y8uhZ#~d;i?IbGrBYQ>SMhedP4S>2!ML z-gR$jE&jc}{xiR#Kop1qQ6LILfhf=^aObVJwp_PggU;uQ$NSsw{qnH#w_A_@@jpFq z`+K09^NPp&+wXnO|LgE@&n#B+-L3DrXZAigeJ7vg(-)tAwpG`spM1DgSG6bWwVwY) zn@aDsuKn^@OfjdqC}x=ZW2(747Ly;-Tof}b{-+1(9+=LYW?W2OOmk7pu-f~j%~W%F zEG9puxhQ5>{7(t1_uo$j4#u6<(4bGlD4_ptbX{`3Ft-LJOl+T)e>PJR6S_dmAk`u2xkw#ik+ zTkF$2Rr^s)vnp13zt)e+b*ihpns~qVDCRWJqjXbTc`O$H>oX<(*UuaEV#lVsYF0D< zv(F-B{xkno4|H=p<(dEUpR0Pti&fsQ^{KzzzW9IL{&)Vq@aoswURP-zb;gTTv%l7LKQVc~ z*mG}wf7Mr4zqFe&c`O$H&)?K zOufI@HJ|;fwyu6@H)Wb}vC8|ku3S`O=dV7irLW^F?(}O4im6;wgJLcw?WDPwJQj=p z_4yzF>-Q<@;jaHRJJMWnGygaH_kZ!f{(cknY0m%e`F%Q>|N6|$^S^#p=lsw4Kj{IT z)jEsx8b&d1)A_IW24bBL`W)Bu&py3XSEuW_d6manR}EeL-+Xg-n_M@2{>x*ru6}7J z&Cl&`7f-pUhOYjJ_iIo5ub=;&mlGcU>z@sf`JaDoF!Nt)rf95~)Kk0$#>%R})e`l*MJ;%k=7n63T5S2h=xW>P7XiVlJlH(ah!i&-q{VfX;X^^_!S_xa)t- zj%H4AtJz=c+9#%5RD)tJCheqo<^5Wh$6~tQl&QYOI(;Tw{)8S3RI-yqNlJ%DVpV=4Zk+<6@J0b$hz{ zr@B_&uXTAWHqAx*y81i)CR`qiP43n0>FSquV(~xcf7Jt;aWVDVl<9jz7VGAt;+i|n zxR_#Ad35Vt{Zm~l?;o`DzpKB~uj2Ab9*Zevl}ESU)gS+7KmS=?$zw6a?0l|x{P)lQ zsC4Vr<&``ZQ_L=(ipT%>uO7(xpYwmx19|;-v+w_^^SxWw>@PO)e(kvy|6h9LwXM3o z{OImhUDCDlq~eNM<+0YQ{MWi-?(tuJqxbk?iaBLn{nAdFD{kd)x2~AVMKvhqVpCnU zPacaY=9G2yOFL<*|+w(p+&npDV7I%0)FO=3-M_wND<4Ddv=Q^-DWxuDF%I-MV5b7uBGc zi%oUaK6xyrm{Zo(FYTnc;&wh)Trri4YEaC@rn+jMJQn-$r|-7v(*5-Pi_&JX*WY-r zy!4q{#qBh&xMC_7)u5P*NmsEW-v3wLuXW`T|LY!(|Mk~*mNuFl&75?t=DS;0Oy!~) z6mv1nm00G#?yJt{ip#6`U;m7coc}rhs~*ryEvBx5}{|K%b7gH|MP5U}_gyzas z?1)^t`j6o9SWLM{H|^`#5t=Jku_JQn>OX?ZV=?6--L$V`M`*5G#g53OtN#ctkHwUW zbkn|$9ih2$6+0rAuKpvqJQhoo{NH^{4*b$m5SFs~< z>FPg%%VV)$fBSi>uIg(IYh5)+?~ch2F~wD`iYca;d)QQSc`R0Wzt*R@$cv6ib1}u0 zABrian0we%b9pQl|Ld>+#sB*MMW-IuEOtzqiz%*hRZKC(+`}^evw4jF_0M?A`CosI zK{GD4XU4TR^Pl;zdO&@n85h&v|BC5#<0Y7KeHfTL7Mp5w36FKpX&u+HPaca+>z5ao zVADFTWuH72o7R5`k9E&!9oMo?9*a%umlu~{(>ktYpF9?u)_)0)b&-dRh ae~JQ8APPi*DDYq^aP;%=-|_$N0S6j?$u9;G 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 VbFe+s^s9rxm=Q7It#nE8e*x@7{$>CG literal 0 HcmV?d00001 diff --git a/resources/styles/profile.qss b/resources/styles/profile.qss new file mode 100644 index 0000000..ec9fdf5 --- /dev/null +++ b/resources/styles/profile.qss @@ -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; +} \ No newline at end of file