summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/ui/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/ui/dialogs')
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp197
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h57
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui390
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp330
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h63
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui228
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp530
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h129
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui205
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp118
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h82
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui89
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp353
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h102
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui447
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp264
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h83
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui264
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp63
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h51
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp240
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h99
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui83
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp284
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h79
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui267
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp264
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h84
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui276
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp217
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h76
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui67
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp101
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h52
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui81
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp233
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h72
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp79
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h50
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp294
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h78
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui429
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp154
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h72
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui101
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp381
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h131
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui91
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp116
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h57
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui130
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp133
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h62
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui80
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp143
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h111
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui62
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp345
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h109
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui77
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp361
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h134
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui144
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp572
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h278
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp659
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h98
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp188
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h66
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui74
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp41
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h43
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui84
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp115
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h83
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui155
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp190
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h107
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp715
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h100
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui223
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp342
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h85
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp238
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h75
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp374
-rw-r--r--archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h109
87 files changed, 15558 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp
new file mode 100644
index 0000000000..48c77aa492
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QIcon>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Markdown.h"
+#include "ScrollMessageBox.h"
+#include "StringUtils.h"
+#include "ui_AboutDialog.h"
+
+#include <net/NetJob.h>
+#include <qobject.h>
+#include <QPixmap>
+
+namespace
+{
+ QString getCreditsHtml()
+ {
+ QFile dataFile(":/documents/credits.html");
+ if (!dataFile.open(QIODevice::ReadOnly))
+ {
+ qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!";
+ return QString();
+ }
+
+ QString fileContent = QString::fromUtf8(dataFile.readAll());
+
+ return fileContent.arg(QObject::tr("%1 Developers").arg(BuildConfig.LAUNCHER_DISPLAYNAME),
+ QObject::tr("Prism Launcher Developers"),
+ QObject::tr("MultiMC Developers"),
+ QObject::tr("With special thanks to"));
+ }
+
+ QString getLicenseHtml()
+ {
+ QFile dataFile(":/documents/COPYING");
+ if (dataFile.open(QIODevice::ReadOnly))
+ {
+ QString output = markdownToHTML(dataFile.readAll());
+ dataFile.close();
+ return output;
+ }
+ else
+ {
+ qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!";
+ return QString();
+ }
+ }
+
+ QString getManifestoHtml()
+ {
+ QFile dataFile(":/documents/manifesto.md");
+ if (dataFile.open(QIODevice::ReadOnly))
+ {
+ QString output = markdownToHTML(dataFile.readAll());
+ dataFile.close();
+ return StringUtils::htmlListPatch(output);
+ }
+ else
+ {
+ qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!";
+ return QString();
+ }
+ }
+
+} // namespace
+
+AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+
+ QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME;
+
+ setWindowTitle(tr("About %1").arg(launcherName));
+
+ QString chtml = getCreditsHtml();
+ ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
+
+ QString lhtml = getLicenseHtml();
+ ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
+
+ ui->urlLabel->setOpenExternalLinks(true);
+
+ ui->icon->setPixmap(APPLICATION->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.LAUNCHER_GIT));
+
+ ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT);
+ ui->licenseBadgeTextLabel->setText(tr("This project is licensed under"));
+ ui->licenseBadgeLabel->setPixmap(QPixmap(QStringLiteral(":/gplv3-127x51.png")));
+
+ connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close);
+
+ const QString manifestoHtml = getManifestoHtml();
+ if (!manifestoHtml.isEmpty())
+ {
+ connect(ui->aboutProjectTick,
+ &QPushButton::clicked,
+ this,
+ [this, manifestoHtml]
+ {
+ ScrollMessageBox dialog(this, tr("About Project Tick"), tr("Project Tick Overview"), manifestoHtml);
+ dialog.exec();
+ });
+ }
+ else
+ {
+ ui->aboutProjectTick->setEnabled(false);
+ }
+}
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h
new file mode 100644
index 0000000000..4ccf8acc54
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit AboutDialog(QWidget* parent = 0);
+ ~AboutDialog();
+
+ private:
+ Ui::AboutDialog* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui
new file mode 100644
index 0000000000..a1ed29cbfc
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui
@@ -0,0 +1,390 @@
+<?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">Launcher</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="QLabel" name="licenseBadgeTextLabel">
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>This project is licensed under</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="licenseBadgeLabel">
+ <property name="minimumSize">
+ <size>
+ <width>127</width>
+ <height>51</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>127</width>
+ <height>51</height>
+ </size>
+ </property>
+ <property name="text">
+ <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 alignment="Qt::AlignHCenter">
+ <widget class="QLabel" name="qtPoweredLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>ProjT Launcher is powered by the Qt framework.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>false</bool>
+ </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="aboutProjectTick">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>About Project Tick</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>aboutProjectTick</tabstop>
+ <tabstop>closeButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp
new file mode 100644
index 0000000000..34bb087bdf
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "BackupDialog.h"
+#include <QInputDialog>
+#include <QMessageBox>
+#include "ui_BackupDialog.h"
+
+BackupDialog::BackupDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent),
+ ui(new Ui::BackupDialog),
+ m_instance(instance),
+ m_backupManager(new BackupManager(this))
+{
+ ui->setupUi(this);
+
+ setWindowTitle(tr("Manage Backups - %1").arg(instance->name()));
+
+ // Connect signals
+ connect(m_backupManager, &BackupManager::backupCreated, this, &BackupDialog::onBackupCreated);
+ connect(m_backupManager, &BackupManager::backupRestored, this, &BackupDialog::onBackupRestored);
+
+ // Load backups
+ refreshBackupList();
+}
+
+BackupDialog::~BackupDialog()
+{
+ delete ui;
+}
+
+void BackupDialog::refreshBackupList()
+{
+ ui->backupList->clear();
+ m_backups = m_backupManager->listBackups(m_instance);
+
+ for (const InstanceBackup& backup : m_backups)
+ {
+ QString displayText = QString("%1 - %2 (%3)")
+ .arg(backup.name())
+ .arg(backup.createdAt().toString("yyyy-MM-dd HH:mm"))
+ .arg(backup.displaySize());
+ ui->backupList->addItem(displayText);
+ }
+
+ updateButtons();
+}
+
+void BackupDialog::updateBackupDetails()
+{
+ int currentRow = ui->backupList->currentRow();
+ if (currentRow < 0 || currentRow >= m_backups.size())
+ {
+ ui->backupDetails->clear();
+ return;
+ }
+
+ const InstanceBackup& backup = m_backups[currentRow];
+
+ QString details;
+ details += tr("<b>Name:</b> %1<br>").arg(backup.name());
+ details += tr("<b>Created:</b> %1<br>").arg(backup.createdAt().toString("yyyy-MM-dd HH:mm:ss"));
+ details += tr("<b>Size:</b> %1<br>").arg(backup.displaySize());
+
+ if (!backup.description().isEmpty())
+ {
+ details += tr("<b>Description:</b> %1<br>").arg(backup.description());
+ }
+
+ if (!backup.includedPaths().isEmpty())
+ {
+ details += tr("<b>Included:</b> %1").arg(backup.includedPaths().join(", "));
+ }
+
+ ui->backupDetails->setHtml(details);
+}
+
+void BackupDialog::updateButtons()
+{
+ bool hasSelection = ui->backupList->currentRow() >= 0;
+ ui->restoreButton->setEnabled(hasSelection);
+ ui->deleteButton->setEnabled(hasSelection);
+}
+
+void BackupDialog::on_createButton_clicked()
+{
+ bool ok;
+ QString backupName =
+ QInputDialog::getText(this, tr("Create Backup"), tr("Backup name:"), QLineEdit::Normal, QString(), &ok);
+
+ if (!ok)
+ {
+ return;
+ }
+
+ BackupOptions options = getSelectedOptions();
+
+ // Disable UI during backup
+ ui->createButton->setEnabled(false);
+ ui->restoreButton->setEnabled(false);
+ ui->deleteButton->setEnabled(false);
+ ui->createButton->setText(tr("Creating..."));
+
+ // Connect signals for this operation
+ connect(
+ m_backupManager,
+ &BackupManager::backupCreated,
+ this,
+ [this](const QString&, const QString&)
+ {
+ ui->createButton->setEnabled(true);
+ ui->restoreButton->setEnabled(true);
+ ui->deleteButton->setEnabled(true);
+ ui->createButton->setText(tr("Create Backup"));
+ QMessageBox::information(this, tr("Success"), tr("Backup created successfully!"));
+ refreshBackupList();
+ disconnect(m_backupManager, &BackupManager::backupCreated, this, nullptr);
+ disconnect(m_backupManager, &BackupManager::backupFailed, this, nullptr);
+ },
+ Qt::SingleShotConnection);
+
+ connect(
+ m_backupManager,
+ &BackupManager::backupFailed,
+ this,
+ [this](const QString&, const QString& error)
+ {
+ ui->createButton->setEnabled(true);
+ ui->restoreButton->setEnabled(true);
+ ui->deleteButton->setEnabled(true);
+ ui->createButton->setText(tr("Create Backup"));
+ QMessageBox::critical(this, tr("Error"), tr("Failed to create backup: %1").arg(error));
+ disconnect(m_backupManager, &BackupManager::backupCreated, this, nullptr);
+ disconnect(m_backupManager, &BackupManager::backupFailed, this, nullptr);
+ },
+ Qt::SingleShotConnection);
+
+ m_backupManager->createBackupAsync(m_instance, backupName, options);
+}
+
+void BackupDialog::on_restoreButton_clicked()
+{
+ int currentRow = ui->backupList->currentRow();
+ if (currentRow < 0 || currentRow >= m_backups.size())
+ {
+ return;
+ }
+
+ const InstanceBackup& backup = m_backups[currentRow];
+
+ auto result = QMessageBox::question(
+ this,
+ tr("Restore Backup"),
+ tr("Are you sure you want to restore backup '%1'?\nThis will overwrite current instance data.")
+ .arg(backup.name()),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+
+ if (result != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ bool createSafetyBackup = QMessageBox::question(this,
+ tr("Safety Backup"),
+ tr("Create a safety backup before restoring?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes)
+ == QMessageBox::Yes;
+
+ // Disable UI during restore
+ ui->createButton->setEnabled(false);
+ ui->restoreButton->setEnabled(false);
+ ui->deleteButton->setEnabled(false);
+ ui->restoreButton->setText(tr("Restoring..."));
+
+ // Connect signals for this operation
+ connect(
+ m_backupManager,
+ &BackupManager::backupRestored,
+ this,
+ [this](const QString&, const QString&)
+ {
+ ui->createButton->setEnabled(true);
+ ui->restoreButton->setEnabled(true);
+ ui->deleteButton->setEnabled(true);
+ ui->restoreButton->setText(tr("Restore"));
+ QMessageBox::information(this, tr("Success"), tr("Backup restored successfully!"));
+ refreshBackupList();
+ disconnect(m_backupManager, &BackupManager::backupRestored, this, nullptr);
+ disconnect(m_backupManager, &BackupManager::restoreFailed, this, nullptr);
+ },
+ Qt::SingleShotConnection);
+
+ connect(
+ m_backupManager,
+ &BackupManager::restoreFailed,
+ this,
+ [this](const QString&, const QString& error)
+ {
+ ui->createButton->setEnabled(true);
+ ui->restoreButton->setEnabled(true);
+ ui->deleteButton->setEnabled(true);
+ ui->restoreButton->setText(tr("Restore"));
+ QMessageBox::critical(this, tr("Error"), tr("Failed to restore backup: %1").arg(error));
+ disconnect(m_backupManager, &BackupManager::backupRestored, this, nullptr);
+ disconnect(m_backupManager, &BackupManager::restoreFailed, this, nullptr);
+ },
+ Qt::SingleShotConnection);
+
+ m_backupManager->restoreBackupAsync(m_instance, backup, createSafetyBackup);
+}
+
+void BackupDialog::on_deleteButton_clicked()
+{
+ int currentRow = ui->backupList->currentRow();
+ if (currentRow < 0 || currentRow >= m_backups.size())
+ {
+ return;
+ }
+
+ const InstanceBackup& backup = m_backups[currentRow];
+
+ auto result = QMessageBox::question(this,
+ tr("Delete Backup"),
+ tr("Are you sure you want to delete backup '%1'?").arg(backup.name()),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+
+ if (result != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ if (m_backupManager->deleteBackup(backup))
+ {
+ refreshBackupList();
+ QMessageBox::information(this, tr("Success"), tr("Backup deleted successfully!"));
+ }
+ else
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Failed to delete backup."));
+ }
+}
+
+void BackupDialog::on_refreshButton_clicked()
+{
+ refreshBackupList();
+}
+
+void BackupDialog::on_backupList_currentRowChanged(int)
+{
+ updateBackupDetails();
+ updateButtons();
+}
+
+void BackupDialog::onBackupCreated(const QString& instanceId, const QString& backupName)
+{
+ if (instanceId == m_instance->id())
+ {
+ refreshBackupList();
+ }
+}
+
+void BackupDialog::onBackupRestored(const QString& instanceId, const QString& backupName)
+{
+ if (instanceId == m_instance->id())
+ {
+ refreshBackupList();
+ }
+}
+
+BackupOptions BackupDialog::getSelectedOptions() const
+{
+ BackupOptions options;
+ options.includeSaves = ui->includeSaves->isChecked();
+ options.includeConfig = ui->includeConfig->isChecked();
+ options.includeMods = ui->includeMods->isChecked();
+ options.includeResourcePacks = ui->includeResourcePacks->isChecked();
+ options.includeShaderPacks = ui->includeShaderPacks->isChecked();
+ options.includeScreenshots = ui->includeScreenshots->isChecked();
+ options.includeOptions = ui->includeOptions->isChecked();
+ options.customPaths = m_customPaths;
+ return options;
+}
+
+void BackupDialog::on_addCustomPathButton_clicked()
+{
+ QString path = QInputDialog::getText(this,
+ tr("Add Custom Path"),
+ tr("Enter relative path to include (e.g., \"logs\", \"crash-reports\"):"),
+ QLineEdit::Normal,
+ QString(),
+ nullptr);
+
+ if (!path.isEmpty() && !m_customPaths.contains(path))
+ {
+ m_customPaths.append(path);
+ ui->customPathsList->addItem(path);
+ }
+}
+
+void BackupDialog::on_removeCustomPathButton_clicked()
+{
+ int currentRow = ui->customPathsList->currentRow();
+ if (currentRow >= 0 && currentRow < m_customPaths.size())
+ {
+ m_customPaths.removeAt(currentRow);
+ delete ui->customPathsList->takeItem(currentRow);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h
new file mode 100644
index 0000000000..3c85d628a9
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "BaseInstance.h"
+#include "minecraft/BackupManager.h"
+
+namespace Ui
+{
+ class BackupDialog;
+}
+
+class BackupDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit BackupDialog(InstancePtr instance, QWidget* parent = nullptr);
+ ~BackupDialog();
+
+ private slots:
+ void on_createButton_clicked();
+ void on_restoreButton_clicked();
+ void on_deleteButton_clicked();
+ void on_refreshButton_clicked();
+ void on_backupList_currentRowChanged(int currentRow);
+ void on_addCustomPathButton_clicked();
+ void on_removeCustomPathButton_clicked();
+ void onBackupCreated(const QString& instanceId, const QString& backupName);
+ void onBackupRestored(const QString& instanceId, const QString& backupName);
+
+ private:
+ void refreshBackupList();
+ void updateBackupDetails();
+ void updateButtons();
+ BackupOptions getSelectedOptions() const;
+
+ Ui::BackupDialog* ui;
+ InstancePtr m_instance;
+ BackupManager* m_backupManager;
+ QList<InstanceBackup> m_backups;
+ QStringList m_customPaths;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui
new file mode 100644
index 0000000000..d699588691
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui
@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BackupDialog</class>
+ <widget class="QWidget" name="BackupDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>700</width>
+ <height>500</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Manage Backups</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="QWidget" name="leftWidget" native="true">
+ <layout class="QVBoxLayout" name="leftLayout">
+ <item>
+ <widget class="QLabel" name="backupListLabel">
+ <property name="text">
+ <string>Available Backups:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListWidget" name="backupList"/>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="buttonLayout">
+ <item>
+ <widget class="QPushButton" name="createButton">
+ <property name="text">
+ <string>Create</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreButton">
+ <property name="text">
+ <string>Restore</string>
+ </property>
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="deleteButton">
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshButton">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="rightWidget" native="true">
+ <layout class="QVBoxLayout" name="rightLayout">
+ <item>
+ <widget class="QGroupBox" name="detailsGroup">
+ <property name="title">
+ <string>Backup Details</string>
+ </property>
+ <layout class="QVBoxLayout" name="detailsLayout">
+ <item>
+ <widget class="QTextBrowser" name="backupDetails">
+ <property name="openExternalLinks">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="optionsGroup">
+ <property name="title">
+ <string>Backup Options</string>
+ </property>
+ <layout class="QVBoxLayout" name="optionsLayout">
+ <item>
+ <widget class="QCheckBox" name="includeSaves">
+ <property name="text">
+ <string>Include Saves</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeConfig">
+ <property name="text">
+ <string>Include Config</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeMods">
+ <property name="text">
+ <string>Include Mods</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeResourcePacks">
+ <property name="text">
+ <string>Include Resource Packs</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeShaderPacks">
+ <property name="text">
+ <string>Include Shader Packs</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeScreenshots">
+ <property name="text">
+ <string>Include Screenshots</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="includeOptions">
+ <property name="text">
+ <string>Include Options (options.txt)</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="separator">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="customPathsLabel">
+ <property name="text">
+ <string>Custom Paths:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListWidget" name="customPathsList">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="customPathsButtons">
+ <item>
+ <widget class="QPushButton" name="addCustomPathButton">
+ <property name="text">
+ <string>Add...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="removeCustomPathButton">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="estimatedSizeLabel">
+ <property name="text">
+ <string>Estimated Size: Calculating...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp
new file mode 100644
index 0000000000..c154de7b2f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -0,0 +1,530 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+
+#include "BlockedModsDialog.h"
+#include "ui_BlockedModsDialog.h"
+
+#include "Application.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include <QDebug>
+#include <QDesktopServices>
+#include <QDialogButtonBox>
+#include <QDir>
+#include <QDirIterator>
+#include <QDragEnterEvent>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QMimeData>
+#include <QPushButton>
+#include <QStandardPaths>
+#include <QTimer>
+
+BlockedModsDialog::BlockedModsDialog(QWidget* parent,
+ const QString& title,
+ const QString& text,
+ QList<BlockedMod>& mods,
+ QString hash_type)
+ : QDialog(parent),
+ ui(new Ui::BlockedModsDialog),
+ m_mods(mods),
+ m_hashType(hash_type)
+{
+ m_hashingTask = shared_qobject_ptr<ConcurrentTask>(
+ new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
+ connect(m_hashingTask.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished);
+
+ ui->setupUi(this);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+ connect(ui->openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); });
+ connect(ui->downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder);
+
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged);
+
+ qDebug() << "[Blocked Mods Dialog] Mods List: " << mods;
+
+ // defer setup of file system watchers until after the dialog is shown
+ // this allows OS (namely macOS) permission prompts to show after the relevant dialog appears
+ QTimer::singleShot(0,
+ this,
+ [this]
+ {
+ setupWatch();
+ scanPaths();
+ update();
+ });
+
+ this->setWindowTitle(title);
+ ui->labelDescription->setText(text);
+
+ // force all URL handling as external
+ connect(ui->textBrowserWatched,
+ &QTextBrowser::anchorClicked,
+ this,
+ [](const QUrl url) { QDesktopServices::openUrl(url); });
+
+ setAcceptDrops(true);
+
+ update();
+}
+
+BlockedModsDialog::~BlockedModsDialog()
+{
+ delete ui;
+}
+
+void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e)
+{
+ if (e->mimeData()->hasUrls())
+ {
+ e->acceptProposedAction();
+ }
+}
+
+void BlockedModsDialog::dropEvent(QDropEvent* e)
+{
+ for (QUrl& url : e->mimeData()->urls())
+ {
+ if (url.scheme().isEmpty())
+ { // ensure isLocalFile() works correctly
+ url.setScheme("file");
+ }
+
+ if (!url.isLocalFile())
+ { // can't drop external files here.
+ continue;
+ }
+
+ QString filePath = url.toLocalFile();
+ qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath;
+ addHashTask(filePath);
+
+ // watch for changes
+ QFileInfo file = QFileInfo(filePath);
+ QString path = file.dir().absolutePath();
+ qDebug() << "[Blocked Mods Dialog] Adding watch path:" << path;
+ m_watcher.addPath(path);
+ }
+ scanPaths();
+ update();
+}
+
+void BlockedModsDialog::done(int r)
+{
+ QDialog::done(r);
+ disconnect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged);
+}
+
+void BlockedModsDialog::openAll(bool missingOnly)
+{
+ for (auto& mod : m_mods)
+ {
+ if (!missingOnly || !mod.matched)
+ {
+ QDesktopServices::openUrl(mod.websiteUrl);
+ }
+ }
+}
+
+void BlockedModsDialog::addDownloadFolder()
+{
+ QString dir = QFileDialog::getExistingDirectory(this,
+ tr("Select directory where you downloaded the mods"),
+ QStandardPaths::writableLocation(QStandardPaths::DownloadLocation),
+ QFileDialog::ShowDirsOnly);
+ qDebug() << "[Blocked Mods Dialog] Adding watch path:" << dir;
+ m_watcher.addPath(dir);
+ scanPath(dir, true);
+ update();
+}
+
+/// @brief update UI with current status of the blocked mod detection
+void BlockedModsDialog::update()
+{
+ QString text;
+ QString span;
+
+ for (auto& mod : m_mods)
+ {
+ if (mod.matched)
+ {
+ // &#x2714; -> html for HEAVY CHECK MARK : ✔
+ span = QString(tr("<span style=\"color:green\"> &#x2714; Found at %1 </span>")).arg(mod.localPath);
+ }
+ else
+ {
+ // &#x2718; -> html for HEAVY BALLOT X : ✘
+ span = QString(tr("<span style=\"color:red\"> &#x2718; Not Found </span>"));
+ }
+ text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>"))
+ .arg(mod.name, mod.websiteUrl, mod.hash, span);
+ }
+
+ ui->textBrowserModsListing->setText(text);
+
+ QString watching;
+ for (auto& dir : m_watcher.directories())
+ {
+ QUrl fileURL = QUrl::fromLocalFile(dir);
+ watching += QString("<a href=\"%1\">%2</a><br/>").arg(fileURL.toString(), dir);
+ }
+
+ ui->textBrowserWatched->setText(watching);
+
+ if (allModsMatched())
+ {
+ ui->labelModsFound->setText("<span style=\"color:green\">✔</span>" + tr("All mods found"));
+ ui->openMissingButton->setDisabled(true);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+ }
+ else
+ {
+ ui->labelModsFound->setText(tr("Please download the missing mods."));
+ ui->openMissingButton->setDisabled(false);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Skip"));
+ }
+}
+
+/// @brief Signal fired when a watched directory has changed
+/// @param path the path to the changed directory
+void BlockedModsDialog::directoryChanged(QString path)
+{
+ qDebug() << "[Blocked Mods Dialog] Directory changed: " << path;
+ validateMatchedMods();
+ scanPath(path, true);
+}
+
+/// @brief add the user downloads folder and the global mods folder to the filesystem watcher
+void BlockedModsDialog::setupWatch()
+{
+ const QString downloadsFolder = APPLICATION->settings()->get("DownloadsDir").toString();
+ const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString();
+ const bool downloadsFolderWatchRecursive = APPLICATION->settings()->get("DownloadsDirWatchRecursive").toBool();
+ watchPath(downloadsFolder, downloadsFolderWatchRecursive);
+ watchPath(modsFolder, true);
+}
+
+void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
+{
+ auto to_watch = QFileInfo(path);
+ if (!to_watch.isReadable())
+ {
+ qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path;
+ return;
+ }
+ auto to_watch_path = to_watch.canonicalFilePath();
+ if (m_watcher.directories().contains(to_watch_path))
+ return; // don't watch the same path twice (no loops!)
+
+ qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path;
+ m_watcher.addPath(to_watch_path);
+
+ if (!to_watch.isDir() || !watch_recursive)
+ return;
+
+ QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags);
+ while (it.hasNext())
+ {
+ QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths
+ watchPath(watch_dir, watch_recursive);
+ }
+}
+
+/// @brief scan all watched folder
+void BlockedModsDialog::scanPaths()
+{
+ for (auto& dir : m_watcher.directories())
+ {
+ scanPath(dir, false);
+ }
+ runHashTask();
+}
+
+/// @brief Scan the directory at path, skip paths that do not contain a file name
+/// of a blocked mod we are looking for
+/// @param path the directory to scan
+void BlockedModsDialog::scanPath(QString path, bool start_task)
+{
+ QDir scan_dir(path);
+ QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags);
+ while (scan_it.hasNext())
+ {
+ QString file = scan_it.next();
+
+ if (!checkValidPath(file))
+ {
+ continue;
+ }
+
+ addHashTask(file);
+ }
+
+ if (start_task)
+ {
+ runHashTask();
+ }
+}
+
+/// @brief add a hashing task for the file located at path, add the path to the pending set if the hashing task is already running
+/// @param path the path to the local file being hashed
+void BlockedModsDialog::addHashTask(QString path)
+{
+ qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set.";
+ m_pendingHashPaths.insert(path);
+}
+
+/// @brief add a hashing task for the file located at path and connect it to check that hash against
+/// our blocked mods list
+/// @param path the path to the local file being hashed
+void BlockedModsDialog::buildHashTask(QString path)
+{
+ auto hash_task = Hashing::createHasher(path, m_hashType);
+
+ qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;
+
+ connect(hash_task.get(),
+ &Task::succeeded,
+ this,
+ [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); });
+ connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; });
+
+ m_hashingTask->addTask(hash_task);
+}
+
+/// @brief check if the computed hash for the provided path matches a blocked
+/// mod we are looking for
+/// @param hash the computed hash for the provided path
+/// @param path the path to the local file being compared
+void BlockedModsDialog::checkMatchHash(QString hash, QString path)
+{
+ bool match = false;
+
+ qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
+
+ auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
+ auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
+ for (auto& mod : m_mods)
+ {
+ if (mod.matched)
+ {
+ continue;
+ }
+ if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0)
+ {
+ mod.matched = true;
+ mod.localPath = path;
+ if (moveFiles)
+ {
+ mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
+ }
+ match = true;
+
+ qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
+
+ break;
+ }
+ }
+
+ if (match)
+ {
+ update();
+ }
+}
+
+/// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for
+/// @param path the path to check
+/// @return boolean: did the path match the name of a blocked mod?
+bool BlockedModsDialog::checkValidPath(QString path)
+{
+ const QFileInfo file = QFileInfo(path);
+ const QString filename = file.fileName();
+
+ auto compare = [](QString fsFilename, QString metadataFilename)
+ { return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0; };
+
+ // super lax compare (but not fuzzy)
+ // convert to lowercase
+ // convert all speratores to whitespace
+ // simplify sequence of internal whitespace to a single space
+ // efectivly compare two strings ignoring all separators and case
+ auto laxCompare = [](QString fsfilename, QString metadataFilename)
+ {
+ // allowed character seperators
+ QList<QChar> allowedSeperators = { '-', '+', '.', '_' };
+
+ // copy in lowercase
+ auto fsName = fsfilename.toLower();
+ auto metaName = metadataFilename.toLower();
+
+ // replace all potential allowed seperatores with whitespace
+ for (auto sep : allowedSeperators)
+ {
+ fsName = fsName.replace(sep, ' ');
+ metaName = metaName.replace(sep, ' ');
+ }
+
+ // remove extraneous whitespace
+ fsName = fsName.simplified();
+ metaName = metaName.simplified();
+
+ return fsName.compare(metaName) == 0;
+ };
+
+ auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
+ auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
+ for (auto& mod : m_mods)
+ {
+ if (compare(filename, mod.name))
+ {
+ // if the mod is not yet matched and doesn't have a hash then
+ // just match it with the file that has the exact same name
+ if (!mod.matched && mod.hash.isEmpty())
+ {
+ mod.matched = true;
+ mod.localPath = path;
+ if (moveFiles)
+ {
+ mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
+ }
+ return false;
+ }
+ qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
+ return true;
+ }
+ if (laxCompare(filename, mod.name))
+ {
+ qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool BlockedModsDialog::allModsMatched()
+{
+ return std::all_of(m_mods.begin(), m_mods.end(), [](auto const& mod) { return mod.matched; });
+}
+
+/// @brief ensure matched file paths still exist
+void BlockedModsDialog::validateMatchedMods()
+{
+ bool changed = false;
+ for (auto& mod : m_mods)
+ {
+ if (mod.matched)
+ {
+ QFileInfo file = QFileInfo(mod.localPath);
+ if (!file.exists() || !file.isFile())
+ {
+ qDebug() << "[Blocked Mods Dialog] File" << mod.localPath << "for mod" << mod.name
+ << "has vanshed! marking as not matched.";
+ mod.localPath = "";
+ mod.matched = false;
+ changed = true;
+ }
+ }
+ }
+ if (changed)
+ {
+ update();
+ }
+}
+
+/// @brief run hash task or mark a pending run if it is already running
+void BlockedModsDialog::runHashTask()
+{
+ if (!m_hashingTask->isRunning())
+ {
+ m_rehashPending = false;
+
+ if (!m_pendingHashPaths.isEmpty())
+ {
+ qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks";
+
+ auto path = m_pendingHashPaths.begin();
+ while (path != m_pendingHashPaths.end())
+ {
+ buildHashTask(*path);
+ path = m_pendingHashPaths.erase(path);
+ }
+
+ m_hashingTask->start();
+ }
+ }
+ else
+ {
+ qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task";
+ qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pendingHashPaths;
+ m_rehashPending = true;
+ }
+}
+
+void BlockedModsDialog::hashTaskFinished()
+{
+ qDebug() << "[Blocked Mods Dialog] All hash tasks finished";
+ if (m_rehashPending)
+ {
+ qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning";
+ runHashTask();
+ }
+}
+
+/// qDebug print support for the BlockedMod struct
+QDebug operator<<(QDebug debug, const BlockedMod& m)
+{
+ QDebugStateSaver saver(debug);
+
+ debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl << ", hash: " << m.hash
+ << ", matched: " << m.matched << ", localPath: " << m.localPath << "}";
+
+ return debug;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h
new file mode 100644
index 0000000000..6be4eccaea
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include <QList>
+#include <QString>
+
+#include <QFileSystemWatcher>
+
+#include "tasks/ConcurrentTask.h"
+
+class QPushButton;
+
+struct BlockedMod
+{
+ QString name;
+ QString websiteUrl;
+ QString hash;
+ bool matched;
+ QString localPath;
+ QString targetFolder;
+ bool disabled = false;
+ bool move = false;
+};
+
+QT_BEGIN_NAMESPACE
+namespace Ui
+{
+ class BlockedModsDialog;
+}
+QT_END_NAMESPACE
+
+class BlockedModsDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ BlockedModsDialog(QWidget* parent,
+ const QString& title,
+ const QString& text,
+ QList<BlockedMod>& mods,
+ QString hash_type = "sha1");
+
+ ~BlockedModsDialog() override;
+
+ protected:
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dropEvent(QDropEvent* event) override;
+
+ protected slots:
+ void done(int r) override;
+
+ private:
+ Ui::BlockedModsDialog* ui;
+ QList<BlockedMod>& m_mods;
+ QFileSystemWatcher m_watcher;
+ shared_qobject_ptr<ConcurrentTask> m_hashingTask;
+ QSet<QString> m_pendingHashPaths;
+ bool m_rehashPending;
+ QString m_hashType;
+
+ void openAll(bool missingOnly);
+ void addDownloadFolder();
+ void update();
+ void directoryChanged(QString path);
+ void setupWatch();
+ void watchPath(QString path, bool watch_recursive = false);
+ void scanPaths();
+ void scanPath(QString path, bool start_task);
+ void addHashTask(QString path);
+ void buildHashTask(QString path);
+ void checkMatchHash(QString hash, QString path);
+ void validateMatchedMods();
+ void runHashTask();
+ void hashTaskFinished();
+
+ bool checkValidPath(QString path);
+ bool allModsMatched();
+};
+
+QDebug operator<<(QDebug debug, const BlockedMod& m);
diff --git a/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui
new file mode 100644
index 0000000000..850ad713e6
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlockedModsDialog</class>
+ <widget class="QDialog" name="BlockedModsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>500</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+ <horstretch>2</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>700</width>
+ <height>350</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">BlockedModsDialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+ <item>
+ <widget class="QLabel" name="labelDescription">
+ <property name="text">
+ <string notr="true">Placeholder description</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelExplain">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.&lt;/p&gt;&lt;p&gt;Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Click 'Open Missing' to open all the download links in the browser. &lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Blocked Mods</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTextBrowser" name="textBrowserModsListing">
+ <property name="acceptRichText">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="openMissingLayout">
+ <item>
+ <widget class="QPushButton" name="openMissingButton">
+ <property name="text">
+ <string>Open Missing</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="openMissingSpacer">
+ <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>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>Watched Folders</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTextBrowser" name="textBrowserWatched">
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>12</height>
+ </size>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="downloadFolderLayout">
+ <item>
+ <widget class="QPushButton" name="downloadFolderButton">
+ <property name="text">
+ <string>Add Download Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="downloadFolderSpacer">
+ <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>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="bottomBoxH">
+ <item>
+ <widget class="QLabel" name="labelModsFound">
+ <property name="text">
+ <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>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp
new file mode 100644
index 0000000000..a0e1fed11f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ChooseProviderDialog.h"
+#include "ui_ChooseProviderDialog.h"
+
+#include <QPushButton>
+#include <QRadioButton>
+
+#include "modplatform/ModIndex.h"
+
+ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping)
+ : QDialog(parent),
+ ui(new Ui::ChooseProviderDialog)
+{
+ ui->setupUi(this);
+
+ addProviders();
+ m_providers.button(0)->click();
+
+ connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne);
+ connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll);
+
+ connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne);
+ connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll);
+
+ if (single_choice)
+ {
+ ui->providersLayout->removeWidget(ui->skipAllButton);
+ ui->providersLayout->removeWidget(ui->confirmAllButton);
+ }
+
+ if (!allow_skipping)
+ {
+ ui->providersLayout->removeWidget(ui->skipOneButton);
+ ui->providersLayout->removeWidget(ui->skipAllButton);
+ }
+}
+
+ChooseProviderDialog::~ChooseProviderDialog()
+{
+ delete ui;
+}
+
+void ChooseProviderDialog::setDescription(QString desc)
+{
+ ui->explanationLabel->setText(desc);
+}
+
+void ChooseProviderDialog::skipOne()
+{
+ reject();
+}
+void ChooseProviderDialog::skipAll()
+{
+ m_response.skip_all = true;
+ reject();
+}
+
+void ChooseProviderDialog::confirmOne()
+{
+ m_response.chosen = getSelectedProvider();
+ m_response.try_others = ui->tryOthersCheckbox->isChecked();
+ accept();
+}
+void ChooseProviderDialog::confirmAll()
+{
+ m_response.chosen = getSelectedProvider();
+ m_response.confirm_all = true;
+ m_response.try_others = ui->tryOthersCheckbox->isChecked();
+ accept();
+}
+
+auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider
+{
+ return ModPlatform::ResourceProvider(m_providers.checkedId());
+}
+
+void ChooseProviderDialog::addProviders()
+{
+ int btn_index = 0;
+ QRadioButton* btn;
+
+ for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME })
+ {
+ btn = new QRadioButton(ModPlatform::ProviderCapabilities::readableName(provider), this);
+ m_providers.addButton(btn, btn_index++);
+ ui->providersLayout->addWidget(btn);
+ }
+}
+
+void ChooseProviderDialog::disableInput()
+{
+ for (auto& btn : m_providers.buttons())
+ btn->setEnabled(false);
+
+ ui->skipOneButton->setEnabled(false);
+ ui->skipAllButton->setEnabled(false);
+ ui->confirmOneButton->setEnabled(false);
+ ui->confirmAllButton->setEnabled(false);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h
new file mode 100644
index 0000000000..6a8974fd75
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QButtonGroup>
+#include <QDialog>
+
+namespace Ui
+{
+ class ChooseProviderDialog;
+}
+
+namespace ModPlatform
+{
+ enum class ResourceProvider;
+}
+
+class Mod;
+class NetJob;
+
+class ChooseProviderDialog : public QDialog
+{
+ Q_OBJECT
+
+ struct Response
+ {
+ bool skip_all = false;
+ bool confirm_all = false;
+
+ bool try_others = false;
+
+ ModPlatform::ResourceProvider chosen;
+ };
+
+ public:
+ explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true);
+ ~ChooseProviderDialog();
+
+ auto getResponse() const -> Response
+ {
+ return m_response;
+ }
+
+ void setDescription(QString desc);
+
+ private slots:
+ void skipOne();
+ void skipAll();
+ void confirmOne();
+ void confirmAll();
+
+ private:
+ void addProviders();
+ void disableInput();
+
+ auto getSelectedProvider() const -> ModPlatform::ResourceProvider;
+
+ private:
+ Ui::ChooseProviderDialog* ui;
+
+ QButtonGroup m_providers;
+
+ Response m_response;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui
new file mode 100644
index 0000000000..78cd9613bb
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ChooseProviderDialog</class>
+ <widget class="QDialog" name="ChooseProviderDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>453</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose a mod provider</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="explanationLabel">
+ <property name="alignment">
+ <set>Qt::AlignJustify|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="indent">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QFormLayout" name="providersLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignHCenter|Qt::AlignTop</set>
+ </property>
+ <property name="formAlignment">
+ <set>Qt::AlignHCenter|Qt::AlignTop</set>
+ </property>
+ </layout>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <layout class="QHBoxLayout" name="buttonsLayout">
+ <item>
+ <widget class="QPushButton" name="skipOneButton">
+ <property name="text">
+ <string>Skip this mod</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skipAllButton">
+ <property name="text">
+ <string>Skip all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="confirmAllButton">
+ <property name="text">
+ <string>Confirm for all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="confirmOneButton">
+ <property name="text">
+ <string>Confirm</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="tryOthersCheckbox">
+ <property name="text">
+ <string>Try to automatically use other providers if the chosen one fails</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp
new file mode 100644
index 0000000000..941b867e92
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "BuildConfig.h"
+#include "CopyInstanceDialog.h"
+#include "ui_CopyInstanceDialog.h"
+
+#include "ui/dialogs/IconPickerDialog.h"
+
+#include "BaseInstance.h"
+#include "BaseVersion.h"
+#include "DesktopServices.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
+#include "icons/IconList.hpp"
+
+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();
+
+ QStringList groups = APPLICATION->instances()->getGroups();
+ groups.prepend("");
+ ui->groupBox->addItems(groups);
+ int index = groups.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_selectedOptions.isCopySavesEnabled());
+ ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.isKeepPlaytimeEnabled());
+ ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.isCopyGameOptionsEnabled());
+ ui->copyResPacksCheckbox->setChecked(m_selectedOptions.isCopyResourcePacksEnabled());
+ ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled());
+ ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled());
+ ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled());
+ ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled());
+
+ ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled());
+ ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled());
+
+ ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled());
+ ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled());
+
+ auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType;
+
+ m_cloneSupported = FS::canCloneOnFS(detectedFS);
+ m_linkSupported = FS::canLinkOnFS(detectedFS);
+
+ if (m_cloneSupported)
+ {
+ ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS)));
+ }
+ else
+ {
+ ui->cloneSupportedLabel->setText(
+ tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS)));
+ }
+
+#if defined(Q_OS_WIN)
+ ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield));
+ ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") + "\n"
+ + tr("On Windows, symbolic links may require admin permission to create."));
+#endif
+
+ updateLinkOptions();
+ updateUseCloneCheckbox();
+
+ auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help);
+ connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help);
+ HelpButton->setText(tr("Help"));
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+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();
+}
+
+const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const
+{
+ return m_selectedOptions;
+}
+
+void CopyInstanceDialog::help()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy")));
+}
+
+void CopyInstanceDialog::checkAllCheckboxes(const bool& b)
+{
+ ui->keepPlaytimeCheckbox->setChecked(b);
+ ui->copySavesCheckbox->setChecked(b);
+ ui->copyGameOptionsCheckbox->setChecked(b);
+ ui->copyResPacksCheckbox->setChecked(b);
+ ui->copyShaderPacksCheckbox->setChecked(b);
+ ui->copyServersCheckbox->setChecked(b);
+ ui->copyModsCheckbox->setChecked(b);
+ ui->copyScreenshotsCheckbox->setChecked(b);
+}
+
+// Check the "Select all" checkbox if all options are already selected:
+void CopyInstanceDialog::updateSelectAllCheckbox()
+{
+ ui->selectAllCheckbox->blockSignals(true);
+ ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue());
+ ui->selectAllCheckbox->blockSignals(false);
+}
+
+void CopyInstanceDialog::updateUseCloneCheckbox()
+{
+ ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked()
+ && !ui->hardLinksCheckbox->isChecked());
+ ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled()
+ && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked());
+}
+
+void CopyInstanceDialog::updateLinkOptions()
+{
+ ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked()
+ && !ui->useCloneCheckbox->isChecked());
+ ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked()
+ && !ui->useCloneCheckbox->isChecked());
+
+ ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled()
+ && !ui->useCloneCheckbox->isChecked());
+ ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled()
+ && !ui->useCloneCheckbox->isChecked());
+
+ bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked());
+ ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked());
+ ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse);
+ ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse
+ && m_selectedOptions.isLinkRecursivelyEnabled());
+ ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled());
+
+#if defined(Q_OS_WIN)
+ auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield)
+ : QIcon());
+#endif
+}
+
+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([[maybe_unused]] const QString& arg1)
+{
+ updateDialogState();
+}
+
+void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state)
+{
+ bool checked;
+ checked = (state == Qt::Checked);
+ checkAllCheckboxes(checked);
+}
+
+void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopySaves(state == Qt::Checked);
+ ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked());
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableKeepPlaytime(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyGameOptions(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyResourcePacks(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyShaderPacks(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyServers(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyMods(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableCopyScreenshots(state == Qt::Checked);
+ updateSelectAllCheckbox();
+}
+
+void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseSymLinks(state == Qt::Checked);
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseHardLinks(state == Qt::Checked);
+ if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked())
+ {
+ ui->recursiveLinkCheckbox->setChecked(true);
+ }
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableLinkRecursively(state == Qt::Checked);
+ updateLinkOptions();
+}
+
+void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableDontLinkSaves(state == Qt::Checked);
+}
+
+void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state)
+{
+ m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked));
+ updateUseCloneCheckbox();
+ updateLinkOptions();
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h
new file mode 100644
index 0000000000..448616995a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 "BaseInstance.h"
+#include "BaseVersion.h"
+#include "InstanceCopyPrefs.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;
+ const InstanceCopyPrefs& getChosenOptions() const;
+
+ public slots:
+ void help();
+
+ private slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString& arg1);
+ // Checkboxes
+ void on_selectAllCheckbox_stateChanged(int state);
+ void on_copySavesCheckbox_stateChanged(int state);
+ void on_keepPlaytimeCheckbox_stateChanged(int state);
+ void on_copyGameOptionsCheckbox_stateChanged(int state);
+ void on_copyResPacksCheckbox_stateChanged(int state);
+ void on_copyShaderPacksCheckbox_stateChanged(int state);
+ void on_copyServersCheckbox_stateChanged(int state);
+ void on_copyModsCheckbox_stateChanged(int state);
+ void on_copyScreenshotsCheckbox_stateChanged(int state);
+ void on_symbolicLinksCheckbox_stateChanged(int state);
+ void on_hardLinksCheckbox_stateChanged(int state);
+ void on_recursiveLinkCheckbox_stateChanged(int state);
+ void on_dontLinkSavesCheckbox_stateChanged(int state);
+ void on_useCloneCheckbox_stateChanged(int state);
+
+ private:
+ void checkAllCheckboxes(const bool& b);
+ void updateSelectAllCheckbox();
+ void updateUseCloneCheckbox();
+ void updateLinkOptions();
+
+ /* data */
+ Ui::CopyInstanceDialog* ui;
+ QString InstIconKey;
+ InstancePtr m_original;
+ InstanceCopyPrefs m_selectedOptions;
+ bool m_cloneSupported = false;
+ bool m_linkSupported = false;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui
new file mode 100644
index 0000000000..5060debcf2
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui
@@ -0,0 +1,447 @@
+<?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>575</width>
+ <height>695</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>60</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>60</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="groupDropdownLayout">
+ <property name="verticalSpacing">
+ <number>6</number>
+ </property>
+ <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="QGroupBox" name="copyOptionsGroup">
+ <property name="title">
+ <string>Instance Copy Options</string>
+ </property>
+ <layout class="QGridLayout" name="copyOptionsLayout">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="keepPlaytimeCheckbox">
+ <property name="text">
+ <string>Keep play time</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QCheckBox" name="copyModsCheckbox">
+ <property name="toolTip">
+ <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string>
+ </property>
+ <property name="text">
+ <string>Copy mods</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QCheckBox" name="copyResPacksCheckbox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Copy resource packs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QCheckBox" name="copyGameOptionsCheckbox">
+ <property name="toolTip">
+ <string>Copy the in-game options like FOV, max framerate, etc.</string>
+ </property>
+ <property name="text">
+ <string>Copy game options</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QCheckBox" name="copyShaderPacksCheckbox">
+ <property name="text">
+ <string>Copy shader packs</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QCheckBox" name="copyServersCheckbox">
+ <property name="text">
+ <string>Copy servers</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="copySavesCheckbox">
+ <property name="text">
+ <string>Copy saves</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="copyScreenshotsCheckbox">
+ <property name="text">
+ <string>Copy screenshots</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QCheckBox" name="selectAllCheckbox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="text">
+ <string>Select all</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="advancedOptionsLabel">
+ <property name="text">
+ <string>Advanced Copy Options</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="copyModeLayout">
+ <item>
+ <widget class="QGroupBox" name="linkFilesGroup">
+ <property name="toolTip">
+ <string>Use symbolic or hard links instead of copying files.</string>
+ </property>
+ <property name="title">
+ <string>Symbolic and Hard Link Options</string>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="linkOptionsLayout">
+ <item>
+ <widget class="QLabel" name="linkOptionsLabel">
+ <property name="text">
+ <string>Links are supported on most filesystems except FAT</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="linkOptionsGridLayout" rowstretch="0,0,0,0" columnstretch="0,0" rowminimumheight="0,0,0,0" columnminimumwidth="0,0">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item row="2" column="1">
+ <widget class="QCheckBox" name="recursiveLinkCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Link each resource individually instead of linking whole folders at once</string>
+ </property>
+ <property name="text">
+ <string>Link files recursively</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QCheckBox" name="dontLinkSavesCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>If &quot;copy saves&quot; is selected world save data will be copied instead of linked and thus not shared between instances.</string>
+ </property>
+ <property name="text">
+ <string>Don't link saves</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="hardLinksCheckbox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="toolTip">
+ <string>Use hard links instead of copying files.</string>
+ </property>
+ <property name="text">
+ <string>Use hard links</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="symbolicLinksCheckbox">
+ <property name="toolTip">
+ <string>Use symbolic links instead of copying files.</string>
+ </property>
+ <property name="text">
+ <string>Use symbolic links</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="horizontalGroupBox">
+ <property name="title">
+ <string>CoW (Copy-on-Write) Options</string>
+ </property>
+ <layout class="QHBoxLayout" name="useCloneLayout">
+ <item>
+ <widget class="QCheckBox" name="useCloneCheckbox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Files cloned with reflinks take up no extra space until they are modified.</string>
+ </property>
+ <property name="text">
+ <string>Clone instead of copying</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="CoWSpacer">
+ <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="cloneSupportedLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>1</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Your filesystem and/or OS doesn't support reflinks</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="margin">
+ <number>4</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ <tabstop>keepPlaytimeCheckbox</tabstop>
+ <tabstop>copyScreenshotsCheckbox</tabstop>
+ <tabstop>copySavesCheckbox</tabstop>
+ <tabstop>copyShaderPacksCheckbox</tabstop>
+ <tabstop>copyGameOptionsCheckbox</tabstop>
+ <tabstop>copyServersCheckbox</tabstop>
+ <tabstop>copyResPacksCheckbox</tabstop>
+ <tabstop>copyModsCheckbox</tabstop>
+ <tabstop>symbolicLinksCheckbox</tabstop>
+ <tabstop>recursiveLinkCheckbox</tabstop>
+ <tabstop>hardLinksCheckbox</tabstop>
+ <tabstop>dontLinkSavesCheckbox</tabstop>
+ <tabstop>useCloneCheckbox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>269</x>
+ <y>692</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>337</x>
+ <y>692</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp
new file mode 100644
index 0000000000..61e991ec9d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2025 Yihe Li <winmikedows@hotmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "BuildConfig.h"
+#include "CreateShortcutDialog.h"
+#include "ui_CreateShortcutDialog.h"
+
+#include "ui/dialogs/IconPickerDialog.h"
+
+#include "BaseInstance.h"
+#include "DesktopServices.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
+#include "icons/IconList.hpp"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/ShortcutUtils.h"
+#include "minecraft/WorldList.h"
+#include "minecraft/auth/AccountList.hpp"
+
+CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent),
+ ui(new Ui::CreateShortcutDialog),
+ m_instance(instance)
+{
+ ui->setupUi(this);
+
+ InstIconKey = instance->iconKey();
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ ui->instNameTextBox->setPlaceholderText(instance->name());
+
+ auto mInst = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer");
+ auto worldList = mInst->worldList();
+ worldList->update();
+ if (!m_QuickJoinSupported || worldList->empty())
+ {
+ ui->worldTarget->hide();
+ ui->worldSelectionBox->hide();
+ ui->serverTarget->setChecked(true);
+ ui->serverTarget->hide();
+ ui->serverLabel->show();
+ }
+
+ // Populate save targets
+ if (!DesktopServices::isFlatpak())
+ {
+ QString desktopDir = FS::getDesktopDir();
+ QString applicationDir = FS::getApplicationsDir();
+
+ if (!desktopDir.isEmpty())
+ ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(ShortcutTarget::Desktop));
+
+ if (!applicationDir.isEmpty())
+ ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(ShortcutTarget::Applications));
+ }
+ ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(ShortcutTarget::Other));
+
+ // Populate worlds
+ if (m_QuickJoinSupported)
+ {
+ for (const auto& world : worldList->allWorlds())
+ {
+ // Entry name: World Name [Game Mode] - Last Played: DateTime
+ QString entry_name =
+ tr("%1 [%2] - Last Played: %3")
+ .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate));
+ ui->worldSelectionBox->addItem(entry_name, world.name());
+ }
+ }
+
+ // Populate accounts
+ auto accounts = APPLICATION->accounts();
+ MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
+ if (accounts->count() <= 0)
+ {
+ ui->overrideAccountCheckbox->setEnabled(false);
+ }
+ else
+ {
+ for (int i = 0; i < accounts->count(); i++)
+ {
+ MinecraftAccountPtr account = accounts->at(i);
+ auto profileLabel = account->profileName();
+ if (account->isInUse())
+ profileLabel = tr("%1 (in use)").arg(profileLabel);
+ auto face = account->getFace();
+ QIcon icon = face.isNull() ? QIcon::fromTheme("noaccount") : face;
+ ui->accountSelectionBox->addItem(profileLabel, account->profileName());
+ ui->accountSelectionBox->setItemIcon(i, icon);
+ if (defaultAccount == account)
+ ui->accountSelectionBox->setCurrentIndex(i);
+ }
+ }
+}
+
+CreateShortcutDialog::~CreateShortcutDialog()
+{
+ delete ui;
+}
+
+void CreateShortcutDialog::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 CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state)
+{
+ ui->accountOptionsGroup->setEnabled(state == Qt::Checked);
+}
+
+void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state)
+{
+ ui->targetOptionsGroup->setEnabled(state == Qt::Checked);
+ ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked());
+ ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked());
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_worldTarget_toggled(bool checked)
+{
+ ui->worldSelectionBox->setEnabled(checked);
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_serverTarget_toggled(bool checked)
+{
+ ui->serverAddressBox->setEnabled(checked);
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index)
+{
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text)
+{
+ stateChanged();
+}
+
+void CreateShortcutDialog::stateChanged()
+{
+ QString result = m_instance->name();
+ if (ui->targetCheckbox->isChecked())
+ {
+ if (ui->worldTarget->isChecked())
+ result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString());
+ else if (ui->serverTarget->isChecked())
+ result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text());
+ }
+ ui->instNameTextBox->setPlaceholderText(result);
+ if (!ui->targetCheckbox->isChecked())
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+ else
+ {
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1)
+ || (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty()));
+ }
+}
+
+// Real work
+void CreateShortcutDialog::createShortcut()
+{
+ QString targetString = tr("instance");
+ QStringList extraArgs;
+ if (ui->targetCheckbox->isChecked())
+ {
+ if (ui->worldTarget->isChecked())
+ {
+ targetString = tr("world");
+ extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() };
+ }
+ else if (ui->serverTarget->isChecked())
+ {
+ targetString = tr("server");
+ extraArgs = { "--server", ui->serverAddressBox->text() };
+ }
+ }
+
+ auto target = ui->saveTargetSelectionBox->currentData().value<ShortcutTarget>();
+ auto name = ui->instNameTextBox->text();
+ if (name.isEmpty())
+ name = ui->instNameTextBox->placeholderText();
+ if (ui->overrideAccountCheckbox->isChecked())
+ extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() });
+
+ ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey, target };
+ if (target == ShortcutTarget::Desktop)
+ ShortcutUtils::createInstanceShortcutOnDesktop(args);
+ else if (target == ShortcutTarget::Applications)
+ ShortcutUtils::createInstanceShortcutInApplications(args);
+ else
+ ShortcutUtils::createInstanceShortcutInOther(args);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h
new file mode 100644
index 0000000000..9e2381170b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 "BaseInstance.h"
+
+class BaseInstance;
+
+namespace Ui
+{
+ class CreateShortcutDialog;
+}
+
+class CreateShortcutDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr);
+ ~CreateShortcutDialog();
+
+ void createShortcut();
+
+ private slots:
+ // Icon, target and name
+ void on_iconButton_clicked();
+
+ // Override account
+ void on_overrideAccountCheckbox_stateChanged(int state);
+
+ // Override target (world, server)
+ void on_targetCheckbox_stateChanged(int state);
+ void on_worldTarget_toggled(bool checked);
+ void on_serverTarget_toggled(bool checked);
+ void on_worldSelectionBox_currentIndexChanged(int index);
+ void on_serverAddressBox_textChanged(const QString& text);
+
+ private:
+ // Data
+ Ui::CreateShortcutDialog* ui;
+ QString InstIconKey;
+ InstancePtr m_instance;
+ bool m_QuickJoinSupported = false;
+
+ // Functions
+ void stateChanged();
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui
new file mode 100644
index 0000000000..24d4dc2dcd
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CreateShortcutDialog</class>
+ <widget class="QDialog" name="CreateShortcutDialog">
+ <property name="windowModality">
+ <enum>Qt::WindowModality::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>450</width>
+ <height>370</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Create Instance Shortcut</string>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="iconBtnLayout">
+ <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>
+ <layout class="QGridLayout" name="iconBtnGridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="saveToLabel">
+ <property name="text">
+ <string>Save To:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="saveTargetSelectionBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="instNameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="overrideAccountCheckbox">
+ <property name="toolTip">
+ <string>Use a different account than the default specified.</string>
+ </property>
+ <property name="text">
+ <string>Override the default account</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="accountOptionsGroup">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="accountOptionsLayout">
+ <item>
+ <widget class="QComboBox" name="accountSelectionBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="targetCheckbox">
+ <property name="toolTip">
+ <string>Specify a world or server to automatically join on launch.</string>
+ </property>
+ <property name="text">
+ <string>Select a target to join on launch</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="targetOptionsGroup">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QGridLayout" name="targetOptionsGridLayout">
+ <item row="0" column="0">
+ <layout class="QVBoxLayout" name="worldOverlap">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QRadioButton" name="worldTarget">
+ <property name="text">
+ <string>World:</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">targetBtnGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="worldSelectionBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <layout class="QVBoxLayout" name="serverOverlap">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QRadioButton" name="serverTarget">
+ <property name="text">
+ <string>Server Address:</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">targetBtnGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="serverLabel">
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Server Address:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="serverAddressBox">
+ <property name="placeholderText">
+ <string>Server Address</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="noteLabel">
+ <property name="text">
+ <string>Note: If a shortcut is moved after creation, it won't be deleted when deleting the instance.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="noteLabel2">
+ <property name="text">
+ <string>You'll need to delete them manually if that is the case.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>CreateShortcutDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>CreateShortcutDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+ <buttongroups>
+ <buttongroup name="targetBtnGroup"/>
+ </buttongroups>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp
new file mode 100644
index 0000000000..783b08dcc0
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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,
+ QCheckBox* checkBox)
+ {
+ 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);
+ if (checkBox)
+ messageBox->setCheckBox(checkBox);
+
+ return messageBox;
+ }
+} // namespace CustomMessageBox
diff --git a/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h
new file mode 100644
index 0000000000..2682021cbe
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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,
+ QCheckBox* checkBox = nullptr);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp
new file mode 100644
index 0000000000..143908ee72
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -0,0 +1,240 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <BaseInstance.h>
+#include <MMCZip.h>
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QMessageBox>
+#include "QObjectPtr.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/widgets/FileIgnoreProxy.h"
+#include "ui_ExportInstanceDialog.h"
+
+#include <FileSystem.h>
+#include <icons/IconList.hpp>
+#include <QDebug>
+#include <QFileInfo>
+#include <QPushButton>
+#include <QSaveFile>
+#include <QSortFilterProxyModel>
+#include <QStack>
+#include <functional>
+#include "Application.h"
+#include "SeparatorPrefixTree.h"
+
+ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent),
+ m_ui(new Ui::ExportInstanceDialog),
+ m_instance(instance)
+{
+ m_ui->setupUi(this);
+ auto model = new QFileSystemModel(this);
+ model->setIconProvider(&m_icons);
+ auto root = instance->instanceRoot();
+ m_proxyModel = new FileIgnoreProxy(root, this);
+ m_proxyModel->setSourceModel(model);
+ auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
+ for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" })
+ {
+ m_proxyModel->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path));
+ }
+ m_proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
+ m_proxyModel->loadBlockedPathsFromFile(ignoreFileName());
+
+ m_ui->treeView->setModel(m_proxyModel);
+ m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root)));
+ m_ui->treeView->sortByColumn(0, Qt::AscendingOrder);
+
+ connect(m_proxyModel, &QAbstractItemModel::rowsInserted, this, &ExportInstanceDialog::rowsInserted);
+
+ model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
+ model->setRootPath(root);
+ auto headerView = m_ui->treeView->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+
+ m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+ExportInstanceDialog::~ExportInstanceDialog()
+{
+ delete m_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"));
+}
+
+void 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);
+ if (output.isEmpty())
+ {
+ QDialog::done(QDialog::Rejected);
+ return;
+ }
+
+ SaveIcon(m_instance);
+
+ auto files = QFileInfoList();
+ if (!MMCZip::collectFileListRecursively(
+ m_instance->instanceRoot(),
+ nullptr,
+ &files,
+ std::bind(&FileIgnoreProxy::filterFile, m_proxyModel, std::placeholders::_1)))
+ {
+ QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
+ QDialog::done(QDialog::Rejected);
+ return;
+ }
+
+ auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true);
+
+ connect(task.get(),
+ &Task::failed,
+ this,
+ [this, output](QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
+ connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); });
+
+ ProgressDialog progress(this);
+ progress.setSkipButton(true, tr("Abort"));
+ auto result = progress.execWithTask(*task);
+ QDialog::done(result);
+}
+
+void ExportInstanceDialog::done(int result)
+{
+ m_proxyModel->saveBlockedPathsToFile(ignoreFileName());
+ if (result == QDialog::Accepted)
+ {
+ doExport();
+ 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 = m_proxyModel->index(i, 0, parent);
+ if (m_proxyModel->shouldExpand(node))
+ {
+ auto expNode = node.parent();
+ if (!expNode.isValid())
+ {
+ continue;
+ }
+ m_ui->treeView->expand(node);
+ }
+ }
+}
+
+QString ExportInstanceDialog::ignoreFileName()
+{
+ return FS::PathCombine(m_instance->instanceRoot(), ".packignore");
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h
new file mode 100644
index 0000000000..a3348b9fd3
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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>
+#include "ui/widgets/FastFileIconProvider.h"
+#include "ui/widgets/FileIgnoreProxy.h"
+
+class BaseInstance;
+using InstancePtr = std::shared_ptr<BaseInstance>;
+
+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:
+ void doExport();
+ QString ignoreFileName();
+
+ private:
+ Ui::ExportInstanceDialog* m_ui;
+ InstancePtr m_instance;
+ FileIgnoreProxy* m_proxyModel;
+ FastFileIconProvider m_icons;
+
+ private slots:
+ void rowsInserted(QModelIndex parent, int top, int bottom);
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui
new file mode 100644
index 0000000000..bcd4e84a4d
--- /dev/null
+++ b/archived/projt-launcher/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/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp
new file mode 100644
index 0000000000..052804fe37
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ExportPackDialog.h"
+#include "minecraft/mod/ResourceFolderModel.hpp"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlamePackExportTask.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui_ExportPackDialog.h"
+
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QJsonDocument>
+#include <QMessageBox>
+#include <QPushButton>
+#include "FileSystem.h"
+#include "MMCZip.h"
+#include "modplatform/modrinth/ModrinthPackExportTask.h"
+
+ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance,
+ QWidget* parent,
+ ModPlatform::ResourceProvider provider)
+ : QDialog(parent),
+ m_instance(instance),
+ m_ui(new Ui::ExportPackDialog),
+ m_provider(provider)
+{
+ Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH
+ || m_provider == ModPlatform::ResourceProvider::FLAME);
+
+ m_ui->setupUi(this);
+ m_ui->name->setPlaceholderText(instance->name());
+ m_ui->name->setText(instance->settings()->get("ExportName").toString());
+ m_ui->version->setText(instance->settings()->get("ExportVersion").toString());
+ m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool());
+
+ connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled);
+
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ {
+ setWindowTitle(tr("Export Modrinth Pack"));
+
+ m_ui->authorLabel->hide();
+ m_ui->author->hide();
+
+ m_ui->recommendedMemoryWidget->hide();
+
+ m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString());
+ }
+ else
+ {
+ setWindowTitle(tr("Export CurseForge Pack"));
+
+ m_ui->summaryLabel->hide();
+ m_ui->summary->hide();
+
+ const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt();
+
+ if (recommendedRAM > 0)
+ {
+ m_ui->recommendedMemoryCheckBox->setChecked(true);
+ m_ui->recommendedMemory->setValue(recommendedRAM);
+ }
+ else
+ {
+ m_ui->recommendedMemoryCheckBox->setChecked(false);
+
+ // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount)
+ const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12);
+ m_ui->recommendedMemory->setValue(defaultRecommendation);
+ }
+
+ m_ui->author->setText(instance->settings()->get("ExportAuthor").toString());
+ }
+
+ // ensure a valid pack is generated
+ // the name and version fields mustn't be empty
+ connect(m_ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
+ connect(m_ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
+ // the instance name can technically be empty
+ validate();
+
+ QFileSystemModel* model = new QFileSystemModel(this);
+ model->setIconProvider(&m_icons);
+
+ // use the game root - everything outside cannot be exported
+ const QDir instanceRoot(instance->instanceRoot());
+ m_proxy = new FileIgnoreProxy(instance->instanceRoot(), this);
+ auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
+ for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" })
+ {
+ m_proxy->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path));
+ }
+ m_proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
+ m_proxy->setSourceModel(model);
+ m_proxy->loadBlockedPathsFromFile(ignoreFileName());
+
+ const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
+
+ MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
+ if (mcInstance)
+ {
+ for (auto resourceModel : mcInstance->resourceLists())
+ {
+ if (resourceModel && resourceModel->indexDir().exists())
+ m_proxy->ignoreFilesWithPath().insert(
+ instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath()));
+ }
+ }
+
+ m_ui->files->setModel(m_proxy);
+ m_ui->files->setRootIndex(m_proxy->mapFromSource(model->index(instance->gameRoot())));
+ m_ui->files->sortByColumn(0, Qt::AscendingOrder);
+
+ model->setFilter(filter);
+ model->setRootPath(instance->gameRoot());
+
+ QHeaderView* headerView = m_ui->files->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+
+ m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+ExportPackDialog::~ExportPackDialog()
+{
+ delete m_ui;
+}
+
+void ExportPackDialog::done(int result)
+{
+ m_proxy->saveBlockedPathsToFile(ignoreFileName());
+ auto settings = m_instance->settings();
+ settings->set("ExportName", m_ui->name->text());
+ settings->set("ExportVersion", m_ui->version->text());
+ settings->set("ExportOptionalFiles", m_ui->optionalFiles->isChecked());
+
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ settings->set("ExportSummary", m_ui->summary->toPlainText());
+ else
+ {
+ settings->set("ExportAuthor", m_ui->author->text());
+
+ if (m_ui->recommendedMemoryCheckBox->isChecked())
+ settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value());
+ else
+ settings->reset("ExportRecommendedRAM");
+ }
+
+ if (result == Accepted)
+ {
+ const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text();
+ const QString filename = FS::RemoveInvalidFilenameChars(name);
+
+ QString output;
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ {
+ output = QFileDialog::getSaveFileName(this,
+ tr("Export %1").arg(name),
+ FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
+ tr("Modrinth pack") + " (*.mrpack *.zip)",
+ nullptr);
+ if (output.isEmpty())
+ return;
+ if (!(output.endsWith(".zip") || output.endsWith(".mrpack")))
+ output.append(".mrpack");
+ }
+ else
+ {
+ output = QFileDialog::getSaveFileName(this,
+ tr("Export %1").arg(name),
+ FS::PathCombine(QDir::homePath(), filename + ".zip"),
+ tr("CurseForge pack") + " (*.zip)",
+ nullptr);
+ if (output.isEmpty())
+ return;
+ if (!output.endsWith(".zip"))
+ output.append(".zip");
+ }
+
+ Task* task;
+ if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
+ {
+ task = new ModrinthPackExportTask(name,
+ m_ui->version->text(),
+ m_ui->summary->toPlainText(),
+ m_ui->optionalFiles->isChecked(),
+ m_instance,
+ output,
+ std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
+ }
+ else
+ {
+ FlamePackExportOptions options{};
+
+ options.name = name;
+ options.version = m_ui->version->text();
+ options.author = m_ui->author->text();
+ options.optionalFiles = m_ui->optionalFiles->isChecked();
+ options.instance = m_instance;
+ options.output = output;
+ options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1);
+ options.recommendedRAM =
+ m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0;
+
+ task = new FlamePackExportTask(std::move(options));
+ }
+
+ connect(task,
+ &Task::failed,
+ [this](const QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
+ connect(task,
+ &Task::aborted,
+ [this]
+ {
+ CustomMessageBox::selectable(this,
+ tr("Task aborted"),
+ tr("The task has been aborted by the user."),
+ QMessageBox::Information)
+ ->show();
+ });
+ connect(task, &Task::finished, [task] { task->deleteLater(); });
+
+ ProgressDialog progress(this);
+ progress.setSkipButton(true, tr("Abort"));
+ if (progress.execWithTask(*task) != QDialog::Accepted)
+ return;
+ }
+
+ QDialog::done(result);
+}
+
+void ExportPackDialog::validate()
+{
+ m_ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && m_ui->version->text().isEmpty());
+}
+
+QString ExportPackDialog::ignoreFileName()
+{
+ return FS::PathCombine(m_instance->instanceRoot(), ".packignore");
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h
new file mode 100644
index 0000000000..31d74c2828
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include "BaseInstance.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/ModIndex.h"
+#include "ui/widgets/FastFileIconProvider.h"
+#include "ui/widgets/FileIgnoreProxy.h"
+
+namespace Ui
+{
+ class ExportPackDialog;
+}
+
+class ExportPackDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ExportPackDialog(MinecraftInstancePtr instance,
+ QWidget* parent = nullptr,
+ ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
+ ~ExportPackDialog();
+
+ void done(int result) override;
+ void validate();
+
+ private:
+ QString ignoreFileName();
+
+ private:
+ const MinecraftInstancePtr m_instance;
+ Ui::ExportPackDialog* m_ui;
+ FileIgnoreProxy* m_proxy;
+ FastFileIconProvider m_icons;
+ const ModPlatform::ResourceProvider m_provider;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui
new file mode 100644
index 0000000000..bda8b8dd0a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportPackDialog</class>
+ <widget class="QDialog" name="ExportPackDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>650</width>
+ <height>532</height>
+ </rect>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="information">
+ <property name="title">
+ <string>&amp;Description</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>name</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="name"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string>&amp;Version:</string>
+ </property>
+ <property name="buddy">
+ <cstring>version</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="version">
+ <property name="text">
+ <string>1.0.0</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="authorLabel">
+ <property name="text">
+ <string>&amp;Author:</string>
+ </property>
+ <property name="buddy">
+ <cstring>author</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="author"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="summaryLabel">
+ <property name="text">
+ <string>&amp;Summary</string>
+ </property>
+ <property name="buddy">
+ <cstring>summary</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="summary">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="tabChangesFocus">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="options">
+ <property name="title">
+ <string>&amp;Options</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QWidget" name="recommendedMemoryWidget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="recommendedMemoryCheckBox">
+ <property name="text">
+ <string>&amp;Recommended Memory:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="recommendedMemory">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>8</number>
+ </property>
+ <property name="maximum">
+ <number>32768</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ </widget>
+ </item>
+ <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>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="filesLabel">
+ <property name="text">
+ <string>&amp;Files</string>
+ </property>
+ <property name="buddy">
+ <cstring>files</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTreeView" name="files">
+ <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="QCheckBox" name="optionalFiles">
+ <property name="text">
+ <string>&amp;Mark disabled files as optional</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>files</tabstop>
+ <tabstop>optionalFiles</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportPackDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportPackDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp
new file mode 100644
index 0000000000..19c3a84ad6
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ExportToModListDialog.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QTextEdit>
+#include "FileSystem.h"
+#include "Markdown.h"
+#include "StringUtils.h"
+#include "modplatform/helpers/ExportToModList.h"
+#include "ui_ExportToModListDialog.h"
+
+#include <QFileDialog>
+#include <QFileSystemModel>
+#include <QJsonDocument>
+#include <QMessageBox>
+#include <QPushButton>
+
+const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLines = {
+ { ExportToModList::HTML, "<li><a href=\"{url}\">{name}</a> [{version}] by {authors}</li>" },
+ { ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" },
+ { ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" },
+ { ExportToModList::JSON,
+ "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," },
+ { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
+};
+
+ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent)
+ : QDialog(parent),
+ m_mods(mods),
+ m_template_changed(false),
+ m_name(name),
+ ui(new Ui::ExportToModListDialog)
+{
+ ui->setupUi(this);
+ enableCustom(false);
+
+ connect(ui->formatComboBox, &QComboBox::currentIndexChanged, this, &ExportToModListDialog::formatChanged);
+ connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
+ connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
+ connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
+ connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
+ connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); });
+ connect(ui->templateText,
+ &QTextEdit::textChanged,
+ this,
+ [this]
+ {
+ if (ui->templateText->toPlainText() != exampleLines[m_format])
+ ui->formatComboBox->setCurrentIndex(5);
+ triggerImp();
+ });
+ connect(ui->copyButton,
+ &QPushButton::clicked,
+ this,
+ [this](bool)
+ {
+ this->ui->finalText->selectAll();
+ this->ui->finalText->copy();
+ });
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save"));
+ triggerImp();
+}
+
+ExportToModListDialog::~ExportToModListDialog()
+{
+ delete ui;
+}
+
+void ExportToModListDialog::formatChanged(int index)
+{
+ switch (index)
+ {
+ case 0:
+ {
+ enableCustom(false);
+ ui->resultText->show();
+ m_format = ExportToModList::HTML;
+ break;
+ }
+ case 1:
+ {
+ enableCustom(false);
+ ui->resultText->show();
+ m_format = ExportToModList::MARKDOWN;
+ break;
+ }
+ case 2:
+ {
+ enableCustom(false);
+ ui->resultText->hide();
+ m_format = ExportToModList::PLAINTXT;
+ break;
+ }
+ case 3:
+ {
+ enableCustom(false);
+ ui->resultText->hide();
+ m_format = ExportToModList::JSON;
+ break;
+ }
+ case 4:
+ {
+ enableCustom(false);
+ ui->resultText->hide();
+ m_format = ExportToModList::CSV;
+ break;
+ }
+ case 5:
+ {
+ m_template_changed = true;
+ enableCustom(true);
+ ui->resultText->hide();
+ m_format = ExportToModList::CUSTOM;
+ break;
+ }
+ }
+ triggerImp();
+}
+
+void ExportToModListDialog::triggerImp()
+{
+ if (m_format == ExportToModList::CUSTOM)
+ {
+ ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText()));
+ return;
+ }
+ auto opt = 0;
+ if (ui->authorsCheckBox->isChecked())
+ opt |= ExportToModList::Authors;
+ if (ui->versionCheckBox->isChecked())
+ opt |= ExportToModList::Version;
+ if (ui->urlCheckBox->isChecked())
+ opt |= ExportToModList::Url;
+ if (ui->filenameCheckBox->isChecked())
+ opt |= ExportToModList::FileName;
+ auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast<ExportToModList::OptionalData>(opt));
+ ui->finalText->setPlainText(txt);
+ switch (m_format)
+ {
+ case ExportToModList::CUSTOM: return;
+ case ExportToModList::HTML: ui->resultText->setHtml(StringUtils::htmlListPatch(txt)); break;
+ case ExportToModList::MARKDOWN: ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt))); break;
+ case ExportToModList::PLAINTXT: break;
+ case ExportToModList::JSON: break;
+ case ExportToModList::CSV: break;
+ }
+ auto exampleLine = exampleLines[m_format];
+ if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
+ ui->templateText->setPlainText(exampleLine);
+}
+
+void ExportToModListDialog::done(int result)
+{
+ if (result == Accepted)
+ {
+ const QString filename = FS::RemoveInvalidFilenameChars(m_name);
+ const QString output = QFileDialog::getSaveFileName(this,
+ tr("Export %1").arg(m_name),
+ FS::PathCombine(QDir::homePath(), filename + extension()),
+ tr("File") + " (*.txt *.html *.md *.json *.csv)",
+ nullptr);
+
+ if (output.isEmpty())
+ return;
+
+ try
+ {
+ FS::write(output, ui->finalText->toPlainText().toUtf8());
+ }
+ catch (const FS::FileSystemException& e)
+ {
+ qCritical() << "Failed to save mod list file :" << e.cause();
+ }
+ }
+
+ QDialog::done(result);
+}
+
+QString ExportToModListDialog::extension()
+{
+ switch (m_format)
+ {
+ case ExportToModList::HTML: return ".html";
+ case ExportToModList::MARKDOWN: return ".md";
+ case ExportToModList::PLAINTXT: return ".txt";
+ case ExportToModList::CUSTOM: return ".txt";
+ case ExportToModList::JSON: return ".json";
+ case ExportToModList::CSV: return ".csv";
+ }
+ return ".txt";
+}
+
+void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
+{
+ if (m_format != ExportToModList::CUSTOM)
+ return;
+ switch (option)
+ {
+ case ExportToModList::Authors: ui->templateText->insertPlainText("{authors}"); break;
+ case ExportToModList::Url: ui->templateText->insertPlainText("{url}"); break;
+ case ExportToModList::Version: ui->templateText->insertPlainText("{version}"); break;
+ case ExportToModList::FileName: ui->templateText->insertPlainText("{filename}"); break;
+ }
+}
+void ExportToModListDialog::enableCustom(bool enabled)
+{
+ ui->authorsCheckBox->setHidden(enabled);
+ ui->authorsButton->setHidden(!enabled);
+
+ ui->versionCheckBox->setHidden(enabled);
+ ui->versionButton->setHidden(!enabled);
+
+ ui->urlCheckBox->setHidden(enabled);
+ ui->urlButton->setHidden(!enabled);
+
+ ui->filenameCheckBox->setHidden(enabled);
+ ui->filenameButton->setHidden(!enabled);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h
new file mode 100644
index 0000000000..05e7c7b2d8
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include <QList>
+#include "minecraft/mod/Mod.hpp"
+#include "modplatform/helpers/ExportToModList.h"
+
+namespace Ui
+{
+ class ExportToModListDialog;
+}
+
+class ExportToModListDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr);
+ ~ExportToModListDialog();
+
+ void done(int result) override;
+
+ protected slots:
+ void formatChanged(int index);
+ void triggerImp();
+ void trigger(int)
+ {
+ triggerImp();
+ };
+ void addExtra(ExportToModList::OptionalData option);
+
+ private:
+ QString extension();
+ void enableCustom(bool enabled);
+
+ QList<Mod*> m_mods;
+ bool m_template_changed;
+ QString m_name;
+ ExportToModList::Formats m_format = ExportToModList::Formats::HTML;
+ Ui::ExportToModListDialog* ui;
+ static const QHash<ExportToModList::Formats, QString> exampleLines;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui
new file mode 100644
index 0000000000..ec049d7e76
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportToModListDialog</class>
+ <widget class="QDialog" name="ExportToModListDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>650</width>
+ <height>522</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Pack to ModList</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Settings</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <widget class="QComboBox" name="formatComboBox">
+ <item>
+ <property name="text">
+ <string>HTML</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Markdown</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Plaintext</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>JSON</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>CSV</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Custom</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QGroupBox" name="templateGroup">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Template</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTextEdit" name="templateText">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>This text supports the following placeholders:&#10;{name} - Mod name&#10;{mod_id} - Mod ID&#10;{url} - Mod URL&#10;{version} - Mod version&#10;{authors} - Mod authors</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QGroupBox" name="optionsGroup">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Optional Info</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="versionCheckBox">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="authorsCheckBox">
+ <property name="text">
+ <string>Authors</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="urlCheckBox">
+ <property name="text">
+ <string>URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="filenameCheckBox">
+ <property name="text">
+ <string>Filename</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="versionButton">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="authorsButton">
+ <property name="text">
+ <string>Authors</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="urlButton">
+ <property name="text">
+ <string>URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="filenameButton">
+ <property name="text">
+ <string>Filename</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="text">
+ <string>Format</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>Result</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPlainTextEdit" name="finalText">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>143</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="resultText">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="warningLabel">
+ <property name="text">
+ <string>This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="copyButton">
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportToModListDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>334</x>
+ <y>435</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportToModListDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>324</x>
+ <y>390</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>324</x>
+ <y>206</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp
new file mode 100644
index 0000000000..450d1235ea
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QFileDialog>
+#include <QKeyEvent>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+
+#include "Application.h"
+
+#include "IconPickerDialog.h"
+#include "ui_IconPickerDialog.h"
+
+#include "ui/instanceview/InstanceDelegate.h"
+
+#include <DesktopServices.h>
+#include "icons/IconList.hpp"
+#include "icons/IconUtils.hpp"
+
+IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::IconPickerDialog)
+{
+ ui->setupUi(this);
+ setWindowModality(Qt::WindowModal);
+
+ searchBar = new QLineEdit(this);
+ searchBar->setPlaceholderText(tr("Search..."));
+ ui->verticalLayout->insertWidget(0, searchBar);
+
+ proxyModel = new QSortFilterProxyModel(this);
+ proxyModel->setSourceModel(APPLICATION->icons().get());
+ proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ ui->iconView->setModel(proxyModel);
+
+ 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->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget));
+
+ // contentsWidget->setAcceptDrops(true);
+ contentsWidget->setDropIndicatorShown(true);
+ contentsWidget->viewport()->setAcceptDrops(true);
+ contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
+ contentsWidget->setDefaultDropAction(Qt::CopyAction);
+
+ contentsWidget->installEventFilter(this);
+
+ contentsWidget->setModel(proxyModel);
+
+ // 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);
+ buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+
+ connect(buttonAdd, &QPushButton::clicked, this, &IconPickerDialog::addNewIcon);
+ connect(buttonRemove, &QPushButton::clicked, this, &IconPickerDialog::removeSelectedIcon);
+
+ connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &IconPickerDialog::activated);
+
+ connect(contentsWidget->selectionModel(),
+ &QItemSelectionModel::selectionChanged,
+ this,
+ &IconPickerDialog::selectionChanged);
+
+ auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
+ connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
+ connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons);
+ // Prevent incorrect indices from e.g. filesystem changes
+ connect(APPLICATION->icons().get(),
+ &projt::icons::IconList::iconUpdated,
+ this,
+ [this]() { proxyModel->invalidate(); });
+}
+
+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 = projt::icons::getIconFilter();
+ QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter));
+ APPLICATION->icons()->installIcons(fileNames);
+}
+
+void IconPickerDialog::removeSelectedIcon()
+{
+ if (APPLICATION->icons()->trashIcon(selectedIconKey))
+ return;
+
+ 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;
+ }
+ buttonRemove->setEnabled(APPLICATION->icons()->iconFileExists(selectedIconKey));
+}
+
+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);
+ auto proxyIndex = proxyModel->mapFromSource(model_index);
+
+ if (proxyIndex.isValid())
+ {
+ contentsWidget->selectionModel()->select(proxyIndex,
+ QItemSelectionModel::Current | QItemSelectionModel::Select);
+ QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, Q_ARG(QModelIndex, proxyIndex));
+ }
+ return QDialog::exec();
+}
+
+void IconPickerDialog::delayed_scroll(QModelIndex proxy_index)
+{
+ if (proxy_index.isValid())
+ {
+ ui->iconView->scrollTo(proxy_index);
+ }
+}
+
+IconPickerDialog::~IconPickerDialog()
+{
+ delete ui;
+}
+
+void IconPickerDialog::openFolder()
+{
+ DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true);
+}
+
+void IconPickerDialog::filterIcons(const QString& query)
+{
+ proxyModel->setFilterFixedString(query);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h
new file mode 100644
index 0000000000..f4aae5cb13
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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>
+#include <QLineEdit>
+#include <QSortFilterProxyModel>
+
+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;
+ QPushButton* buttonRemove;
+ QLineEdit* searchBar;
+ QSortFilterProxyModel* proxyModel;
+
+ private slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+ void delayed_scroll(QModelIndex);
+ void addNewIcon();
+ void removeSelectedIcon();
+ void openFolder();
+ void filterIcons(const QString& text);
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui
new file mode 100644
index 0000000000..c548edfb7a
--- /dev/null
+++ b/archived/projt-launcher/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/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp
new file mode 100644
index 0000000000..303fa9caf3
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ImportResourceDialog.h"
+#include "ui_ImportResourceDialog.h"
+
+#include <QFileDialog>
+#include <QPushButton>
+
+#include "Application.h"
+#include "InstanceList.h"
+
+#include <InstanceList.h>
+#include "modplatform/ResourceType.h"
+#include "ui/instanceview/InstanceDelegate.h"
+#include "ui/instanceview/InstanceProxyModel.h"
+
+ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent)
+ : QDialog(parent),
+ ui(new Ui::ImportResourceDialog),
+ m_resource_type(type),
+ m_file_path(file_path)
+{
+ ui->setupUi(this);
+ setWindowModality(Qt::WindowModal);
+
+ auto contentsWidget = ui->instanceView;
+ contentsWidget->setViewMode(QListView::ListMode);
+ 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(true);
+ contentsWidget->setWrapping(true);
+ // NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the
+ // wrapped text.
+ contentsWidget->setUniformItemSizes(false);
+ contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->setItemDelegate(new ListViewDelegate());
+
+ proxyModel = new InstanceProxyModel(this);
+ proxyModel->setSourceModel(APPLICATION->instances().get());
+ proxyModel->sort(0);
+ contentsWidget->setModel(proxyModel);
+
+ connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &ImportResourceDialog::activated);
+ connect(contentsWidget->selectionModel(),
+ &QItemSelectionModel::selectionChanged,
+ this,
+ &ImportResourceDialog::selectionChanged);
+
+ ui->label->setText(tr("Choose the instance you would like to import this %1 to.")
+ .arg(ModPlatform::ResourceTypeUtils::getName(m_resource_type)));
+ ui->label_file_path->setText(tr("File: %1").arg(m_file_path));
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+void ImportResourceDialog::activated(QModelIndex index)
+{
+ selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString();
+ accept();
+}
+
+void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
+{
+ if (selected.empty())
+ return;
+
+ QString key = selected.first().indexes().first().data(InstanceList::InstanceIDRole).toString();
+ if (!key.isEmpty())
+ {
+ selectedInstanceKey = key;
+ }
+}
+
+ImportResourceDialog::~ImportResourceDialog()
+{
+ delete ui;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h
new file mode 100644
index 0000000000..4cfd1cb8b4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QDialog>
+#include <QItemSelection>
+
+#include "modplatform/ResourceType.h"
+#include "ui/instanceview/InstanceProxyModel.h"
+
+namespace Ui
+{
+ class ImportResourceDialog;
+}
+
+class ImportResourceDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent = nullptr);
+ ~ImportResourceDialog() override;
+ QString selectedInstanceKey;
+
+ private:
+ Ui::ImportResourceDialog* ui;
+ ModPlatform::ResourceType m_resource_type;
+ QString m_file_path;
+ InstanceProxyModel* proxyModel;
+
+ private slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui
new file mode 100644
index 0000000000..cc3f4ec113
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ImportResourceDialog</class>
+ <widget class="QDialog" name="ImportResourceDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>676</width>
+ <height>555</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose instance to import to</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Choose the instance you would like to import this resource pack to.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_file_path">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="instanceView"/>
+ </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>ImportResourceDialog</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>ImportResourceDialog</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/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp
new file mode 100644
index 0000000000..803f27f074
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp
@@ -0,0 +1,233 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "InstallLoaderDialog.h"
+
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "DesktopServices.h"
+#include "meta/Index.hpp"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ui/widgets/PageContainer.h"
+#include "ui/widgets/VersionSelectWidget.h"
+
+class InstallLoaderPage : public VersionSelectWidget, public BasePage
+{
+ Q_OBJECT
+ public:
+ InstallLoaderPage(const QString& id,
+ const QString& iconName,
+ const QString& name,
+ const Version& oldestVersion,
+ const std::shared_ptr<PackProfile> profile)
+ : VersionSelectWidget(nullptr),
+ uid(id),
+ iconName(iconName),
+ name(name)
+ {
+ const QString minecraftVersion = profile->getComponentVersion("net.minecraft");
+ setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ setExactIfPresentFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+
+ if (oldestVersion != Version() && Version(minecraftVersion) < oldestVersion)
+ setExactFilter(BaseVersionList::ParentVersionRole, "AAA");
+
+ if (const QString currentVersion = profile->getComponentVersion(id); !currentVersion.isNull())
+ setCurrentVersion(currentVersion);
+ }
+
+ QString id() const override
+ {
+ return uid;
+ }
+ QString displayName() const override
+ {
+ return name;
+ }
+ QIcon icon() const override
+ {
+ return QIcon::fromTheme(iconName);
+ }
+
+ void openedImpl() override
+ {
+ if (loaded)
+ return;
+
+ const auto versions = APPLICATION->metadataIndex()->component(uid);
+ if (!versions)
+ return;
+
+ initialize(versions.get());
+ loaded = true;
+ }
+
+ void setParentContainer(BasePageContainer* container) override
+ {
+ auto* pageContainer = dynamic_cast<PageContainer*>(container);
+ auto* dialog = pageContainer ? qobject_cast<QDialog*>(pageContainer->parent()) : nullptr;
+ if (!dialog || !view())
+ return;
+ connect(view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept);
+ }
+
+ private:
+ const QString uid;
+ const QString iconName;
+ const QString name;
+ bool loaded = false;
+};
+
+static InstallLoaderPage* pageCast(BasePage* page)
+{
+ auto result = dynamic_cast<InstallLoaderPage*>(page);
+ Q_ASSERT(result != nullptr);
+ return result;
+}
+
+InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr<PackProfile> profile, const QString& uid, QWidget* parent)
+ : QDialog(parent),
+ profile(std::move(profile)),
+ container(new PageContainer(this, QString(), this)),
+ buttons(new QDialogButtonBox(this))
+{
+ auto layout = new QVBoxLayout(this);
+
+ container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+ layout->addWidget(container);
+
+ auto buttonLayout = new QHBoxLayout(this);
+
+ auto refreshButton = new QPushButton(tr("&Refresh"), this);
+ connect(refreshButton,
+ &QPushButton::clicked,
+ this,
+ [this]
+ {
+ if (auto* selectedPage = pageCast(container->selectedPage()))
+ selectedPage->loadList();
+ });
+ buttonLayout->addWidget(refreshButton);
+
+ buttons->setOrientation(Qt::Horizontal);
+ buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
+ buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok"));
+ buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ buttonLayout->addWidget(buttons);
+
+ layout->addLayout(buttonLayout);
+
+ setWindowTitle(dialogTitle());
+ setWindowModality(Qt::WindowModal);
+ resize(520, 347);
+
+ for (BasePage* page : container->getPages())
+ {
+ if (page->id() == uid)
+ container->selectPage(page->id());
+
+ connect(pageCast(page),
+ &VersionSelectWidget::selectedVersionChanged,
+ this,
+ [this, page]
+ {
+ auto* selectedPage = container->selectedPage();
+ if (selectedPage && page->id() == selectedPage->id())
+ validate(selectedPage);
+ });
+ }
+ connect(container,
+ &PageContainer::selectedPageChanged,
+ this,
+ [this](BasePage* previous, BasePage* current) { validate(current); });
+ if (auto* selectedPage = pageCast(container->selectedPage()))
+ selectedPage->selectSearch();
+ validate(container->selectedPage());
+}
+
+QList<BasePage*> InstallLoaderDialog::getPages()
+{
+ return { // NeoForge
+ new InstallLoaderPage("net.neoforged", "neoforged", tr("NeoForge"), {}, profile),
+ // Forge
+ new InstallLoaderPage("net.minecraftforge", "forge", tr("Forge"), {}, profile),
+ // Fabric
+ new InstallLoaderPage("net.fabricmc.fabric-loader", "fabricmc", tr("Fabric"), Version("1.14"), profile),
+ // Quilt
+ new InstallLoaderPage("org.quiltmc.quilt-loader", "quiltmc", tr("Quilt"), Version("1.14"), profile),
+ // LiteLoader
+ new InstallLoaderPage("com.mumfrey.liteloader", "liteloader", tr("LiteLoader"), {}, profile)
+ };
+}
+
+QString InstallLoaderDialog::dialogTitle()
+{
+ return tr("Install Loader");
+}
+
+void InstallLoaderDialog::validate(BasePage* page)
+{
+ auto* loaderPage = pageCast(page);
+ buttons->button(QDialogButtonBox::Ok)->setEnabled(loaderPage && loaderPage->selectedVersion() != nullptr);
+}
+
+void InstallLoaderDialog::done(int result)
+{
+ if (result == Accepted)
+ {
+ auto* page = pageCast(container->selectedPage());
+ if (page && page->selectedVersion())
+ {
+ profile->setComponentVersion(page->id(), page->selectedVersion()->descriptor());
+ profile->resolve(Net::Mode::Online);
+ }
+ }
+
+ QDialog::done(result);
+}
+#include "InstallLoaderDialog.moc"
diff --git a/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h
new file mode 100644
index 0000000000..309a3e2481
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include "ui/pages/BasePageProvider.h"
+
+class MinecraftInstance;
+class PageContainer;
+class PackProfile;
+class QDialogButtonBox;
+
+class InstallLoaderDialog final : public QDialog, protected BasePageProvider
+{
+ Q_OBJECT
+
+ public:
+ explicit InstallLoaderDialog(std::shared_ptr<PackProfile> instance,
+ const QString& uid = QString(),
+ QWidget* parent = nullptr);
+
+ QList<BasePage*> getPages() override;
+ QString dialogTitle() override;
+
+ void validate(BasePage* page);
+ void done(int result) override;
+
+ private:
+ std::shared_ptr<PackProfile> profile;
+ PageContainer* container;
+ QDialogButtonBox* buttons;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp
new file mode 100644
index 0000000000..0ce7c500cf
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "LauncherHubDialog.h"
+
+#include <QVBoxLayout>
+
+#include "ui/widgets/LauncherHubWidget.h"
+
+LauncherHubDialog::LauncherHubDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowTitle(tr("Launcher Hub"));
+ setAttribute(Qt::WA_DeleteOnClose);
+ resize(1100, 720);
+
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ m_widget = new LauncherHubWidget(this);
+ layout->addWidget(m_widget);
+ m_widget->ensureLoaded();
+
+ connect(m_widget, &LauncherHubWidget::selectInstanceRequested, this, &LauncherHubDialog::selectInstanceRequested);
+ connect(m_widget, &LauncherHubWidget::launchInstanceRequested, this, &LauncherHubDialog::launchInstanceRequested);
+ connect(m_widget, &LauncherHubWidget::editInstanceRequested, this, &LauncherHubDialog::editInstanceRequested);
+ connect(m_widget, &LauncherHubWidget::backupsRequested, this, &LauncherHubDialog::backupsRequested);
+ connect(m_widget,
+ &LauncherHubWidget::openInstanceFolderRequested,
+ this,
+ &LauncherHubDialog::openInstanceFolderRequested);
+}
+
+LauncherHubDialog::~LauncherHubDialog() = default;
+
+void LauncherHubDialog::openUrl(const QUrl& url)
+{
+ if (!m_widget)
+ {
+ return;
+ }
+ m_widget->ensureLoaded();
+ m_widget->openUrl(url);
+}
+
+void LauncherHubDialog::setSelectedInstanceId(const QString& id)
+{
+ if (!m_widget)
+ {
+ return;
+ }
+ m_widget->setSelectedInstanceId(id);
+}
+
+void LauncherHubDialog::refreshCockpit()
+{
+ if (!m_widget)
+ {
+ return;
+ }
+ m_widget->refreshCockpit();
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h
new file mode 100644
index 0000000000..6d70abeb4c
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QDialog>
+#include <QString>
+#include <QUrl>
+
+class LauncherHubWidget;
+
+class LauncherHubDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit LauncherHubDialog(QWidget* parent = nullptr);
+ ~LauncherHubDialog() override;
+
+ void openUrl(const QUrl& url);
+ void setSelectedInstanceId(const QString& id);
+ void refreshCockpit();
+
+ signals:
+ void selectInstanceRequested(const QString& instanceId);
+ void launchInstanceRequested(const QString& instanceId);
+ void editInstanceRequested(const QString& instanceId);
+ void backupsRequested(const QString& instanceId);
+ void openInstanceFolderRequested(const QString& instanceId);
+
+ private:
+ LauncherHubWidget* m_widget = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp
new file mode 100644
index 0000000000..6a9cb13d75
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "Application.h"
+
+#include "ui_MSALoginDialog.h"
+
+#include "DesktopServices.h"
+#include "minecraft/auth/AuthFlow.hpp"
+
+#include <QApplication>
+#include <QClipboard>
+#include <QColor>
+#include <QPainter>
+#include <QPixmap>
+#include <QSize>
+#include <QUrl>
+#include <QtWidgets/QPushButton>
+
+#include <qrencode.h>
+
+MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
+{
+ ui->setupUi(this);
+
+ // make font monospace
+ QFont font;
+ font.setPixelSize(ui->code->fontInfo().pixelSize());
+ font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString());
+ font.setStyleHint(QFont::Monospace);
+ font.setFixedPitch(true);
+ ui->code->setFont(font);
+
+ connect(ui->copyCode,
+ &QPushButton::clicked,
+ this,
+ [this] { QApplication::clipboard()->setText(ui->code->text()); });
+ connect(ui->loginButton,
+ &QPushButton::clicked,
+ this,
+ [this]
+ {
+ if (m_url.isValid())
+ {
+ if (!DesktopServices::openUrl(m_url))
+ {
+ QApplication::clipboard()->setText(m_url.toString());
+ }
+ }
+ });
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+}
+
+int MSALoginDialog::exec()
+{
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createBlankMSA();
+ m_authflow_task = m_account->login(false);
+ connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
+ connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept);
+ connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
+ connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onAuthFlowStatus);
+ connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
+ connect(m_authflow_task.get(),
+ &AuthFlow::authorizeWithBrowserWithExtra,
+ this,
+ &MSALoginDialog::authorizeWithBrowserWithExtra);
+ connect(ui->buttonBox->button(QDialogButtonBox::Cancel),
+ &QPushButton::clicked,
+ m_authflow_task.get(),
+ &Task::abort);
+
+ m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode));
+ connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
+ connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept);
+ connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
+ connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onDeviceFlowStatus);
+ connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
+ connect(m_devicecode_task.get(),
+ &AuthFlow::authorizeWithBrowserWithExtra,
+ this,
+ &MSALoginDialog::authorizeWithBrowserWithExtra);
+ connect(ui->buttonBox->button(QDialogButtonBox::Cancel),
+ &QPushButton::clicked,
+ m_devicecode_task.get(),
+ &Task::abort);
+ QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection);
+ QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection);
+
+ return QDialog::exec();
+}
+
+MSALoginDialog::~MSALoginDialog()
+{
+ delete ui;
+}
+
+void MSALoginDialog::onTaskFailed(QString reason)
+{
+ // Set message
+ m_authflow_task->disconnect();
+ m_devicecode_task->disconnect();
+ ui->stackedWidget->setCurrentIndex(0);
+ 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->status->setText(processed);
+ auto task = m_authflow_task;
+ if (task->failReason().isEmpty())
+ {
+ task = m_devicecode_task;
+ }
+ if (task)
+ {
+ ui->loadingLabel->setText(task->getStatus());
+ }
+ disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel),
+ &QPushButton::clicked,
+ m_authflow_task.get(),
+ &Task::abort);
+ disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel),
+ &QPushButton::clicked,
+ m_devicecode_task.get(),
+ &Task::abort);
+ connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject);
+}
+
+void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
+{
+ ui->stackedWidget2->setCurrentIndex(1);
+ ui->stackedWidget2->adjustSize();
+ ui->stackedWidget2->updateGeometry();
+ this->adjustSize();
+ ui->loginButton->setToolTip(QString("<div style='width: 200px;'>%1</div>").arg(url.toString()));
+ m_url = url;
+}
+
+// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c
+void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
+{
+ QRcode* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QR_ECLEVEL_L, QR_MODE_8, 1);
+ if (!qr)
+ {
+ return;
+ }
+
+ const int s = qr->width > 0 ? qr->width : 1;
+ const double w = sz.width();
+ const double h = sz.height();
+ const double aspect = w / h;
+ const double size = ((aspect > 1.0) ? h : w);
+ const double scale = size / (s + 2);
+
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(fg);
+ for (int y = 0; y < s; y++)
+ {
+ for (int x = 0; x < s; x++)
+ {
+ const int color = qr->data[y * s + x] & 1; // LSB is 1 for black, 0 for white
+ if (0 != color)
+ {
+ const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
+ QRectF r(rx1, ry1, scale, scale);
+ painter.drawRect(r);
+ }
+ }
+ }
+ QRcode_free(qr);
+}
+
+void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn)
+{
+ ui->stackedWidget->setCurrentIndex(1);
+ ui->stackedWidget->adjustSize();
+ ui->stackedWidget->updateGeometry();
+ this->adjustSize();
+
+ const auto linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
+ if (url == "https://www.microsoft.com/link" && !code.isEmpty())
+ {
+ url += QString("?otc=%1").arg(code);
+ }
+ ui->code->setText(code);
+
+ auto size = QSize(150, 150);
+ QPixmap pixmap(size);
+ pixmap.fill(Qt::white);
+
+ QPainter painter(&pixmap);
+ paintQR(painter, size, url, Qt::black);
+
+ // Set the generated pixmap to the label
+ ui->qr->setPixmap(pixmap);
+
+ ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString));
+}
+
+void MSALoginDialog::onDeviceFlowStatus(QString status)
+{
+ ui->stackedWidget->setCurrentIndex(0);
+ ui->stackedWidget->adjustSize();
+ ui->stackedWidget->updateGeometry();
+ this->adjustSize();
+ ui->status->setText(status);
+}
+
+void MSALoginDialog::onAuthFlowStatus(QString status)
+{
+ ui->stackedWidget2->setCurrentIndex(0);
+ ui->stackedWidget2->adjustSize();
+ ui->stackedWidget2->updateGeometry();
+ this->adjustSize();
+ ui->status2->setText(status);
+}
+
+// Public interface
+MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent)
+{
+ MSALoginDialog dlg(parent);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return nullptr;
+} \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h
new file mode 100644
index 0000000000..5a93f544f5
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QTimer>
+#include <QtWidgets/QDialog>
+
+#include "minecraft/auth/AuthFlow.hpp"
+#include "minecraft/auth/MinecraftAccount.hpp"
+
+namespace Ui
+{
+ class MSALoginDialog;
+}
+
+class MSALoginDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ ~MSALoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget* parent);
+ int exec() override;
+
+ private:
+ explicit MSALoginDialog(QWidget* parent = 0);
+
+ protected slots:
+ void onTaskFailed(QString reason);
+ void onDeviceFlowStatus(QString status);
+ void onAuthFlowStatus(QString status);
+ void authorizeWithBrowser(const QUrl& url);
+ void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
+
+ private:
+ Ui::MSALoginDialog* ui;
+ MinecraftAccountPtr m_account;
+ shared_qobject_ptr<AuthFlow> m_devicecode_task;
+ shared_qobject_ptr<AuthFlow> m_authflow_task;
+
+ QUrl m_url;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui
new file mode 100644
index 0000000000..69cd2e1ab9
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui
@@ -0,0 +1,429 @@
+<?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>440</width>
+ <height>447</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>430</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Add Microsoft Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget2">
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <widget class="QWidget" name="loadingPage2">
+ <layout class="QVBoxLayout" name="verticalLayout_31">
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="loadingLabel2">
+ <property name="font">
+ <font>
+ <pointsize>16</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Please wait...</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="status2">
+ <property name="text">
+ <string>Status</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_31">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="mpPage1">
+ <layout class="QVBoxLayout" name="verticalLayout_21" stretch="0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <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="loginButton">
+ <property name="minimumSize">
+ <size>
+ <width>250</width>
+ <height>40</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Sign in with Microsoft</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <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>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="Line" name="line_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="orLabel">
+ <property name="font">
+ <font>
+ <pointsize>16</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>Or</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <widget class="QWidget" name="loadingPage">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <spacer name="verticalSpacer_41">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="loadingLabel">
+ <property name="font">
+ <font>
+ <pointsize>16</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Please wait...</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="status">
+ <property name="text">
+ <string>Status</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="mpPage">
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
+ <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="qr">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>150</width>
+ <height>150</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>150</width>
+ <height>150</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="scaledContents">
+ <bool>true</bool>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </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>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <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="QLabel" name="code">
+ <property name="font">
+ <font>
+ <pointsize>30</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>CODE</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="copyCode">
+ <property name="toolTip">
+ <string>Copy code to clipboard</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="copy">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>22</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <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="qrMessage">
+ <property name="text">
+ <string>Info</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </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>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp
new file mode 100644
index 0000000000..26f21baa68
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "NewComponentDialog.h"
+#include "Application.h"
+#include "ui_NewComponentDialog.h"
+
+#include <BaseVersion.h>
+#include <InstanceList.h>
+#include <icons/IconList.hpp>
+#include <tasks/Task.h>
+
+#include "IconPickerDialog.h"
+#include "ProgressDialog.h"
+#include "VersionSelectDialog.h"
+
+#include <QFileDialog>
+#include <QLayout>
+#include <QPushButton>
+#include <QValidator>
+
+#include <meta/Index.hpp>
+#include <meta/VersionList.hpp>
+
+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);
+
+ ui->nameTextBox->setFocus();
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+
+ originalPlaceholderText = ui->uidTextBox->placeholderText();
+ updateDialogState();
+}
+
+NewComponentDialog::~NewComponentDialog()
+{
+ delete ui;
+}
+
+void NewComponentDialog::updateDialogState()
+{
+ auto protoUid = ui->nameTextBox->text().toLower();
+ static const QRegularExpression s_removeChars("[^a-z]");
+ protoUid.remove(s_removeChars);
+ if (protoUid.isEmpty())
+ {
+ ui->uidTextBox->setPlaceholderText(originalPlaceholderText);
+ }
+ else
+ {
+ QString suggestedUid = "org.multimc.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/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h
new file mode 100644
index 0000000000..ccac984f6b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui
new file mode 100644
index 0000000000..03b0d22294
--- /dev/null
+++ b/archived/projt-launcher/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/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp
new file mode 100644
index 0000000000..21af782160
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "NewInstanceDialog.h"
+#include "Application.h"
+#include "ui/pages/modplatform/ModpackProviderBasePage.h"
+#include "ui/pages/modplatform/import_ftb/ImportFTBPage.h"
+#include "ui_NewInstanceDialog.h"
+
+#include <BaseVersion.h>
+#include <InstanceList.h>
+#include <icons/IconList.hpp>
+#include <tasks/Task.h>
+
+#include "IconPickerDialog.h"
+#include "ProgressDialog.h"
+#include "VersionSelectDialog.h"
+
+#include <QDialogButtonBox>
+#include <QFileDialog>
+#include <QLayout>
+#include <QPushButton>
+#include <QScreen>
+#include <QValidator>
+#include <utility>
+
+#include "ui/pages/modplatform/CustomPage.h"
+#include "ui/pages/modplatform/ImportPage.h"
+#include "ui/pages/modplatform/atlauncher/AtlPage.h"
+#include "ui/pages/modplatform/flame/FlamePage.h"
+#include "ui/pages/modplatform/legacy_ftb/Page.h"
+#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
+#include "ui/pages/modplatform/technic/TechnicPage.h"
+#include "ui/widgets/PageContainer.h"
+
+NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
+ const QString& url,
+ const QMap<QString, QString>& extra_info,
+ QWidget* parent)
+ : QDialog(parent),
+ ui(new Ui::NewInstanceDialog)
+{
+ ui->setupUi(this);
+
+ setWindowIcon(QIcon::fromTheme("new"));
+
+ InstIconKey = "default";
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+
+ QStringList groups = APPLICATION->instances()->getGroups();
+ groups.prepend("");
+ int index = groups.indexOf(initialGroup);
+ if (index == -1)
+ {
+ index = 1;
+ groups.insert(index, initialGroup);
+ }
+ ui->groupBox->addItems(groups);
+ 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, {}, this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ ui->verticalLayout->insertWidget(2, m_container);
+
+ // Block updates while setting up buttons to avoid multiple layout recalculations
+ setUpdatesEnabled(false);
+
+ m_container->addButtons(m_buttons);
+ connect(m_container,
+ &PageContainer::selectedPageChanged,
+ this,
+ [this](BasePage* previous, BasePage* selected)
+ { m_buttons->button(QDialogButtonBox::Ok)->setEnabled(creationTask && !instName().isEmpty()); });
+
+ // 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);
+ OkButton->setText(tr("OK"));
+ connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept);
+
+ auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+ CancelButton->setText(tr("Cancel"));
+ connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject);
+
+ auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+ HelpButton->setText(tr("Help"));
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+
+ // Re-enable updates after all setup is complete
+ setUpdatesEnabled(true);
+
+ if (!url.isEmpty())
+ {
+ QUrl actualUrl(url);
+ m_container->selectPage("import");
+ importPage->setUrl(url);
+ importPage->setExtraInfo(extra_info);
+ }
+
+ updateDialogState();
+
+ if (APPLICATION->settings()->get("NewInstanceGeometry").isValid())
+ {
+ restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toString().toUtf8()));
+ }
+ else
+ {
+ auto screen = parent->screen();
+ auto geometry = screen->availableSize();
+ resize(width(), qMin(geometry.height() - 50, 710));
+ }
+
+ connect(m_container, &PageContainer::selectedPageChanged, this, &NewInstanceDialog::selectedPageChanged);
+}
+
+void NewInstanceDialog::reject()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64()));
+
+ // This is just so that the pages get the close() call and can react to it, if needed.
+ m_container->prepareToClose();
+
+ QDialog::reject();
+}
+
+void NewInstanceDialog::accept()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64()));
+ importIconNow();
+
+ // This is just so that the pages get the close() call and can react to it, if needed.
+ m_container->prepareToClose();
+
+ QDialog::accept();
+}
+
+QList<BasePage*> NewInstanceDialog::getPages()
+{
+ QList<BasePage*> pages;
+
+ importPage = new ImportPage(this);
+
+ pages.append(new CustomPage(this));
+ pages.append(importPage);
+ pages.append(new AtlPage(this));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(new FlamePage(this));
+ pages.append(new LegacyFTB::Page(this));
+ pages.append(new FTBImportAPP::ImportFTBPage(this));
+ pages.append(new ModrinthPage(this));
+ pages.append(new TechnicPage(this));
+
+ return pages;
+}
+
+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);
+ importVersion.clear();
+
+ if (!task)
+ {
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ importIcon = false;
+ }
+
+ auto allowOK = task && !instName().isEmpty();
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, InstanceTask* task)
+{
+ creationTask.reset(task);
+
+ ui->instNameTextBox->setPlaceholderText(name);
+ importVersion = std::move(version);
+
+ if (!task)
+ {
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ 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)
+{
+ if (key == "default")
+ return;
+
+ auto icon = APPLICATION->icons()->getIcon(key);
+ importIcon = false;
+
+ ui->iconButton->setIcon(icon);
+}
+
+InstanceTask* NewInstanceDialog::extractTask()
+{
+ InstanceTask* extracted = creationTask.release();
+
+ InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion);
+ inst_name.setName(ui->instNameTextBox->text().trimmed());
+ extracted->setName(inst_name);
+
+ 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([[maybe_unused]] const QString& arg1)
+{
+ updateDialogState();
+}
+
+void NewInstanceDialog::importIconNow()
+{
+ if (importIcon)
+ {
+ APPLICATION->icons()->installIcon(importIconPath, importIconName);
+ InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.'));
+ importIcon = false;
+ }
+ APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64()));
+}
+
+void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+{
+ auto prevPage = dynamic_cast<ModpackProviderBasePage*>(previous);
+ if (prevPage)
+ {
+ m_searchTerm = prevPage->getSerachTerm();
+ }
+
+ auto nextPage = dynamic_cast<ModpackProviderBasePage*>(selected);
+ if (nextPage)
+ {
+ nextPage->setSearchTerm(m_searchTerm);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h
new file mode 100644
index 0000000000..4ca6eae8e0
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "InstanceTask.h"
+#include "ui/pages/BasePageProvider.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(),
+ const QMap<QString, QString>& extra_info = {},
+ QWidget* parent = 0);
+ ~NewInstanceDialog();
+
+ void updateDialogState();
+
+ void setSuggestedPack(const QString& name = QString(), InstanceTask* task = nullptr);
+ void setSuggestedPack(const QString& name, QString version, 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);
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ private:
+ Ui::NewInstanceDialog* ui = nullptr;
+ PageContainer* m_container = nullptr;
+ QDialogButtonBox* m_buttons = nullptr;
+
+ QString InstIconKey;
+ ImportPage* importPage = nullptr;
+ std::unique_ptr<InstanceTask> creationTask;
+
+ bool importIcon = false;
+ QString importIconPath;
+ QString importIconName;
+
+ QString importVersion;
+
+ QString m_searchTerm;
+
+ void importIconNow();
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui
new file mode 100644
index 0000000000..8ca0b786f2
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui
@@ -0,0 +1,91 @@
+<?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">
+ <property name="maxLength">
+ <number>128</number>
+ </property>
+ </widget>
+ </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/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp
new file mode 100644
index 0000000000..a05f25082c
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "NewsDialog.h"
+
+#include "Application.h"
+#include "settings/SettingsObject.h"
+
+#include "ui_NewsDialog.h"
+
+#include <QUrl>
+
+NewsDialog::NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent) : QDialog(parent), ui(new Ui::NewsDialog())
+{
+ ui->setupUi(this);
+
+ for (auto entry : entries)
+ {
+ ui->articleListWidget->addItem(entry->title);
+ m_entries.insert(entry->title, entry);
+ }
+
+ connect(ui->articleListWidget, &QListWidget::currentTextChanged, this, &NewsDialog::selectedArticleChanged);
+ connect(ui->toggleListButton, &QPushButton::clicked, this, &NewsDialog::toggleArticleList);
+ connect(ui->openInHubButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ if (m_current_link.isEmpty())
+ return;
+ emit openHubRequested(QUrl(m_current_link));
+ accept();
+ });
+#if defined(PROJT_DISABLE_LAUNCHER_HUB)
+ ui->openInHubButton->setEnabled(false);
+ ui->openInHubButton->setToolTip(tr("Launcher Hub is not available in this build."));
+#endif
+
+ m_article_list_hidden = ui->articleListWidget->isHidden();
+
+ auto first_item = ui->articleListWidget->item(0);
+ if (!first_item)
+ return;
+ first_item->setSelected(true);
+
+ auto article_it = m_entries.constFind(first_item->text());
+ if (article_it == m_entries.cend() || !article_it.value())
+ return;
+ auto article_entry = article_it.value();
+ ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, first_item->text()));
+ m_current_link = article_entry->link;
+#if !defined(PROJT_DISABLE_LAUNCHER_HUB)
+ ui->openInHubButton->setEnabled(!m_current_link.isEmpty());
+#endif
+
+ ui->currentArticleContentBrowser->setText(article_entry->content);
+ ui->currentArticleContentBrowser->flush();
+
+ connect(this, &QDialog::finished, this, [this] {
+ APPLICATION->settings()->set("NewsGeometry", QString::fromUtf8(saveGeometry().toBase64()));
+ });
+ const QByteArray base64Geometry = APPLICATION->settings()->get("NewsGeometry").toString().toUtf8();
+ restoreGeometry(QByteArray::fromBase64(base64Geometry));
+}
+
+NewsDialog::~NewsDialog()
+{
+ delete ui;
+}
+
+void NewsDialog::selectedArticleChanged(const QString& new_title)
+{
+ auto article_it = m_entries.constFind(new_title);
+ if (article_it == m_entries.cend() || !article_it.value())
+ return;
+ auto article_entry = article_it.value();
+
+ ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title));
+ m_current_link = article_entry->link;
+#if !defined(PROJT_DISABLE_LAUNCHER_HUB)
+ ui->openInHubButton->setEnabled(!m_current_link.isEmpty());
+#endif
+
+ ui->currentArticleContentBrowser->setText(article_entry->content);
+ ui->currentArticleContentBrowser->flush();
+}
+
+void NewsDialog::toggleArticleList()
+{
+ m_article_list_hidden = !m_article_list_hidden;
+
+ ui->articleListWidget->setHidden(m_article_list_hidden);
+
+ if (m_article_list_hidden)
+ ui->toggleListButton->setText(tr("Show article list"));
+ else
+ ui->toggleListButton->setText(tr("Hide article list"));
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h
new file mode 100644
index 0000000000..2a566297b4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QDialog>
+#include <QHash>
+#include <QPointer>
+
+#include "news/NewsEntry.h"
+
+namespace Ui
+{
+ class NewsDialog;
+}
+
+class NewsDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ NewsDialog(QList<NewsEntryPtr> entries, QWidget* parent = nullptr);
+ ~NewsDialog();
+
+ signals:
+ void openHubRequested(const QUrl& url);
+
+ public slots:
+ void toggleArticleList();
+
+ private slots:
+ void selectedArticleChanged(const QString& new_title);
+
+ private:
+ Ui::NewsDialog* ui;
+
+ QHash<QString, NewsEntryPtr> m_entries;
+ bool m_article_list_hidden = false;
+ QString m_current_link;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui
new file mode 100644
index 0000000000..4bf469d589
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewsDialog</class>
+ <widget class="QDialog" name="NewsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>500</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>News</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="leftVerticalLayout">
+ <item>
+ <widget class="QListWidget" name="articleListWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="rightVerticalLayout">
+ <item>
+ <widget class="QLabel" name="articleTitleLabel">
+ <property name="text">
+ <string notr="true">Placeholder</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ProjectDescriptionPage" name="currentArticleContentBrowser">
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="2">
+ <widget class="QPushButton" name="closeButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>10</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="openInHubButton">
+ <property name="text">
+ <string>Open in Hub</string>
+ </property>
+ <property name="icon">
+ <iconset theme="internet-web-browser"/>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QPushButton" name="toggleListButton">
+ <property name="text">
+ <string>Hide article list</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ProjectDescriptionPage</class>
+ <extends>QTextBrowser</extends>
+ <header>ui/widgets/ProjectDescriptionPage.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>closeButton</sender>
+ <signal>clicked()</signal>
+ <receiver>NewsDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>277</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>149</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp
new file mode 100644
index 0000000000..d2128b36ed
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "OfflineLoginDialog.h"
+#include "ui_OfflineLoginDialog.h"
+
+#include <QtWidgets/QPushButton>
+
+OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+OfflineLoginDialog::~OfflineLoginDialog()
+{
+ delete ui;
+}
+
+// Stage 1: User interaction
+void OfflineLoginDialog::accept()
+{
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createOffline(ui->userTextBox->text());
+ m_loginTask = m_account->login();
+ connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress);
+ m_loginTask->start();
+}
+
+void OfflineLoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->userTextBox->setEnabled(enable);
+ ui->buttonBox->setEnabled(enable);
+}
+
+void OfflineLoginDialog::on_allowLongUsernames_stateChanged(int value)
+{
+ if (value == Qt::Checked)
+ {
+ ui->userTextBox->setMaxLength(INT_MAX);
+ }
+ else
+ {
+ ui->userTextBox->setMaxLength(16);
+ }
+}
+
+// Enable the OK button only when the textbox contains something.
+void OfflineLoginDialog::on_userTextBox_textEdited(const QString& newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty());
+}
+
+void OfflineLoginDialog::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 OfflineLoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void OfflineLoginDialog::onTaskStatus(const QString& status)
+{
+ ui->label->setText(status);
+}
+
+void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget* parent, QString msg)
+{
+ OfflineLoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return nullptr;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h
new file mode 100644
index 0000000000..7f3707b503
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QtWidgets/QDialog>
+
+#include "minecraft/auth/MinecraftAccount.hpp"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class OfflineLoginDialog;
+}
+
+class OfflineLoginDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ ~OfflineLoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
+
+ private:
+ explicit OfflineLoginDialog(QWidget* parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+ protected slots:
+ void accept();
+
+ void onTaskFailed(const QString& reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString& status);
+ void onTaskProgress(qint64 current, qint64 total);
+
+ void on_userTextBox_textEdited(const QString& newText);
+ void on_allowLongUsernames_stateChanged(int value);
+
+ private:
+ Ui::OfflineLoginDialog* ui;
+ MinecraftAccountPtr m_account;
+ Task::Ptr m_loginTask;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui
new file mode 100644
index 0000000000..c2b5c427ab
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OfflineLoginDialog</class>
+ <widget class="QDialog" name="OfflineLoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>150</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Account</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::LinksAccessibleByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="maxLength">
+ <number>16</number>
+ </property>
+ <property name="placeholderText">
+ <string>Username</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="allowLongUsernames">
+ <property name="toolTip">
+ <string>Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers.</string>
+ </property>
+ <property name="text">
+ <string>Allow long usernames</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>69</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|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp
new file mode 100644
index 0000000000..87c7b73a98
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QDebug>
+#include <QItemSelectionModel>
+#include <QPushButton>
+
+#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);
+ // Note: Manually populating QTreeWidget for custom rendering, pending refactor to QListView + Delegate.
+ 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, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+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/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h
new file mode 100644
index 0000000000..dfea596591
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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.hpp"
+
+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/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui
new file mode 100644
index 0000000000..e779b51bf1
--- /dev/null
+++ b/archived/projt-launcher/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/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp
new file mode 100644
index 0000000000..2c4a523121
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "net/RawHeaderProxy.h"
+#include "ui_ProfileSetupDialog.h"
+
+#include <QAction>
+#include <QDebug>
+#include <QJsonDocument>
+#include <QPushButton>
+#include <QRegularExpressionValidator>
+
+#include "ui/dialogs/ProgressDialog.h"
+
+#include <Application.h>
+#include "minecraft/auth/Parsers.hpp"
+#include "net/Upload.h"
+
+ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent)
+ : QDialog(parent),
+ m_accountToSetup(accountToSetup),
+ ui(new Ui::ProfileSetupDialog)
+{
+ ui->setupUi(this);
+ ui->errorLabel->setVisible(false);
+
+ goodIcon = QIcon::fromTheme("status-good");
+ yellowIcon = QIcon::fromTheme("status-yellow");
+ badIcon = QIcon::fromTheme("status-bad");
+
+ static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}");
+ auto nameEdit = ui->nameEdit;
+ nameEdit->setValidator(new QRegularExpressionValidator(s_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());
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+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;
+
+ QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name));
+ auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ { "Authorization",
+ QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } };
+
+ m_check_response.reset(new QByteArray());
+ if (m_check_task)
+ disconnect(m_check_task.get(), nullptr, this, nullptr);
+ m_check_task = Net::Download::makeByteArray(url, m_check_response);
+ m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers));
+
+ connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished);
+
+ m_check_task->setNetwork(APPLICATION->network());
+ m_check_task->start();
+}
+
+void ProfileSetupDialog::checkFinished()
+{
+ if (m_check_task->error() == QNetworkReply::NoError)
+ {
+ auto doc = QJsonDocument::fromJson(*m_check_response);
+ 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;
+ }
+
+ QString payloadTemplate("{\"profileName\":\"%1\"}");
+
+ QUrl url("https://api.minecraftservices.com/minecraft/profile");
+ auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ { "Authorization",
+ QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } };
+
+ m_profile_response.reset(new QByteArray());
+ m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8());
+ m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers));
+
+ connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished);
+
+ m_profile_task->setNetwork(APPLICATION->network());
+ m_profile_task->start();
+
+ isWorking = true;
+
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(false);
+}
+
+namespace
+{
+
+ struct MojangError
+ {
+ static MojangError fromJSON(QByteArray data)
+ {
+ MojangError out;
+ out.rawError = QString::fromUtf8(data);
+ auto doc = QJsonDocument::fromJson(data, &out.parseError);
+
+ out.fullyParsed = false;
+ if (!out.parseError.error)
+ {
+ 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()
+{
+ isWorking = false;
+ if (m_profile_task->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(*m_profile_response);
+ ui->errorLabel->setVisible(true);
+
+ QString errorMessage =
+ tr("Network Error: %1\nHTTP Status: %2")
+ .arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode()));
+
+ if (parsedError.fullyParsed)
+ {
+ errorMessage += "Path: " + parsedError.path + "\n";
+ errorMessage += "Error: " + parsedError.error + "\n";
+ errorMessage += "Message: " + parsedError.errorMessage + "\n";
+ }
+ else
+ {
+ errorMessage += "Failed to parse error from Mojang API: " + parsedError.parseError.errorString() + "\n";
+ errorMessage += "Log:\n" + parsedError.rawError + "\n";
+ }
+
+ ui->errorLabel->setText(tr("The server responded with the following error:") + "\n\n" + errorMessage);
+ qDebug() << parsedError.rawError;
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(true);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h
new file mode 100644
index 0000000000..f46af1e118
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QNetworkReply>
+#include <QTimer>
+
+#include <minecraft/auth/MinecraftAccount.hpp>
+#include <memory>
+#include "net/Download.h"
+#include "net/Upload.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 startCheck();
+
+ void checkFinished();
+ void setupProfileFinished();
+
+ 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;
+
+ std::shared_ptr<QByteArray> m_check_response;
+ Net::Download::Ptr m_check_task;
+
+ std::shared_ptr<QByteArray> m_profile_response;
+ Net::Upload::Ptr m_profile_task;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui
new file mode 100644
index 0000000000..947110da74
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui
@@ -0,0 +1,77 @@
+<?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="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </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/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp
new file mode 100644
index 0000000000..75ee8575a6
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp
@@ -0,0 +1,361 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QPoint>
+#include "ui_ProgressDialog.h"
+
+#include <QDebug>
+#include <QKeyEvent>
+#include <limits>
+
+#include "tasks/Task.h"
+
+#include "ui/widgets/SubTaskProgressBar.h"
+
+// map a value in a numeric range of an arbitrary type to between 0 and INT_MAX
+// for getting the best precision out of the qt progress bar
+template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, bool> = true>
+std::tuple<int, int> map_int_zero_max(T current, T range_max, T range_min)
+{
+ int int_max = std::numeric_limits<int>::max();
+
+ auto type_range = range_max - range_min;
+ double percentage = static_cast<double>(current - range_min) / static_cast<double>(type_range);
+ int mapped_current = percentage * int_max;
+
+ return { mapped_current, int_max };
+}
+
+ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog)
+{
+ ui->setupUi(this);
+ ui->taskProgressScrollArea->setHidden(true);
+ this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
+ changeProgress(0, 100);
+ updateSize(true);
+ setSkipButton(false);
+}
+
+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);
+ if (ui->skipButton->isEnabled()) // prevent other triggers from aborting
+ m_task->abort();
+}
+
+ProgressDialog::~ProgressDialog()
+{
+ for (auto conn : this->m_taskConnections)
+ {
+ disconnect(conn);
+ }
+ delete ui;
+}
+
+void ProgressDialog::updateSize(bool recenterParent)
+{
+ QSize lastSize = this->size();
+ QPoint lastPos = this->pos();
+ int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2);
+ minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing();
+ if (!ui->taskProgressScrollArea->isHidden())
+ minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing();
+ if (ui->skipButton->isVisible())
+ minHeight += ui->skipButton->height() + ui->verticalLayout->spacing();
+ minHeight = std::max(minHeight, 60);
+ QSize minSize = QSize(480, minHeight);
+
+ setMinimumSize(minSize);
+ adjustSize();
+
+ QSize newSize = this->size();
+ // if the current window is a different size
+ auto parent = this->parentWidget();
+ if (recenterParent && parent)
+ {
+ auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2));
+ auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2));
+ this->move(newX, newY);
+ }
+ else if (lastSize != newSize)
+ {
+ // center on old position after resize
+ QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative
+ auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2));
+ auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2));
+ this->move(newX, newY);
+ }
+}
+
+int ProgressDialog::execWithTask(Task* task)
+{
+ return execWithTaskInternal(task);
+}
+
+int ProgressDialog::execWithTask(Task& task)
+{
+ return execWithTaskInternal(&task);
+}
+
+// Preferred overloads: Take ownership of the task via unique_ptr
+// The task will be automatically deleted when the dialog is destroyed
+int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task)
+{
+ if (task)
+ {
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ }
+ return execWithTaskInternal(task.release());
+}
+
+int ProgressDialog::execWithTask(std::unique_ptr<Task>& task)
+{
+ if (task)
+ {
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ }
+ return execWithTaskInternal(task.release());
+}
+
+int ProgressDialog::execWithTaskInternal(Task* task)
+{
+ this->m_task = task;
+
+ if (!task)
+ {
+ qDebug() << "Programmer error: Progress dialog created with null task.";
+ return QDialog::DialogCode::Accepted;
+ }
+
+ QDialog::DialogCode result{};
+ if (handleImmediateResult(result))
+ {
+ return result;
+ }
+
+ // Connect signals.
+ this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted));
+ this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed));
+ this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded));
+ this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus));
+ this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus));
+ this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress));
+ this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress));
+ this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide));
+ this->m_taskConnections.push_back(
+ connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled));
+ this->m_taskConnections.push_back(
+ connect(task, &Task::abortButtonTextChanged, ui->skipButton, &QPushButton::setText));
+
+ m_is_multi_step = task->isMultiStep();
+ ui->taskProgressScrollArea->setHidden(!m_is_multi_step);
+ updateSize();
+
+ // It's a good idea to start the task after we entered the dialog's event loop :^)
+ if (!task->isRunning())
+ {
+ QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection);
+ }
+ else
+ {
+ changeStatus(task->getStatus());
+ changeProgress(task->getProgress(), task->getTotalProgress());
+ }
+
+ return QDialog::exec();
+}
+
+bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result)
+{
+ if (m_task->isFinished())
+ {
+ if (m_task->wasSuccessful())
+ {
+ result = QDialog::Accepted;
+ }
+ else
+ {
+ result = QDialog::Rejected;
+ }
+ return true;
+ }
+ return false;
+}
+
+Task* ProgressDialog::getTask()
+{
+ return m_task;
+}
+
+void ProgressDialog::onTaskStarted()
+{}
+
+void ProgressDialog::onTaskFailed([[maybe_unused]] QString failure)
+{
+ reject();
+ hide();
+}
+
+void ProgressDialog::onTaskSucceeded()
+{
+ accept();
+ hide();
+}
+
+void ProgressDialog::changeStatus([[maybe_unused]] const QString& status)
+{
+ ui->globalStatusLabel->setText(m_task->getStatus());
+ ui->globalStatusLabel->adjustSize();
+ ui->globalStatusDetailsLabel->setText(m_task->getDetails());
+ ui->globalStatusDetailsLabel->adjustSize();
+
+ updateSize();
+}
+
+void ProgressDialog::addTaskProgress(TaskStepProgress const& progress)
+{
+ SubTaskProgressBar* task_bar = new SubTaskProgressBar(this);
+ taskProgress.insert(progress.uid, task_bar);
+ ui->taskProgressLayout->addWidget(task_bar);
+}
+
+void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress)
+{
+ m_is_multi_step = true;
+ if (ui->taskProgressScrollArea->isHidden())
+ {
+ ui->taskProgressScrollArea->setHidden(false);
+ updateSize();
+ }
+
+ if (!taskProgress.contains(task_progress.uid))
+ addTaskProgress(task_progress);
+ auto task_bar = taskProgress.value(task_progress.uid);
+
+ auto const [mapped_current, mapped_total] = map_int_zero_max<qint64>(task_progress.current, task_progress.total, 0);
+ if (task_progress.total <= 0)
+ {
+ task_bar->setRange(0, 0);
+ }
+ else
+ {
+ task_bar->setRange(0, mapped_total);
+ }
+
+ task_bar->setValue(mapped_current);
+ task_bar->setStatus(task_progress.status);
+ task_bar->setDetails(task_progress.details);
+
+ if (task_progress.isDone())
+ {
+ task_bar->setVisible(false);
+ }
+}
+
+void ProgressDialog::changeProgress(qint64 current, qint64 total)
+{
+ ui->globalProgressBar->setMaximum(total);
+ ui->globalProgressBar->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 (m_task && m_task->isRunning())
+ {
+ e->ignore();
+ }
+ else
+ {
+ QDialog::closeEvent(e);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h
new file mode 100644
index 0000000000..442765ea88
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QHash>
+#include <QUuid>
+#include <memory>
+
+#include "QObjectPtr.h"
+#include "tasks/Task.h"
+
+#include "ui/widgets/SubTaskProgressBar.h"
+
+class Task;
+class SequentialTask;
+
+namespace Ui
+{
+ class ProgressDialog;
+}
+
+class ProgressDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit ProgressDialog(QWidget* parent = 0);
+ ~ProgressDialog();
+
+ void updateSize(bool recenterParent = false);
+
+ // Deprecated: Use unique_ptr overloads instead for better ownership semantics
+ [[deprecated("Use execWithTask(std::unique_ptr<Task>&&) instead")]] int execWithTask(Task* task);
+
+ // Non-owning overload for tasks managed elsewhere
+ int execWithTask(Task& task);
+
+ // Preferred: Takes ownership of the 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);
+ void changeStepProgress(TaskStepProgress const& task_progress);
+
+ 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);
+ void addTaskProgress(TaskStepProgress const& progress);
+ int execWithTaskInternal(Task* task);
+
+ private:
+ Ui::ProgressDialog* ui;
+
+ Task* m_task;
+
+ QList<QMetaObject::Connection> m_taskConnections;
+
+ bool m_is_multi_step = false;
+ QHash<QUuid, SubTaskProgressBar*> taskProgress;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui
new file mode 100644
index 0000000000..156ff247f8
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui
@@ -0,0 +1,144 @@
+<?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>480</width>
+ <height>210</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>1</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>480</width>
+ <height>210</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Please wait...</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <item>
+ <widget class="QLabel" name="globalStatusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>15</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Global Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="globalStatusDetailsLabel">
+ <property name="text">
+ <string>Global Status Details...</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="globalProgressBar">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="value">
+ <number>24</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="taskProgressScrollArea">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAsNeeded</enum>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="taskProgressContainer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>460</width>
+ <height>108</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="taskProgressLayout">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skipButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <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/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp
new file mode 100644
index 0000000000..c75859b6db
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -0,0 +1,572 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ResourceDownloadDialog.h"
+#include <QList>
+
+#include <QInputDialog>
+#include <QLineEdit>
+#include <QPushButton>
+#include <algorithm>
+
+#include "Application.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.hpp"
+#include "minecraft/mod/ResourcePackFolderModel.hpp"
+#include "minecraft/mod/ShaderPackFolderModel.hpp"
+#include "minecraft/mod/TexturePackFolderModel.hpp"
+
+#include "minecraft/mod/tasks/GetModDependenciesTask.hpp"
+#include "modplatform/ModIndex.h"
+#include "modplatform/modrinth/ModrinthCollectionImportTask.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/ReviewMessageBox.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
+
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
+
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "ui/widgets/PageContainer.h"
+
+namespace ResourceDownload
+{
+
+ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ResourceFolderModel> base_model)
+ : QDialog(parent),
+ m_base_model(base_model),
+ m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel),
+ m_vertical_layout(this)
+ {
+ setObjectName(QStringLiteral("ResourceDownloadDialog"));
+
+ resize(static_cast<int>(std::max(0.5 * parent->width(), 400.0)),
+ static_cast<int>(std::max(0.75 * parent->height(), 400.0)));
+
+ setWindowIcon(QIcon::fromTheme("new"));
+
+ // 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->setEnabled(false);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ OkButton->setText(tr("Review and confirm"));
+ OkButton->setShortcut(tr("Ctrl+Return"));
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+
+ setWindowModality(Qt::WindowModal);
+ }
+
+ void ResourceDownloadDialog::accept()
+ {
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64()));
+
+ QDialog::accept();
+ }
+
+ void ResourceDownloadDialog::reject()
+ {
+ auto selected = getTasks();
+ if (selected.count() > 0)
+ {
+ auto reply = CustomMessageBox::selectable(this,
+ tr("Confirmation Needed"),
+ tr("You have %1 selected resources.\n"
+ "Are you sure you want to close this dialog?")
+ .arg(selected.count()),
+ QMessageBox::Question,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No)
+ ->exec();
+ if (reply != QMessageBox::Yes)
+ {
+ return;
+ }
+ }
+
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64()));
+
+ QDialog::reject();
+ }
+
+ // NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so
+ // won't work with subclasses if we put it in this ctor.
+ void ResourceDownloadDialog::initializeContainer()
+ {
+ m_container = new PageContainer(this, {}, this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ m_vertical_layout.addWidget(m_container);
+
+ m_container->addButtons(&m_buttons);
+
+ connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged);
+ }
+
+ void ResourceDownloadDialog::connectButtons()
+ {
+ auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
+ OkButton->setToolTip(
+ tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return")
+ .arg(resourcesString()));
+ connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+ }
+
+ void ResourceDownloadDialog::confirm()
+ {
+ auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
+ confirm_dialog->retranslateUi(resourcesString());
+
+ QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo;
+ QStringList depNames;
+ if (auto task = getModDependenciesTask(); task)
+ {
+ connect(task.get(),
+ &Task::failed,
+ this,
+ [this](QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ auto weak = task.toWeakRef();
+ connect(
+ task.get(),
+ &Task::succeeded,
+ this,
+ [this, weak]()
+ {
+ QStringList warnings;
+ if (auto task = weak.lock())
+ {
+ warnings = task->warnings();
+ }
+ if (warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)
+ ->exec();
+ }
+ });
+
+ // Check for updates
+ ProgressDialog progress_dialog(this);
+ progress_dialog.setSkipButton(true, tr("Abort"));
+ progress_dialog.setWindowTitle(tr("Checking for dependencies..."));
+ auto ret = progress_dialog.execWithTask(*task);
+
+ // If the dialog was skipped / some download error happened
+ if (ret == QDialog::DialogCode::Rejected)
+ {
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ else
+ {
+ for (auto dep : task->getDependecies())
+ {
+ addResource(dep->pack, dep->version);
+ depNames << dep->pack->name;
+ }
+ dependencyExtraInfo = task->getExtraInfo();
+ }
+ }
+
+ auto selected = getTasks();
+ std::sort(selected.begin(),
+ selected.end(),
+ [](const DownloadTaskPtr& a, const DownloadTaskPtr& b)
+ { return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; });
+ for (auto& task : selected)
+ {
+ auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString());
+ confirm_dialog->appendResource({ task->getName(),
+ task->getFilename(),
+ task->getCustomPath(),
+ ModPlatform::ProviderCapabilities::name(task->getProvider()),
+ extraInfo.required_by,
+ task->getVersion().version_type.toString(),
+ !extraInfo.maybe_installed });
+ }
+
+ if (confirm_dialog->exec())
+ {
+ auto deselected = confirm_dialog->deselectedResources();
+ for (auto page : m_container->getPages())
+ {
+ auto res = static_cast<ResourcePage*>(page);
+ for (auto name : deselected)
+ res->removeResourceFromPage(name);
+ }
+
+ this->accept();
+ }
+ else
+ {
+ for (auto name : depNames)
+ removeResource(name);
+ }
+ }
+
+ bool ResourceDownloadDialog::selectPage(QString pageId)
+ {
+ return m_container->selectPage(pageId);
+ }
+
+ ResourcePage* ResourceDownloadDialog::selectedPage()
+ {
+ return dynamic_cast<ResourcePage*>(m_container->selectedPage());
+ }
+
+ void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver)
+ {
+ removeResource(pack->name);
+ if (auto* page = selectedPage())
+ page->addResourceToPage(pack, ver, getBaseModel());
+ setButtonStatus();
+ }
+
+ void ResourceDownloadDialog::removeResource(const QString& pack_name)
+ {
+ for (auto page : m_container->getPages())
+ {
+ if (auto* resourcePage = dynamic_cast<ResourcePage*>(page))
+ resourcePage->removeResourceFromPage(pack_name);
+ }
+ setButtonStatus();
+ }
+
+ void ResourceDownloadDialog::setButtonStatus()
+ {
+ auto selected = false;
+ for (auto page : m_container->getPages())
+ {
+ if (auto* resourcePage = dynamic_cast<ResourcePage*>(page))
+ selected = selected || resourcePage->hasSelectedPacks();
+ }
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected);
+ }
+
+ const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
+ {
+ QList<DownloadTaskPtr> selected;
+ for (auto page : m_container->getPages())
+ {
+ if (auto* resourcePage = dynamic_cast<ResourcePage*>(page))
+ selected.append(resourcePage->selectedPacks());
+ }
+ return selected;
+ }
+
+ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+ {
+ auto* prev_page = dynamic_cast<ResourcePage*>(previous);
+ if (!prev_page)
+ {
+ qCritical() << "Selected previous page in ResourceDownloadDialog is not a ResourcePage!";
+ return;
+ }
+
+ // Same effect as having a global search bar
+ if (auto* result = dynamic_cast<ResourcePage*>(selected))
+ result->setSearchTerm(prev_page->getSearchTerm());
+ }
+
+ ModDownloadDialog::ModDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ModFolderModel>& mods,
+ BaseInstance* instance)
+ : ResourceDownloadDialog(parent, mods),
+ m_instance(instance)
+ {
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (modrinthPage())
+ {
+ m_importModrinthCollectionButton =
+ m_buttons.addButton(tr("Import Modrinth Collection"), QDialogButtonBox::ActionRole);
+ connect(m_importModrinthCollectionButton,
+ &QPushButton::clicked,
+ this,
+ &ModDownloadDialog::importModrinthCollection);
+ }
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8()));
+ }
+
+ QList<BasePage*> ModDownloadDialog::getPages()
+ {
+ QList<BasePage*> pages;
+
+ auto loaders = static_cast<MinecraftInstance*>(m_instance)->getPackProfile()->getSupportedModLoaders().value();
+
+ if (ModrinthAPI::validateModLoaders(loaders))
+ pages.append(ModrinthModPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders))
+ pages.append(FlameModPage::create(this, *m_instance));
+
+ return pages;
+ }
+
+ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask()
+ {
+ if (!APPLICATION->settings()->get("ModDependenciesDisabled").toBool())
+ { // dependencies
+ if (auto model = dynamic_cast<ModFolderModel*>(getBaseModel().get()); model)
+ {
+ QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers;
+ for (auto& selected : getTasks())
+ {
+ selectedVers.append(
+ std::make_shared<GetModDependenciesTask::PackDependency>(selected->getPack(),
+ selected->getVersion()));
+ }
+
+ return makeShared<GetModDependenciesTask>(m_instance, model, selectedVers);
+ }
+ }
+ return nullptr;
+ }
+
+ ResourcePage* ModDownloadDialog::modrinthPage() const
+ {
+ for (auto* page : m_container->getPages())
+ {
+ if (auto* resource_page = dynamic_cast<ResourcePage*>(page);
+ resource_page && resource_page->id() == "modrinth")
+ return resource_page;
+ }
+ return nullptr;
+ }
+
+ void ModDownloadDialog::importModrinthCollection()
+ {
+ auto* page = modrinthPage();
+ if (!page)
+ return;
+
+ bool ok = false;
+ auto input = QInputDialog::getText(this,
+ tr("Import Modrinth Collection"),
+ tr("Enter a Modrinth collection URL or collection ID:"),
+ QLineEdit::Normal,
+ QString(),
+ &ok);
+ if (!ok || input.trimmed().isEmpty())
+ return;
+
+ auto task = makeShared<ModrinthCollectionImportTask>(input, static_cast<MinecraftInstance*>(m_instance));
+
+ ProgressDialog progress_dialog(this);
+ progress_dialog.setSkipButton(true, tr("Abort"));
+ progress_dialog.setWindowTitle(tr("Importing Modrinth collection..."));
+
+ if (progress_dialog.execWithTask(*task) == QDialog::Rejected)
+ return;
+
+ selectPage(page->id());
+ auto imported_resources = task->importedResources();
+ for (auto& imported : imported_resources)
+ addResource(imported.pack, imported.version);
+
+ QString message;
+ if (task->collectionName().isEmpty())
+ message = tr("Imported %1 mod(s) from Modrinth collection `%2`.")
+ .arg(task->importedResources().size())
+ .arg(input.trimmed());
+ else
+ message =
+ tr("Imported %1 mod(s) from `%2`.").arg(task->importedResources().size()).arg(task->collectionName());
+
+ if (!task->skippedResources().isEmpty())
+ {
+ message += "\n\n"
+ + tr("Skipped %1 project(s) without a compatible version:").arg(task->skippedResources().size())
+ + "\n" + task->skippedResources().join(", ");
+ }
+
+ CustomMessageBox::selectable(this, tr("Collection imported"), message, QMessageBox::Information)->exec();
+ }
+
+ ResourcePackDownloadDialog::ResourcePackDownloadDialog(
+ QWidget* parent,
+ const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
+ BaseInstance* instance)
+ : ResourceDownloadDialog(parent, resource_packs),
+ m_instance(instance)
+ {
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8()));
+ }
+
+ QList<BasePage*> ResourcePackDownloadDialog::getPages()
+ {
+ QList<BasePage*> pages;
+
+ pages.append(ModrinthResourcePackPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameResourcePackPage::create(this, *m_instance));
+
+ return pages;
+ }
+
+ TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<TexturePackFolderModel>& resource_packs,
+ BaseInstance* instance)
+ : ResourceDownloadDialog(parent, resource_packs),
+ m_instance(instance)
+ {
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8()));
+ }
+
+ QList<BasePage*> TexturePackDownloadDialog::getPages()
+ {
+ QList<BasePage*> pages;
+
+ pages.append(ModrinthTexturePackPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameTexturePackPage::create(this, *m_instance));
+
+ return pages;
+ }
+
+ ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ShaderPackFolderModel>& shaders,
+ BaseInstance* instance)
+ : ResourceDownloadDialog(parent, shaders),
+ m_instance(instance)
+ {
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8()));
+ }
+
+ QList<BasePage*> ShaderPackDownloadDialog::getPages()
+ {
+ QList<BasePage*> pages;
+ pages.append(ModrinthShaderPackPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameShaderPackPage::create(this, *m_instance));
+ return pages;
+ }
+
+ void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr<Metadata::ModStruct>& meta)
+ {
+ switch (meta->provider)
+ {
+ case ModPlatform::ResourceProvider::MODRINTH: selectPage(Modrinth::id()); break;
+ case ModPlatform::ResourceProvider::FLAME: selectPage(Flame::id()); break;
+ }
+ setWindowTitle(tr("Change %1 version").arg(meta->name));
+ m_container->hidePageList();
+ m_buttons.hide();
+ auto page = selectedPage();
+ page->openProject(meta->project_id);
+ }
+
+ DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<DataPackFolderModel>& data_packs,
+ BaseInstance* instance)
+ : ResourceDownloadDialog(parent, data_packs),
+ m_instance(instance)
+ {
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
+ }
+
+ QList<BasePage*> DataPackDownloadDialog::getPages()
+ {
+ QList<BasePage*> pages;
+ pages.append(ModrinthDataPackPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameDataPackPage::create(this, *m_instance));
+ return pages;
+ }
+
+} // namespace ResourceDownload
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h
new file mode 100644
index 0000000000..c7862a7e2c
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHash>
+#include <QLayout>
+
+#include "QObjectPtr.h"
+#include "minecraft/mod/DataPackFolderModel.hpp"
+#include "minecraft/mod/tasks/GetModDependenciesTask.hpp"
+#include "modplatform/ModIndex.h"
+#include "ui/pages/BasePageProvider.h"
+
+class BaseInstance;
+class ModFolderModel;
+class PageContainer;
+class QVBoxLayout;
+class QDialogButtonBox;
+class ResourceDownloadTask;
+class ResourceFolderModel;
+class ResourcePackFolderModel;
+class TexturePackFolderModel;
+class ShaderPackFolderModel;
+class QPushButton;
+
+namespace ResourceDownload
+{
+
+ class ResourcePage;
+
+ class ResourceDownloadDialog : public QDialog, public BasePageProvider
+ {
+ Q_OBJECT
+
+ public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
+ ResourceDownloadDialog(QWidget* parent, std::shared_ptr<ResourceFolderModel> base_model);
+
+ void initializeContainer();
+ void connectButtons();
+
+ //: String that gets appended to the download dialog title ("Download " + resourcesString())
+ virtual QString resourcesString() const
+ {
+ return tr("resources");
+ }
+
+ QString dialogTitle() override
+ {
+ return tr("Download %1").arg(resourcesString());
+ };
+
+ bool selectPage(QString pageId);
+ ResourcePage* selectedPage();
+
+ void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
+ void removeResource(const QString&);
+
+ const QList<DownloadTaskPtr> getTasks();
+ const std::shared_ptr<ResourceFolderModel> getBaseModel() const
+ {
+ return m_base_model;
+ }
+
+ void setResourceMetadata(const std::shared_ptr<Metadata::ModStruct>& meta);
+
+ public slots:
+ void accept() override;
+ void reject() override;
+
+ protected slots:
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ virtual void confirm();
+
+ protected:
+ virtual QString geometrySaveKey() const
+ {
+ return "";
+ }
+ void setButtonStatus();
+
+ virtual GetModDependenciesTask::Ptr getModDependenciesTask()
+ {
+ return nullptr;
+ }
+
+ protected:
+ const std::shared_ptr<ResourceFolderModel> m_base_model;
+
+ PageContainer* m_container = nullptr;
+
+ QDialogButtonBox m_buttons;
+ QVBoxLayout m_vertical_layout;
+ };
+
+ class ModDownloadDialog final : public ResourceDownloadDialog
+ {
+ Q_OBJECT
+
+ public:
+ explicit ModDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ModFolderModel>& mods,
+ BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
+
+ //: String that gets appended to the mod download dialog title ("Download " + resourcesString())
+ QString resourcesString() const override
+ {
+ return tr("mods");
+ }
+ QString geometrySaveKey() const override
+ {
+ return "ModDownloadGeometry";
+ }
+
+ QList<BasePage*> getPages() override;
+ GetModDependenciesTask::Ptr getModDependenciesTask() override;
+
+ private slots:
+ void importModrinthCollection();
+
+ private:
+ ResourcePage* modrinthPage() const;
+
+ BaseInstance* m_instance;
+ QPushButton* m_importModrinthCollectionButton = nullptr;
+ };
+
+ class ResourcePackDownloadDialog final : public ResourceDownloadDialog
+ {
+ Q_OBJECT
+
+ public:
+ explicit ResourcePackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
+ BaseInstance* instance);
+ ~ResourcePackDownloadDialog() override = default;
+
+ //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString())
+ QString resourcesString() const override
+ {
+ return tr("resource packs");
+ }
+ QString geometrySaveKey() const override
+ {
+ return "RPDownloadGeometry";
+ }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+ };
+
+ class TexturePackDownloadDialog final : public ResourceDownloadDialog
+ {
+ Q_OBJECT
+
+ public:
+ explicit TexturePackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<TexturePackFolderModel>& resource_packs,
+ BaseInstance* instance);
+ ~TexturePackDownloadDialog() override = default;
+
+ //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString())
+ QString resourcesString() const override
+ {
+ return tr("texture packs");
+ }
+ QString geometrySaveKey() const override
+ {
+ return "TPDownloadGeometry";
+ }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+ };
+
+ class ShaderPackDownloadDialog final : public ResourceDownloadDialog
+ {
+ Q_OBJECT
+
+ public:
+ explicit ShaderPackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<ShaderPackFolderModel>& shader_packs,
+ BaseInstance* instance);
+ ~ShaderPackDownloadDialog() override = default;
+
+ //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString())
+ QString resourcesString() const override
+ {
+ return tr("shader packs");
+ }
+ QString geometrySaveKey() const override
+ {
+ return "ShaderDownloadGeometry";
+ }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+ };
+
+ class DataPackDownloadDialog final : public ResourceDownloadDialog
+ {
+ Q_OBJECT
+
+ public:
+ explicit DataPackDownloadDialog(QWidget* parent,
+ const std::shared_ptr<DataPackFolderModel>& data_packs,
+ BaseInstance* instance);
+ ~DataPackDownloadDialog() override = default;
+
+ //: String that gets appended to the data pack download dialog title ("Download " + resourcesString())
+ QString resourcesString() const override
+ {
+ return tr("data packs");
+ }
+ QString geometrySaveKey() const override
+ {
+ return "DataPackDownloadGeometry";
+ }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+ };
+
+} // namespace ResourceDownload
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp
new file mode 100644
index 0000000000..3bc49dc519
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp
@@ -0,0 +1,659 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ResourceUpdateDialog.h"
+#include "Application.h"
+#include "ChooseProviderDialog.h"
+#include "CustomMessageBox.h"
+#include "ProgressDialog.h"
+#include "ScrollMessageBox.h"
+#include "StringUtils.h"
+#include "minecraft/mod/tasks/GetModDependenciesTask.hpp"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "tasks/SequentialTask.h"
+#include "ui_ReviewMessageBox.h"
+
+#include "Markdown.h"
+
+#include "tasks/ConcurrentTask.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+#include "modplatform/EnsureMetadataTask.h"
+#include "modplatform/flame/FlameCheckUpdate.h"
+#include "modplatform/modrinth/ModrinthCheckUpdate.h"
+
+#include <QClipboard>
+#include <QShortcut>
+#include <QTextBrowser>
+#include <QTreeWidgetItem>
+
+#include <optional>
+
+static std::list<Version> mcVersions(BaseInstance* inst)
+{
+ return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
+}
+
+ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ const std::shared_ptr<ResourceFolderModel> resourceModel,
+ QList<Resource*>& searchFor,
+ bool includeDeps,
+ QList<ModPlatform::ModLoaderType> loadersList)
+ : ReviewMessageBox(parent, tr("Confirm resources to update"), ""),
+ m_parent(parent),
+ m_resourceModel(resourceModel),
+ m_candidates(searchFor),
+ m_secondTryMetadata(new ConcurrentTask("Second Metadata Search",
+ APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())),
+ m_instance(instance),
+ m_includeDeps(includeDeps),
+ m_loadersList(std::move(loadersList))
+{
+ ReviewMessageBox::setGeometry(0, 0, 800, 600);
+
+ ui->explainLabel->setText(tr("You're about to update the following resources:"));
+ ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!"));
+}
+
+void ResourceUpdateDialog::checkCandidates()
+{
+ // Ensure mods have valid metadata
+ auto went_well = ensureMetadata();
+ if (!went_well)
+ {
+ m_aborted = true;
+ return;
+ }
+
+ // Report failed metadata generation
+ if (!m_failedMetadata.empty())
+ {
+ QString text;
+ for (const auto& failed : m_failedMetadata)
+ {
+ const auto& mod = std::get<0>(failed);
+ const auto& reason = std::get<1>(failed);
+ text += tr("Mod name: %1<br>File name: %2<br>Reason: %3<br><br>")
+ .arg(mod->name(), mod->fileinfo().fileName(), reason);
+ }
+
+ ScrollMessageBox message_dialog(m_parent,
+ tr("Metadata generation failed"),
+ tr("Could not generate metadata for the following resources:<br>"
+ "Do you wish to proceed without those resources?"),
+ text);
+ message_dialog.setModal(true);
+ if (message_dialog.exec() == QDialog::Rejected)
+ {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ }
+
+ auto versions = mcVersions(m_instance);
+
+ SequentialTask check_task(tr("Checking for updates"));
+
+ if (!m_modrinthToUpdate.empty())
+ {
+ m_modrinthCheckTask.reset(
+ new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel));
+ connect(m_modrinthCheckTask.get(),
+ &CheckUpdateTask::checkFailed,
+ this,
+ [this](Resource* resource, QString reason, QUrl recover_url)
+ { m_failedCheckUpdate.append({ resource, reason, recover_url }); });
+ check_task.addTask(m_modrinthCheckTask);
+ }
+
+ if (!m_flameToUpdate.empty())
+ {
+ m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel));
+ connect(m_flameCheckTask.get(),
+ &CheckUpdateTask::checkFailed,
+ this,
+ [this](Resource* resource, QString reason, QUrl recover_url)
+ { m_failedCheckUpdate.append({ resource, reason, recover_url }); });
+ check_task.addTask(m_flameCheckTask);
+ }
+
+ connect(&check_task,
+ &Task::failed,
+ this,
+ [this](QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ connect(
+ &check_task,
+ &Task::succeeded,
+ this,
+ [this, &check_task]()
+ {
+ QStringList warnings = check_task.warnings();
+ if (warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
+ }
+ });
+
+ // Check for updates
+ ProgressDialog progress_dialog(m_parent);
+ progress_dialog.setSkipButton(true, tr("Abort"));
+ progress_dialog.setWindowTitle(tr("Checking for updates..."));
+ auto ret = progress_dialog.execWithTask(check_task);
+
+ // If the dialog was skipped / some download error happened
+ if (ret == QDialog::DialogCode::Rejected)
+ {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+
+ QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers;
+
+ // Add found updates for Modrinth
+ if (m_modrinthCheckTask)
+ {
+ auto modrinth_updates = m_modrinthCheckTask->getUpdates();
+ for (auto& updatable : modrinth_updates)
+ {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendResource(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ selectedVers.append(m_modrinthCheckTask->getDependencies());
+ }
+
+ // Add found updated for Flame
+ if (m_flameCheckTask)
+ {
+ auto flame_updates = m_flameCheckTask->getUpdates();
+ for (auto& updatable : flame_updates)
+ {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendResource(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ selectedVers.append(m_flameCheckTask->getDependencies());
+ }
+
+ // Report failed update checking
+ if (!m_failedCheckUpdate.empty())
+ {
+ QString text;
+ for (const auto& failed : m_failedCheckUpdate)
+ {
+ const auto& mod = std::get<0>(failed);
+ const auto& reason = std::get<1>(failed);
+ const auto& recover_url = std::get<2>(failed);
+
+ qDebug() << mod->name() << " failed to check for updates!";
+
+ text += tr("Mod name: %1").arg(mod->name()) + "<br>";
+ if (!reason.isEmpty())
+ text += tr("Reason: %1").arg(reason) + "<br>";
+ if (!recover_url.isEmpty())
+ //: %1 is the link to download it manually
+ text += tr("Possible solution: Getting the latest version manually:<br>%1<br>")
+ .arg(QString("<a href='%1'>%1</a>").arg(recover_url.toString()));
+ text += "<br>";
+ }
+
+ ScrollMessageBox message_dialog(m_parent,
+ tr("Failed to check for updates"),
+ tr("Could not check or get the following resources for updates:<br>"
+ "Do you wish to proceed without those resources?"),
+ text);
+ message_dialog.setModal(true);
+ if (message_dialog.exec() == QDialog::Rejected)
+ {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ }
+
+ if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool())
+ { // dependencies
+ auto* mod_model = dynamic_cast<ModFolderModel*>(m_resourceModel.get());
+
+ if (mod_model != nullptr)
+ {
+ auto depTask = makeShared<GetModDependenciesTask>(m_instance, mod_model, selectedVers);
+
+ connect(depTask.get(),
+ &Task::failed,
+ this,
+ [this](const QString& reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+ auto weak = depTask.toWeakRef();
+ connect(
+ depTask.get(),
+ &Task::succeeded,
+ this,
+ [this, weak]()
+ {
+ QStringList warnings;
+ if (auto depTask = weak.lock())
+ {
+ warnings = depTask->warnings();
+ }
+ if (warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)
+ ->exec();
+ }
+ });
+
+ ProgressDialog progress_dialog_deps(m_parent);
+ progress_dialog_deps.setSkipButton(true, tr("Abort"));
+ progress_dialog_deps.setWindowTitle(tr("Checking for dependencies..."));
+ auto dret = progress_dialog_deps.execWithTask(*depTask);
+
+ // If the dialog was skipped / some download error happened
+ if (dret == QDialog::DialogCode::Rejected)
+ {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ static FlameAPI api;
+
+ auto dependencyExtraInfo = depTask->getExtraInfo();
+
+ for (const auto& dep : depTask->getDependecies())
+ {
+ auto changelog = dep->version.changelog;
+ if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME)
+ changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt());
+ auto download_task = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_resourceModel);
+ auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString());
+ CheckUpdateTask::Update updatable = { dep->pack->name, dep->version.hash,
+ tr("Not installed"), dep->version.version,
+ dep->version.version_type, changelog,
+ dep->pack->provider, download_task,
+ !extraInfo.maybe_installed };
+
+ appendResource(updatable, extraInfo.required_by);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ }
+ }
+
+ // If there's no resource to be updated
+ if (ui->modTreeWidget->topLevelItemCount() == 0)
+ {
+ m_noUpdates = true;
+ }
+ else
+ {
+ // Sort top-level items alphabetically, then sort each item's children in descending order
+ // We disable sorting during population and enable only for the final sort
+ ui->modTreeWidget->setSortingEnabled(false);
+
+ // Collect all top-level items and sort them
+ QList<QTreeWidgetItem*> items;
+ while (ui->modTreeWidget->topLevelItemCount() > 0)
+ {
+ items.append(ui->modTreeWidget->takeTopLevelItem(0));
+ }
+
+ // Sort items alphabetically by their text
+ std::sort(items.begin(),
+ items.end(),
+ [](QTreeWidgetItem* a, QTreeWidgetItem* b)
+ { return a->text(0).compare(b->text(0), Qt::CaseInsensitive) < 0; });
+
+ // Re-add items and sort each item's children in descending order
+ for (auto* item : items)
+ {
+ // Sort children before adding back
+ QList<QTreeWidgetItem*> children;
+ while (item->childCount() > 0)
+ {
+ children.append(item->takeChild(0));
+ }
+ std::sort(children.begin(),
+ children.end(),
+ [](QTreeWidgetItem* a, QTreeWidgetItem* b)
+ {
+ return a->text(0).compare(b->text(0), Qt::CaseInsensitive) > 0; // Descending
+ });
+ for (auto* child : children)
+ {
+ item->addChild(child);
+ }
+
+ ui->modTreeWidget->addTopLevelItem(item);
+ }
+ }
+
+ if (m_aborted || m_noUpdates)
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+}
+
+// Part 1: Ensure we have a valid metadata
+auto ResourceUpdateDialog::ensureMetadata() -> bool
+{
+ auto index_dir = indexDir();
+
+ SequentialTask seq(tr("Looking for metadata"));
+
+ // A better use of data structures here could remove the need for this QHash
+ QHash<QString, bool> should_try_others;
+ QList<Resource*> modrinth_tmp;
+ QList<Resource*> flame_tmp;
+
+ bool confirm_rest = false;
+ bool try_others_rest = false;
+ bool skip_rest = false;
+ ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
+
+ // adds resource to list based on provider
+ auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p)
+ {
+ switch (p)
+ {
+ case ModPlatform::ResourceProvider::MODRINTH: modrinth_tmp.push_back(resource); break;
+ case ModPlatform::ResourceProvider::FLAME: flame_tmp.push_back(resource); break;
+ }
+ };
+
+ // ask the user on what provider to seach for the mod first
+ for (auto candidate : m_candidates)
+ {
+ if (candidate->status() != ResourceStatus::NO_METADATA)
+ {
+ onMetadataEnsured(candidate);
+ continue;
+ }
+
+ if (skip_rest)
+ continue;
+
+ if (candidate->type() == ResourceType::FOLDER)
+ {
+ continue;
+ }
+
+ if (confirm_rest)
+ {
+ addToTmp(candidate, provider_rest);
+ should_try_others.insert(candidate->internal_id(), try_others_rest);
+ continue;
+ }
+
+ ChooseProviderDialog chooser(this);
+ chooser.setDescription(
+ tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant "
+ "information on how to update this mod. "
+ "To do this, please select a mod provider which we can use to check for updates for this mod.")
+ .arg(candidate->name()));
+ auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted;
+
+ auto response = chooser.getResponse();
+
+ if (response.skip_all)
+ skip_rest = true;
+ if (response.confirm_all)
+ {
+ confirm_rest = true;
+ provider_rest = response.chosen;
+ try_others_rest = response.try_others;
+ }
+
+ should_try_others.insert(candidate->internal_id(), response.try_others);
+
+ if (confirmed)
+ addToTmp(candidate, response.chosen);
+ }
+
+ // prepare task for the modrinth mods
+ if (!modrinth_tmp.empty())
+ {
+ auto modrinth_task =
+ makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
+ connect(modrinth_task.get(),
+ &EnsureMetadataTask::metadataReady,
+ [this](Resource* candidate) { onMetadataEnsured(candidate); });
+ connect(modrinth_task.get(),
+ &EnsureMetadataTask::metadataFailed,
+ [this, &should_try_others](Resource* candidate)
+ {
+ onMetadataFailed(candidate,
+ should_try_others.find(candidate->internal_id()).value(),
+ ModPlatform::ResourceProvider::MODRINTH);
+ });
+ connect(modrinth_task.get(),
+ &EnsureMetadataTask::failed,
+ [this](QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ if (modrinth_task->getHashingTask())
+ seq.addTask(modrinth_task->getHashingTask());
+
+ seq.addTask(modrinth_task);
+ }
+
+ // prepare task for the flame mods
+ if (!flame_tmp.empty())
+ {
+ auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
+ connect(flame_task.get(),
+ &EnsureMetadataTask::metadataReady,
+ [this](Resource* candidate) { onMetadataEnsured(candidate); });
+ connect(flame_task.get(),
+ &EnsureMetadataTask::metadataFailed,
+ [this, &should_try_others](Resource* candidate)
+ {
+ onMetadataFailed(candidate,
+ should_try_others.find(candidate->internal_id()).value(),
+ ModPlatform::ResourceProvider::FLAME);
+ });
+ connect(flame_task.get(),
+ &EnsureMetadataTask::failed,
+ [this](QString reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ if (flame_task->getHashingTask())
+ seq.addTask(flame_task->getHashingTask());
+
+ seq.addTask(flame_task);
+ }
+
+ seq.addTask(m_secondTryMetadata);
+
+ // execute all the tasks
+ ProgressDialog checking_dialog(m_parent);
+ checking_dialog.setSkipButton(true, tr("Abort"));
+ checking_dialog.setWindowTitle(tr("Generating metadata..."));
+ auto ret_metadata = checking_dialog.execWithTask(seq);
+
+ return (ret_metadata != QDialog::DialogCode::Rejected);
+}
+
+void ResourceUpdateDialog::onMetadataEnsured(Resource* resource)
+{
+ // When the mod is a folder, for instance
+ if (!resource->metadata())
+ return;
+
+ switch (resource->metadata()->provider)
+ {
+ case ModPlatform::ResourceProvider::MODRINTH: m_modrinthToUpdate.push_back(resource); break;
+ case ModPlatform::ResourceProvider::FLAME: m_flameToUpdate.push_back(resource); break;
+ }
+}
+
+ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
+{
+ switch (p)
+ {
+ case ModPlatform::ResourceProvider::MODRINTH: return ModPlatform::ResourceProvider::FLAME;
+ case ModPlatform::ResourceProvider::FLAME: return ModPlatform::ResourceProvider::MODRINTH;
+ }
+
+ return ModPlatform::ResourceProvider::FLAME;
+}
+
+void ResourceUpdateDialog::onMetadataFailed(Resource* resource,
+ bool try_others,
+ ModPlatform::ResourceProvider first_choice)
+{
+ if (try_others)
+ {
+ auto index_dir = indexDir();
+
+ auto task = makeShared<EnsureMetadataTask>(resource, index_dir, next(first_choice));
+ connect(task.get(),
+ &EnsureMetadataTask::metadataReady,
+ [this](Resource* candidate) { onMetadataEnsured(candidate); });
+ connect(task.get(),
+ &EnsureMetadataTask::metadataFailed,
+ [this](Resource* candidate) { onMetadataFailed(candidate, false); });
+ connect(task.get(),
+ &EnsureMetadataTask::failed,
+ [this](const QString& reason)
+ { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+ if (task->getHashingTask())
+ {
+ auto seq = makeShared<SequentialTask>();
+ seq->addTask(task->getHashingTask());
+ seq->addTask(task);
+ m_secondTryMetadata->addTask(seq);
+ }
+ else
+ {
+ m_secondTryMetadata->addTask(task);
+ }
+ }
+ else
+ {
+ QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
+
+ m_failedMetadata.append({ resource, reason });
+ }
+}
+
+void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy)
+{
+ auto item_top = new QTreeWidgetItem(ui->modTreeWidget);
+ item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
+ if (!info.enabled)
+ {
+ item_top->setToolTip(0, tr("Mod was disabled as it may be already installed."));
+ }
+ item_top->setText(0, info.name);
+ item_top->setExpanded(true);
+
+ auto provider_item = new QTreeWidgetItem(item_top);
+ QString provider_name = ModPlatform::ProviderCapabilities::readableName(info.provider);
+ provider_item->setText(0, tr("Provider: %1").arg(provider_name));
+ provider_item->setData(0, Qt::UserRole, provider_name);
+
+ auto old_version_item = new QTreeWidgetItem(item_top);
+ old_version_item->setText(0, tr("Old version: %1").arg(info.old_version));
+ old_version_item->setData(0, Qt::UserRole, info.old_version);
+
+ auto new_version_item = new QTreeWidgetItem(item_top);
+ new_version_item->setText(0, tr("New version: %1").arg(info.new_version));
+ new_version_item->setData(0, Qt::UserRole, info.new_version);
+
+ if (info.new_version_type.has_value())
+ {
+ auto new_version_type_item = new QTreeWidgetItem(item_top);
+ new_version_type_item->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString()));
+ new_version_type_item->setData(0, Qt::UserRole, info.new_version_type.value().toString());
+ }
+
+ if (!requiredBy.isEmpty())
+ {
+ auto requiredByItem = new QTreeWidgetItem(item_top);
+ if (requiredBy.length() == 1)
+ {
+ requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back()));
+ }
+ else
+ {
+ requiredByItem->setText(0, tr("Required by:"));
+ auto i = 0;
+ for (auto req : requiredBy)
+ {
+ auto reqItem = new QTreeWidgetItem(requiredByItem);
+ reqItem->setText(0, req);
+ reqItem->insertChildren(i++, { reqItem });
+ }
+ }
+
+ ui->toggleDepsButton->show();
+ m_deps << item_top;
+ }
+
+ auto changelog_item = new QTreeWidgetItem(item_top);
+ changelog_item->setText(0, tr("Changelog of the latest version"));
+
+ auto changelog = new QTreeWidgetItem(changelog_item);
+ auto changelog_area = new QTextBrowser();
+
+ QString text = info.changelog;
+ changelog->setData(0, Qt::UserRole, text);
+ if (info.provider == ModPlatform::ResourceProvider::MODRINTH)
+ {
+ text = markdownToHTML(info.changelog.toUtf8());
+ }
+
+ changelog_area->setHtml(StringUtils::htmlListPatch(text));
+ changelog_area->setOpenExternalLinks(true);
+ changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
+ changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
+
+ ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area);
+
+ ui->modTreeWidget->addTopLevelItem(item_top);
+}
+
+auto ResourceUpdateDialog::getTasks() -> const QList<ResourceDownloadTask::Ptr>
+{
+ QList<ResourceDownloadTask::Ptr> list;
+
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+
+ for (int i = 1; item != nullptr; ++i)
+ {
+ if (item->checkState(0) == Qt::CheckState::Checked)
+ {
+ auto taskIt = m_tasks.find(item->text(0));
+ if (taskIt != m_tasks.end() && taskIt.value())
+ list.push_back(taskIt.value());
+ }
+
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+
+ return list;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h
new file mode 100644
index 0000000000..6b784fe192
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "BaseInstance.h"
+#include "ResourceDownloadTask.h"
+#include "ReviewMessageBox.h"
+
+#include "minecraft/mod/ModFolderModel.hpp"
+
+#include "modplatform/CheckUpdateTask.h"
+
+class Mod;
+class ModrinthCheckUpdate;
+class FlameCheckUpdate;
+class ConcurrentTask;
+
+class ResourceUpdateDialog final : public ReviewMessageBox
+{
+ Q_OBJECT
+ public:
+ explicit ResourceUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ std::shared_ptr<ResourceFolderModel> resourceModel,
+ QList<Resource*>& searchFor,
+ bool includeDeps,
+ QList<ModPlatform::ModLoaderType> loadersList = {});
+
+ void checkCandidates();
+
+ void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {});
+
+ const QList<ResourceDownloadTask::Ptr> getTasks();
+ auto indexDir() const -> QDir
+ {
+ return m_resourceModel->indexDir();
+ }
+
+ auto noUpdates() const -> bool
+ {
+ return m_noUpdates;
+ };
+ auto aborted() const -> bool
+ {
+ return m_aborted;
+ };
+
+ private:
+ auto ensureMetadata() -> bool;
+
+ private slots:
+ void onMetadataEnsured(Resource* resource);
+ void onMetadataFailed(Resource* resource,
+ bool try_others = false,
+ ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH);
+
+ private:
+ QWidget* m_parent;
+
+ shared_qobject_ptr<ModrinthCheckUpdate> m_modrinthCheckTask;
+ shared_qobject_ptr<FlameCheckUpdate> m_flameCheckTask;
+
+ const std::shared_ptr<ResourceFolderModel> m_resourceModel;
+
+ QList<Resource*>& m_candidates;
+ QList<Resource*> m_modrinthToUpdate;
+ QList<Resource*> m_flameToUpdate;
+
+ ConcurrentTask::Ptr m_secondTryMetadata;
+ QList<std::tuple<Resource*, QString>> m_failedMetadata;
+ QList<std::tuple<Resource*, QString, QUrl>> m_failedCheckUpdate;
+
+ QHash<QString, ResourceDownloadTask::Ptr> m_tasks;
+ BaseInstance* m_instance;
+
+ bool m_noUpdates = false;
+ bool m_aborted = false;
+ bool m_includeDeps = false;
+ QList<ModPlatform::ModLoaderType> m_loadersList;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp
new file mode 100644
index 0000000000..a3a018fb3a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ReviewMessageBox.h"
+#include "ui_ReviewMessageBox.h"
+
+#include <QClipboard>
+#include <QPushButton>
+#include <QShortcut>
+
+ReviewMessageBox::ReviewMessageBox(QWidget* parent,
+ [[maybe_unused]] QString const& title,
+ [[maybe_unused]] QString const& icon)
+ : QDialog(parent),
+ ui(new Ui::ReviewMessageBox)
+{
+ ui->setupUi(this);
+
+ auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ back_button->setText(tr("Back"));
+
+ ui->toggleDepsButton->hide();
+ ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->modTreeWidget->header()->setStretchLastSection(false);
+ ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+
+ // Overwrite Ctrl+C functionality to exclude the label when copying text from tree
+ auto shortcut = new QShortcut(QKeySequence::Copy, ui->modTreeWidget);
+ connect(shortcut,
+ &QShortcut::activated,
+ [this]()
+ {
+ auto currentItem = this->ui->modTreeWidget->currentItem();
+ if (!currentItem)
+ return;
+ auto currentColumn = this->ui->modTreeWidget->currentColumn();
+
+ auto data = currentItem->data(currentColumn, Qt::UserRole);
+ QString txt;
+
+ if (data.isValid())
+ {
+ txt = data.toString();
+ }
+ else
+ {
+ txt = currentItem->text(currentColumn);
+ }
+
+ QApplication::clipboard()->setText(txt);
+ });
+}
+
+ReviewMessageBox::~ReviewMessageBox()
+{
+ delete ui;
+}
+
+auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) -> ReviewMessageBox*
+{
+ return new ReviewMessageBox(parent, title, icon);
+}
+
+void ReviewMessageBox::appendResource(ResourceInformation&& info)
+{
+ auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
+ itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
+ itemTop->setText(0, info.name);
+ if (!info.enabled)
+ {
+ itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed."));
+ }
+
+ auto filenameItem = new QTreeWidgetItem(itemTop);
+ filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
+ filenameItem->setData(0, Qt::UserRole, info.filename);
+
+ auto childIndx = 0;
+ itemTop->insertChildren(childIndx++, { filenameItem });
+
+ if (!info.custom_file_path.isEmpty())
+ {
+ auto customPathItem = new QTreeWidgetItem(itemTop);
+ customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
+
+ itemTop->insertChildren(1, { customPathItem });
+
+ itemTop->setIcon(1, QIcon(QIcon::fromTheme("status-yellow")));
+ itemTop->setToolTip(childIndx++,
+ tr("This file will be downloaded to a folder location different from the default, possibly "
+ "due to its loader requiring it."));
+ }
+
+ auto providerItem = new QTreeWidgetItem(itemTop);
+ providerItem->setText(0, tr("Provider: %1").arg(info.provider));
+ providerItem->setData(0, Qt::UserRole, info.provider);
+
+ itemTop->insertChildren(childIndx++, { providerItem });
+
+ if (!info.required_by.isEmpty())
+ {
+ auto requiredByItem = new QTreeWidgetItem(itemTop);
+ if (info.required_by.length() == 1)
+ {
+ requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back()));
+ }
+ else
+ {
+ requiredByItem->setText(0, tr("Required by:"));
+ auto i = 0;
+ for (auto req : info.required_by)
+ {
+ auto reqItem = new QTreeWidgetItem(requiredByItem);
+ reqItem->setText(0, req);
+ reqItem->insertChildren(i++, { reqItem });
+ }
+ }
+
+ itemTop->insertChildren(childIndx++, { requiredByItem });
+ ui->toggleDepsButton->show();
+ m_deps << itemTop;
+ }
+
+ auto versionTypeItem = new QTreeWidgetItem(itemTop);
+ versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type));
+ versionTypeItem->setData(0, Qt::UserRole, info.version_type);
+
+ itemTop->insertChildren(childIndx++, { versionTypeItem });
+
+ ui->modTreeWidget->addTopLevelItem(itemTop);
+}
+
+auto ReviewMessageBox::deselectedResources() -> QStringList
+{
+ QStringList list;
+
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+
+ for (int i = 1; item != nullptr; ++i)
+ {
+ if (item->checkState(0) == Qt::CheckState::Unchecked)
+ {
+ list.append(item->text(0));
+ }
+
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+
+ return list;
+}
+
+void ReviewMessageBox::retranslateUi(QString resources_name)
+{
+ setWindowTitle(tr("Confirm %1 selection").arg(resources_name));
+
+ ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
+ ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
+}
+void ReviewMessageBox::on_toggleDepsButton_clicked()
+{
+ m_deps_checked = !m_deps_checked;
+ auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked;
+ for (auto dep : m_deps)
+ dep->setCheckState(0, state);
+}; \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h
new file mode 100644
index 0000000000..3a95da46c4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QDialog>
+#include <QTreeWidgetItem>
+
+namespace Ui
+{
+ class ReviewMessageBox;
+}
+
+class ReviewMessageBox : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
+
+ using ResourceInformation = struct res_info
+ {
+ QString name;
+ QString filename;
+ QString custom_file_path{};
+ QString provider;
+ QStringList required_by;
+ QString version_type;
+ bool enabled = true;
+ };
+
+ void appendResource(ResourceInformation&& info);
+ auto deselectedResources() -> QStringList;
+
+ void retranslateUi(QString resources_name);
+
+ ~ReviewMessageBox() override;
+
+ protected slots:
+ void on_toggleDepsButton_clicked();
+
+ protected:
+ ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
+
+ Ui::ReviewMessageBox* ui;
+
+ QList<QTreeWidgetItem*> m_deps;
+ bool m_deps_checked = true;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui
new file mode 100644
index 0000000000..dbe351019c
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReviewMessageBox</class>
+ <widget class="QDialog" name="ReviewMessageBox">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>500</width>
+ <height>350</height>
+ </rect>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0">
+ <widget class="QTreeWidget" name="modTreeWidget">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::NoSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectItems</enum>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="explainLabel"/>
+ </item>
+ <item row="5" column="0" rowspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="toggleDepsButton">
+ <property name="text">
+ <string>Toggle Dependencies</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="onlyCheckedLabel"/>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp
new file mode 100644
index 0000000000..807eb0af12
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ScrollMessageBox.h"
+#include <QPushButton>
+#include "ui_ScrollMessageBox.h"
+
+ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body)
+ : QDialog(parent),
+ ui(new Ui::ScrollMessageBox)
+{
+ ui->setupUi(this);
+ this->setWindowTitle(title);
+ ui->label->setText(text);
+ ui->textBrowser->setText(body);
+
+ ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+}
+
+ScrollMessageBox::~ScrollMessageBox()
+{
+ delete ui;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h
new file mode 100644
index 0000000000..5967c65788
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QDialog>
+
+QT_BEGIN_NAMESPACE
+namespace Ui
+{
+ class ScrollMessageBox;
+}
+QT_END_NAMESPACE
+
+class ScrollMessageBox : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body);
+
+ ~ScrollMessageBox() override;
+
+ private:
+ Ui::ScrollMessageBox* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui
new file mode 100644
index 0000000000..e684185f24
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ScrollMessageBox</class>
+ <widget class="QDialog" name="ScrollMessageBox">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>500</width>
+ <height>455</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">ScrollMessageBox</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <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>
+ <item row="1" column="0">
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="acceptRichText">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ScrollMessageBox</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ScrollMessageBox</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp
new file mode 100644
index 0000000000..3a793d0afa
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+
+#include "UpdateAvailableDialog.h"
+#include <QPushButton>
+#include "BuildConfig.h"
+#include "Markdown.h"
+#include "StringUtils.h"
+#include "ui_UpdateAvailableDialog.h"
+
+UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
+ const QString& availableVersion,
+ const QString& releaseNotes,
+ Mode mode,
+ QWidget* parent)
+ : QDialog(parent),
+ ui(new Ui::UpdateAvailableDialog)
+{
+ ui->setupUi(this);
+
+ QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME;
+
+ if (mode == Mode::Migration)
+ {
+ ui->headerLabel->setText(tr("A new release line of %1 is available!").arg(launcherName));
+ ui->versionAvailableLabel->setText(tr("Version %1 is part of a new release line.\n"
+ "You are currently on %2. Would you like to migrate now?")
+ .arg(availableVersion)
+ .arg(currentVersion));
+ }
+ else
+ {
+ ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName));
+ ui->versionAvailableLabel->setText(
+ tr("Version %1 is now available - you have %2 . Would you like to download it now?")
+ .arg(availableVersion)
+ .arg(currentVersion));
+ }
+ ui->icon->setPixmap(QIcon::fromTheme("checkupdate").pixmap(64));
+
+ auto releaseNotesHtml = markdownToHTML(releaseNotes);
+ ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
+ ui->releaseNotes->setOpenExternalLinks(true);
+
+ connect(ui->skipButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ setResult(ResultCode::Skip);
+ done(ResultCode::Skip);
+ });
+
+ connect(ui->delayButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ setResult(ResultCode::DontInstall);
+ done(ResultCode::DontInstall);
+ });
+
+ connect(ui->installButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ setResult(ResultCode::Install);
+ done(ResultCode::Install);
+ });
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h
new file mode 100644
index 0000000000..27ba508261
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+#pragma once
+
+#include <QDialog>
+
+namespace Ui
+{
+ class UpdateAvailableDialog;
+}
+
+class UpdateAvailableDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ enum class Mode
+ {
+ Update,
+ Migration
+ };
+
+ enum ResultCode
+ {
+ Install = 10,
+ DontInstall = 11,
+ Skip = 12,
+ };
+
+ explicit UpdateAvailableDialog(const QString& currentVersion,
+ const QString& availableVersion,
+ const QString& releaseNotes,
+ Mode mode = Mode::Update,
+ QWidget* parent = 0);
+ ~UpdateAvailableDialog() = default;
+
+ private:
+ Ui::UpdateAvailableDialog* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui
new file mode 100644
index 0000000000..b0d85f6f00
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateAvailableDialog</class>
+ <widget class="QDialog" name="UpdateAvailableDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>636</width>
+ <height>352</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Update Available</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
+ <item>
+ <layout class="QVBoxLayout" name="leftsideLayout">
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="minimumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </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>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <property name="leftMargin">
+ <number>9</number>
+ </property>
+ <property name="topMargin">
+ <number>9</number>
+ </property>
+ <property name="rightMargin">
+ <number>9</number>
+ </property>
+ <property name="bottomMargin">
+ <number>9</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="headerLabel">
+ <property name="font">
+ <font>
+ <pointsize>11</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>A new version is available!</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="versionAvailableLabel">
+ <property name="text">
+ <string>Version %1 is now available - you have %2 . Would you like to download it now?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="releaseNotesLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Release Notes:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="releaseNotes"/>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="skipButton">
+ <property name="text">
+ <string>Skip This Version</string>
+ </property>
+ </widget>
+ </item>
+ <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="delayButton">
+ <property name="text">
+ <string>Remind Me Later</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="installButton">
+ <property name="text">
+ <string>Install Update</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp
new file mode 100644
index 0000000000..e0ed670ed6
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QDebug>
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QDialogButtonBox>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QPushButton>
+#include <QtWidgets/QVBoxLayout>
+
+#include "ui/widgets/VersionSelectWidget.h"
+
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+
+VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent, bool cancelable)
+ : QDialog(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectDialog"));
+ resize(400, 347);
+ m_title = title; // Store title for retranslation
+ 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_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok"));
+ m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ m_horizontalLayout->addWidget(m_buttonBox);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ retranslate();
+
+ connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(m_versionWidget->view(), &QAbstractItemView::doubleClicked, this, &QDialog::accept);
+ connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ QMetaObject::connectSlotsByName(this);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle(title);
+
+ m_vlist = vlist;
+
+ if (!cancelable)
+ {
+ m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+ }
+}
+
+void VersionSelectDialog::retranslate()
+{
+ setWindowTitle(m_title.isEmpty() ? tr("Choose Version") : m_title);
+ 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, true);
+ m_versionWidget->selectSearch();
+ if (resizeOnColumn != -1)
+ {
+ m_versionWidget->setResizeOn(resizeOnColumn);
+ }
+ return QDialog::exec();
+}
+
+void VersionSelectDialog::selectRecommended()
+{
+ m_versionWidget->selectRecommended();
+}
+
+BaseVersion::Ptr 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::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setExactIfPresentFilter(role, filter);
+}
+
+void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setFuzzyFilter(role, filter);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h
new file mode 100644
index 0000000000..ce74dff647
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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>
+
+class VersionSelectWidget;
+class QDialogButtonBox;
+class QVBoxLayout;
+class QHBoxLayout;
+class QPushButton;
+class BaseVersionList;
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+
+class VersionSelectDialog : public QDialog
+{
+ Q_OBJECT
+ public:
+ VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true);
+
+ int exec() override;
+ BaseVersion::Ptr selectedVersion() const;
+ void setResizeOn(int column);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setCurrentVersion(const QString& version);
+ void selectRecommended();
+
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+
+ public slots:
+ virtual void retranslate();
+
+ private slots:
+ void on_refreshButton_clicked();
+
+ private:
+ QVBoxLayout* m_verticalLayout;
+ QHBoxLayout* m_horizontalLayout;
+ VersionSelectWidget* m_versionWidget;
+ QPushButton* m_refreshButton;
+ QDialogButtonBox* m_buttonBox;
+ BaseVersionList* m_vlist = nullptr;
+ QString m_currentVersion;
+ QString m_title; // Added member to store the title
+ int resizeOnColumn = -1;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp
new file mode 100644
index 0000000000..d060ec8117
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp
@@ -0,0 +1,715 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "SkinManageDialog.h"
+#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h"
+#include "ui_SkinManageDialog.h"
+
+#include <FileSystem.h>
+#include <QAction>
+#include <QDialog>
+#include <QEventLoop>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QKeyEvent>
+#include <QListView>
+#include <QMimeDatabase>
+#include <QPainter>
+#include <QUrl>
+
+#include "Application.h"
+#include "DesktopServices.h"
+#include "Json.h"
+#include "QObjectPtr.h"
+
+#include "minecraft/auth/Parsers.hpp"
+#include "minecraft/skins/CapeChange.h"
+#include "minecraft/skins/CapeListModel.h"
+#include "minecraft/skins/SkinDelete.h"
+#include "minecraft/skins/SkinList.h"
+#include "minecraft/skins/SkinModel.h"
+#include "minecraft/skins/SkinUpload.h"
+
+#include "net/Download.h"
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/instanceview/InstanceDelegate.h"
+
+SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
+ : QDialog(parent),
+ m_acct(acct),
+ m_ui(new Ui::SkinManageDialog),
+ m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct)
+{
+ m_ui->setupUi(this);
+
+ m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base));
+
+ setWindowModality(Qt::WindowModal);
+
+ auto contentsWidget = m_ui->listView;
+ 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->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->installEventFilter(this);
+ contentsWidget->setItemDelegate(new ListViewDelegate(this));
+
+ contentsWidget->setAcceptDrops(true);
+ contentsWidget->setDropIndicatorShown(true);
+ contentsWidget->viewport()->setAcceptDrops(true);
+ contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
+ contentsWidget->setDefaultDropAction(Qt::CopyAction);
+
+ contentsWidget->installEventFilter(this);
+ contentsWidget->setModel(&m_list);
+
+ connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &SkinManageDialog::activated);
+
+ connect(contentsWidget->selectionModel(),
+ &QItemSelectionModel::selectionChanged,
+ this,
+ &SkinManageDialog::selectionChanged);
+ connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu);
+ connect(m_ui->elytraCB,
+ &QCheckBox::stateChanged,
+ this,
+ [this]()
+ {
+ m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked());
+ on_capeCombo_currentIndexChanged(0);
+ });
+
+ setupCapes();
+
+ m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin()));
+
+ m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+
+ m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this));
+}
+
+SkinManageDialog::~SkinManageDialog()
+{
+ delete m_ui;
+ delete m_skinPreview;
+}
+
+void SkinManageDialog::activated(QModelIndex index)
+{
+ m_selectedSkinKey = index.data(Qt::UserRole).toString();
+ accept();
+}
+
+void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected)
+{
+ if (selected.empty())
+ return;
+
+ QString key = selected.first().indexes().first().data(Qt::UserRole).toString();
+ if (key.isEmpty())
+ return;
+ m_selectedSkinKey = key;
+ auto skin = getSelectedSkin();
+ if (!skin)
+ return;
+
+ m_skinPreview->updateScene(skin);
+
+ QString capeId = skin->getCapeId();
+ int capeIndex = 0;
+ if (capeId.isEmpty())
+ {
+ capeIndex = m_capeModel->findCapeIndex("no_cape");
+ }
+ else
+ {
+ capeIndex = m_capeModel->findCapeIndex(capeId);
+ }
+
+ if (capeIndex >= 0)
+ {
+ m_ui->capeCombo->setCurrentIndex(capeIndex);
+ }
+
+ m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC);
+ m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM);
+}
+
+void SkinManageDialog::delayed_scroll(QModelIndex model_index)
+{
+ auto contentsWidget = m_ui->listView;
+ contentsWidget->scrollTo(model_index);
+}
+
+void SkinManageDialog::on_openDirBtn_clicked()
+{
+ DesktopServices::openPath(m_list.getDir(), true);
+}
+
+void SkinManageDialog::on_fileBtn_clicked()
+{
+ auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
+ if (raw_path.isNull())
+ {
+ return;
+ }
+ auto message = m_list.installSkin(raw_path, {});
+ if (!message.isEmpty())
+ {
+ CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)
+ ->show();
+ return;
+ }
+}
+
+QPixmap previewCape(QImage capeImage, bool elytra = false)
+{
+ if (elytra)
+ {
+ auto wing = capeImage.copy(34, 0, 12, 22);
+ QImage mirrored = wing.flipped(Qt::Horizontal);
+
+ QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format());
+ combined.fill(Qt::transparent);
+
+ QPainter painter(&combined);
+ painter.drawImage(0, 0, wing);
+ painter.drawImage(wing.width() - 2, 0, mirrored);
+ painter.end();
+ return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation));
+ }
+ return QPixmap::fromImage(
+ capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation));
+}
+
+void SkinManageDialog::setupCapes()
+{
+ // Create the cape model with on-demand loading support
+ m_capeModel = new CapeListModel(this);
+ m_ui->capeCombo->setModel(m_capeModel);
+
+ // Connect signals for async loading
+ connect(m_capeModel, &CapeListModel::loadingFinished, this, &SkinManageDialog::onCapesLoaded);
+ connect(m_capeModel,
+ &CapeListModel::capeLoaded,
+ this,
+ [this](const QString& capeId)
+ {
+ // Cache image for instant preview if needed
+ QImage capeImage = m_capeModel->getCapeImage(capeId);
+ if (!capeImage.isNull())
+ {
+ m_capes[capeId] = capeImage;
+ }
+ });
+
+ // Load capes from account (this triggers download if needed)
+ m_capeModel->loadFromAccount(m_acct, m_list.getDir());
+}
+
+void SkinManageDialog::onCapesLoaded()
+{
+ auto& accountData = *m_acct->accountData();
+ auto currentCape = accountData.minecraftProfile.currentCape;
+
+ // Populate local cache for all available capes from model
+ int rowCount = m_capeModel->rowCount();
+ for (int i = 0; i < rowCount; i++)
+ {
+ QString id = m_capeModel->data(m_capeModel->index(i), CapeListModel::CapeIdRole).toString();
+ QImage image = m_capeModel->data(m_capeModel->index(i), CapeListModel::CapeImageRole).value<QImage>();
+ if (!image.isNull())
+ {
+ m_capes[id] = image;
+ }
+ }
+
+ // Set initial selection
+ int currentIndex = 0;
+ if (currentCape.isEmpty())
+ {
+ currentIndex = m_capeModel->findCapeIndex("no_cape");
+ }
+ else
+ {
+ currentIndex = m_capeModel->findCapeIndex(currentCape);
+ }
+
+ if (currentIndex >= 0)
+ {
+ m_ui->capeCombo->setCurrentIndex(currentIndex);
+ }
+}
+
+void SkinManageDialog::onCapeLoadError(const QString& error)
+{
+ qWarning() << "Failed to load capes:" << error;
+ // Fall back to showing at least "No Cape" option
+ if (m_ui->capeCombo->count() == 0)
+ {
+ m_ui->capeCombo->addItem(tr("No Cape"), QVariant());
+ }
+}
+
+void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
+{
+ if (index < 0 || !m_capeModel)
+ return;
+
+ QModelIndex modelIdx = m_capeModel->index(index);
+ QString id = m_capeModel->data(modelIdx, CapeListModel::CapeIdRole).toString();
+ QImage cape = m_capes.value(id, QImage());
+
+ if (!cape.isNull())
+ {
+ m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked())
+ .scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
+ }
+ else
+ {
+ m_ui->capeImage->clear();
+ }
+
+ // For preview, use empty string for 'no_cape'
+ QString displayId = (id == "no_cape") ? "" : id;
+ m_skinPreview->updateCape(cape);
+
+ if (auto skin = getSelectedSkin(); skin)
+ {
+ skin->setCapeId(displayId);
+ m_skinPreview->updateScene(skin);
+ }
+}
+
+void SkinManageDialog::on_steveBtn_toggled(bool checked)
+{
+ if (auto skin = getSelectedSkin(); skin)
+ {
+ skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM);
+ m_skinPreview->updateScene(skin);
+ }
+}
+
+void SkinManageDialog::accept()
+{
+ auto skin = m_list.skin(m_selectedSkinKey);
+ if (!skin)
+ {
+ reject();
+ return;
+ }
+ auto path = skin->getPath();
+
+ ProgressDialog prog(this);
+ NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) };
+
+ if (!QFile::exists(path))
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)
+ ->exec();
+ reject();
+ return;
+ }
+
+ skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString()));
+
+ auto selectedCape = skin->getCapeId();
+ if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape)
+ {
+ skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape));
+ }
+
+ skinUpload->addTask(m_acct->refresh().staticCast<Task>());
+ if (prog.execWithTask(*skinUpload) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)
+ ->exec();
+ reject();
+ return;
+ }
+ skin->setURL(m_acct->accountData()->minecraftProfile.skin.url);
+ QDialog::accept();
+}
+
+void SkinManageDialog::on_resetBtn_clicked()
+{
+ ProgressDialog prog(this);
+ NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) };
+ skinReset->addNetAction(SkinDelete::make(m_acct->accessToken()));
+ skinReset->addTask(m_acct->refresh().staticCast<Task>());
+ if (prog.execWithTask(*skinReset) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this,
+ tr("Skin Delete"),
+ tr("Failed to delete current skin!"),
+ QMessageBox::Warning)
+ ->exec();
+ reject();
+ return;
+ }
+ QDialog::accept();
+}
+
+void SkinManageDialog::show_context_menu(const QPoint& pos)
+{
+ QMenu myMenu(tr("Context menu"), this);
+ myMenu.addAction(m_ui->action_Rename_Skin);
+ myMenu.addAction(m_ui->action_Delete_Skin);
+
+ myMenu.exec(m_ui->listView->mapToGlobal(pos));
+}
+
+bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
+{
+ if (obj == m_ui->listView)
+ {
+ if (ev->type() == QEvent::KeyPress)
+ {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete: on_action_Delete_Skin_triggered(false); return true;
+ case Qt::Key_F2: on_action_Rename_Skin_triggered(false); return true;
+ default: break;
+ }
+ }
+ }
+ return QDialog::eventFilter(obj, ev);
+}
+
+void SkinManageDialog::on_action_Rename_Skin_triggered(bool)
+{
+ if (!m_selectedSkinKey.isEmpty())
+ {
+ m_ui->listView->edit(m_ui->listView->currentIndex());
+ }
+}
+
+void SkinManageDialog::on_action_Delete_Skin_triggered(bool)
+{
+ if (m_selectedSkinKey.isEmpty())
+ return;
+
+ if (m_list.getSkinIndex(m_selectedSkinKey) == m_list.getSelectedAccountSkin())
+ {
+ CustomMessageBox::selectable(this,
+ tr("Delete error"),
+ tr("Can not delete skin that is in use."),
+ QMessageBox::Warning)
+ ->exec();
+ return;
+ }
+
+ auto skin = m_list.skin(m_selectedSkinKey);
+ if (!skin)
+ return;
+
+ auto response = CustomMessageBox::selectable(this,
+ tr("Confirm Deletion"),
+ tr("You are about to delete \"%1\".\n"
+ "Are you sure?")
+ .arg(skin->name()),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No)
+ ->exec();
+
+ if (response == QMessageBox::Yes)
+ {
+ if (!m_list.deleteSkin(m_selectedSkinKey, true))
+ {
+ m_list.deleteSkin(m_selectedSkinKey, false);
+ }
+ }
+}
+
+void SkinManageDialog::on_urlBtn_clicked()
+{
+ auto url = QUrl(m_ui->urlLine->text());
+ if (!url.isValid())
+ {
+ CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show();
+ return;
+ }
+
+ NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) };
+ job->setAskRetry(false);
+
+ auto path = FS::PathCombine(m_list.getDir(), url.fileName());
+ job->addNetAction(Net::Download::makeFile(url, path));
+ ProgressDialog dlg(this);
+ dlg.execWithTask(*job);
+ SkinModel s(path);
+ if (!s.isValid())
+ {
+ CustomMessageBox::selectable(this,
+ tr("URL is not a valid skin"),
+ QFileInfo::exists(path)
+ ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.")
+ : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()),
+ QMessageBox::Critical)
+ ->show();
+ QFile::remove(path);
+ return;
+ }
+ m_ui->urlLine->setText("");
+ if (QFileInfo(path).suffix().isEmpty())
+ {
+ QFile::rename(path, path + ".png");
+ }
+}
+
+class WaitTask : public Task
+{
+ public:
+ WaitTask() : m_loop(), m_done(false) {};
+ virtual ~WaitTask() = default;
+
+ public slots:
+ void quit()
+ {
+ m_done = true;
+ m_loop.quit();
+ }
+
+ protected:
+ virtual void executeTask()
+ {
+ if (!m_done)
+ m_loop.exec();
+ emitSucceeded();
+ };
+
+ private:
+ QEventLoop m_loop;
+ bool m_done;
+};
+
+void SkinManageDialog::on_userBtn_clicked()
+{
+ auto user = m_ui->urlLine->text();
+ if (user.isEmpty())
+ {
+ return;
+ }
+ MinecraftProfile mcProfile;
+ auto path = FS::PathCombine(m_list.getDir(), user + ".png");
+
+ NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) };
+ job->setAskRetry(false);
+
+ auto uuidOut = std::make_shared<QByteArray>();
+ auto profileOut = std::make_shared<QByteArray>();
+
+ auto uuidLoop = makeShared<WaitTask>();
+ auto profileLoop = makeShared<WaitTask>();
+
+ auto getUUID =
+ Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user,
+ uuidOut);
+ auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut);
+ auto downloadSkin = Net::Download::makeFile(QUrl(), path);
+
+ QString failReason;
+
+ connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit);
+ connect(getUUID.get(),
+ &Task::failed,
+ this,
+ [&failReason](QString reason)
+ {
+ qCritical() << "Couldn't get user UUID:" << reason;
+ failReason = tr("failed to get user UUID");
+ });
+ connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit);
+ connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit);
+ connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit);
+ connect(getProfile.get(),
+ &Task::failed,
+ this,
+ [&failReason](QString reason)
+ {
+ qCritical() << "Couldn't get user profile:" << reason;
+ failReason = tr("failed to get user profile");
+ });
+ connect(downloadSkin.get(),
+ &Task::failed,
+ this,
+ [&failReason](QString reason)
+ {
+ qCritical() << "Couldn't download skin:" << reason;
+ failReason = tr("failed to download skin");
+ });
+
+ connect(getUUID.get(),
+ &Task::succeeded,
+ this,
+ [uuidLoop, uuidOut, job, getProfile, &failReason]
+ {
+ try
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Minecraft skin service at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ failReason = tr("failed to parse get user UUID response");
+ uuidLoop->quit();
+ return;
+ }
+ const auto root = doc.object();
+ auto id = Json::ensureString(root, "id");
+ if (!id.isEmpty())
+ {
+ getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id);
+ }
+ else
+ {
+ failReason = tr("user id is empty");
+ job->abort();
+ }
+ }
+ catch (const Exception& e)
+ {
+ qCritical() << "Couldn't load skin json:" << e.cause();
+ failReason = tr("failed to parse get user UUID response");
+ }
+ uuidLoop->quit();
+ });
+
+ connect(getProfile.get(),
+ &Task::succeeded,
+ this,
+ [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason]
+ {
+ if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile))
+ {
+ downloadSkin->setUrl(mcProfile.skin.url);
+ }
+ else
+ {
+ failReason = tr("failed to parse get user profile response");
+ job->abort();
+ }
+ profileLoop->quit();
+ });
+
+ job->addNetAction(getUUID);
+ job->addTask(uuidLoop);
+ job->addNetAction(getProfile);
+ job->addTask(profileLoop);
+ job->addNetAction(downloadSkin);
+ ProgressDialog dlg(this);
+ dlg.execWithTask(*job);
+
+ SkinModel s(path);
+ if (!s.isValid())
+ {
+ if (failReason.isEmpty())
+ {
+ failReason = tr("the skin is invalid");
+ }
+ CustomMessageBox::selectable(this,
+ tr("Username not found"),
+ tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason),
+ QMessageBox::Critical)
+ ->show();
+ QFile::remove(path);
+ return;
+ }
+ m_ui->urlLine->setText("");
+ s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
+ s.setURL(mcProfile.skin.url);
+ if (m_capes.contains(mcProfile.currentCape))
+ {
+ s.setCapeId(mcProfile.currentCape);
+ }
+ m_list.updateSkin(&s);
+}
+
+void SkinManageDialog::resizeEvent(QResizeEvent* event)
+{
+ QWidget::resizeEvent(event);
+ QSize s = size() * (1. / 3);
+
+ auto id = m_ui->capeCombo->currentData();
+ auto cape = m_capes.value(id.toString(), {});
+ if (!cape.isNull())
+ {
+ m_ui->capeImage->setPixmap(
+ previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
+ }
+ else
+ {
+ m_ui->capeImage->clear();
+ }
+}
+
+SkinModel* SkinManageDialog::getSelectedSkin()
+{
+ if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid())
+ {
+ return skin;
+ }
+ return nullptr;
+}
+
+QHash<QString, QImage> SkinManageDialog::capes()
+{
+ return m_capes;
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h
new file mode 100644
index 0000000000..85b09133f4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include <QItemSelection>
+#include <QPixmap>
+
+#include "minecraft/auth/MinecraftAccount.hpp"
+#include "minecraft/skins/CapeListModel.h"
+#include "minecraft/skins/SkinList.h"
+#include "minecraft/skins/SkinModel.h"
+#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h"
+
+namespace Ui
+{
+ class SkinManageDialog;
+}
+class SkinManageDialog : public QDialog, public SkinProvider
+{
+ Q_OBJECT
+ public:
+ explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct);
+ virtual ~SkinManageDialog();
+ void resizeEvent(QResizeEvent* event) override;
+
+ virtual SkinModel* getSelectedSkin() override;
+ virtual QHash<QString, QImage> capes() override;
+
+ public slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+ void delayed_scroll(QModelIndex);
+ void on_openDirBtn_clicked();
+ void on_fileBtn_clicked();
+ void on_urlBtn_clicked();
+ void on_userBtn_clicked();
+ void accept() override;
+ void on_capeCombo_currentIndexChanged(int index);
+ void on_steveBtn_toggled(bool checked);
+ void on_resetBtn_clicked();
+ void show_context_menu(const QPoint& pos);
+ bool eventFilter(QObject* obj, QEvent* ev) override;
+ void on_action_Rename_Skin_triggered(bool checked);
+ void on_action_Delete_Skin_triggered(bool checked);
+
+ private:
+ void setupCapes();
+ void onCapesLoaded();
+ void onCapeLoadError(const QString& error);
+
+ private:
+ MinecraftAccountPtr m_acct;
+ Ui::SkinManageDialog* m_ui;
+ SkinList m_list;
+ QString m_selectedSkinKey;
+ QHash<QString, QImage> m_capes;
+ SkinOpenGLWindow* m_skinPreview = nullptr;
+ CapeListModel* m_capeModel = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui
new file mode 100644
index 0000000000..aeb5168549
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SkinManageDialog</class>
+ <widget class="QDialog" name="SkinManageDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>968</width>
+ <height>757</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Skin Upload</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="mainHlLayout" stretch="3,8">
+ <item>
+ <layout class="QVBoxLayout" name="selectedVLayout" stretch="2,1,3">
+ <item>
+ <layout class="QVBoxLayout" name="skinLayout"/>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="modelBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Model</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QRadioButton" name="steveBtn">
+ <property name="text">
+ <string>Classic</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="alexBtn">
+ <property name="text">
+ <string>Slim</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_4">
+ <item>
+ <widget class="QCheckBox" name="elytraCB">
+ <property name="text">
+ <string>Preview Elytra</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="capeCombo"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="capeImage">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="scaledContents">
+ <bool>false</bool>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QListView" name="listView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="acceptDrops">
+ <bool>false</bool>
+ </property>
+ <property name="modelColumn">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="buttonsHLayout" stretch="0,0,3,0,0,0,1">
+ <item>
+ <widget class="QPushButton" name="openDirBtn">
+ <property name="text">
+ <string>Open Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="resetBtn">
+ <property name="text">
+ <string>Reset Skin</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="urlLine">
+ <property name="placeholderText">
+ <string extracomment="URL or username"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="urlBtn">
+ <property name="text">
+ <string>Import URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="userBtn">
+ <property name="text">
+ <string>Import user</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="fileBtn">
+ <property name="text">
+ <string>Import File</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ <action name="action_Delete_Skin">
+ <property name="text">
+ <string>&amp;Delete Skin</string>
+ </property>
+ <property name="toolTip">
+ <string>Deletes selected skin</string>
+ </property>
+ <property name="shortcut">
+ <string>Del</string>
+ </property>
+ </action>
+ <action name="action_Rename_Skin">
+ <property name="text">
+ <string>&amp;Rename Skin</string>
+ </property>
+ <property name="toolTip">
+ <string>Rename selected skin</string>
+ </property>
+ <property name="shortcut">
+ <string>F2</string>
+ </property>
+ </action>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>SkinManageDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>617</x>
+ <y>736</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>483</x>
+ <y>378</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>SkinManageDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>617</x>
+ <y>736</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>483</x>
+ <y>378</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp
new file mode 100644
index 0000000000..b453d734a8
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp
@@ -0,0 +1,342 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "BoxGeometry.h"
+
+#include <QList>
+#include <QMatrix4x4>
+#include <QVector2D>
+#include <QVector3D>
+
+struct VertexData
+{
+ QVector4D position;
+ QVector2D texCoord;
+ VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex)
+ {}
+};
+
+// For cube we would need only 8 vertices but we have to
+// duplicate vertex for each face because texture coordinate
+// is different.
+static const QList<QVector4D> vertices = {
+ // Vertex data for face 0
+ QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0
+ QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1
+ QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2
+ QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3
+ // Vertex data for face 1
+ QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4
+ QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5
+ QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6
+ QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7
+
+ // Vertex data for face 2
+ QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8
+ QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9
+ QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10
+ QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11
+
+ // Vertex data for face 3
+ QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12
+ QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13
+ QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14
+ QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15
+
+ // Vertex data for face 4
+ QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16
+ QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17
+ QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18
+ QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19
+
+ // Vertex data for face 5
+ QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20
+ QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21
+ QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22
+ QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23
+};
+
+// Indices for drawing cube faces using triangle strips.
+// Triangle strips can be connected by duplicating indices
+// between the strips. If connecting strips have opposite
+// vertex order then last index of the first strip and first
+// index of the second strip needs to be duplicated. If
+// connecting strips have same vertex order then only last
+// index of the first strip needs to be duplicated.
+static const QList<GLushort> indices = {
+ 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3)
+ 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7)
+ 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11)
+ 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15)
+ 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19)
+ 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23)
+};
+
+static const QList<VertexData> planeVertices = {
+ { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left
+ { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right
+ { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left
+ { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right
+};
+static const QList<GLushort> planeIndices = {
+ 0,
+ 1,
+ 2,
+ 3,
+ 3 // Face 0 - triangle strip ( v0, v1, v2, v3)
+};
+
+QList<QVector4D> transformVectors(const QMatrix4x4& matrix, const QList<QVector4D>& vectors)
+{
+ QList<QVector4D> transformedVectors;
+ transformedVectors.reserve(vectors.size());
+
+ for (const QVector4D& vec : vectors)
+ {
+ if (!matrix.isIdentity())
+ {
+ transformedVectors.append(matrix * vec);
+ }
+ else
+ {
+ transformedVectors.append(vec);
+ }
+ }
+
+ return transformedVectors;
+}
+
+// Function to calculate UV coordinates
+// this is pure magic (if something is wrong with textures this is at fault)
+QList<QVector2D> getCubeUVs(float u,
+ float v,
+ float width,
+ float height,
+ float depth,
+ float textureWidth,
+ float textureHeight)
+{
+ auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList<QVector2D>
+ {
+ return {
+ QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight),
+ QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight),
+ QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight),
+ QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight),
+ };
+ };
+
+ auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth);
+ auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth);
+ auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height);
+ auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height);
+ auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth);
+ auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth);
+
+ auto uvRight = {
+ right[0],
+ right[1],
+ right[3],
+ right[2],
+ };
+ auto uvLeft = {
+ left[0],
+ left[1],
+ left[3],
+ left[2],
+ };
+ auto uvTop = {
+ top[0],
+ top[1],
+ top[3],
+ top[2],
+ };
+ auto uvBottom = {
+ bottom[3],
+ bottom[2],
+ bottom[0],
+ bottom[1],
+ };
+ auto uvFront = {
+ front[0],
+ front[1],
+ front[3],
+ front[2],
+ };
+ auto uvBack = {
+ back[0],
+ back[1],
+ back[3],
+ back[2],
+ };
+ // Create a new array to hold the modified UV data
+ QList<QVector2D> uvData;
+ uvData.reserve(24);
+
+ // Iterate over the arrays and copy the data to newUVData
+ for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop })
+ {
+ uvData.append(uvArray);
+ }
+
+ return uvData;
+}
+
+namespace opengl
+{
+ BoxGeometry::BoxGeometry(QVector3D size, QVector3D position)
+ : QOpenGLFunctions(),
+ m_indexBuf(QOpenGLBuffer::IndexBuffer),
+ m_size(size),
+ m_position(position)
+ {
+ initializeOpenGLFunctions();
+
+ // Generate 2 VBOs
+ m_vertexBuf.create();
+ m_indexBuf.create();
+ }
+
+ BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize)
+ : BoxGeometry(size, position)
+ {
+ initGeometry(uv.x(),
+ uv.y(),
+ textureDim.x(),
+ textureDim.y(),
+ textureDim.z(),
+ textureSize.width(),
+ textureSize.height());
+ }
+
+ BoxGeometry::~BoxGeometry()
+ {
+ m_vertexBuf.destroy();
+ m_indexBuf.destroy();
+ }
+
+ void BoxGeometry::draw(QOpenGLShaderProgram* program)
+ {
+ // Tell OpenGL which VBOs to use
+ program->setUniformValue("model_matrix", m_matrix);
+ m_vertexBuf.bind();
+ m_indexBuf.bind();
+
+ // Offset for position
+ quintptr offset = 0;
+
+ // Tell OpenGL programmable pipeline how to locate vertex position data
+ int vertexLocation = program->attributeLocation("a_position");
+ program->enableAttributeArray(vertexLocation);
+ program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData));
+
+ // Offset for texture coordinate
+ offset += sizeof(QVector4D);
+ // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data
+ int texcoordLocation = program->attributeLocation("a_texcoord");
+ program->enableAttributeArray(texcoordLocation);
+ program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));
+
+ // Draw cube geometry using indices from VBO 1
+ glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr);
+ }
+
+ void BoxGeometry::initGeometry(float u,
+ float v,
+ float width,
+ float height,
+ float depth,
+ float textureWidth,
+ float textureHeight)
+ {
+ auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight);
+
+ // this should not be needed to be done on each render for most of the objects
+ QMatrix4x4 transformation;
+ transformation.translate(m_position);
+ transformation.scale(m_size);
+ auto positions = transformVectors(transformation, vertices);
+
+ QList<VertexData> verticesData;
+ verticesData.reserve(positions.size()); // Reserve space for efficiency
+
+ for (int i = 0; i < positions.size(); ++i)
+ {
+ verticesData.append(VertexData(positions[i], textureCord[i]));
+ }
+
+ // Transfer vertex data to VBO 0
+ m_vertexBuf.bind();
+ m_vertexBuf.allocate(verticesData.constData(), static_cast<int>(verticesData.size() * sizeof(VertexData)));
+
+ // Transfer index data to VBO 1
+ m_indexBuf.bind();
+ m_indexBuf.allocate(indices.constData(), static_cast<int>(indices.size() * sizeof(GLushort)));
+ m_indecesCount = indices.size();
+ }
+
+ void BoxGeometry::rotate(float angle, const QVector3D& vector)
+ {
+ m_matrix.rotate(angle, vector);
+ }
+
+ BoxGeometry* BoxGeometry::Plane()
+ {
+ auto b = new BoxGeometry(QVector3D(), QVector3D());
+
+ // Transfer vertex data to VBO 0
+ b->m_vertexBuf.bind();
+ b->m_vertexBuf.allocate(planeVertices.constData(), static_cast<int>(planeVertices.size() * sizeof(VertexData)));
+
+ // Transfer index data to VBO 1
+ b->m_indexBuf.bind();
+ b->m_indexBuf.allocate(planeIndices.constData(), static_cast<int>(planeIndices.size() * sizeof(GLushort)));
+ b->m_indecesCount = planeIndices.size();
+
+ return b;
+ }
+
+ void BoxGeometry::scale(const QVector3D& vector)
+ {
+ m_matrix.scale(vector);
+ }
+} // namespace opengl
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h
new file mode 100644
index 0000000000..9b762ffc00
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QMatrix4x4>
+#include <QOpenGLBuffer>
+#include <QOpenGLFunctions>
+#include <QOpenGLShaderProgram>
+#include <QVector3D>
+
+namespace opengl
+{
+ class BoxGeometry : protected QOpenGLFunctions
+ {
+ public:
+ BoxGeometry(QVector3D size, QVector3D position);
+ BoxGeometry(QVector3D size,
+ QVector3D position,
+ QPoint uv,
+ QVector3D textureDim,
+ QSize textureSize = { 64, 64 });
+ static BoxGeometry* Plane();
+ virtual ~BoxGeometry();
+
+ void draw(QOpenGLShaderProgram* program);
+
+ void initGeometry(float u,
+ float v,
+ float width,
+ float height,
+ float depth,
+ float textureWidth = 64,
+ float textureHeight = 64);
+ void rotate(float angle, const QVector3D& vector);
+ void scale(const QVector3D& vector);
+
+ private:
+ QOpenGLBuffer m_vertexBuf;
+ QOpenGLBuffer m_indexBuf;
+ QVector3D m_size;
+ QVector3D m_position;
+ QMatrix4x4 m_matrix;
+ GLsizei m_indecesCount;
+ };
+} // namespace opengl
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp
new file mode 100644
index 0000000000..c95f585080
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ui/dialogs/skins/draw/Scene.h"
+
+#include <QOpenGLFunctions>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLTexture>
+#include <QOpenGLWindow>
+
+namespace opengl
+{
+ Scene::Scene(const QImage& skin, bool slim, const QImage& cape)
+ : QOpenGLFunctions(),
+ m_slim(slim),
+ m_capeVisible(!cape.isNull())
+ {
+ initializeOpenGLFunctions();
+ m_staticComponents = {
+ // head
+ new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)),
+ new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)),
+ // body
+ new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5),
+ QVector3D(0, -6, 0),
+ QPoint(16, 32),
+ QVector3D(8, 12, 4)),
+ // right leg
+ new opengl::BoxGeometry(QVector3D(4, 12, 4),
+ QVector3D(-1.9f, -18, -0.1f),
+ QPoint(0, 16),
+ QVector3D(4, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5),
+ QVector3D(-1.9f, -18, -0.1f),
+ QPoint(0, 32),
+ QVector3D(4, 12, 4)),
+ // left leg
+ new opengl::BoxGeometry(QVector3D(4, 12, 4),
+ QVector3D(1.9f, -18, -0.1f),
+ QPoint(16, 48),
+ QVector3D(4, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5),
+ QVector3D(1.9f, -18, -0.1f),
+ QPoint(0, 48),
+ QVector3D(4, 12, 4)),
+ };
+ m_normalArms = {
+ // Right Arm
+ new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5),
+ QVector3D(-6, -6, 0),
+ QPoint(40, 32),
+ QVector3D(4, 12, 4)),
+ // Left Arm
+ new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5),
+ QVector3D(6, -6, 0),
+ QPoint(48, 48),
+ QVector3D(4, 12, 4)),
+ };
+
+ m_slimArms = {
+ // Right Arm
+ new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5),
+ QVector3D(-5.5, -6, 0),
+ QPoint(40, 32),
+ QVector3D(3, 12, 4)),
+ // Left Arm
+ new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)),
+ new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5),
+ QVector3D(5.5, -6, 0),
+ QPoint(48, 48),
+ QVector3D(3, 12, 4)),
+ };
+
+ m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1),
+ QVector3D(0, -8, 2.5),
+ QPoint(0, 0),
+ QVector3D(10, 16, 1),
+ QSize(64, 32));
+ m_cape->rotate(10.8f, QVector3D(1, 0, 0));
+ m_cape->rotate(180, QVector3D(0, 1, 0));
+
+ auto leftWing = new opengl::BoxGeometry(QVector3D(12, 22, 4),
+ QVector3D(0, -13, -2),
+ QPoint(22, 0),
+ QVector3D(10, 20, 2),
+ QSize(64, 32));
+ leftWing->rotate(15, QVector3D(1, 0, 0));
+ leftWing->rotate(15, QVector3D(0, 0, 1));
+ leftWing->rotate(1, QVector3D(1, 0, 0));
+ auto rightWing = new opengl::BoxGeometry(QVector3D(12, 22, 4),
+ QVector3D(0, -13, -2),
+ QPoint(22, 0),
+ QVector3D(10, 20, 2),
+ QSize(64, 32));
+ rightWing->scale(QVector3D(-1, 1, 1));
+ rightWing->rotate(15, QVector3D(1, 0, 0));
+ rightWing->rotate(15, QVector3D(0, 0, 1));
+ rightWing->rotate(1, QVector3D(1, 0, 0));
+ m_elytra << leftWing << rightWing;
+
+ // texture init
+ m_skinTexture = new QOpenGLTexture(skin.mirrored());
+ m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest);
+ m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest);
+
+ m_capeTexture = new QOpenGLTexture(cape.mirrored());
+ m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest);
+ m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest);
+ }
+ Scene::~Scene()
+ {
+ for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra })
+ {
+ for (auto g : array)
+ {
+ delete g;
+ }
+ }
+ delete m_cape;
+
+ m_skinTexture->destroy();
+ delete m_skinTexture;
+
+ m_capeTexture->destroy();
+ delete m_capeTexture;
+ }
+
+ void Scene::draw(QOpenGLShaderProgram* program)
+ {
+ m_skinTexture->bind();
+ program->setUniformValue("texture", 0);
+ for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms })
+ {
+ for (auto g : toDraw)
+ {
+ g->draw(program);
+ }
+ }
+ m_skinTexture->release();
+ if (m_capeVisible)
+ {
+ m_capeTexture->bind();
+ program->setUniformValue("texture", 0);
+ if (!m_elytraVisible)
+ {
+ m_cape->draw(program);
+ }
+ else
+ {
+ for (auto e : m_elytra)
+ {
+ e->draw(program);
+ }
+ }
+ m_capeTexture->release();
+ }
+ }
+
+ void updateTexture(QOpenGLTexture* texture, const QImage& img)
+ {
+ if (texture)
+ {
+ if (texture->isBound())
+ texture->release();
+ texture->destroy();
+ texture->create();
+ texture->setSize(img.width(), img.height());
+ texture->setData(img);
+ texture->setMinificationFilter(QOpenGLTexture::Nearest);
+ texture->setMagnificationFilter(QOpenGLTexture::Nearest);
+ }
+ }
+
+ void Scene::setSkin(const QImage& skin)
+ {
+ updateTexture(m_skinTexture, skin.mirrored());
+ }
+
+ void Scene::setMode(bool slim)
+ {
+ m_slim = slim;
+ }
+ void Scene::setCape(const QImage& cape)
+ {
+ updateTexture(m_capeTexture, cape.mirrored());
+ }
+ void Scene::setCapeVisible(bool visible)
+ {
+ m_capeVisible = visible;
+ }
+ void Scene::setElytraVisible(bool elytraVisible)
+ {
+ m_elytraVisible = elytraVisible;
+ }
+} // namespace opengl
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h
new file mode 100644
index 0000000000..4b7755c17f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include "ui/dialogs/skins/draw/BoxGeometry.h"
+
+#include <QOpenGLTexture>
+namespace opengl
+{
+ class Scene : protected QOpenGLFunctions
+ {
+ public:
+ Scene(const QImage& skin, bool slim, const QImage& cape);
+ virtual ~Scene();
+
+ void draw(QOpenGLShaderProgram* program);
+ void setSkin(const QImage& skin);
+ void setCape(const QImage& cape);
+ void setMode(bool slim);
+ void setCapeVisible(bool visible);
+ void setElytraVisible(bool elytraVisible);
+
+ private:
+ QList<BoxGeometry*> m_staticComponents;
+ QList<BoxGeometry*> m_normalArms;
+ QList<BoxGeometry*> m_slimArms;
+ BoxGeometry* m_cape = nullptr;
+ QList<BoxGeometry*> m_elytra;
+ QOpenGLTexture* m_skinTexture = nullptr;
+ QOpenGLTexture* m_capeTexture = nullptr;
+ bool m_slim = false;
+ bool m_capeVisible = false;
+ bool m_elytraVisible = false;
+ };
+} // namespace opengl \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp
new file mode 100644
index 0000000000..3937e80d65
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h"
+
+#include <QMouseEvent>
+#include <QOpenGLBuffer>
+#include <QVector2D>
+#include <QVector3D>
+#include <QtMath>
+
+#include "minecraft/skins/SkinModel.h"
+#include "rainbow.h"
+#include "ui/dialogs/skins/draw/BoxGeometry.h"
+#include "ui/dialogs/skins/draw/Scene.h"
+
+SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color)
+ : QOpenGLWindow(),
+ QOpenGLFunctions(),
+ m_baseColor(color),
+ m_parent(parent)
+{
+ QSurfaceFormat format = QSurfaceFormat::defaultFormat();
+ format.setDepthBufferSize(24);
+ setFormat(format);
+}
+
+SkinOpenGLWindow::~SkinOpenGLWindow()
+{
+ // Make sure the context is current when deleting the texture
+ // and the buffers.
+ makeCurrent();
+ // double check if resources were initialized because they are not
+ // initialized together with the object
+ if (m_scene)
+ {
+ delete m_scene;
+ }
+ if (m_background)
+ {
+ delete m_background;
+ }
+ if (m_backgroundTexture)
+ {
+ if (m_backgroundTexture->isCreated())
+ {
+ m_backgroundTexture->destroy();
+ }
+ delete m_backgroundTexture;
+ }
+ if (m_modelProgram)
+ {
+ if (m_modelProgram->isLinked())
+ {
+ m_modelProgram->release();
+ }
+ m_modelProgram->removeAllShaders();
+ delete m_modelProgram;
+ }
+ if (m_backgroundProgram)
+ {
+ if (m_backgroundProgram->isLinked())
+ {
+ m_backgroundProgram->release();
+ }
+ m_backgroundProgram->removeAllShaders();
+ delete m_backgroundProgram;
+ }
+ doneCurrent();
+}
+
+void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e)
+{
+ // Save mouse press position
+ m_mousePosition = QVector2D(e->pos());
+ m_isMousePressed = true;
+}
+
+void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event)
+{
+ // Prevents mouse sticking on Wayland compositors
+ if (!(event->buttons() & Qt::MouseButton::LeftButton))
+ {
+ m_isMousePressed = false;
+ return;
+ }
+
+ if (m_isMousePressed)
+ {
+ int dx = event->position().x() - m_mousePosition.x();
+ int dy = event->position().y() - m_mousePosition.y();
+
+ m_yaw += dx * 0.5f;
+ m_pitch += dy * 0.5f;
+
+ // Normalize yaw to keep it manageable
+ if (m_yaw > 360.0f)
+ m_yaw -= 360.0f;
+ else if (m_yaw < 0.0f)
+ m_yaw += 360.0f;
+
+ m_mousePosition = QVector2D(event->pos());
+ update(); // Trigger a repaint
+ }
+}
+
+void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e)
+{
+ m_isMousePressed = false;
+}
+
+void SkinOpenGLWindow::initializeGL()
+{
+ initializeOpenGLFunctions();
+
+ glClearColor(0, 0, 1, 1);
+
+ initShaders();
+
+ generateBackgroundTexture(32, 32, 1);
+
+ QImage skin, cape;
+ bool slim = false;
+ if (m_parent)
+ {
+ if (auto s = m_parent->getSelectedSkin())
+ {
+ skin = s->getTexture();
+ slim = s->getModel() == SkinModel::SLIM;
+ cape = m_parent->capes().value(s->getCapeId(), {});
+ }
+ }
+
+ m_scene = new opengl::Scene(skin, slim, cape);
+ m_background = opengl::BoxGeometry::Plane();
+ glEnable(GL_TEXTURE_2D);
+}
+
+void SkinOpenGLWindow::initShaders()
+{
+ // Skin model shaders
+ m_modelProgram = new QOpenGLShaderProgram(this);
+ // Compile vertex shader
+ if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_model.glsl"))
+ close();
+
+ // Compile fragment shader
+ if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl"))
+ close();
+
+ // Link shader pipeline
+ if (!m_modelProgram->link())
+ close();
+
+ // Bind shader pipeline for use
+ if (!m_modelProgram->bind())
+ close();
+
+ // Background shaders
+ m_backgroundProgram = new QOpenGLShaderProgram(this);
+ // Compile vertex shader
+ if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex,
+ ":/shaders/vshader_skin_background.glsl"))
+ close();
+
+ // Compile fragment shader
+ if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl"))
+ close();
+
+ // Link shader pipeline
+ if (!m_backgroundProgram->link())
+ close();
+
+ // Bind shader pipeline for use (verification)
+ if (!m_backgroundProgram->bind())
+ close();
+}
+
+void SkinOpenGLWindow::resizeGL(int w, int h)
+{
+ // Calculate aspect ratio
+ qreal aspect = qreal(w) / qreal(h ? h : 1);
+
+ const qreal zNear = 15., fov = 45;
+
+ // Reset projection
+ m_projection.setToIdentity();
+
+ // Build the reverse z perspective projection matrix
+ double radians = qDegreesToRadians(fov / 2.);
+ double sine = std::sin(radians);
+ if (sine == 0)
+ return;
+ double cotan = std::cos(radians) / sine;
+
+ m_projection(0, 0) = cotan / aspect;
+ m_projection(1, 1) = cotan;
+ m_projection(2, 2) = 0.;
+ m_projection(3, 2) = -1.;
+ m_projection(2, 3) = zNear;
+ m_projection(3, 3) = 0.;
+}
+
+void SkinOpenGLWindow::paintGL()
+{
+ // Adjust the viewport to account for fractional scaling
+ qreal dpr = devicePixelRatio();
+ if (dpr != 1.f)
+ {
+ QSize scaledSize = size() * dpr;
+ glViewport(0, 0, scaledSize.width(), scaledSize.height());
+ }
+
+ // Clear color and depth buffer
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ // Enable depth buffer
+ glEnable(GL_DEPTH_TEST);
+ glDepthFunc(GL_LESS);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ m_backgroundProgram->bind();
+ renderBackground();
+ m_backgroundProgram->release();
+
+ // Calculate model view transformation
+ QMatrix4x4 matrix;
+ float yawRad = qDegreesToRadians(m_yaw);
+ float pitchRad = qDegreesToRadians(m_pitch);
+ matrix.lookAt(QVector3D( //
+ m_distance * qCos(pitchRad) * qCos(yawRad), //
+ m_distance * qSin(pitchRad) - 8, //
+ m_distance * qCos(pitchRad) * qSin(yawRad)),
+ QVector3D(0, -8, 0),
+ QVector3D(0, 1, 0));
+
+ // Set modelview-projection matrix
+ m_modelProgram->bind();
+ m_modelProgram->setUniformValue("mvp_matrix", m_projection * matrix);
+
+ m_scene->draw(m_modelProgram);
+ m_modelProgram->release();
+
+ // Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not
+ // negotiated properly on the first frame
+ if (m_isFirstFrame)
+ {
+ m_isFirstFrame = false;
+ update();
+ }
+}
+
+void SkinOpenGLWindow::updateScene(SkinModel* skin)
+{
+ if (skin && m_scene)
+ {
+ m_scene->setMode(skin->getModel() == SkinModel::SLIM);
+ m_scene->setSkin(skin->getTexture());
+ update();
+ }
+}
+void SkinOpenGLWindow::updateCape(const QImage& cape)
+{
+ if (m_scene)
+ {
+ m_scene->setCapeVisible(!cape.isNull());
+ m_scene->setCape(cape);
+ update();
+ }
+}
+
+QColor calculateContrastingColor(const QColor& color)
+{
+ constexpr float contrast = 0.2f;
+ auto luma = Rainbow::luma(color);
+ if (luma < 0.5)
+ {
+ return Rainbow::lighten(color, contrast);
+ }
+ else
+ {
+ return Rainbow::darken(color, contrast);
+ }
+}
+
+QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor)
+{
+ QImage image(width, height, QImage::Format_RGB888);
+ auto white = baseColor;
+ auto black = calculateContrastingColor(baseColor);
+ for (int y = 0; y < height; ++y)
+ {
+ for (int x = 0; x < width; ++x)
+ {
+ bool isWhite = ((x / tileSize) % 2) == ((y / tileSize) % 2);
+ image.setPixelColor(x, y, isWhite ? white : black);
+ }
+ }
+ return image;
+}
+
+void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize)
+{
+ m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor));
+ m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest);
+ m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest);
+}
+
+void SkinOpenGLWindow::renderBackground()
+{
+ glDisable(GL_DEPTH_TEST);
+ glDepthMask(GL_FALSE); // Disable depth buffer writing
+ m_backgroundTexture->bind();
+ m_backgroundProgram->setUniformValue("texture", 0);
+ m_background->draw(m_backgroundProgram);
+ m_backgroundTexture->release();
+ glDepthMask(GL_TRUE); // Re-enable depth buffer writing
+ glEnable(GL_DEPTH_TEST);
+}
+
+void SkinOpenGLWindow::wheelEvent(QWheelEvent* event)
+{
+ // Adjust distance based on scroll
+ int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down
+ m_distance -= delta * 0.01f; // Adjust sensitivity factor
+ m_distance = qMax(16.f, m_distance); // Clamp distance
+ update(); // Trigger a repaint
+}
+void SkinOpenGLWindow::setElytraVisible(bool visible)
+{
+ if (m_scene)
+ m_scene->setElytraVisible(visible);
+}
diff --git a/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h
new file mode 100644
index 0000000000..77a7cce731
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QMatrix4x4>
+#include <QOpenGLFunctions>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLTexture>
+#include <QOpenGLWindow>
+#include <QVector2D>
+#include "minecraft/skins/SkinModel.h"
+#include "ui/dialogs/skins/draw/BoxGeometry.h"
+#include "ui/dialogs/skins/draw/Scene.h"
+
+class SkinProvider
+{
+ public:
+ virtual ~SkinProvider() = default;
+ virtual SkinModel* getSelectedSkin() = 0;
+ virtual QHash<QString, QImage> capes() = 0;
+};
+class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions
+{
+ Q_OBJECT
+
+ public:
+ SkinOpenGLWindow(SkinProvider* parent, QColor color);
+ virtual ~SkinOpenGLWindow();
+
+ void updateScene(SkinModel* skin);
+ void updateCape(const QImage& cape);
+ void setElytraVisible(bool visible);
+
+ protected:
+ void mousePressEvent(QMouseEvent* e) override;
+ void mouseReleaseEvent(QMouseEvent* e) override;
+ void mouseMoveEvent(QMouseEvent* event) override;
+ void wheelEvent(QWheelEvent* event) override;
+
+ void initializeGL() override;
+ void resizeGL(int w, int h) override;
+ void paintGL() override;
+
+ void initShaders();
+
+ void generateBackgroundTexture(int width, int height, int tileSize);
+ void renderBackground();
+
+ private:
+ QOpenGLShaderProgram* m_modelProgram;
+ QOpenGLShaderProgram* m_backgroundProgram;
+ opengl::Scene* m_scene = nullptr;
+
+ QMatrix4x4 m_projection;
+
+ QVector2D m_mousePosition;
+
+ bool m_isMousePressed = false;
+ float m_distance = 48;
+ float m_yaw = 90; // Horizontal rotation angle
+ float m_pitch = 0; // Vertical rotation angle
+
+ bool m_isFirstFrame = true;
+
+ opengl::BoxGeometry* m_background = nullptr;
+ QOpenGLTexture* m_backgroundTexture = nullptr;
+ QColor m_baseColor;
+ SkinProvider* m_parent = nullptr;
+};