summaryrefslogtreecommitdiff
path: root/meshmc/launcher/ui/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/ui/dialogs')
-rw-r--r--meshmc/launcher/ui/dialogs/AboutDialog.cpp146
-rw-r--r--meshmc/launcher/ui/dialogs/AboutDialog.h62
-rw-r--r--meshmc/launcher/ui/dialogs/AboutDialog.ui335
-rw-r--r--meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp178
-rw-r--r--meshmc/launcher/ui/dialogs/BlockedModsDialog.h74
-rw-r--r--meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp157
-rw-r--r--meshmc/launcher/ui/dialogs/CopyInstanceDialog.h80
-rw-r--r--meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui182
-rw-r--r--meshmc/launcher/ui/dialogs/CustomMessageBox.cpp59
-rw-r--r--meshmc/launcher/ui/dialogs/CustomMessageBox.h50
-rw-r--r--meshmc/launcher/ui/dialogs/EditAccountDialog.cpp85
-rw-r--r--meshmc/launcher/ui/dialogs/EditAccountDialog.h78
-rw-r--r--meshmc/launcher/ui/dialogs/EditAccountDialog.ui94
-rw-r--r--meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp462
-rw-r--r--meshmc/launcher/ui/dialogs/ExportInstanceDialog.h77
-rw-r--r--meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui83
-rw-r--r--meshmc/launcher/ui/dialogs/IconPickerDialog.cpp197
-rw-r--r--meshmc/launcher/ui/dialogs/IconPickerDialog.h71
-rw-r--r--meshmc/launcher/ui/dialogs/IconPickerDialog.ui67
-rw-r--r--meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp399
-rw-r--r--meshmc/launcher/ui/dialogs/JavaDownloadDialog.h82
-rw-r--r--meshmc/launcher/ui/dialogs/MSALoginDialog.cpp147
-rw-r--r--meshmc/launcher/ui/dialogs/MSALoginDialog.h77
-rw-r--r--meshmc/launcher/ui/dialogs/MSALoginDialog.ui65
-rw-r--r--meshmc/launcher/ui/dialogs/NewComponentDialog.cpp129
-rw-r--r--meshmc/launcher/ui/dialogs/NewComponentDialog.h73
-rw-r--r--meshmc/launcher/ui/dialogs/NewComponentDialog.ui101
-rw-r--r--meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp278
-rw-r--r--meshmc/launcher/ui/dialogs/NewInstanceDialog.h106
-rw-r--r--meshmc/launcher/ui/dialogs/NewInstanceDialog.ui87
-rw-r--r--meshmc/launcher/ui/dialogs/NotificationDialog.cpp103
-rw-r--r--meshmc/launcher/ui/dialogs/NotificationDialog.h63
-rw-r--r--meshmc/launcher/ui/dialogs/NotificationDialog.ui85
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp137
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSelectDialog.h115
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui62
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp298
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSetupDialog.h105
-rw-r--r--meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui74
-rw-r--r--meshmc/launcher/ui/dialogs/ProgressDialog.cpp201
-rw-r--r--meshmc/launcher/ui/dialogs/ProgressDialog.h91
-rw-r--r--meshmc/launcher/ui/dialogs/ProgressDialog.ui66
-rw-r--r--meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp163
-rw-r--r--meshmc/launcher/ui/dialogs/SkinUploadDialog.h51
-rw-r--r--meshmc/launcher/ui/dialogs/SkinUploadDialog.ui97
-rw-r--r--meshmc/launcher/ui/dialogs/UpdateDialog.cpp77
-rw-r--r--meshmc/launcher/ui/dialogs/UpdateDialog.h80
-rw-r--r--meshmc/launcher/ui/dialogs/UpdateDialog.ui91
-rw-r--r--meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp166
-rw-r--r--meshmc/launcher/ui/dialogs/VersionSelectDialog.h101
50 files changed, 6307 insertions, 0 deletions
diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.cpp b/meshmc/launcher/ui/dialogs/AboutDialog.cpp
new file mode 100644
index 0000000000..45fdcf6bc5
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/AboutDialog.cpp
@@ -0,0 +1,146 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AboutDialog.h"
+#include "ui_AboutDialog.h"
+#include <QIcon>
+#include "Application.h"
+#include "BuildConfig.h"
+
+#include <net/NetJob.h>
+
+#include "HoeDown.h"
+#include "MMCStrings.h"
+
+namespace
+{
+ // Credits
+ QString getCreditsHtml()
+ {
+ QFile dataFile(":/documents/credits.html");
+ if (!dataFile.open(QIODevice::ReadOnly)) {
+ qWarning() << "Failed to open file" << dataFile.fileName()
+ << "for reading:" << dataFile.errorString();
+ return {};
+ }
+ QString fileContent = QString::fromUtf8(dataFile.readAll());
+ dataFile.close();
+
+ return fileContent.arg(
+ QObject::tr("%1 Developers").arg(BuildConfig.MESHMC_DISPLAYNAME),
+ QObject::tr("MultiMC Developers"));
+ }
+
+ QString getLicenseHtml()
+ {
+ QFile dataFile(":/documents/COPYING.md");
+ if (dataFile.open(QIODevice::ReadOnly)) {
+ HoeDown hoedown;
+ QString output = hoedown.process(dataFile.readAll());
+ dataFile.close();
+ return output;
+ } else {
+ qWarning() << "Failed to open file" << dataFile.fileName()
+ << "for reading:" << dataFile.errorString();
+ return QString();
+ }
+ }
+
+} // namespace
+
+AboutDialog::AboutDialog(QWidget* parent)
+ : QDialog(parent), ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+
+ QString launcherName = BuildConfig.MESHMC_DISPLAYNAME;
+
+ setWindowTitle(tr("About %1").arg(launcherName));
+
+ QString chtml = getCreditsHtml();
+ ui->creditsText->setHtml(Strings::htmlListPatch(chtml));
+
+ QString lhtml = getLicenseHtml();
+ ui->licenseText->setHtml(Strings::htmlListPatch(lhtml));
+
+ ui->urlLabel->setOpenExternalLinks(true);
+
+ ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64));
+ ui->title->setText(launcherName);
+
+ ui->versionLabel->setText(BuildConfig.printableVersionString());
+
+ if (!BuildConfig.BUILD_PLATFORM.isEmpty())
+ ui->platformLabel->setText(tr("Platform") + ": " +
+ BuildConfig.BUILD_PLATFORM);
+ else
+ ui->platformLabel->setVisible(false);
+
+ if (!BuildConfig.GIT_COMMIT.isEmpty())
+ ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT));
+ else
+ ui->commitLabel->setVisible(false);
+
+ if (!BuildConfig.BUILD_DATE.isEmpty())
+ ui->buildDateLabel->setText(
+ tr("Build date: %1").arg(BuildConfig.BUILD_DATE));
+ else
+ ui->buildDateLabel->setVisible(false);
+
+ if (!BuildConfig.VERSION_CHANNEL.isEmpty())
+ ui->channelLabel->setText(tr("Channel") + ": " +
+ BuildConfig.VERSION_CHANNEL);
+ else
+ ui->channelLabel->setVisible(false);
+
+ QString urlText(
+ "<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>");
+ ui->urlLabel->setText(urlText.arg(BuildConfig.MESHMC_GIT));
+
+ QString copyText("© 2026 %1");
+ ui->copyLabel->setText(copyText.arg(BuildConfig.MESHMC_COPYRIGHT));
+
+ connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close);
+
+ connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt);
+}
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.h b/meshmc/launcher/ui/dialogs/AboutDialog.h
new file mode 100644
index 0000000000..c4a4dcaa7b
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/AboutDialog.h
@@ -0,0 +1,62 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <net/NetJob.h>
+
+namespace Ui
+{
+ class AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit AboutDialog(QWidget* parent = 0);
+ ~AboutDialog();
+
+ private:
+ Ui::AboutDialog* ui;
+
+ NetJob::Ptr netJob;
+ QByteArray dataSink;
+};
diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.ui b/meshmc/launcher/ui/dialogs/AboutDialog.ui
new file mode 100644
index 0000000000..b4eb31e982
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/AboutDialog.ui
@@ -0,0 +1,335 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AboutDialog</class>
+ <widget class="QDialog" name="AboutDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>573</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>450</width>
+ <height>400</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="title">
+ <property name="font">
+ <font>
+ <pointsize>15</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">MeshMC</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="versionLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="aboutTab">
+ <attribute name="title">
+ <string>About</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QLabel" name="aboutLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="urlLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">GIT URL</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="copyLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <kerning>true</kerning>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">COPYRIGHT</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="platformLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Platform:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="buildDateLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Build Date:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="commitLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Commit:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="channelLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Channel:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>212</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="creditsTab">
+ <attribute name="title">
+ <string>Credits</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTextBrowser" name="creditsText">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="licenseTab">
+ <attribute name="title">
+ <string>License</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTextEdit" name="licenseText">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <family>DejaVu Sans Mono</family>
+ </font>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="aboutQt">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>About Qt</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeButton">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>creditsText</tabstop>
+ <tabstop>licenseText</tabstop>
+ <tabstop>aboutQt</tabstop>
+ <tabstop>closeButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp b/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp
new file mode 100644
index 0000000000..884c1d186d
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -0,0 +1,178 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "BlockedModsDialog.h"
+
+#include <QDesktopServices>
+#include <QDir>
+#include <QFont>
+#include <QGridLayout>
+#include <QScrollArea>
+#include <QStandardPaths>
+#include <QUrl>
+
+BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title,
+ const QString& text,
+ QList<BlockedMod>& mods)
+ : QDialog(parent), m_mods(mods)
+{
+ setWindowTitle(title);
+ setMinimumSize(550, 300);
+ resize(620, 420);
+ setWindowModality(Qt::WindowModal);
+
+ auto* mainLayout = new QVBoxLayout(this);
+
+ // Description label at top
+ auto* descLabel = new QLabel(text, this);
+ descLabel->setWordWrap(true);
+ mainLayout->addWidget(descLabel);
+
+ // Scrollable area for mod list
+ auto* scrollArea = new QScrollArea(this);
+ scrollArea->setWidgetResizable(true);
+
+ auto* scrollWidget = new QWidget();
+ auto* grid = new QGridLayout(scrollWidget);
+ grid->setColumnStretch(0, 3); // mod name
+ grid->setColumnStretch(1, 1); // status
+ grid->setColumnStretch(2, 0); // button
+
+ // Header row
+ auto* headerName = new QLabel(tr("<b>Mod</b>"), scrollWidget);
+ auto* headerStatus = new QLabel(tr("<b>Status</b>"), scrollWidget);
+ grid->addWidget(headerName, 0, 0);
+ grid->addWidget(headerStatus, 0, 1);
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ int row = i + 1;
+
+ auto* nameLabel = new QLabel(m_mods[i].fileName, scrollWidget);
+ nameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ auto* statusLabel = new QLabel(tr("Missing"), scrollWidget);
+ statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;");
+
+ auto* downloadBtn = new QPushButton(tr("Download"), scrollWidget);
+ connect(downloadBtn, &QPushButton::clicked, this,
+ [this, i]() { openModDownload(i); });
+
+ grid->addWidget(nameLabel, row, 0);
+ grid->addWidget(statusLabel, row, 1);
+ grid->addWidget(downloadBtn, row, 2);
+
+ m_rows.append({nameLabel, statusLabel, downloadBtn});
+ }
+
+ // Add stretch at bottom of grid
+ grid->setRowStretch(m_mods.size() + 1, 1);
+
+ scrollWidget->setLayout(grid);
+ scrollArea->setWidget(scrollWidget);
+ mainLayout->addWidget(scrollArea, 1);
+
+ // Button box at bottom
+ m_buttons = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ m_buttons->button(QDialogButtonBox::Ok)->setText(tr("Continue"));
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
+ connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ mainLayout->addWidget(m_buttons);
+
+ setLayout(mainLayout);
+
+ // Set up Downloads folder watching
+ setupWatch();
+
+ // Initial scan
+ scanDownloadsFolder();
+}
+
+void BlockedModsDialog::setupWatch()
+{
+ m_downloadDir =
+ QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+ if (!m_downloadDir.isEmpty() && QDir(m_downloadDir).exists()) {
+ m_watcher.addPath(m_downloadDir);
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this,
+ &BlockedModsDialog::onDownloadDirChanged);
+ }
+}
+
+void BlockedModsDialog::onDownloadDirChanged(const QString& path)
+{
+ Q_UNUSED(path);
+ scanDownloadsFolder();
+}
+
+void BlockedModsDialog::scanDownloadsFolder()
+{
+ if (m_downloadDir.isEmpty())
+ return;
+
+ QDir dir(m_downloadDir);
+ QStringList files = dir.entryList(QDir::Files);
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (!m_mods[i].found && files.contains(m_mods[i].fileName)) {
+ m_mods[i].found = true;
+ }
+ }
+
+ updateModStatus();
+}
+
+void BlockedModsDialog::updateModStatus()
+{
+ bool allFound = true;
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (m_mods[i].found) {
+ m_rows[i].statusLabel->setText(QString::fromUtf8("\u2714 ") +
+ tr("Found"));
+ m_rows[i].statusLabel->setStyleSheet(
+ "color: #33aa33; font-weight: bold;");
+ m_rows[i].downloadButton->setEnabled(false);
+ } else {
+ m_rows[i].statusLabel->setText(tr("Missing"));
+ m_rows[i].statusLabel->setStyleSheet(
+ "color: #cc3333; font-weight: bold;");
+ m_rows[i].downloadButton->setEnabled(true);
+ allFound = false;
+ }
+ }
+
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allFound);
+}
+
+void BlockedModsDialog::openModDownload(int index)
+{
+ if (index < 0 || index >= m_mods.size())
+ return;
+
+ const auto& mod = m_mods[index];
+ QString url =
+ QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download")
+ .arg(mod.projectId)
+ .arg(mod.fileId);
+ QDesktopServices::openUrl(QUrl(url));
+}
diff --git a/meshmc/launcher/ui/dialogs/BlockedModsDialog.h b/meshmc/launcher/ui/dialogs/BlockedModsDialog.h
new file mode 100644
index 0000000000..77db2fc11b
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/BlockedModsDialog.h
@@ -0,0 +1,74 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QFileSystemWatcher>
+#include <QLabel>
+#include <QPushButton>
+#include <QDialogButtonBox>
+#include <QVBoxLayout>
+
+struct BlockedMod {
+ int projectId;
+ int fileId;
+ QString fileName;
+ QString targetPath;
+ bool found = false;
+};
+
+class BlockedModsDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit BlockedModsDialog(QWidget* parent, const QString& title,
+ const QString& text, QList<BlockedMod>& mods);
+
+ /// Returns the list of mods with updated `found` status
+ QList<BlockedMod>& resultMods()
+ {
+ return m_mods;
+ }
+
+ private slots:
+ void onDownloadDirChanged(const QString& path);
+ void openModDownload(int index);
+
+ private:
+ void scanDownloadsFolder();
+ void updateModStatus();
+ void setupWatch();
+
+ QList<BlockedMod>& m_mods;
+ QString m_downloadDir;
+ QFileSystemWatcher m_watcher;
+
+ struct ModRow {
+ QLabel* nameLabel;
+ QLabel* statusLabel;
+ QPushButton* downloadButton;
+ };
+ QList<ModRow> m_rows;
+
+ QDialogButtonBox* m_buttons;
+};
diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp
new file mode 100644
index 0000000000..926f0b00a1
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp
@@ -0,0 +1,157 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QLayout>
+#include <QPushButton>
+
+#include "Application.h"
+#include "CopyInstanceDialog.h"
+#include "ui_CopyInstanceDialog.h"
+
+#include "ui/dialogs/IconPickerDialog.h"
+
+#include "BaseVersion.h"
+#include "icons/IconList.h"
+#include "tasks/Task.h"
+#include "BaseInstance.h"
+#include "InstanceList.h"
+
+CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent)
+ : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+ layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ InstIconKey = original->iconKey();
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ ui->instNameTextBox->setText(original->name());
+ ui->instNameTextBox->setFocus();
+ auto groupList = APPLICATION->instances()->getGroups();
+ groupList.removeDuplicates();
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(
+ APPLICATION->instances()->getInstanceGroup(m_original->id()));
+ if (index == -1) {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+ ui->copySavesCheckbox->setChecked(m_copySaves);
+ ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime);
+}
+
+CopyInstanceDialog::~CopyInstanceDialog()
+{
+ delete ui;
+}
+
+void CopyInstanceDialog::updateDialogState()
+{
+ auto allowOK = !instName().isEmpty();
+ auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ if (OkButton->isEnabled() != allowOK) {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString CopyInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if (result.size()) {
+ return result;
+ }
+ return QString();
+}
+
+QString CopyInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+QString CopyInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+
+void CopyInstanceDialog::on_iconButton_clicked()
+{
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted) {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ }
+}
+
+void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1)
+{
+ updateDialogState();
+}
+
+bool CopyInstanceDialog::shouldCopySaves() const
+{
+ return m_copySaves;
+}
+
+void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
+{
+ if (state == Qt::Unchecked) {
+ m_copySaves = false;
+ } else if (state == Qt::Checked) {
+ m_copySaves = true;
+ }
+}
+
+bool CopyInstanceDialog::shouldKeepPlaytime() const
+{
+ return m_keepPlaytime;
+}
+
+void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
+{
+ if (state == Qt::Unchecked) {
+ m_keepPlaytime = false;
+ } else if (state == Qt::Checked) {
+ m_keepPlaytime = true;
+ }
+}
diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h
new file mode 100644
index 0000000000..adbbbd36fb
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "BaseVersion.h"
+#include <BaseInstance.h>
+
+class BaseInstance;
+
+namespace Ui
+{
+ class CopyInstanceDialog;
+}
+
+class CopyInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0);
+ ~CopyInstanceDialog();
+
+ void updateDialogState();
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+ bool shouldCopySaves() const;
+ bool shouldKeepPlaytime() const;
+
+ private slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString& arg1);
+ void on_copySavesCheckbox_stateChanged(int state);
+ void on_keepPlaytimeCheckbox_stateChanged(int state);
+
+ private:
+ Ui::CopyInstanceDialog* ui;
+ QString InstIconKey;
+ InstancePtr m_original;
+ bool m_copySaves = true;
+ bool m_keepPlaytime = true;
+};
diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui
new file mode 100644
index 0000000000..f4b191e272
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CopyInstanceDialog</class>
+ <widget class="QDialog" name="CopyInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>323</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Copy Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="iconBtnLayout">
+ <item>
+ <spacer name="iconBtnLeftSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QToolButton" name="iconButton">
+ <property name="icon">
+ <iconset>
+ <normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="iconBtnRightSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="instNameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelVersion_3">
+ <property name="text">
+ <string>&amp;Group</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="copySavesCheckbox">
+ <property name="text">
+ <string>Copy saves</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="keepPlaytimeCheckbox">
+ <property name="text">
+ <string>Keep play time</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ <tabstop>copySavesCheckbox</tabstop>
+ <tabstop>keepPlaytimeCheckbox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp b/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp
new file mode 100644
index 0000000000..80a87d8d0b
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp
@@ -0,0 +1,59 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "CustomMessageBox.h"
+
+namespace CustomMessageBox
+{
+ QMessageBox* selectable(QWidget* parent, const QString& title,
+ const QString& text, QMessageBox::Icon icon,
+ QMessageBox::StandardButtons buttons,
+ QMessageBox::StandardButton defaultButton)
+ {
+ QMessageBox* messageBox = new QMessageBox(parent);
+ messageBox->setWindowTitle(title);
+ messageBox->setText(text);
+ messageBox->setStandardButtons(buttons);
+ messageBox->setDefaultButton(defaultButton);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(icon);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+ return messageBox;
+ }
+} // namespace CustomMessageBox
diff --git a/meshmc/launcher/ui/dialogs/CustomMessageBox.h b/meshmc/launcher/ui/dialogs/CustomMessageBox.h
new file mode 100644
index 0000000000..18656c71db
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/CustomMessageBox.h
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMessageBox>
+
+namespace CustomMessageBox
+{
+ QMessageBox* selectable(
+ QWidget* parent, const QString& title, const QString& text,
+ QMessageBox::Icon icon = QMessageBox::NoIcon,
+ QMessageBox::StandardButtons buttons = QMessageBox::Ok,
+ QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
+}
diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp b/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp
new file mode 100644
index 0000000000..30e9f142f9
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp
@@ -0,0 +1,85 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "EditAccountDialog.h"
+#include "ui_EditAccountDialog.h"
+#include <DesktopServices.h>
+#include <QUrl>
+
+EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent,
+ int flags)
+ : QDialog(parent), ui(new Ui::EditAccountDialog)
+{
+ ui->setupUi(this);
+
+ ui->label->setText(text);
+ ui->label->setVisible(!text.isEmpty());
+
+ ui->userTextBox->setEnabled(flags & UsernameField);
+ ui->passTextBox->setEnabled(flags & PasswordField);
+}
+
+EditAccountDialog::~EditAccountDialog()
+{
+ delete ui;
+}
+
+void EditAccountDialog::on_label_linkActivated(const QString& link)
+{
+ DesktopServices::openUrl(QUrl(link));
+}
+
+void EditAccountDialog::setUsername(const QString& user) const
+{
+ ui->userTextBox->setText(user);
+}
+
+QString EditAccountDialog::username() const
+{
+ return ui->userTextBox->text();
+}
+
+void EditAccountDialog::setPassword(const QString& pass) const
+{
+ ui->passTextBox->setText(pass);
+}
+
+QString EditAccountDialog::password() const
+{
+ return ui->passTextBox->text();
+}
diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.h b/meshmc/launcher/ui/dialogs/EditAccountDialog.h
new file mode 100644
index 0000000000..ac1af0efbf
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.h
@@ -0,0 +1,78 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+namespace Ui
+{
+ class EditAccountDialog;
+}
+
+class EditAccountDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0,
+ int flags = UsernameField | PasswordField);
+ ~EditAccountDialog();
+
+ void setUsername(const QString& user) const;
+ void setPassword(const QString& pass) const;
+
+ QString username() const;
+ QString password() const;
+
+ enum Flags {
+ NoFlags = 0,
+
+ //! Specifies that the dialog should have a username field.
+ UsernameField,
+
+ //! Specifies that the dialog should have a password field.
+ PasswordField,
+ };
+
+ private slots:
+ void on_label_linkActivated(const QString& link);
+
+ private:
+ Ui::EditAccountDialog* ui;
+};
diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.ui b/meshmc/launcher/ui/dialogs/EditAccountDialog.ui
new file mode 100644
index 0000000000..e87509bcbc
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditAccountDialog</class>
+ <widget class="QDialog" name="EditAccountDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>148</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Login</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="placeholderText">
+ <string>Email</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="passTextBox">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp
new file mode 100644
index 0000000000..059c994dfd
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -0,0 +1,462 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ExportInstanceDialog.h"
+#include "ui_ExportInstanceDialog.h"
+#include <BaseInstance.h>
+#include <MMCZip.h>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <qfilesystemmodel.h>
+
+#include <QSortFilterProxyModel>
+#include <QDebug>
+#include <qstack.h>
+#include <QSaveFile>
+#include "MMCStrings.h"
+#include "SeparatorPrefixTree.h"
+#include "Application.h"
+#include <icons/IconList.h>
+#include <FileSystem.h>
+
+class PackIgnoreProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+ public:
+ PackIgnoreProxy(InstancePtr instance, QObject* parent)
+ : QSortFilterProxyModel(parent)
+ {
+ m_instance = instance;
+ }
+ // NOTE: Sadly, we have to do sorting ourselves.
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const
+ {
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm) {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
+
+ QFileInfo leftFileInfo = fsm->fileInfo(left);
+ QFileInfo rightFileInfo = fsm->fileInfo(right);
+
+ if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
+ return !asc;
+ }
+ if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
+ return asc;
+ }
+
+ // sort and proxy model breaks the original model...
+ if (sortColumn() == 0) {
+ return Strings::naturalCompare(leftFileInfo.fileName(),
+ rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0;
+ }
+ if (sortColumn() == 1) {
+ auto leftSize = leftFileInfo.size();
+ auto rightSize = rightFileInfo.size();
+ if ((leftSize == rightSize) ||
+ (leftFileInfo.isDir() && rightFileInfo.isDir())) {
+ return Strings::naturalCompare(leftFileInfo.fileName(),
+ rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0
+ ? asc
+ : !asc;
+ }
+ return leftSize < rightSize;
+ }
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+
+ virtual Qt::ItemFlags flags(const QModelIndex& index) const
+ {
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ auto sourceIndex = mapToSource(index);
+ Qt::ItemFlags flags = sourceIndex.flags();
+ if (index.column() == 0) {
+ flags |= Qt::ItemIsUserCheckable;
+ if (sourceIndex.model()->hasChildren(sourceIndex)) {
+ flags |= Qt::ItemIsAutoTristate;
+ }
+ }
+
+ return flags;
+ }
+
+ virtual QVariant data(const QModelIndex& index,
+ int role = Qt::DisplayRole) const
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::CheckStateRole) {
+ QFileSystemModel* fsm =
+ qobject_cast<QFileSystemModel*>(sourceModel());
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto cover = blocked.cover(blockedPath);
+ if (!cover.isNull()) {
+ return QVariant(Qt::Unchecked);
+ } else if (blocked.exists(blockedPath)) {
+ return QVariant(Qt::PartiallyChecked);
+ } else {
+ return QVariant(Qt::Checked);
+ }
+ }
+
+ return sourceIndex.data(role);
+ }
+
+ virtual bool setData(const QModelIndex& index, const QVariant& value,
+ int role = Qt::EditRole)
+ {
+ if (index.column() == 0 && role == Qt::CheckStateRole) {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ return setFilterState(index, state);
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value,
+ role);
+ }
+
+ QString relPath(const QString& path) const
+ {
+ QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
+ prefix += '/';
+ if (!path.startsWith(prefix)) {
+ return QString();
+ }
+ return path.mid(prefix.size());
+ }
+
+ bool setFilterState(QModelIndex index, Qt::CheckState state)
+ {
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+
+ if (!fsm) {
+ return false;
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ bool changed = false;
+ if (state == Qt::Unchecked) {
+ // blocking a path
+ auto& node = blocked.insert(blockedPath);
+ // get rid of all blocked nodes below
+ node.clear();
+ changed = true;
+ } else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
+ if (!blocked.remove(blockedPath)) {
+ auto cover = blocked.cover(blockedPath);
+ qDebug() << "Blocked by cover" << cover;
+ // uncover
+ blocked.remove(cover);
+ // block all contents, except for any cover
+ QModelIndex rootIndex = fsm->index(
+ FS::PathCombine(m_instance->instanceRoot(), cover));
+ QModelIndex doing = rootIndex;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1) {
+ auto node = fsm->index(row, 0, doing);
+ if (!node.isValid()) {
+ if (!todo.size()) {
+ break;
+ } else {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ auto relpath = relPath(fsm->filePath(node));
+ if (blockedPath.startsWith(relpath)) // cover found?
+ {
+ // continue processing cover later
+ todo.push(node);
+ } else {
+ // or just block this one.
+ blocked.insert(relpath);
+ }
+ row++;
+ }
+ }
+ changed = true;
+ }
+ if (changed) {
+ // update the thing
+ emit dataChanged(index, index, {Qt::CheckStateRole});
+ // update everything above index
+ QModelIndex up = index.parent();
+ while (1) {
+ if (!up.isValid())
+ break;
+ emit dataChanged(up, up, {Qt::CheckStateRole});
+ up = up.parent();
+ }
+ // and everything below the index
+ QModelIndex doing = index;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1) {
+ auto node = doing.model()->index(row, 0, doing);
+ if (!node.isValid()) {
+ if (!todo.size()) {
+ break;
+ } else {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ emit dataChanged(node, node, {Qt::CheckStateRole});
+ todo.push(node);
+ row++;
+ }
+ // siblings and unrelated nodes are ignored
+ }
+ return true;
+ }
+
+ bool shouldExpand(QModelIndex index)
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm) {
+ return false;
+ }
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto found = blocked.find(blockedPath);
+ if (found) {
+ return !found->leaf();
+ }
+ return false;
+ }
+
+ void setBlockedPaths(QStringList paths)
+ {
+ beginResetModel();
+ blocked.clear();
+ blocked.insert(paths);
+ endResetModel();
+ }
+
+ const SeparatorPrefixTree<'/'>& blockedPaths() const
+ {
+ return blocked;
+ }
+
+ protected:
+ bool filterAcceptsColumn(int source_column,
+ const QModelIndex& source_parent) const
+ {
+ Q_UNUSED(source_parent)
+
+ // adjust the columns you want to filter out here
+ // return false for those that will be hidden
+ if (source_column == 2 || source_column == 3)
+ return false;
+
+ return true;
+ }
+
+ private:
+ InstancePtr m_instance;
+ SeparatorPrefixTree<'/'> blocked;
+};
+
+ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance,
+ QWidget* parent)
+ : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
+{
+ ui->setupUi(this);
+ auto model = new QFileSystemModel(this);
+ proxyModel = new PackIgnoreProxy(m_instance, this);
+ loadPackIgnore();
+ proxyModel->setSourceModel(model);
+ auto root = instance->instanceRoot();
+ ui->treeView->setModel(proxyModel);
+ ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
+ ui->treeView->sortByColumn(0, Qt::AscendingOrder);
+
+ connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)),
+ SLOT(rowsInserted(QModelIndex, int, int)));
+
+ model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs |
+ QDir::Hidden);
+ model->setRootPath(root);
+ auto headerView = ui->treeView->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+}
+
+ExportInstanceDialog::~ExportInstanceDialog()
+{
+ delete ui;
+}
+
+/// Save icon to instance's folder is needed
+void SaveIcon(InstancePtr m_instance)
+{
+ auto iconKey = m_instance->iconKey();
+ auto iconList = APPLICATION->icons();
+ auto mmcIcon = iconList->icon(iconKey);
+ if (!mmcIcon || mmcIcon->isBuiltIn()) {
+ return;
+ }
+ auto path = mmcIcon->getFilePath();
+ if (!path.isNull()) {
+ QFileInfo inInfo(path);
+ FS::copy(path, FS::PathCombine(m_instance->instanceRoot(),
+ inInfo.fileName()))();
+ return;
+ }
+ auto& image = mmcIcon->m_images[mmcIcon->type()];
+ auto& icon = image.icon;
+ auto sizes = icon.availableSizes();
+ if (sizes.size() == 0) {
+ return;
+ }
+ auto areaOf = [](QSize size) { return size.width() * size.height(); };
+ QSize largest = sizes[0];
+ // find variant with largest area
+ for (auto size : sizes) {
+ if (areaOf(largest) < areaOf(size)) {
+ largest = size;
+ }
+ }
+ auto pixmap = icon.pixmap(largest);
+ pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png"));
+}
+
+bool ExportInstanceDialog::doExport()
+{
+ auto name = FS::RemoveInvalidFilenameChars(m_instance->name());
+
+ const QString output = QFileDialog::getSaveFileName(
+ this, tr("Export %1").arg(m_instance->name()),
+ FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)",
+ nullptr, QFileDialog::DontConfirmOverwrite);
+ if (output.isEmpty()) {
+ return false;
+ }
+ if (QFile::exists(output)) {
+ int ret = QMessageBox::question(
+ this, tr("Overwrite?"),
+ tr("This file already exists. Do you want to overwrite it?"),
+ QMessageBox::No, QMessageBox::Yes);
+ if (ret == QMessageBox::No) {
+ return false;
+ }
+ }
+
+ SaveIcon(m_instance);
+
+ auto& blocked = proxyModel->blockedPaths();
+ using std::placeholders::_1;
+ if (!MMCZip::compressDir(
+ output, m_instance->instanceRoot(),
+ std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) {
+ QMessageBox::warning(this, tr("Error"),
+ tr("Unable to export instance"));
+ return false;
+ }
+ return true;
+}
+
+void ExportInstanceDialog::done(int result)
+{
+ savePackIgnore();
+ if (result == QDialog::Accepted) {
+ if (doExport()) {
+ QDialog::done(QDialog::Accepted);
+ return;
+ } else {
+ return;
+ }
+ }
+ QDialog::done(result);
+}
+
+void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom)
+{
+ // WARNING: possible off-by-one?
+ for (int i = top; i < bottom; i++) {
+ auto node = parent.model()->index(i, 0, parent);
+ if (proxyModel->shouldExpand(node)) {
+ auto expNode = node.parent();
+ if (!expNode.isValid()) {
+ continue;
+ }
+ ui->treeView->expand(node);
+ }
+ }
+}
+
+QString ExportInstanceDialog::ignoreFileName()
+{
+ return FS::PathCombine(m_instance->instanceRoot(), ".packignore");
+}
+
+void ExportInstanceDialog::loadPackIgnore()
+{
+ auto filename = ignoreFileName();
+ QFile ignoreFile(filename);
+ if (!ignoreFile.open(QIODevice::ReadOnly)) {
+ return;
+ }
+ auto data = ignoreFile.readAll();
+ auto string = QString::fromUtf8(data);
+ proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts));
+}
+
+void ExportInstanceDialog::savePackIgnore()
+{
+ auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8();
+ auto filename = ignoreFileName();
+ try {
+ FS::write(filename, data);
+ } catch (const Exception& e) {
+ qWarning() << e.cause();
+ }
+}
+
+#include "ExportInstanceDialog.moc"
diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h
new file mode 100644
index 0000000000..e9e9549d4e
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h
@@ -0,0 +1,77 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QModelIndex>
+#include <memory>
+
+class BaseInstance;
+class PackIgnoreProxy;
+typedef std::shared_ptr<BaseInstance> InstancePtr;
+
+namespace Ui
+{
+ class ExportInstanceDialog;
+}
+
+class ExportInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0);
+ ~ExportInstanceDialog();
+
+ virtual void done(int result);
+
+ private:
+ bool doExport();
+ void loadPackIgnore();
+ void savePackIgnore();
+ QString ignoreFileName();
+
+ private:
+ Ui::ExportInstanceDialog* ui;
+ InstancePtr m_instance;
+ PackIgnoreProxy* proxyModel;
+
+ private slots:
+ void rowsInserted(QModelIndex parent, int top, int bottom);
+};
diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui
new file mode 100644
index 0000000000..bcd4e84a4d
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportInstanceDialog</class>
+ <widget class="QDialog" name="ExportInstanceDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>720</width>
+ <height>625</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Instance</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTreeView" name="treeView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>treeView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp b/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp
new file mode 100644
index 0000000000..1768b44fd4
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp
@@ -0,0 +1,197 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QKeyEvent>
+#include <QPushButton>
+#include <QFileDialog>
+
+#include "Application.h"
+
+#include "IconPickerDialog.h"
+#include "ui_IconPickerDialog.h"
+
+#include "ui/instanceview/InstanceDelegate.h"
+
+#include "icons/IconList.h"
+#include "icons/IconUtils.h"
+#include <DesktopServices.h>
+
+IconPickerDialog::IconPickerDialog(QWidget* parent)
+ : QDialog(parent), ui(new Ui::IconPickerDialog)
+{
+ ui->setupUi(this);
+ setWindowModality(Qt::WindowModal);
+
+ auto contentsWidget = ui->iconView;
+ contentsWidget->setViewMode(QListView::IconMode);
+ contentsWidget->setFlow(QListView::LeftToRight);
+ contentsWidget->setIconSize(QSize(48, 48));
+ contentsWidget->setMovement(QListView::Static);
+ contentsWidget->setResizeMode(QListView::Adjust);
+ contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+ contentsWidget->setSpacing(5);
+ contentsWidget->setWordWrap(false);
+ contentsWidget->setWrapping(true);
+ contentsWidget->setUniformItemSizes(true);
+ contentsWidget->setTextElideMode(Qt::ElideRight);
+ contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->setItemDelegate(new ListViewDelegate());
+
+ // contentsWidget->setAcceptDrops(true);
+ contentsWidget->setDropIndicatorShown(true);
+ contentsWidget->viewport()->setAcceptDrops(true);
+ contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
+ contentsWidget->setDefaultDropAction(Qt::CopyAction);
+
+ contentsWidget->installEventFilter(this);
+
+ contentsWidget->setModel(APPLICATION->icons().get());
+
+ // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel
+ // ones are on the right. We win.
+ auto buttonAdd =
+ ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
+ auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"),
+ QDialogButtonBox::ResetRole);
+
+ connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon()));
+ connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon()));
+
+ connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)),
+ SLOT(activated(QModelIndex)));
+
+ connect(contentsWidget->selectionModel(),
+ SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
+ SLOT(selectionChanged(QItemSelection, QItemSelection)));
+
+ auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"),
+ QDialogButtonBox::ResetRole);
+ connect(buttonFolder, &QPushButton::clicked, this,
+ &IconPickerDialog::openFolder);
+}
+
+bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
+{
+ if (obj != ui->iconView)
+ return QDialog::eventFilter(obj, evt);
+ if (evt->type() != QEvent::KeyPress) {
+ return QDialog::eventFilter(obj, evt);
+ }
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(evt);
+ switch (keyEvent->key()) {
+ case Qt::Key_Delete:
+ removeSelectedIcon();
+ return true;
+ case Qt::Key_Plus:
+ addNewIcon();
+ return true;
+ default:
+ break;
+ }
+ return QDialog::eventFilter(obj, evt);
+}
+
+void IconPickerDialog::addNewIcon()
+{
+ //: The title of the select icons open file dialog
+ QString selectIcons = tr("Select Icons");
+ //: The type of icon files
+ auto filter = IconUtils::getIconFilter();
+ QStringList fileNames = QFileDialog::getOpenFileNames(
+ this, selectIcons, QString(), tr("Icons %1").arg(filter));
+ APPLICATION->icons()->installIcons(fileNames);
+}
+
+void IconPickerDialog::removeSelectedIcon()
+{
+ APPLICATION->icons()->deleteIcon(selectedIconKey);
+}
+
+void IconPickerDialog::activated(QModelIndex index)
+{
+ selectedIconKey = index.data(Qt::UserRole).toString();
+ accept();
+}
+
+void IconPickerDialog::selectionChanged(QItemSelection selected,
+ QItemSelection deselected)
+{
+ if (selected.empty())
+ return;
+
+ QString key =
+ selected.first().indexes().first().data(Qt::UserRole).toString();
+ if (!key.isEmpty()) {
+ selectedIconKey = key;
+ }
+}
+
+int IconPickerDialog::execWithSelection(QString selection)
+{
+ auto list = APPLICATION->icons();
+ auto contentsWidget = ui->iconView;
+ selectedIconKey = selection;
+
+ int index_nr = list->getIconIndex(selection);
+ auto model_index = list->index(index_nr);
+ contentsWidget->selectionModel()->select(model_index,
+ QItemSelectionModel::Current |
+ QItemSelectionModel::Select);
+
+ QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection,
+ Q_ARG(QModelIndex, model_index));
+ return QDialog::exec();
+}
+
+void IconPickerDialog::delayed_scroll(QModelIndex model_index)
+{
+ auto contentsWidget = ui->iconView;
+ contentsWidget->scrollTo(model_index);
+}
+
+IconPickerDialog::~IconPickerDialog()
+{
+ delete ui;
+}
+
+void IconPickerDialog::openFolder()
+{
+ DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true);
+}
diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.h b/meshmc/launcher/ui/dialogs/IconPickerDialog.h
new file mode 100644
index 0000000000..0a92be4a53
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.h
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QDialog>
+#include <QItemSelection>
+
+namespace Ui
+{
+ class IconPickerDialog;
+}
+
+class IconPickerDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit IconPickerDialog(QWidget* parent = 0);
+ ~IconPickerDialog();
+ int execWithSelection(QString selection);
+ QString selectedIconKey;
+
+ protected:
+ virtual bool eventFilter(QObject*, QEvent*);
+
+ private:
+ Ui::IconPickerDialog* ui;
+
+ private slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+ void delayed_scroll(QModelIndex);
+ void addNewIcon();
+ void removeSelectedIcon();
+ void openFolder();
+};
diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.ui b/meshmc/launcher/ui/dialogs/IconPickerDialog.ui
new file mode 100644
index 0000000000..c548edfb7a
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.ui
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>IconPickerDialog</class>
+ <widget class="QDialog" name="IconPickerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>676</width>
+ <height>555</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Pick icon</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListView" name="iconView"/>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp
new file mode 100644
index 0000000000..c83542543c
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp
@@ -0,0 +1,399 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "JavaDownloadDialog.h"
+
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QGroupBox>
+#include <QMessageBox>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QDebug>
+#include <QDir>
+#include <QHeaderView>
+#include <QSplitter>
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "java/JavaUtils.h"
+#include "net/Download.h"
+
+JavaDownloadDialog::JavaDownloadDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowTitle(tr("Download Java"));
+ setMinimumSize(700, 450);
+ resize(800, 500);
+ m_providers = JavaDownload::JavaProviderInfo::availableProviders();
+ setupUi();
+}
+
+void JavaDownloadDialog::setupUi()
+{
+ auto* mainLayout = new QVBoxLayout(this);
+
+ // --- Three-column selection area ---
+ auto* columnsLayout = new QHBoxLayout();
+
+ // Left: Provider list
+ auto* providerGroup = new QGroupBox(tr("Provider"), this);
+ auto* providerLayout = new QVBoxLayout(providerGroup);
+ m_providerList = new QListWidget(this);
+ m_providerList->setIconSize(QSize(24, 24));
+ for (const auto& provider : m_providers) {
+ auto* item = new QListWidgetItem(
+ APPLICATION->getThemedIcon(provider.iconName), provider.name);
+ m_providerList->addItem(item);
+ }
+ providerLayout->addWidget(m_providerList);
+ columnsLayout->addWidget(providerGroup, 1);
+
+ // Center: Major version list
+ auto* versionGroup = new QGroupBox(tr("Version"), this);
+ auto* versionLayout = new QVBoxLayout(versionGroup);
+ m_versionList = new QListWidget(this);
+ versionLayout->addWidget(m_versionList);
+ columnsLayout->addWidget(versionGroup, 1);
+
+ // Right: Sub-version / build list
+ auto* subVersionGroup = new QGroupBox(tr("Build"), this);
+ auto* subVersionLayout = new QVBoxLayout(subVersionGroup);
+ m_subVersionList = new QListWidget(this);
+ subVersionLayout->addWidget(m_subVersionList);
+ columnsLayout->addWidget(subVersionGroup, 1);
+
+ mainLayout->addLayout(columnsLayout, 1);
+
+ // Info label
+ m_infoLabel = new QLabel(this);
+ m_infoLabel->setWordWrap(true);
+ m_infoLabel->setText(
+ tr("Select a Java provider, version, and build to download."));
+ mainLayout->addWidget(m_infoLabel);
+
+ // Progress bar
+ m_progressBar = new QProgressBar(this);
+ m_progressBar->setVisible(false);
+ m_progressBar->setRange(0, 100);
+ mainLayout->addWidget(m_progressBar);
+
+ // Status label
+ m_statusLabel = new QLabel(this);
+ m_statusLabel->setVisible(false);
+ mainLayout->addWidget(m_statusLabel);
+
+ // Buttons
+ auto* buttonLayout = new QHBoxLayout();
+ buttonLayout->addStretch();
+
+ m_downloadBtn = new QPushButton(tr("Download"), this);
+ m_downloadBtn->setEnabled(false);
+ buttonLayout->addWidget(m_downloadBtn);
+
+ m_cancelBtn = new QPushButton(tr("Cancel"), this);
+ buttonLayout->addWidget(m_cancelBtn);
+
+ mainLayout->addLayout(buttonLayout);
+
+ // Connections
+ connect(m_providerList, &QListWidget::currentRowChanged, this,
+ &JavaDownloadDialog::providerChanged);
+ connect(m_versionList, &QListWidget::currentRowChanged, this,
+ &JavaDownloadDialog::majorVersionChanged);
+ connect(m_subVersionList, &QListWidget::currentRowChanged, this,
+ &JavaDownloadDialog::subVersionChanged);
+ connect(m_downloadBtn, &QPushButton::clicked, this,
+ &JavaDownloadDialog::onDownloadClicked);
+ connect(m_cancelBtn, &QPushButton::clicked, this,
+ &JavaDownloadDialog::onCancelClicked);
+
+ // Select first provider
+ if (m_providerList->count() > 0) {
+ m_providerList->setCurrentRow(0);
+ }
+}
+
+void JavaDownloadDialog::providerChanged(int index)
+{
+ if (index < 0 || index >= m_providers.size())
+ return;
+
+ m_versionList->clear();
+ m_subVersionList->clear();
+ m_downloadBtn->setEnabled(false);
+ m_versions.clear();
+ m_runtimes.clear();
+
+ const auto& provider = m_providers[index];
+ m_infoLabel->setText(tr("Loading versions for %1...").arg(provider.name));
+
+ fetchVersionList(provider.uid);
+}
+
+void JavaDownloadDialog::majorVersionChanged(int index)
+{
+ if (index < 0 || index >= m_versions.size())
+ return;
+
+ m_subVersionList->clear();
+ m_downloadBtn->setEnabled(false);
+ m_runtimes.clear();
+
+ const auto& version = m_versions[index];
+ m_infoLabel->setText(tr("Loading builds for %1...").arg(version.versionId));
+
+ fetchRuntimes(version.uid, version.versionId);
+}
+
+void JavaDownloadDialog::subVersionChanged(int index)
+{
+ if (index < 0 || index >= m_runtimes.size()) {
+ m_downloadBtn->setEnabled(false);
+ return;
+ }
+
+ const auto& rt = m_runtimes[index];
+ m_infoLabel->setText(tr("Ready to download: %1\n"
+ "Version: %2\n"
+ "Platform: %3\n"
+ "Checksum: %4")
+ .arg(rt.name, rt.version.toString(), rt.runtimeOS,
+ rt.checksumHash.isEmpty()
+ ? tr("None")
+ : tr("Yes (%1)").arg(rt.checksumType)));
+ m_downloadBtn->setEnabled(true);
+}
+
+void JavaDownloadDialog::fetchVersionList(const QString& uid)
+{
+ m_fetchJob.reset();
+ m_fetchData.clear();
+
+ QString url = QString("%1%2/index.json").arg(BuildConfig.META_URL, uid);
+ m_fetchJob = new NetJob(tr("Fetch Java versions"), APPLICATION->network());
+ auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
+ m_fetchJob->addNetAction(dl);
+
+ connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, uid]() {
+ m_fetchJob.reset();
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_fetchData);
+ } catch (const Exception& e) {
+ m_infoLabel->setText(
+ tr("Failed to parse version list: %1").arg(e.cause()));
+ return;
+ }
+ if (!doc.isObject()) {
+ m_infoLabel->setText(tr("Failed to parse version list."));
+ return;
+ }
+
+ m_versions = JavaDownload::parseVersionIndex(doc.object(), uid);
+ m_versionList->clear();
+
+ for (const auto& ver : m_versions) {
+ QString displayName = ver.versionId;
+ if (displayName.startsWith("java")) {
+ displayName = "Java " + displayName.mid(4);
+ }
+ m_versionList->addItem(displayName);
+ }
+
+ if (m_versions.size() > 0) {
+ m_infoLabel->setText(tr("Select a version."));
+ } else {
+ m_infoLabel->setText(
+ tr("No versions available for this provider."));
+ }
+ });
+
+ connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
+ m_fetchJob.reset();
+ m_infoLabel->setText(tr("Failed to load versions: %1").arg(reason));
+ });
+
+ m_fetchJob->start();
+}
+
+void JavaDownloadDialog::fetchRuntimes(const QString& uid,
+ const QString& versionId)
+{
+ m_fetchJob.reset();
+ m_fetchData.clear();
+
+ QString url =
+ QString("%1%2/%3.json").arg(BuildConfig.META_URL, uid, versionId);
+ m_fetchJob =
+ new NetJob(tr("Fetch Java runtime details"), APPLICATION->network());
+ auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
+ m_fetchJob->addNetAction(dl);
+
+ connect(m_fetchJob.get(), &NetJob::succeeded, this, [this]() {
+ m_fetchJob.reset();
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_fetchData);
+ } catch (const Exception& e) {
+ m_infoLabel->setText(
+ tr("Failed to parse runtime details: %1").arg(e.cause()));
+ return;
+ }
+ if (!doc.isObject()) {
+ m_infoLabel->setText(tr("Failed to parse runtime details."));
+ return;
+ }
+
+ auto allRuntimes = JavaDownload::parseRuntimes(doc.object());
+ QString myOS = JavaDownload::currentRuntimeOS();
+
+ m_runtimes.clear();
+ m_subVersionList->clear();
+ for (const auto& rt : allRuntimes) {
+ if (rt.runtimeOS == myOS) {
+ m_runtimes.append(rt);
+ m_subVersionList->addItem(rt.version.toString());
+ }
+ }
+
+ if (m_runtimes.isEmpty()) {
+ m_infoLabel->setText(
+ tr("No builds available for your platform (%1).").arg(myOS));
+ m_downloadBtn->setEnabled(false);
+ } else {
+ m_infoLabel->setText(tr("Select a build to download."));
+ m_subVersionList->setCurrentRow(0);
+ }
+ });
+
+ connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
+ m_fetchJob.reset();
+ m_infoLabel->setText(
+ tr("Failed to load runtime details: %1").arg(reason));
+ });
+
+ m_fetchJob->start();
+}
+
+QString JavaDownloadDialog::javaInstallDir() const
+{
+ return JavaUtils::managedJavaRoot();
+}
+
+void JavaDownloadDialog::onDownloadClicked()
+{
+ int idx = m_subVersionList->currentRow();
+ if (idx < 0 || idx >= m_runtimes.size())
+ return;
+
+ const auto& runtime = m_runtimes[idx];
+
+ // Build target directory path: {dataPath}/java/{vendor}/{name}-{version}/
+ QString dirName =
+ QString("%1-%2").arg(runtime.name, runtime.version.toString());
+ QString targetDir =
+ FS::PathCombine(javaInstallDir(), runtime.vendor, dirName);
+
+ // Check if already installed
+ if (QDir(targetDir).exists()) {
+ auto result = QMessageBox::question(
+ this, tr("Already Installed"),
+ tr("This Java version appears to be already installed at:\n%1\n\n"
+ "Do you want to reinstall it?")
+ .arg(targetDir),
+ QMessageBox::Yes | QMessageBox::No);
+ if (result != QMessageBox::Yes) {
+ return;
+ }
+ // Remove existing installation
+ QDir(targetDir).removeRecursively();
+ }
+
+ m_downloadBtn->setEnabled(false);
+ m_providerList->setEnabled(false);
+ m_versionList->setEnabled(false);
+ m_subVersionList->setEnabled(false);
+ m_progressBar->setVisible(true);
+ m_progressBar->setValue(0);
+ m_statusLabel->setVisible(true);
+
+ m_downloadTask =
+ std::make_unique<JavaDownloadTask>(runtime, targetDir, this);
+
+ connect(m_downloadTask.get(), &Task::progress, this,
+ [this](qint64 current, qint64 total) {
+ if (total > 0) {
+ m_progressBar->setValue(
+ static_cast<int>(current * 100 / total));
+ }
+ });
+
+ connect(m_downloadTask.get(), &Task::status, this,
+ [this](const QString& status) { m_statusLabel->setText(status); });
+
+ connect(m_downloadTask.get(), &Task::succeeded, this, [this]() {
+ m_installedJavaPath = m_downloadTask->installedJavaPath();
+ m_progressBar->setValue(100);
+ m_statusLabel->setText(tr("Java installed successfully!"));
+
+ QMessageBox::information(
+ this, tr("Download Complete"),
+ tr("Java has been downloaded and installed successfully.\n\n"
+ "Java binary: %1")
+ .arg(m_installedJavaPath));
+
+ accept();
+ });
+
+ connect(m_downloadTask.get(), &Task::failed, this,
+ [this](const QString& reason) {
+ m_progressBar->setVisible(false);
+ m_statusLabel->setText(tr("Download failed: %1").arg(reason));
+ m_downloadBtn->setEnabled(true);
+ m_providerList->setEnabled(true);
+ m_versionList->setEnabled(true);
+ m_subVersionList->setEnabled(true);
+
+ QMessageBox::warning(
+ this, tr("Download Failed"),
+ tr("Failed to download Java:\n%1").arg(reason));
+ });
+
+ m_downloadTask->start();
+}
+
+void JavaDownloadDialog::onCancelClicked()
+{
+ if (m_fetchJob) {
+ m_fetchJob->abort();
+ m_fetchJob.reset();
+ }
+ if (m_downloadTask) {
+ m_downloadTask->abort();
+ m_downloadTask.reset();
+ }
+ reject();
+}
diff --git a/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h
new file mode 100644
index 0000000000..48d29a7328
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h
@@ -0,0 +1,82 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QListWidget>
+#include <QPushButton>
+#include <QProgressBar>
+#include <QLabel>
+#include <QSplitter>
+
+#include "java/download/JavaRuntime.h"
+#include "java/download/JavaDownloadTask.h"
+#include "net/NetJob.h"
+
+class JavaDownloadDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaDownloadDialog(QWidget* parent = nullptr);
+ ~JavaDownloadDialog() override = default;
+
+ QString installedJavaPath() const
+ {
+ return m_installedJavaPath;
+ }
+
+ private slots:
+ void providerChanged(int index);
+ void majorVersionChanged(int index);
+ void subVersionChanged(int index);
+ void onDownloadClicked();
+ void onCancelClicked();
+
+ private:
+ void setupUi();
+ void fetchVersionList(const QString& uid);
+ void fetchRuntimes(const QString& uid, const QString& versionId);
+ QString javaInstallDir() const;
+
+ // Left panel: providers
+ QListWidget* m_providerList = nullptr;
+ // Center panel: major versions (Java 25, Java 21, ...)
+ QListWidget* m_versionList = nullptr;
+ // Right panel: sub-versions / builds
+ QListWidget* m_subVersionList = nullptr;
+
+ QLabel* m_infoLabel = nullptr;
+ QLabel* m_statusLabel = nullptr;
+ QPushButton* m_downloadBtn = nullptr;
+ QPushButton* m_cancelBtn = nullptr;
+ QProgressBar* m_progressBar = nullptr;
+
+ QList<JavaDownload::JavaProviderInfo> m_providers;
+ QList<JavaDownload::JavaVersionInfo> m_versions;
+ QList<JavaDownload::RuntimeEntry> m_runtimes;
+
+ NetJob::Ptr m_fetchJob;
+ QByteArray m_fetchData;
+ std::unique_ptr<JavaDownloadTask> m_downloadTask;
+ QString m_installedJavaPath;
+};
diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp b/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp
new file mode 100644
index 0000000000..2038ab8bdf
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -0,0 +1,147 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MSALoginDialog.h"
+#include "ui_MSALoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include <QtWidgets/QPushButton>
+#include <QUrl>
+
+MSALoginDialog::MSALoginDialog(QWidget* parent)
+ : QDialog(parent), ui(new Ui::MSALoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+int MSALoginDialog::exec()
+{
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+ ui->progressBar->setMaximum(0); // Indeterminate progress
+ ui->label->setText(tr("Opening your browser for Microsoft login..."));
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createBlankMSA();
+ m_loginTask = m_account->loginMSA();
+ connect(m_loginTask.get(), &Task::failed, this,
+ &MSALoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this,
+ &MSALoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this,
+ &MSALoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this,
+ &MSALoginDialog::onTaskProgress);
+ connect(m_loginTask.get(), &AccountTask::authorizeWithBrowser, this,
+ &MSALoginDialog::onAuthorizeWithBrowser);
+ m_loginTask->start();
+
+ return QDialog::exec();
+}
+
+MSALoginDialog::~MSALoginDialog()
+{
+ delete ui;
+}
+
+void MSALoginDialog::onAuthorizeWithBrowser(const QUrl& url)
+{
+ QString urlString = url.toString();
+ QString linkString =
+ QString("<a href=\"%1\">%2</a>").arg(urlString, tr("here"));
+ ui->label->setText(
+ tr("<p>A browser window will open for Microsoft login.</p>"
+ "<p>If it doesn't open automatically, click %1.</p>")
+ .arg(linkString));
+}
+
+void MSALoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->buttonBox->setEnabled(enable);
+}
+
+void MSALoginDialog::onTaskFailed(const QString& reason)
+{
+ // Set message
+ auto lines = reason.split('\n');
+ QString processed;
+ for (auto line : lines) {
+ if (line.size()) {
+ processed += "<font color='red'>" + line + "</font><br />";
+ } else {
+ processed += "<br />";
+ }
+ }
+ ui->label->setText(processed);
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void MSALoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void MSALoginDialog::onTaskStatus(const QString& status)
+{
+ ui->label->setText(status);
+}
+
+void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg)
+{
+ MSALoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted) {
+ return dlg.m_account;
+ }
+ return nullptr;
+}
diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.h b/meshmc/launcher/ui/dialogs/MSALoginDialog.h
new file mode 100644
index 0000000000..b68074d232
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.h
@@ -0,0 +1,77 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QtWidgets/QDialog>
+#include <QtCore/QEventLoop>
+
+#include "minecraft/auth/MinecraftAccount.h"
+
+namespace Ui
+{
+ class MSALoginDialog;
+}
+
+class MSALoginDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ ~MSALoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
+ int exec() override;
+
+ private:
+ explicit MSALoginDialog(QWidget* parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+ protected slots:
+ void onTaskFailed(const QString& reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString& status);
+ void onTaskProgress(qint64 current, qint64 total);
+ void onAuthorizeWithBrowser(const QUrl& url);
+
+ private:
+ Ui::MSALoginDialog* ui;
+ MinecraftAccountPtr m_account;
+ shared_qobject_ptr<AccountTask> m_loginTask;
+};
diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.ui b/meshmc/launcher/ui/dialogs/MSALoginDialog.ui
new file mode 100644
index 0000000000..78cbfb269f
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MSALoginDialog</class>
+ <widget class="QDialog" name="MSALoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>491</width>
+ <height>143</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Microsoft Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.
+
+aaaaa</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp b/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp
new file mode 100644
index 0000000000..f87e94afb6
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp
@@ -0,0 +1,129 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Application.h"
+#include "NewComponentDialog.h"
+#include "ui_NewComponentDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+
+NewComponentDialog::NewComponentDialog(const QString& initialName,
+ const QString& initialUid,
+ QWidget* parent)
+ : QDialog(parent), ui(new Ui::NewComponentDialog)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+
+ ui->nameTextBox->setText(initialName);
+ ui->uidTextBox->setText(initialUid);
+
+ connect(ui->nameTextBox, &QLineEdit::textChanged, this,
+ &NewComponentDialog::updateDialogState);
+ connect(ui->uidTextBox, &QLineEdit::textChanged, this,
+ &NewComponentDialog::updateDialogState);
+
+ auto groups = APPLICATION->instances()->getGroups();
+ groups.removeDuplicates();
+ ui->nameTextBox->setFocus();
+
+ originalPlaceholderText = ui->uidTextBox->placeholderText();
+ updateDialogState();
+}
+
+NewComponentDialog::~NewComponentDialog()
+{
+ delete ui;
+}
+
+void NewComponentDialog::updateDialogState()
+{
+ auto protoUid = ui->nameTextBox->text().toLower();
+ protoUid.remove(QRegularExpression("[^a-z]"));
+ if (protoUid.isEmpty()) {
+ ui->uidTextBox->setPlaceholderText(originalPlaceholderText);
+ } else {
+ QString suggestedUid = "org.projecttick.custom." + protoUid;
+ ui->uidTextBox->setPlaceholderText(suggestedUid);
+ }
+ bool allowOK =
+ !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid());
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+QString NewComponentDialog::name() const
+{
+ auto result = ui->nameTextBox->text();
+ if (result.size()) {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+QString NewComponentDialog::uid() const
+{
+ auto result = ui->uidTextBox->text();
+ if (result.size()) {
+ return result.trimmed();
+ }
+ result = ui->uidTextBox->placeholderText();
+ if (result.size() && result != originalPlaceholderText) {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+void NewComponentDialog::setBlacklist(QStringList badUids)
+{
+ uidBlacklist = badUids;
+}
diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.h b/meshmc/launcher/ui/dialogs/NewComponentDialog.h
new file mode 100644
index 0000000000..7ad8b89ee2
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.h
@@ -0,0 +1,73 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <QString>
+#include <QStringList>
+
+namespace Ui
+{
+ class NewComponentDialog;
+}
+
+class NewComponentDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit NewComponentDialog(const QString& initialName = QString(),
+ const QString& initialUid = QString(),
+ QWidget* parent = 0);
+ virtual ~NewComponentDialog();
+ void setBlacklist(QStringList badUids);
+
+ QString name() const;
+ QString uid() const;
+
+ private slots:
+ void updateDialogState();
+
+ private:
+ Ui::NewComponentDialog* ui;
+
+ QString originalPlaceholderText;
+ QStringList uidBlacklist;
+};
diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.ui b/meshmc/launcher/ui/dialogs/NewComponentDialog.ui
new file mode 100644
index 0000000000..03b0d22294
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.ui
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewComponentDialog</class>
+ <widget class="QDialog" name="NewComponentDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>146</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Add Empty Component</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLineEdit" name="nameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="uidTextBox">
+ <property name="placeholderText">
+ <string>uid</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>nameTextBox</tabstop>
+ <tabstop>uidTextBox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp
new file mode 100644
index 0000000000..571bc3717e
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -0,0 +1,278 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Application.h"
+#include "NewInstanceDialog.h"
+#include "ui_NewInstanceDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+#include <QDialogButtonBox>
+
+#include "ui/widgets/PageContainer.h"
+#include "ui/pages/modplatform/VanillaPage.h"
+#include "ui/pages/modplatform/atlauncher/AtlPage.h"
+#include "ui/pages/modplatform/ftb/FtbPage.h"
+#include "ui/pages/modplatform/legacy_ftb/Page.h"
+#include "ui/pages/modplatform/flame/FlamePage.h"
+#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
+#include "ui/pages/modplatform/ImportPage.h"
+#include "ui/pages/modplatform/technic/TechnicPage.h"
+
+NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
+ const QString& url, QWidget* parent)
+ : QDialog(parent), ui(new Ui::NewInstanceDialog)
+{
+ ui->setupUi(this);
+
+ setWindowIcon(APPLICATION->getThemedIcon("new"));
+
+ InstIconKey = "default";
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+
+ auto groupList = APPLICATION->instances()->getGroups();
+ groupList.removeDuplicates();
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front(initialGroup);
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(initialGroup);
+ if (index == -1) {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+
+ // NOTE: m_buttons must be initialized before PageContainer, because it
+ // indirectly accesses m_buttons through setSuggestedPack! Do not move
+ // this below.
+ m_buttons =
+ new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok |
+ QDialogButtonBox::Cancel);
+
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred,
+ QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ ui->verticalLayout->insertWidget(2, m_container);
+
+ m_container->addButtons(m_buttons);
+
+ // Bonk Qt over its stupid head and make sure it understands which button is
+ // the default one... See:
+ // https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept);
+
+ auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+ connect(CancelButton, &QPushButton::clicked, this,
+ &NewInstanceDialog::reject);
+
+ auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+ connect(HelpButton, &QPushButton::clicked, m_container,
+ &PageContainer::help);
+
+ if (!url.isEmpty()) {
+ QUrl actualUrl(url);
+ m_container->selectPage("import");
+ importPage->setUrl(url);
+ }
+
+ updateDialogState();
+
+ restoreGeometry(QByteArray::fromBase64(
+ APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
+}
+
+void NewInstanceDialog::reject()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry",
+ saveGeometry().toBase64());
+ QDialog::reject();
+}
+
+void NewInstanceDialog::accept()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry",
+ saveGeometry().toBase64());
+ importIconNow();
+ QDialog::accept();
+}
+
+QList<BasePage*> NewInstanceDialog::getPages()
+{
+ importPage = new ImportPage(this);
+ flamePage = new FlamePage(this);
+ auto technicPage = new TechnicPage(this);
+ return {new VanillaPage(this), importPage,
+ new AtlPage(this), flamePage,
+ new ModrinthPage(this), new FtbPage(this),
+ new LegacyFTB::Page(this), technicPage};
+}
+
+QString NewInstanceDialog::dialogTitle()
+{
+ return tr("New Instance");
+}
+
+NewInstanceDialog::~NewInstanceDialog()
+{
+ delete ui;
+}
+
+void NewInstanceDialog::setSuggestedPack(const QString& name,
+ InstanceTask* task)
+{
+ creationTask.reset(task);
+ ui->instNameTextBox->setPlaceholderText(name);
+
+ if (!task) {
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
+ importIcon = false;
+ }
+
+ auto allowOK = task && !instName().isEmpty();
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+void NewInstanceDialog::setSuggestedIconFromFile(const QString& path,
+ const QString& name)
+{
+ importIcon = true;
+ importIconPath = path;
+ importIconName = name;
+
+ // Hmm, for some reason they can be to small
+ ui->iconButton->setIcon(QIcon(path));
+}
+
+void NewInstanceDialog::setSuggestedIcon(const QString& key)
+{
+ auto icon = APPLICATION->icons()->getIcon(key);
+ importIcon = false;
+
+ ui->iconButton->setIcon(icon);
+}
+
+InstanceTask* NewInstanceDialog::extractTask()
+{
+ InstanceTask* extracted = creationTask.get();
+ creationTask.release();
+ extracted->setName(instName());
+ extracted->setGroup(instGroup());
+ extracted->setIcon(iconKey());
+ return extracted;
+}
+
+void NewInstanceDialog::updateDialogState()
+{
+ auto allowOK = creationTask && !instName().isEmpty();
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ if (OkButton->isEnabled() != allowOK) {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString NewInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if (result.size()) {
+ return result;
+ }
+ result = ui->instNameTextBox->placeholderText().trimmed();
+ if (result.size()) {
+ return result;
+ }
+ return QString();
+}
+
+QString NewInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+QString NewInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+void NewInstanceDialog::on_iconButton_clicked()
+{
+ importIconNow(); // so the user can switch back
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted) {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ importIcon = false;
+ }
+}
+
+void NewInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1)
+{
+ updateDialogState();
+}
+
+void NewInstanceDialog::importIconNow()
+{
+ if (importIcon) {
+ APPLICATION->icons()->installIcon(importIconPath, importIconName);
+ InstIconKey = importIconName;
+ importIcon = false;
+ }
+ APPLICATION->settings()->set("NewInstanceGeometry",
+ saveGeometry().toBase64());
+}
diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.h b/meshmc/launcher/ui/dialogs/NewInstanceDialog.h
new file mode 100644
index 0000000000..ba042eca1a
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.h
@@ -0,0 +1,106 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include "BaseVersion.h"
+#include "ui/pages/BasePageProvider.h"
+#include "InstanceTask.h"
+
+namespace Ui
+{
+ class NewInstanceDialog;
+}
+
+class PageContainer;
+class QDialogButtonBox;
+class ImportPage;
+class FlamePage;
+
+class NewInstanceDialog : public QDialog, public BasePageProvider
+{
+ Q_OBJECT
+
+ public:
+ explicit NewInstanceDialog(const QString& initialGroup,
+ const QString& url = QString(),
+ QWidget* parent = 0);
+ ~NewInstanceDialog();
+
+ void updateDialogState();
+
+ void setSuggestedPack(const QString& name = QString(),
+ InstanceTask* task = nullptr);
+ void setSuggestedIconFromFile(const QString& path, const QString& name);
+ void setSuggestedIcon(const QString& key);
+
+ InstanceTask* extractTask();
+
+ QString dialogTitle() override;
+ QList<BasePage*> getPages() override;
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+
+ public slots:
+ void accept() override;
+ void reject() override;
+
+ private slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString& arg1);
+
+ private:
+ Ui::NewInstanceDialog* ui = nullptr;
+ PageContainer* m_container = nullptr;
+ QDialogButtonBox* m_buttons = nullptr;
+
+ QString InstIconKey;
+ ImportPage* importPage = nullptr;
+ FlamePage* flamePage = nullptr;
+ std::unique_ptr<InstanceTask> creationTask;
+
+ bool importIcon = false;
+ QString importIconPath;
+ QString importIconName;
+
+ void importIconNow();
+};
diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui b/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui
new file mode 100644
index 0000000000..7fb19ff5cf
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewInstanceDialog</class>
+ <widget class="QDialog" name="NewInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>730</width>
+ <height>127</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>New Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.cpp b/meshmc/launcher/ui/dialogs/NotificationDialog.cpp
new file mode 100644
index 0000000000..6d43d09ebd
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NotificationDialog.cpp
@@ -0,0 +1,103 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "NotificationDialog.h"
+#include "ui_NotificationDialog.h"
+
+#include <QTimerEvent>
+#include <QStyle>
+
+NotificationDialog::NotificationDialog(
+ const NotificationChecker::NotificationEntry& entry, QWidget* parent)
+ : QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint |
+ Qt::CustomizeWindowHint),
+ ui(new Ui::NotificationDialog)
+{
+ ui->setupUi(this);
+
+ QStyle::StandardPixmap icon;
+ switch (entry.type) {
+ case NotificationChecker::NotificationEntry::Critical:
+ icon = QStyle::SP_MessageBoxCritical;
+ break;
+ case NotificationChecker::NotificationEntry::Warning:
+ icon = QStyle::SP_MessageBoxWarning;
+ break;
+ default:
+ case NotificationChecker::NotificationEntry::Information:
+ icon = QStyle::SP_MessageBoxInformation;
+ break;
+ }
+ ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this));
+ ui->messageLabel->setText(entry.message);
+
+ m_dontShowAgainText = tr("Don't show again");
+ m_closeText = tr("Close");
+
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText +
+ QString(" (%1)").arg(m_dontShowAgainTime));
+ ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime));
+
+ startTimer(1000);
+}
+
+NotificationDialog::~NotificationDialog()
+{
+ delete ui;
+}
+
+void NotificationDialog::timerEvent(QTimerEvent* event)
+{
+ if (m_dontShowAgainTime > 0) {
+ m_dontShowAgainTime--;
+ if (m_dontShowAgainTime == 0) {
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText);
+ ui->dontShowAgainBtn->setEnabled(true);
+ } else {
+ ui->dontShowAgainBtn->setText(
+ m_dontShowAgainText +
+ QString(" (%1)").arg(m_dontShowAgainTime));
+ }
+ }
+ if (m_closeTime > 0) {
+ m_closeTime--;
+ if (m_closeTime == 0) {
+ ui->closeBtn->setText(m_closeText);
+ ui->closeBtn->setEnabled(true);
+ } else {
+ ui->closeBtn->setText(m_closeText +
+ QString(" (%1)").arg(m_closeTime));
+ }
+ }
+
+ if (m_closeTime == 0 && m_dontShowAgainTime == 0) {
+ killTimer(event->timerId());
+ }
+}
+
+void NotificationDialog::on_dontShowAgainBtn_clicked()
+{
+ done(DontShowAgain);
+}
+void NotificationDialog::on_closeBtn_clicked()
+{
+ done(Normal);
+}
diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.h b/meshmc/launcher/ui/dialogs/NotificationDialog.h
new file mode 100644
index 0000000000..82f578f5fb
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NotificationDialog.h
@@ -0,0 +1,63 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef NOTIFICATIONDIALOG_H
+#define NOTIFICATIONDIALOG_H
+
+#include <QDialog>
+
+#include "notifications/NotificationChecker.h"
+
+namespace Ui
+{
+ class NotificationDialog;
+}
+
+class NotificationDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit NotificationDialog(
+ const NotificationChecker::NotificationEntry& entry,
+ QWidget* parent = 0);
+ ~NotificationDialog();
+
+ enum ExitCode { Normal, DontShowAgain };
+
+ protected:
+ void timerEvent(QTimerEvent* event);
+
+ private:
+ Ui::NotificationDialog* ui;
+
+ int m_dontShowAgainTime = 10;
+ int m_closeTime = 5;
+
+ QString m_dontShowAgainText;
+ QString m_closeText;
+
+ private slots:
+ void on_dontShowAgainBtn_clicked();
+ void on_closeBtn_clicked();
+};
+
+#endif // NOTIFICATIONDIALOG_H
diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.ui b/meshmc/launcher/ui/dialogs/NotificationDialog.ui
new file mode 100644
index 0000000000..3e6c22bc80
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/NotificationDialog.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NotificationDialog</class>
+ <widget class="QDialog" name="NotificationDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>320</width>
+ <height>240</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Notification</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="iconLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="messageLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="dontShowAgainBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Don't show again</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp
new file mode 100644
index 0000000000..77687c699e
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp
@@ -0,0 +1,137 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProfileSelectDialog.h"
+#include "ui_ProfileSelectDialog.h"
+
+#include <QItemSelectionModel>
+#include <QDebug>
+
+#include "SkinUtils.h"
+#include "Application.h"
+
+#include "ui/dialogs/ProgressDialog.h"
+
+ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags,
+ QWidget* parent)
+ : QDialog(parent), ui(new Ui::ProfileSelectDialog)
+{
+ ui->setupUi(this);
+
+ m_accounts = APPLICATION->accounts();
+ auto view = ui->listView;
+ // view->setModel(m_accounts.get());
+ // view->hideColumn(AccountList::ActiveColumn);
+ view->setColumnCount(1);
+ view->setRootIsDecorated(false);
+ // FIXME: use a real model, not this
+ if (QTreeWidgetItem* header = view->headerItem()) {
+ header->setText(0, tr("Name"));
+ } else {
+ view->setHeaderLabel(tr("Name"));
+ }
+ QList<QTreeWidgetItem*> items;
+ for (int i = 0; i < m_accounts->count(); i++) {
+ MinecraftAccountPtr account = m_accounts->at(i);
+ QString profileLabel;
+ if (account->isInUse()) {
+ profileLabel = tr("%1 (in use)").arg(account->profileName());
+ } else {
+ profileLabel = account->profileName();
+ }
+ auto item = new QTreeWidgetItem(view);
+ item->setText(0, profileLabel);
+ item->setIcon(0, account->getFace());
+ item->setData(0, AccountList::PointerRole,
+ QVariant::fromValue(account));
+ items.append(item);
+ }
+ view->addTopLevelItems(items);
+
+ // Set the message label.
+ ui->msgLabel->setVisible(!message.isEmpty());
+ ui->msgLabel->setText(message);
+
+ // Flags...
+ ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox);
+ ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox);
+ qDebug() << flags;
+
+ // Select the first entry in the list.
+ ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0));
+
+ connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)),
+ SLOT(on_buttonBox_accepted()));
+}
+
+ProfileSelectDialog::~ProfileSelectDialog()
+{
+ delete ui;
+}
+
+MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const
+{
+ return m_selected;
+}
+
+bool ProfileSelectDialog::useAsGlobalDefault() const
+{
+ return ui->globalDefaultCheck->isChecked();
+}
+
+bool ProfileSelectDialog::useAsInstDefaullt() const
+{
+ return ui->instDefaultCheck->isChecked();
+}
+
+void ProfileSelectDialog::on_buttonBox_accepted()
+{
+ QModelIndexList selection =
+ ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0) {
+ QModelIndex selected = selection.first();
+ m_selected = selected.data(AccountList::PointerRole)
+ .value<MinecraftAccountPtr>();
+ }
+ close();
+}
+
+void ProfileSelectDialog::on_buttonBox_rejected()
+{
+ close();
+}
diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h
new file mode 100644
index 0000000000..f121dfcc15
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h
@@ -0,0 +1,115 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <memory>
+
+#include "minecraft/auth/AccountList.h"
+
+namespace Ui
+{
+ class ProfileSelectDialog;
+}
+
+class ProfileSelectDialog : public QDialog
+{
+ Q_OBJECT
+ public:
+ enum Flags {
+ NoFlags = 0,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that
+ * the account they've selected should be used as the global default for
+ * all instances.
+ */
+ GlobalDefaultCheckbox,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that
+ * the account they've selected should be used as the default for the
+ * instance they are currently launching. This is not currently
+ * implemented.
+ */
+ InstanceDefaultCheckbox,
+ };
+
+ /*!
+ * Constructs a new account select dialog with the given parent and message.
+ * The message will be shown at the top of the dialog. It is an empty string
+ * by default.
+ */
+ explicit ProfileSelectDialog(const QString& message = "", int flags = 0,
+ QWidget* parent = 0);
+ ~ProfileSelectDialog();
+
+ /*!
+ * Gets a pointer to the account that the user selected.
+ * This is null if the user clicked cancel or hasn't clicked OK yet.
+ */
+ MinecraftAccountPtr selectedAccount() const;
+
+ /*!
+ * Returns true if the user checked the "use as global default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsGlobalDefault() const;
+
+ /*!
+ * Returns true if the user checked the "use as instance default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsInstDefaullt() const;
+
+ public slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+ protected:
+ shared_qobject_ptr<AccountList> m_accounts;
+
+ //! The account that was selected when the user clicked OK.
+ MinecraftAccountPtr m_selected;
+
+ private:
+ Ui::ProfileSelectDialog* ui;
+};
diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui
new file mode 100644
index 0000000000..e779b51bf1
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProfileSelectDialog</class>
+ <widget class="QDialog" name="ProfileSelectDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>465</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select an Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="msgLabel">
+ <property name="text">
+ <string>Select a profile.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTreeWidget" name="listView">
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="globalDefaultCheck">
+ <property name="text">
+ <string>Use as default?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="instDefaultCheck">
+ <property name="text">
+ <string>Use as default for this instance only?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp
new file mode 100644
index 0000000000..9798af8199
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp
@@ -0,0 +1,298 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProfileSetupDialog.h"
+#include "ui_ProfileSetupDialog.h"
+
+#include <QPushButton>
+#include <QAction>
+#include <QRegularExpressionValidator>
+#include <QJsonDocument>
+#include <QDebug>
+
+#include "ui/dialogs/ProgressDialog.h"
+
+#include <Application.h>
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup,
+ QWidget* parent)
+ : QDialog(parent), m_accountToSetup(accountToSetup),
+ ui(new Ui::ProfileSetupDialog)
+{
+ ui->setupUi(this);
+ ui->errorLabel->setVisible(false);
+
+ goodIcon = APPLICATION->getThemedIcon("status-good");
+ yellowIcon = APPLICATION->getThemedIcon("status-yellow");
+ badIcon = APPLICATION->getThemedIcon("status-bad");
+
+ QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}");
+ auto nameEdit = ui->nameEdit;
+ nameEdit->setValidator(new QRegularExpressionValidator(permittedNames));
+ nameEdit->setClearButtonEnabled(true);
+ validityAction =
+ nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition);
+ connect(nameEdit, &QLineEdit::textEdited, this,
+ &ProfileSetupDialog::nameEdited);
+
+ checkStartTimer.setSingleShot(true);
+ connect(&checkStartTimer, &QTimer::timeout, this,
+ &ProfileSetupDialog::startCheck);
+
+ setNameStatus(NameStatus::NotSet, QString());
+}
+
+ProfileSetupDialog::~ProfileSetupDialog()
+{
+ delete ui;
+}
+
+void ProfileSetupDialog::on_buttonBox_accepted()
+{
+ setupProfile(currentCheck);
+}
+
+void ProfileSetupDialog::on_buttonBox_rejected()
+{
+ reject();
+}
+
+void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status,
+ QString errorString = QString())
+{
+ nameStatus = status;
+ auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ switch (nameStatus) {
+ case NameStatus::Available: {
+ validityAction->setIcon(goodIcon);
+ okButton->setEnabled(true);
+ } break;
+ case NameStatus::NotSet:
+ case NameStatus::Pending:
+ validityAction->setIcon(yellowIcon);
+ okButton->setEnabled(false);
+ break;
+ case NameStatus::Exists:
+ case NameStatus::Error:
+ validityAction->setIcon(badIcon);
+ okButton->setEnabled(false);
+ break;
+ }
+ if (!errorString.isEmpty()) {
+ ui->errorLabel->setText(errorString);
+ ui->errorLabel->setVisible(true);
+ } else {
+ ui->errorLabel->setVisible(false);
+ }
+}
+
+void ProfileSetupDialog::nameEdited(const QString& name)
+{
+ if (!ui->nameEdit->hasAcceptableInput()) {
+ setNameStatus(NameStatus::NotSet,
+ tr("Name is too short - must be between 3 and 16 "
+ "characters long."));
+ return;
+ }
+ scheduleCheck(name);
+}
+
+void ProfileSetupDialog::scheduleCheck(const QString& name)
+{
+ queuedCheck = name;
+ setNameStatus(NameStatus::Pending);
+ checkStartTimer.start(1000);
+}
+
+void ProfileSetupDialog::startCheck()
+{
+ if (isChecking) {
+ return;
+ }
+ if (queuedCheck.isNull()) {
+ return;
+ }
+ checkName(queuedCheck);
+}
+
+void ProfileSetupDialog::checkName(const QString& name)
+{
+ if (isChecking) {
+ return;
+ }
+
+ currentCheck = name;
+ isChecking = true;
+
+ auto token = m_accountToSetup->accessToken();
+
+ auto url = QString("https://api.minecraftservices.com/minecraft/profile/"
+ "name/%1/available")
+ .arg(name);
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(token).toUtf8());
+
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &ProfileSetupDialog::checkFinished);
+ requestor->get(request);
+}
+
+void ProfileSetupDialog::checkFinished(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error == QNetworkReply::NoError) {
+ auto doc = QJsonDocument::fromJson(data);
+ auto root = doc.object();
+ auto statusValue = root.value("status").toString("INVALID");
+ if (statusValue == "AVAILABLE") {
+ setNameStatus(NameStatus::Available);
+ } else if (statusValue == "DUPLICATE") {
+ setNameStatus(NameStatus::Exists,
+ tr("Minecraft profile with name %1 already exists.")
+ .arg(currentCheck));
+ } else if (statusValue == "NOT_ALLOWED") {
+ setNameStatus(NameStatus::Exists,
+ tr("The name %1 is not allowed.").arg(currentCheck));
+ } else {
+ setNameStatus(
+ NameStatus::Error,
+ tr("Unhandled profile name status: %1").arg(statusValue));
+ }
+ } else {
+ setNameStatus(NameStatus::Error,
+ tr("Failed to check name availability."));
+ }
+ isChecking = false;
+}
+
+void ProfileSetupDialog::setupProfile(const QString& profileName)
+{
+ if (isWorking) {
+ return;
+ }
+
+ auto token = m_accountToSetup->accessToken();
+
+ auto url = QString("https://api.minecraftservices.com/minecraft/profile");
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(token).toUtf8());
+
+ QString payloadTemplate("{\"profileName\":\"%1\"}");
+ auto data = payloadTemplate.arg(profileName).toUtf8();
+
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &ProfileSetupDialog::setupProfileFinished);
+ requestor->post(request, data);
+ isWorking = true;
+
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(false);
+}
+
+namespace
+{
+
+ struct MojangError {
+ static MojangError fromJSON(QByteArray data)
+ {
+ MojangError out;
+ out.error = QString::fromUtf8(data);
+ auto doc = QJsonDocument::fromJson(data, &out.parseError);
+ auto object = doc.object();
+
+ out.fullyParsed = true;
+ out.fullyParsed &=
+ Parsers::getString(object.value("path"), out.path);
+ out.fullyParsed &=
+ Parsers::getString(object.value("error"), out.error);
+ out.fullyParsed &= Parsers::getString(object.value("errorMessage"),
+ out.errorMessage);
+
+ return out;
+ }
+
+ QString rawError;
+ QJsonParseError parseError;
+ bool fullyParsed;
+
+ QString path;
+ QString error;
+ QString errorMessage;
+ };
+
+} // namespace
+
+void ProfileSetupDialog::setupProfileFinished(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ isWorking = false;
+ if (error == QNetworkReply::NoError) {
+ /*
+ * data contains the profile in the response
+ * ... we could parse it and update the account, but let's just return
+ * back to the normal login flow instead...
+ */
+ accept();
+ } else {
+ auto parsedError = MojangError::fromJSON(data);
+ ui->errorLabel->setVisible(true);
+ ui->errorLabel->setText(tr("The server returned the following error:") +
+ "\n\n" + parsedError.errorMessage);
+ qDebug() << parsedError.rawError;
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(true);
+ }
+}
diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h
new file mode 100644
index 0000000000..44cf3d8930
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h
@@ -0,0 +1,105 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QIcon>
+#include <QTimer>
+#include <QNetworkReply>
+
+#include <memory>
+#include <minecraft/auth/MinecraftAccount.h>
+
+namespace Ui
+{
+ class ProfileSetupDialog;
+}
+
+class ProfileSetupDialog : public QDialog
+{
+ Q_OBJECT
+ public:
+ explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup,
+ QWidget* parent = 0);
+ ~ProfileSetupDialog();
+
+ enum class NameStatus {
+ NotSet,
+ Pending,
+ Available,
+ Exists,
+ Error
+ } nameStatus = NameStatus::NotSet;
+
+ private slots:
+ void on_buttonBox_accepted();
+ void on_buttonBox_rejected();
+
+ void nameEdited(const QString& name);
+ void checkFinished(QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers);
+ void startCheck();
+
+ void setupProfileFinished(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers);
+
+ protected:
+ void scheduleCheck(const QString& name);
+ void checkName(const QString& name);
+ void setNameStatus(NameStatus status, QString errorString);
+
+ void setupProfile(const QString& profileName);
+
+ private:
+ MinecraftAccountPtr m_accountToSetup;
+ Ui::ProfileSetupDialog* ui;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+ QAction* validityAction = nullptr;
+
+ QString queuedCheck;
+
+ bool isChecking = false;
+ bool isWorking = false;
+ QString currentCheck;
+
+ QTimer checkStartTimer;
+};
diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui
new file mode 100644
index 0000000000..9dbabb4b3e
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProfileSetupDialog</class>
+ <widget class="QDialog" name="ProfileSetupDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>615</width>
+ <height>208</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose Minecraft name</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>You just need to take one more step to be able to play Minecraft on this account.
+
+Choose your name carefully:</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="buddy">
+ <cstring>nameEdit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLineEdit" name="nameEdit"/>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="errorLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string notr="true">Errors go here</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>nameEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.cpp b/meshmc/launcher/ui/dialogs/ProgressDialog.cpp
new file mode 100644
index 0000000000..61109946bc
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProgressDialog.cpp
@@ -0,0 +1,201 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProgressDialog.h"
+#include "ui_ProgressDialog.h"
+
+#include <QKeyEvent>
+#include <QDebug>
+
+#include "tasks/Task.h"
+
+ProgressDialog::ProgressDialog(QWidget* parent)
+ : QDialog(parent), ui(new Ui::ProgressDialog)
+{
+ ui->setupUi(this);
+ this->setWindowFlags(this->windowFlags() &
+ ~Qt::WindowContextHelpButtonHint);
+ setSkipButton(false);
+ changeProgress(0, 100);
+}
+
+void ProgressDialog::setSkipButton(bool present, QString label)
+{
+ ui->skipButton->setAutoDefault(false);
+ ui->skipButton->setDefault(false);
+ ui->skipButton->setFocusPolicy(Qt::ClickFocus);
+ ui->skipButton->setEnabled(present);
+ ui->skipButton->setVisible(present);
+ ui->skipButton->setText(label);
+ updateSize();
+}
+
+void ProgressDialog::on_skipButton_clicked(bool checked)
+{
+ Q_UNUSED(checked);
+ task->abort();
+}
+
+ProgressDialog::~ProgressDialog()
+{
+ delete ui;
+}
+
+void ProgressDialog::updateSize()
+{
+ QSize qSize = QSize(480, minimumSizeHint().height());
+ resize(qSize);
+ setFixedSize(qSize);
+}
+
+int ProgressDialog::execWithTask(Task* task)
+{
+ this->task = task;
+ QDialog::DialogCode result;
+
+ if (!task) {
+ qDebug() << "Programmer error: progress dialog created with null task.";
+ return Accepted;
+ }
+
+ if (handleImmediateResult(result)) {
+ return result;
+ }
+
+ // Connect signals.
+ connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
+ connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
+ connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
+ connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&)));
+ connect(task, SIGNAL(progress(qint64, qint64)),
+ SLOT(changeProgress(qint64, qint64)));
+
+ // if this didn't connect to an already running task, invoke start
+ if (!task->isRunning()) {
+ task->start();
+ }
+ if (task->isRunning()) {
+ changeProgress(task->getProgress(), task->getTotalProgress());
+ changeStatus(task->getStatus());
+ return QDialog::exec();
+ } else if (handleImmediateResult(result)) {
+ return result;
+ } else {
+ return QDialog::Rejected;
+ }
+}
+
+// TODO: only provide the unique_ptr overloads
+int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+int ProgressDialog::execWithTask(std::unique_ptr<Task>& task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+
+bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result)
+{
+ if (task->isFinished()) {
+ if (task->wasSuccessful()) {
+ result = QDialog::Accepted;
+ } else {
+ result = QDialog::Rejected;
+ }
+ return true;
+ }
+ return false;
+}
+
+Task* ProgressDialog::getTask()
+{
+ return task;
+}
+
+void ProgressDialog::onTaskStarted() {}
+
+void ProgressDialog::onTaskFailed(QString failure)
+{
+ reject();
+}
+
+void ProgressDialog::onTaskSucceeded()
+{
+ accept();
+}
+
+void ProgressDialog::changeStatus(const QString& status)
+{
+ ui->statusLabel->setText(status);
+ updateSize();
+}
+
+void ProgressDialog::changeProgress(qint64 current, qint64 total)
+{
+ ui->taskProgressBar->setMaximum(total);
+ ui->taskProgressBar->setValue(current);
+}
+
+void ProgressDialog::keyPressEvent(QKeyEvent* e)
+{
+ if (ui->skipButton->isVisible()) {
+ if (e->key() == Qt::Key_Escape) {
+ on_skipButton_clicked(true);
+ return;
+ } else if (e->key() == Qt::Key_Tab) {
+ ui->skipButton->setFocusPolicy(Qt::StrongFocus);
+ ui->skipButton->setFocus();
+ ui->skipButton->setAutoDefault(true);
+ ui->skipButton->setDefault(true);
+ return;
+ }
+ }
+ QDialog::keyPressEvent(e);
+}
+
+void ProgressDialog::closeEvent(QCloseEvent* e)
+{
+ if (task && task->isRunning()) {
+ e->ignore();
+ } else {
+ QDialog::closeEvent(e);
+ }
+}
diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.h b/meshmc/launcher/ui/dialogs/ProgressDialog.h
new file mode 100644
index 0000000000..38d4454a26
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProgressDialog.h
@@ -0,0 +1,91 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <memory>
+
+class Task;
+
+namespace Ui
+{
+ class ProgressDialog;
+}
+
+class ProgressDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ProgressDialog(QWidget* parent = 0);
+ ~ProgressDialog();
+
+ void updateSize();
+
+ int execWithTask(Task* task);
+ int execWithTask(std::unique_ptr<Task>&& task);
+ int execWithTask(std::unique_ptr<Task>& task);
+
+ void setSkipButton(bool present, QString label = QString());
+
+ Task* getTask();
+
+ public slots:
+ void onTaskStarted();
+ void onTaskFailed(QString failure);
+ void onTaskSucceeded();
+
+ void changeStatus(const QString& status);
+ void changeProgress(qint64 current, qint64 total);
+
+ private slots:
+ void on_skipButton_clicked(bool checked);
+
+ protected:
+ virtual void keyPressEvent(QKeyEvent* e);
+ virtual void closeEvent(QCloseEvent* e);
+
+ private:
+ bool handleImmediateResult(QDialog::DialogCode& result);
+
+ private:
+ Ui::ProgressDialog* ui;
+
+ Task* task;
+};
diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.ui b/meshmc/launcher/ui/dialogs/ProgressDialog.ui
new file mode 100644
index 0000000000..04b8fef33a
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/ProgressDialog.ui
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProgressDialog</class>
+ <widget class="QDialog" name="ProgressDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>100</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>400</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>600</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Please wait...</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="statusLabel">
+ <property name="text">
+ <string>Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QProgressBar" name="taskProgressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="skipButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Skip</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp b/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp
new file mode 100644
index 0000000000..414e0acaf0
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -0,0 +1,163 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QFileInfo>
+#include <QFileDialog>
+#include <QPainter>
+#include <QRegularExpression>
+
+#include <FileSystem.h>
+
+#include <minecraft/services/SkinUpload.h>
+#include <minecraft/services/CapeChange.h>
+#include <tasks/SequentialTask.h>
+
+#include "SkinUploadDialog.h"
+#include "ui_SkinUploadDialog.h"
+#include "ProgressDialog.h"
+#include "CustomMessageBox.h"
+
+void SkinUploadDialog::on_buttonBox_rejected()
+{
+ close();
+}
+
+void SkinUploadDialog::on_buttonBox_accepted()
+{
+ QString fileName;
+ QString input = ui->skinPathTextBox->text();
+ QRegularExpression urlPrefixMatcher("^([a-z]+)://.+$");
+ bool isLocalFile = false;
+ // it has an URL prefix -> it is an URL
+ if (urlPrefixMatcher.match(input).hasMatch()) {
+ QUrl fileURL = input;
+ if (fileURL.isValid()) {
+ // local?
+ if (fileURL.isLocalFile()) {
+ isLocalFile = true;
+ fileName = fileURL.toLocalFile();
+ } else {
+ CustomMessageBox::selectable(
+ this, tr("Skin Upload"),
+ tr("Using remote URLs for setting skins is not implemented "
+ "yet."),
+ QMessageBox::Warning)
+ ->exec();
+ close();
+ return;
+ }
+ } else {
+ CustomMessageBox::selectable(
+ this, tr("Skin Upload"),
+ tr("You cannot use an invalid URL for uploading skins."),
+ QMessageBox::Warning)
+ ->exec();
+ close();
+ return;
+ }
+ } else {
+ // just assume it's a path then
+ isLocalFile = true;
+ fileName = ui->skinPathTextBox->text();
+ }
+ if (isLocalFile && !QFile::exists(fileName)) {
+ CustomMessageBox::selectable(this, tr("Skin Upload"),
+ tr("Skin file does not exist!"),
+ QMessageBox::Warning)
+ ->exec();
+ close();
+ return;
+ }
+ SkinUpload::Model model = SkinUpload::STEVE;
+ if (ui->steveBtn->isChecked()) {
+ model = SkinUpload::STEVE;
+ } else if (ui->alexBtn->isChecked()) {
+ model = SkinUpload::ALEX;
+ }
+ ProgressDialog prog(this);
+ SequentialTask skinUpload;
+ skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(
+ this, m_acct->accessToken(), FS::read(fileName), model)));
+ auto selectedCape = ui->capeCombo->currentData().toString();
+ if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
+ skinUpload.addTask(shared_qobject_ptr<CapeChange>(
+ new CapeChange(this, m_acct->accessToken(), selectedCape)));
+ }
+ if (prog.execWithTask(&skinUpload) != QDialog::Accepted) {
+ CustomMessageBox::selectable(this, tr("Skin Upload"),
+ tr("Failed to upload skin!"),
+ QMessageBox::Warning)
+ ->exec();
+ close();
+ return;
+ }
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"),
+ QMessageBox::Information)
+ ->exec();
+ close();
+}
+
+void SkinUploadDialog::on_skinBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(
+ this, tr("Select Skin Texture"), QString(), "*.png");
+ if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ ui->skinPathTextBox->setText(cooked_path);
+}
+
+SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent)
+ : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
+{
+ ui->setupUi(this);
+
+ // FIXME: add a model for this, download/refresh the capes on demand
+ auto& data = *acct->accountData();
+ int index = 0;
+ ui->capeCombo->addItem(tr("No Cape"), QVariant());
+ auto currentCape = data.minecraftProfile.currentCape;
+ if (currentCape.isEmpty()) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+
+ for (auto& cape : data.minecraftProfile.capes) {
+ index++;
+ if (cape.data.size()) {
+ QPixmap capeImage;
+ if (capeImage.loadFromData(cape.data, "PNG")) {
+ QPixmap preview = QPixmap(10, 16);
+ QPainter painter(&preview);
+ painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
+ ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
+ if (currentCape == cape.id) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+ continue;
+ }
+ }
+ ui->capeCombo->addItem(cape.alias, cape.id);
+ if (currentCape == cape.id) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+ }
+}
diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.h b/meshmc/launcher/ui/dialogs/SkinUploadDialog.h
new file mode 100644
index 0000000000..3c9b7aceb2
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.h
@@ -0,0 +1,51 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <minecraft/auth/MinecraftAccount.h>
+
+namespace Ui
+{
+ class SkinUploadDialog;
+}
+
+class SkinUploadDialog : public QDialog
+{
+ Q_OBJECT
+ public:
+ explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0);
+ virtual ~SkinUploadDialog() {};
+
+ public slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+ void on_skinBrowseBtn_clicked();
+
+ protected:
+ MinecraftAccountPtr m_acct;
+
+ private:
+ Ui::SkinUploadDialog* ui;
+};
diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui b/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui
new file mode 100644
index 0000000000..f4b0ed0aa7
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SkinUploadDialog</class>
+ <widget class="QDialog" name="SkinUploadDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>394</width>
+ <height>360</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Skin Upload</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="fileBox">
+ <property name="title">
+ <string>Skin File</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLineEdit" name="skinPathTextBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skinBrowseBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>28</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="modelBox">
+ <property name="title">
+ <string>Player Model</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_1">
+ <item>
+ <widget class="QRadioButton" name="steveBtn">
+ <property name="text">
+ <string>Steve Model</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="alexBtn">
+ <property name="text">
+ <string>Alex Model</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="capeBox">
+ <property name="title">
+ <string>Cape</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QComboBox" name="capeCombo"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.cpp b/meshmc/launcher/ui/dialogs/UpdateDialog.cpp
new file mode 100644
index 0000000000..74838fc8be
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/UpdateDialog.cpp
@@ -0,0 +1,77 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "UpdateDialog.h"
+#include "ui_UpdateDialog.h"
+#include "Application.h"
+#include "BuildConfig.h"
+
+UpdateDialog::UpdateDialog(bool hasUpdate, const UpdateAvailableStatus& status,
+ QWidget* parent)
+ : QDialog(parent), ui(new Ui::UpdateDialog)
+{
+ ui->setupUi(this);
+
+ if (hasUpdate) {
+ ui->label->setText(
+ tr("<b>%1 %2</b> is available!")
+ .arg(BuildConfig.MESHMC_DISPLAYNAME, status.version));
+
+ if (!status.releaseNotes.isEmpty()) {
+ ui->changelogBrowser->setHtml(status.releaseNotes);
+ } else {
+ ui->changelogBrowser->setHtml(
+ tr("<center><p>No release notes available.</p></center>"));
+ }
+ } else {
+ ui->label->setText(tr("You are running the latest version of %1.")
+ .arg(BuildConfig.MESHMC_DISPLAYNAME));
+ ui->changelogBrowser->setHtml(
+ tr("<center><p>No updates found.</p></center>"));
+ ui->btnUpdateNow->setHidden(true);
+ ui->btnUpdateLater->setText(tr("Close"));
+ }
+
+ restoreGeometry(QByteArray::fromBase64(
+ APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray()));
+}
+
+UpdateDialog::~UpdateDialog()
+{
+ delete ui;
+}
+
+void UpdateDialog::on_btnUpdateLater_clicked()
+{
+ reject();
+}
+
+void UpdateDialog::on_btnUpdateNow_clicked()
+{
+ done(UPDATE_NOW);
+}
+
+void UpdateDialog::closeEvent(QCloseEvent* evt)
+{
+ APPLICATION->settings()->set("UpdateDialogGeometry",
+ saveGeometry().toBase64());
+ QDialog::closeEvent(evt);
+}
diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.h b/meshmc/launcher/ui/dialogs/UpdateDialog.h
new file mode 100644
index 0000000000..1bc1775710
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/UpdateDialog.h
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "updater/UpdateChecker.h"
+
+namespace Ui
+{
+ class UpdateDialog;
+}
+
+enum UpdateAction {
+ UPDATE_LATER = QDialog::Rejected,
+ UPDATE_NOW = QDialog::Accepted,
+};
+
+class UpdateDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ /*!
+ * Constructs the update dialog.
+ * \a hasUpdate - true when an update is available (shows "Update now"
+ * button).
+ * \a status - update information (version, release notes); ignored
+ * when hasUpdate is false.
+ */
+ explicit UpdateDialog(bool hasUpdate,
+ const UpdateAvailableStatus& status = {},
+ QWidget* parent = nullptr);
+ ~UpdateDialog();
+
+ public slots:
+ void on_btnUpdateNow_clicked();
+ void on_btnUpdateLater_clicked();
+
+ protected:
+ void closeEvent(QCloseEvent*) override;
+
+ private:
+ Ui::UpdateDialog* ui;
+};
diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.ui b/meshmc/launcher/ui/dialogs/UpdateDialog.ui
new file mode 100644
index 0000000000..ed895883c9
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/UpdateDialog.ui
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateDialog</class>
+ <widget class="QDialog" name="UpdateDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>657</width>
+ <height>673</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MeshMC Update</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <pointsize>14</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>changelogBrowser</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="changelogBrowser">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QPushButton" name="btnUpdateNow">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Update now</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="btnUpdateLater">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Don't update yet</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>changelogBrowser</tabstop>
+ <tabstop>btnUpdateNow</tabstop>
+ <tabstop>btnUpdateLater</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../resources/multimc/multimc.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp b/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp
new file mode 100644
index 0000000000..d8ef9b1245
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp
@@ -0,0 +1,166 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "VersionSelectDialog.h"
+
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QDialogButtonBox>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QPushButton>
+#include <QtWidgets/QVBoxLayout>
+#include <QDebug>
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/widgets/VersionSelectWidget.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "Application.h"
+#include "VersionProxyModel.h"
+
+VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title,
+ QWidget* parent, bool cancelable)
+ : QDialog(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectDialog"));
+ resize(400, 347);
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(parent);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+
+ m_refreshButton = new QPushButton(this);
+ m_refreshButton->setObjectName(QStringLiteral("refreshButton"));
+ m_horizontalLayout->addWidget(m_refreshButton);
+
+ m_buttonBox = new QDialogButtonBox(this);
+ m_buttonBox->setObjectName(QStringLiteral("buttonBox"));
+ m_buttonBox->setOrientation(Qt::Horizontal);
+ m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel |
+ QDialogButtonBox::Ok);
+ m_horizontalLayout->addWidget(m_buttonBox);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ retranslate();
+
+ QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+ QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+
+ QMetaObject::connectSlotsByName(this);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle(title);
+
+ m_vlist = vlist;
+
+ if (!cancelable) {
+ m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+ }
+}
+
+void VersionSelectDialog::retranslate()
+{
+ // FIXME: overrides custom title given in constructor!
+ setWindowTitle(tr("Choose Version"));
+ m_refreshButton->setToolTip(tr("Reloads the version list."));
+ m_refreshButton->setText(tr("&Refresh"));
+}
+
+void VersionSelectDialog::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_versionWidget->setCurrentVersion(version);
+}
+
+void VersionSelectDialog::setEmptyString(QString emptyString)
+{
+ m_versionWidget->setEmptyString(emptyString);
+}
+
+void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString)
+{
+ m_versionWidget->setEmptyErrorString(emptyErrorString);
+}
+
+void VersionSelectDialog::setResizeOn(int column)
+{
+ resizeOnColumn = column;
+}
+
+int VersionSelectDialog::exec()
+{
+ QDialog::open();
+ m_versionWidget->initialize(m_vlist);
+ if (resizeOnColumn != -1) {
+ m_versionWidget->setResizeOn(resizeOnColumn);
+ }
+ return QDialog::exec();
+}
+
+void VersionSelectDialog::selectRecommended()
+{
+ m_versionWidget->selectRecommended();
+}
+
+BaseVersionPtr VersionSelectDialog::selectedVersion() const
+{
+ return m_versionWidget->selectedVersion();
+}
+
+void VersionSelectDialog::on_refreshButton_clicked()
+{
+ m_versionWidget->loadList();
+}
+
+void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role,
+ QString filter)
+{
+ m_versionWidget->setExactFilter(role, filter);
+}
+
+void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role,
+ QString filter)
+{
+ m_versionWidget->setFuzzyFilter(role, filter);
+}
diff --git a/meshmc/launcher/ui/dialogs/VersionSelectDialog.h b/meshmc/launcher/ui/dialogs/VersionSelectDialog.h
new file mode 100644
index 0000000000..8db01cc62c
--- /dev/null
+++ b/meshmc/launcher/ui/dialogs/VersionSelectDialog.h
@@ -0,0 +1,101 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QSortFilterProxyModel>
+
+#include "BaseVersionList.h"
+
+class QVBoxLayout;
+class QHBoxLayout;
+class QDialogButtonBox;
+class VersionSelectWidget;
+class QPushButton;
+
+namespace Ui
+{
+ class VersionSelectDialog;
+}
+
+class VersionProxyModel;
+
+class VersionSelectDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit VersionSelectDialog(BaseVersionList* vlist, QString title,
+ QWidget* parent = 0, bool cancelable = true);
+ virtual ~VersionSelectDialog() {};
+
+ int exec() override;
+
+ BaseVersionPtr selectedVersion() const;
+
+ void setCurrentVersion(const QString& version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+ private slots:
+ void on_refreshButton_clicked();
+
+ private:
+ void retranslate();
+ void selectRecommended();
+
+ private:
+ QString m_currentVersion;
+ VersionSelectWidget* m_versionWidget = nullptr;
+ QVBoxLayout* m_verticalLayout = nullptr;
+ QHBoxLayout* m_horizontalLayout = nullptr;
+ QPushButton* m_refreshButton = nullptr;
+ QDialogButtonBox* m_buttonBox = nullptr;
+
+ BaseVersionList* m_vlist = nullptr;
+
+ VersionProxyModel* m_proxyModel = nullptr;
+
+ int resizeOnColumn = -1;
+
+ Task* loadTask = nullptr;
+};