GH-4699 Modrinth pack exporter (WIP)

This commit is contained in:
arthomnix 2023-02-04 21:41:24 +00:00
parent 867d240a2f
commit 16cf56b7a4
10 changed files with 896 additions and 2 deletions

View File

@ -36,6 +36,8 @@ set(CORE_SOURCES
InstanceCopyTask.cpp
InstanceImportTask.h
InstanceImportTask.cpp
ModrinthInstanceExportTask.h
ModrinthInstanceExportTask.cpp
# Use tracking separate from memory management
Usable.h
@ -784,6 +786,10 @@ SET(LAUNCHER_SOURCES
ui/dialogs/SkinUploadDialog.h
ui/dialogs/CreateShortcutDialog.cpp
ui/dialogs/CreateShortcutDialog.h
ui/dialogs/SelectInstanceExportFormatDialog.cpp
ui/dialogs/SelectInstanceExportFormatDialog.h
ui/dialogs/ModrinthExportDialog.cpp
ui/dialogs/ModrinthExportDialog.h
# GUI - widgets
ui/widgets/Common.cpp
@ -882,6 +888,8 @@ qt5_wrap_ui(LAUNCHER_UI
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/SelectInstanceExportFormatDialog.ui
ui/dialogs/ModrinthExportDialog.ui
)
qt5_add_resources(LAUNCHER_RESOURCES

View File

@ -0,0 +1,221 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include <QDir>
#include <QDirIterator>
#include <QCryptographicHash>
#include <QMap>
#include "Json.h"
#include "ModrinthInstanceExportTask.h"
#include "net/NetJob.h"
#include "Application.h"
#include "ui/dialogs/ModrinthExportDialog.h"
#include "JlCompress.h"
#include "FileSystem.h"
ModrinthInstanceExportTask::ModrinthInstanceExportTask(InstancePtr instance, ModrinthExportSettings settings) : m_instance(instance), m_settings(settings) {}
void ModrinthInstanceExportTask::executeTask()
{
QDir modsDir(m_instance->gameRoot() + "/mods");
modsDir.setFilter(QDir::Files);
modsDir.setNameFilters(QStringList() << "*.jar");
QDir resourcePacksDir(m_instance->gameRoot() + "/resourcepacks");
resourcePacksDir.setFilter(QDir::Files);
resourcePacksDir.setNameFilters(QStringList() << "*.zip");
QDir shaderPacksDir(m_instance->gameRoot() + "/shaderpacks");
shaderPacksDir.setFilter(QDir::Files);
shaderPacksDir.setNameFilters(QStringList() << "*.zip");
QStringList filesToResolve;
if (modsDir.exists()) {
QDirIterator modsIterator(modsDir);
while (modsIterator.hasNext()) {
filesToResolve << modsIterator.next();
}
}
if (m_settings.includeResourcePacks && resourcePacksDir.exists()) {
QDirIterator resourcePacksIterator(resourcePacksDir);
while (resourcePacksIterator.hasNext()) {
filesToResolve << resourcePacksIterator.next();
}
}
if (m_settings.includeShaderPacks && shaderPacksDir.exists()) {
QDirIterator shaderPacksIterator(shaderPacksDir);
while (shaderPacksIterator.hasNext()) {
filesToResolve << shaderPacksIterator.next();
}
}
m_netJob = new NetJob(tr("Modrinth pack export"), APPLICATION->network());
for (QString filePath: filesToResolve) {
QFile file(filePath);
if (file.open(QFile::ReadOnly)) {
QByteArray contents = file.readAll();
QCryptographicHash hasher(QCryptographicHash::Sha512);
hasher.addData(contents);
QString hash = hasher.result().toHex();
m_responses.append(ModrinthLookupData {
QFileInfo(file),
QByteArray()
});
m_netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha512").arg(hash),
&m_responses.last().response
));
}
}
connect(m_netJob.get(), &NetJob::succeeded, this, &ModrinthInstanceExportTask::lookupSucceeded);
connect(m_netJob.get(), &NetJob::failed, this, &ModrinthInstanceExportTask::lookupFailed);
connect(m_netJob.get(), &NetJob::progress, this, &ModrinthInstanceExportTask::lookupProgress);
m_netJob->start();
}
void ModrinthInstanceExportTask::lookupSucceeded()
{
QList<ModrinthFile> resolvedFiles;
QFileInfoList failedFiles;
for (const auto &data : m_responses) {
try {
auto document = Json::requireDocument(data.response);
auto object = Json::requireObject(document);
auto file = Json::requireIsArrayOf<QJsonObject>(object, "files").first();
auto url = Json::requireString(file, "url");
auto hashes = Json::requireObject(file, "hashes");
QString sha512Hash = Json::requireString(hashes, "sha512");
QString sha1Hash = Json::requireString(hashes, "sha1");
ModrinthFile fileData;
QDir gameDir(m_instance->gameRoot());
fileData.path = gameDir.relativeFilePath(data.fileInfo.absoluteFilePath());
fileData.download = url;
fileData.sha512 = sha512Hash;
fileData.sha1 = sha1Hash;
fileData.fileSize = data.fileInfo.size();
resolvedFiles << fileData;
} catch (const Json::JsonException &e) {
qDebug() << "File " << data.fileInfo.path() << " failed to process for reason " << e.cause() << ", adding to overrides";
failedFiles << data.fileInfo;
}
}
qDebug() << "Failed files: " << failedFiles;
QJsonObject indexJson;
indexJson.insert("formatVersion", QJsonValue(1));
indexJson.insert("game", QJsonValue("minecraft"));
indexJson.insert("versionId", QJsonValue(m_settings.version));
indexJson.insert("name", QJsonValue(m_settings.name));
if (!m_settings.description.isEmpty()) {
indexJson.insert("summary", QJsonValue(m_settings.description));
}
QJsonArray files;
for (const auto &file : resolvedFiles) {
QJsonObject fileObj;
fileObj.insert("path", file.path);
QJsonObject hashes;
hashes.insert("sha512", file.sha512);
hashes.insert("sha1", file.sha1);
fileObj.insert("hashes", hashes);
QJsonArray downloads;
downloads.append(file.download);
fileObj.insert("downloads", downloads);
fileObj.insert("fileSize", QJsonValue(file.fileSize));
files.append(fileObj);
}
indexJson.insert("files", files);
QJsonObject dependencies;
dependencies.insert("minecraft", m_settings.gameVersion);
if (!m_settings.forgeVersion.isEmpty()) {
dependencies.insert("forge", m_settings.forgeVersion);
}
if (!m_settings.fabricVersion.isEmpty()) {
dependencies.insert("fabric-loader", m_settings.fabricVersion);
}
if (!m_settings.quiltVersion.isEmpty()) {
dependencies.insert("quilt-loader", m_settings.quiltVersion);
}
indexJson.insert("dependencies", dependencies);
QTemporaryDir tmp;
if (tmp.isValid()) {
Json::write(indexJson, tmp.filePath("modrinth.index.json"));
if (!failedFiles.isEmpty()) {
QDir tmpDir(tmp.path());
QDir gameDir(m_instance->gameRoot());
for (const auto &file : failedFiles) {
QString src = file.absoluteFilePath();
tmpDir.mkpath("overrides/" + gameDir.relativeFilePath(file.absolutePath()));
QString dest = tmpDir.path() + "/overrides/" + gameDir.relativeFilePath(src);
qDebug() << dest;
if (!QFile::copy(file.absoluteFilePath(), dest)) {
emitFailed(tr("Failed to copy file %1 to overrides").arg(src));
return;
}
}
if (m_settings.includeGameConfig) {
tmpDir.mkdir("overrides");
QFile::copy(gameDir.absoluteFilePath("options.txt"), tmpDir.absoluteFilePath("overrides/options.txt"));
}
if (m_settings.includeModConfigs) {
tmpDir.mkdir("overrides");
FS::copy copy(m_instance->gameRoot() + "/config", tmpDir.absoluteFilePath("overrides/config"));
copy();
}
}
if (!JlCompress::compressDir(m_settings.exportPath, tmp.path())) {
emitFailed(tr("Failed to create zip file"));
return;
}
} else {
emitFailed(tr("Failed to create temporary directory"));
return;
}
emitSucceeded();
}
void ModrinthInstanceExportTask::lookupFailed(const QString &)
{
lookupSucceeded(); // the NetJob will fail if some files were not found on Modrinth, we still want to continue in that case
// FIXME: the NetJob will retry each download 3 times if it fails, we should probably stop it from doing that
}
void ModrinthInstanceExportTask::lookupProgress(qint64 current, qint64 total)
{
setProgress(current, total);
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include "tasks/Task.h"
#include "BaseInstance.h"
#include "net/NetJob.h"
#include "ui/dialogs/ModrinthExportDialog.h"
struct ModrinthExportSettings {
QString version;
QString name;
QString description;
bool includeGameConfig;
bool includeModConfigs;
bool includeResourcePacks;
bool includeShaderPacks;
QString gameVersion;
QString forgeVersion;
QString fabricVersion;
QString quiltVersion;
QString exportPath;
};
struct ModrinthLookupData {
QFileInfo fileInfo;
QByteArray response;
};
// Using the existing Modrinth::File struct from the importer doesn't actually make much sense here (doesn't support multiple hashes, hash is a byte array rather than a string, no file size, etc)
struct ModrinthFile
{
QString path;
QString sha512;
QString sha1;
QString download;
qint64 fileSize;
};
class ModrinthInstanceExportTask : public Task
{
Q_OBJECT
public:
explicit ModrinthInstanceExportTask(InstancePtr instance, ModrinthExportSettings settings);
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private slots:
void lookupSucceeded();
void lookupFailed(const QString &);
void lookupProgress(qint64 current, qint64 total);
private:
InstancePtr m_instance;
ModrinthExportSettings m_settings;
QList<ModrinthLookupData> m_responses;
NetJob::Ptr m_netJob;
};

View File

@ -84,7 +84,7 @@
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/NotificationDialog.h"
#include "ui/dialogs/CreateShortcutDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/SelectInstanceExportFormatDialog.h"
#include "UpdateController.h"
#include "KonamiCode.h"
@ -1756,7 +1756,7 @@ void MainWindow::on_actionExportInstance_triggered()
{
if (m_selectedInstance)
{
ExportInstanceDialog dlg(m_selectedInstance, this);
SelectInstanceExportFormatDialog dlg(m_selectedInstance, this);
dlg.exec();
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include <QFileDialog>
#include <QStandardPaths>
#include <QProgressDialog>
#include <QMessageBox>
#include "ModrinthExportDialog.h"
#include "ui_ModrinthExportDialog.h"
#include "BaseInstance.h"
#include "ModrinthInstanceExportTask.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ProgressDialog.h"
#include "CustomMessageBox.h"
ModrinthExportDialog::ModrinthExportDialog(InstancePtr instance, QWidget *parent) :
QDialog(parent), ui(new Ui::ModrinthExportDialog), m_instance(instance)
{
ui->setupUi(this);
ui->name->setText(m_instance->name());
ui->version->setText("1.0");
}
void ModrinthExportDialog::updateDialogState()
{
ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(
!ui->name->text().isEmpty()
&& !ui->version->text().isEmpty()
&& !ui->file->text().isEmpty()
);
}
void ModrinthExportDialog::on_fileBrowseButton_clicked()
{
QFileDialog dialog(this, tr("Select modpack file"), QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
dialog.setDefaultSuffix("mrpack");
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.selectFile(ui->name->text() + ".mrpack");
if (dialog.exec()) {
ui->file->setText(dialog.selectedFiles().at(0));
}
updateDialogState();
}
void ModrinthExportDialog::accept()
{
ModrinthExportSettings settings;
settings.name = ui->name->text();
settings.version = ui->version->text();
settings.description = ui->description->text();
settings.includeGameConfig = ui->includeGameConfig->isChecked();
settings.includeModConfigs = ui->includeModConfigs->isChecked();
settings.includeResourcePacks = ui->includeResourcePacks->isChecked();
settings.includeShaderPacks = ui->includeShaderPacks->isChecked();
MinecraftInstancePtr minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(m_instance);
minecraftInstance->getPackProfile()->reload(Net::Mode::Offline);
auto minecraftComponent = minecraftInstance->getPackProfile()->getComponent("net.minecraft");
auto forgeComponent = minecraftInstance->getPackProfile()->getComponent("net.minecraftforge");
auto fabricComponent = minecraftInstance->getPackProfile()->getComponent("net.fabricmc.fabric-loader");
auto quiltComponent = minecraftInstance->getPackProfile()->getComponent("org.quiltmc.quilt-loader");
if (minecraftComponent) {
settings.gameVersion = minecraftComponent->getVersion();
}
if (forgeComponent) {
settings.forgeVersion = forgeComponent->getVersion();
}
if (fabricComponent) {
settings.fabricVersion = fabricComponent->getVersion();
}
if (quiltComponent) {
settings.quiltVersion = quiltComponent->getVersion();
}
settings.exportPath = ui->file->text();
auto *task = new ModrinthInstanceExportTask(m_instance, settings);
connect(task, &Task::failed, [this](QString reason)
{
CustomMessageBox::selectable(parentWidget(), tr("Error"), reason, QMessageBox::Critical)->show();
});
connect(task, &Task::succeeded, [this, task]()
{
QStringList warnings = task->warnings();
if(warnings.count())
{
CustomMessageBox::selectable(parentWidget(), tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
}
});
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
QDialog::accept();
}
ModrinthExportDialog::~ModrinthExportDialog()
{
delete ui;
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include <QDialog>
#include "ExportInstanceDialog.h"
QT_BEGIN_NAMESPACE
namespace Ui
{
class ModrinthExportDialog;
}
QT_END_NAMESPACE
class ModrinthExportDialog : public QDialog
{
Q_OBJECT
public:
explicit ModrinthExportDialog(InstancePtr instance, QWidget *parent = nullptr);
~ModrinthExportDialog() override;
private slots:
void on_fileBrowseButton_clicked();
void accept() override;
void updateDialogState();
private:
Ui::ModrinthExportDialog *ui;
InstancePtr m_instance;
};

View File

@ -0,0 +1,277 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModrinthExportDialog</class>
<widget class="QDialog" name="ModrinthExportDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>679</width>
<height>559</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>ModrinthExportDialog</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>661</width>
<height>541</height>
</rect>
</property>
<layout class="QVBoxLayout" name="mainLayout">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Export Modrinth modpack</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="metadataGroupBox">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="title">
<string>Metadata</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>641</width>
<height>151</height>
</rect>
</property>
<layout class="QVBoxLayout" name="metadataVLayout">
<item>
<layout class="QFormLayout" name="metadataFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name</string>
</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>Version</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="version"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="description"/>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QGroupBox" name="optionsGroupBox">
<property name="title">
<string>Export Options</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget_3">
<property name="geometry">
<rect>
<x>9</x>
<y>29</y>
<width>641</width>
<height>221</height>
</rect>
</property>
<layout class="QVBoxLayout" name="optionsLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="fileLabel">
<property name="text">
<string>File</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="file"/>
</item>
<item>
<widget class="QPushButton" name="fileBrowseButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="includeGameConfig">
<property name="text">
<string>Include Minecraft config</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeModConfigs">
<property name="text">
<string>Include mod configs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeResourcePacks">
<property name="text">
<string>Include resource packs</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeShaderPacks">
<property name="text">
<string>Include shader packs</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>532</y>
</hint>
<hint type="destinationlabel">
<x>338</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>532</y>
</hint>
<hint type="destinationlabel">
<x>338</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>name</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>395</x>
<y>90</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>version</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>395</x>
<y>129</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>file</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>309</x>
<y>329</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateDialogState()</slot>
</slots>
</ui>

View File

@ -0,0 +1,37 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include "SelectInstanceExportFormatDialog.h"
#include "ui_SelectInstanceExportFormatDialog.h"
#include "BuildConfig.h"
#include "ModrinthExportDialog.h"
SelectInstanceExportFormatDialog::SelectInstanceExportFormatDialog(InstancePtr instance, QWidget *parent) :
QDialog(parent), ui(new Ui::SelectInstanceExportFormatDialog), m_instance(instance)
{
ui->setupUi(this);
ui->mmcFormat->setText(BuildConfig.LAUNCHER_NAME);
}
void SelectInstanceExportFormatDialog::accept()
{
if (ui->mmcFormat->isChecked()) {
ExportInstanceDialog dlg(m_instance, parentWidget());
QDialog::accept();
dlg.exec();
} else if (ui->modrinthFormat->isChecked()) {
ModrinthExportDialog dlg(m_instance, parentWidget());
QDialog::accept();
dlg.exec();
}
}
SelectInstanceExportFormatDialog::~SelectInstanceExportFormatDialog()
{
delete ui;
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include <QDialog>
#include "ExportInstanceDialog.h"
QT_BEGIN_NAMESPACE
namespace Ui
{
class SelectInstanceExportFormatDialog;
}
QT_END_NAMESPACE
class SelectInstanceExportFormatDialog : public QDialog
{
Q_OBJECT
public:
explicit SelectInstanceExportFormatDialog(InstancePtr instance, QWidget *parent = nullptr);
~SelectInstanceExportFormatDialog() override;
private slots:
void accept() override;
private:
Ui::SelectInstanceExportFormatDialog *ui;
InstancePtr m_instance;
};

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectInstanceExportFormatDialog</class>
<widget class="QDialog" name="SelectInstanceExportFormatDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>446</width>
<height>181</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Instance Export Format</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>421</width>
<height>161</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select export format</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="mmcFormat">
<property name="text">
<string>Launcher</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="modrinthFormat">
<property name="text">
<string>Modrinth (WIP)</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SelectInstanceExportFormatDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>220</x>
<y>152</y>
</hint>
<hint type="destinationlabel">
<x>222</x>
<y>90</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SelectInstanceExportFormatDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>220</x>
<y>152</y>
</hint>
<hint type="destinationlabel">
<x>222</x>
<y>90</y>
</hint>
</hints>
</connection>
</connections>
</ui>