aboutsummaryrefslogtreecommitdiff
path: root/launcher/ui/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/ui/dialogs')
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp96
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h56
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.ui89
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp408
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h62
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp34
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp2
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.ui2
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.cpp90
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.ui6
10 files changed, 780 insertions, 65 deletions
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp
new file mode 100644
index 00000000..89935d9a
--- /dev/null
+++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -0,0 +1,96 @@
+#include "ChooseProviderDialog.h"
+#include "ui_ChooseProviderDialog.h"
+
+#include <QPushButton>
+#include <QRadioButton>
+
+#include "modplatform/ModIndex.h"
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+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::Provider
+{
+ return ModPlatform::Provider(m_providers.checkedId());
+}
+
+void ChooseProviderDialog::addProviders()
+{
+ int btn_index = 0;
+ QRadioButton* btn;
+
+ for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) {
+ btn = new QRadioButton(ProviderCaps.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/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h
new file mode 100644
index 00000000..4a3b9f29
--- /dev/null
+++ b/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <QButtonGroup>
+#include <QDialog>
+
+namespace Ui {
+class ChooseProviderDialog;
+}
+
+namespace ModPlatform {
+enum class Provider;
+}
+
+class Mod;
+class NetJob;
+class ModUpdateDialog;
+
+class ChooseProviderDialog : public QDialog {
+ Q_OBJECT
+
+ struct Response {
+ bool skip_all = false;
+ bool confirm_all = false;
+
+ bool try_others = false;
+
+ ModPlatform::Provider 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::Provider;
+
+ private:
+ Ui::ChooseProviderDialog* ui;
+
+ QButtonGroup m_providers;
+
+ Response m_response;
+};
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.ui b/launcher/ui/dialogs/ChooseProviderDialog.ui
new file mode 100644
index 00000000..78cd9613
--- /dev/null
+++ b/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/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
new file mode 100644
index 00000000..b6e76ff1
--- /dev/null
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -0,0 +1,408 @@
+#include "ModUpdateDialog.h"
+#include "ChooseProviderDialog.h"
+#include "CustomMessageBox.h"
+#include "ProgressDialog.h"
+#include "ScrollMessageBox.h"
+#include "ui_ReviewMessageBox.h"
+
+#include "FileSystem.h"
+#include "Json.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 <HoeDown.h>
+#include <QTextBrowser>
+#include <QTreeWidgetItem>
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+static std::list<Version> mcVersions(BaseInstance* inst)
+{
+ return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
+}
+
+static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
+{
+ return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
+}
+
+ModUpdateDialog::ModUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ const std::shared_ptr<ModFolderModel> mods,
+ QList<Mod::Ptr>& search_for)
+ : ReviewMessageBox(parent, tr("Confirm mods to update"), "")
+ , m_parent(parent)
+ , m_mod_model(mods)
+ , m_candidates(search_for)
+ , m_second_try_metadata(new ConcurrentTask())
+ , m_instance(instance)
+{
+ ReviewMessageBox::setGeometry(0, 0, 800, 600);
+
+ ui->explainLabel->setText(tr("You're about to update the following mods:"));
+ ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!"));
+}
+
+void ModUpdateDialog::checkCandidates()
+{
+ // Ensure mods have valid metadata
+ auto went_well = ensureMetadata();
+ if (!went_well) {
+ m_aborted = true;
+ return;
+ }
+
+ // Report failed metadata generation
+ if (!m_failed_metadata.empty()) {
+ QString text;
+ for (const auto& failed : m_failed_metadata) {
+ 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 mods:<br>"
+ "Do you wish to proceed without those mods?"),
+ 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);
+ auto loaders = mcLoaders(m_instance);
+
+ SequentialTask check_task(m_parent, tr("Checking for updates"));
+
+ if (!m_modrinth_to_update.empty()) {
+ m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model);
+ connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this,
+ [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
+ check_task.addTask(m_modrinth_check_task);
+ }
+
+ if (!m_flame_to_update.empty()) {
+ m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model);
+ connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this,
+ [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
+ check_task.addTask(m_flame_check_task);
+ }
+
+ connect(&check_task, &Task::failed, this,
+ [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ connect(&check_task, &Task::succeeded, this, [&]() {
+ 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;
+ }
+
+ // Add found updates for Modrinth
+ if (m_modrinth_check_task) {
+ auto modrinth_updates = m_modrinth_check_task->getUpdatable();
+ for (auto& updatable : modrinth_updates) {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendMod(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ }
+
+ // Add found updated for Flame
+ if (m_flame_check_task) {
+ auto flame_updates = m_flame_check_task->getUpdatable();
+ for (auto& updatable : flame_updates) {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendMod(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ }
+
+ // Report failed update checking
+ if (!m_failed_check_update.empty()) {
+ QString text;
+ for (const auto& failed : m_failed_check_update) {
+ 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())
+ text += tr("Possible solution: ") + tr("Getting the latest version manually:") + "<br>" +
+ QString("<a href='%1'>").arg(recover_url.toString()) + recover_url.toString() + "</a><br>";
+ text += "<br>";
+ }
+
+ ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"),
+ tr("Could not check or get the following mods for updates:<br>"
+ "Do you wish to proceed without those mods?"),
+ text);
+ message_dialog.setModal(true);
+ if (message_dialog.exec() == QDialog::Rejected) {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ }
+
+ // If there's no mod to be updated
+ if (ui->modTreeWidget->topLevelItemCount() == 0) {
+ m_no_updates = true;
+ } else {
+ // FIXME: Find a more efficient way of doing this!
+
+ // Sort major items in alphabetical order (also sorts the children unfortunately)
+ ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
+
+ // Re-sort the children
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+ for (int i = 1; item != nullptr; ++i) {
+ item->sortChildren(0, Qt::SortOrder::DescendingOrder);
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+ }
+
+ if (m_aborted || m_no_updates)
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+}
+
+// Part 1: Ensure we have a valid metadata
+auto ModUpdateDialog::ensureMetadata() -> bool
+{
+ auto index_dir = indexDir();
+
+ SequentialTask seq(m_parent, 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<Mod*> modrinth_tmp;
+ QList<Mod*> flame_tmp;
+
+ bool confirm_rest = false;
+ bool try_others_rest = false;
+ bool skip_rest = false;
+ ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH;
+
+ auto addToTmp = [&](Mod* m, ModPlatform::Provider p) {
+ switch (p) {
+ case ModPlatform::Provider::MODRINTH:
+ modrinth_tmp.push_back(m);
+ break;
+ case ModPlatform::Provider::FLAME:
+ flame_tmp.push_back(m);
+ break;
+ }
+ };
+
+ for (auto candidate : m_candidates) {
+ auto* candidate_ptr = candidate.get();
+ if (candidate->status() != ModStatus::NoMetadata) {
+ onMetadataEnsured(candidate_ptr);
+ continue;
+ }
+
+ if (skip_rest)
+ continue;
+
+ if (confirm_rest) {
+ addToTmp(candidate_ptr, provider_rest);
+ should_try_others.insert(candidate->internal_id(), try_others_rest);
+ continue;
+ }
+
+ ChooseProviderDialog chooser(this);
+ chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant "
+ "information on how to update this "
+ "mod. To do this, please select a mod provider from which we can search for updates for %1.")
+ .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_ptr, response.chosen);
+ }
+
+ if (!modrinth_tmp.empty()) {
+ auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH);
+ connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
+ });
+ seq.addTask(modrinth_task);
+ }
+
+ if (!flame_tmp.empty()) {
+ auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME);
+ connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
+ });
+ seq.addTask(flame_task);
+ }
+
+ seq.addTask(m_second_try_metadata);
+
+ 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 ModUpdateDialog::onMetadataEnsured(Mod* mod)
+{
+ // When the mod is a folder, for instance
+ if (!mod->metadata())
+ return;
+
+ switch (mod->metadata()->provider) {
+ case ModPlatform::Provider::MODRINTH:
+ m_modrinth_to_update.push_back(mod);
+ break;
+ case ModPlatform::Provider::FLAME:
+ m_flame_to_update.push_back(mod);
+ break;
+ }
+}
+
+ModPlatform::Provider next(ModPlatform::Provider p)
+{
+ switch (p) {
+ case ModPlatform::Provider::MODRINTH:
+ return ModPlatform::Provider::FLAME;
+ case ModPlatform::Provider::FLAME:
+ return ModPlatform::Provider::MODRINTH;
+ }
+
+ return ModPlatform::Provider::FLAME;
+}
+
+void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice)
+{
+ if (try_others) {
+ auto index_dir = indexDir();
+
+ auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice));
+ connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); });
+
+ m_second_try_metadata->addTask(task);
+ } else {
+ QString reason{ tr("Didn't find a valid version on the selected mod provider(s)") };
+
+ m_failed_metadata.append({mod, reason});
+ }
+}
+
+void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
+{
+ auto item_top = new QTreeWidgetItem(ui->modTreeWidget);
+ item_top->setCheckState(0, Qt::CheckState::Checked);
+ item_top->setText(0, info.name);
+ item_top->setExpanded(true);
+
+ auto provider_item = new QTreeWidgetItem(item_top);
+ provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider)));
+
+ auto old_version_item = new QTreeWidgetItem(item_top);
+ old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version));
+
+ auto new_version_item = new QTreeWidgetItem(item_top);
+ new_version_item->setText(0, tr("New version: %1").arg(info.new_version));
+
+ 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();
+
+ switch (info.provider) {
+ case ModPlatform::Provider::MODRINTH: {
+ HoeDown h;
+ // HoeDown bug?: \n aren't converted to <br>
+ auto text = h.process(info.changelog.toUtf8());
+
+ // Don't convert if there's an HTML tag right after (Qt rendering weirdness)
+ text.remove(QRegularExpression("(\n+)(?=<)"));
+ text.replace('\n', "<br>");
+
+ changelog_area->setHtml(text);
+ break;
+ }
+ case ModPlatform::Provider::FLAME: {
+ changelog_area->setHtml(info.changelog);
+ break;
+ }
+ }
+
+ changelog_area->setOpenExternalLinks(true);
+ changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::NoWrap);
+ changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
+
+ // HACK: Is there a better way of achieving this?
+ auto font_height = QFontMetrics(changelog_area->font()).height();
+ changelog_area->setMaximumHeight((changelog_area->toPlainText().count(QRegularExpression("\n|<br>")) + 2) * font_height);
+
+ ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area);
+
+ ui->modTreeWidget->addTopLevelItem(item_top);
+}
+
+auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*>
+{
+ QList<ModDownloadTask*> list;
+
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+
+ for (int i = 1; item != nullptr; ++i) {
+ if (item->checkState(0) == Qt::CheckState::Checked) {
+ list.push_back(m_tasks.find(item->text(0)).value());
+ }
+
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+
+ return list;
+}
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
new file mode 100644
index 00000000..76aaab36
--- /dev/null
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "BaseInstance.h"
+#include "ModDownloadTask.h"
+#include "ReviewMessageBox.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
+#include "modplatform/CheckUpdateTask.h"
+
+class Mod;
+class ModrinthCheckUpdate;
+class FlameCheckUpdate;
+class ConcurrentTask;
+
+class ModUpdateDialog final : public ReviewMessageBox {
+ Q_OBJECT
+ public:
+ explicit ModUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ const std::shared_ptr<ModFolderModel> mod_model,
+ QList<Mod::Ptr>& search_for);
+
+ void checkCandidates();
+
+ void appendMod(const CheckUpdateTask::UpdatableMod& info);
+
+ const QList<ModDownloadTask*> getTasks();
+ auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
+
+ auto noUpdates() const -> bool { return m_no_updates; };
+ auto aborted() const -> bool { return m_aborted; };
+
+ private:
+ auto ensureMetadata() -> bool;
+
+ private slots:
+ void onMetadataEnsured(Mod*);
+ void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH);
+
+ private:
+ QWidget* m_parent;
+
+ ModrinthCheckUpdate* m_modrinth_check_task = nullptr;
+ FlameCheckUpdate* m_flame_check_task = nullptr;
+
+ const std::shared_ptr<ModFolderModel> m_mod_model;
+
+ QList<Mod::Ptr>& m_candidates;
+ QList<Mod*> m_modrinth_to_update;
+ QList<Mod*> m_flame_to_update;
+
+ ConcurrentTask* m_second_try_metadata;
+ QList<std::tuple<Mod*, QString>> m_failed_metadata;
+ QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
+
+ QHash<QString, ModDownloadTask*> m_tasks;
+ BaseInstance* m_instance;
+
+ bool m_no_updates = false;
+ bool m_aborted = false;
+};
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index e5226016..a79bc837 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -62,24 +62,24 @@ void ProgressDialog::updateSize()
int ProgressDialog::execWithTask(Task* task)
{
this->task = task;
- QDialog::DialogCode result;
if (!task) {
- qDebug() << "Programmer error: progress dialog created with null task.";
- return Accepted;
+ qDebug() << "Programmer error: Progress dialog created with null task.";
+ return QDialog::DialogCode::Accepted;
}
+ QDialog::DialogCode result;
if (handleImmediateResult(result)) {
return result;
}
// Connect signals.
- connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
- connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
- connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
- connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&)));
- connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&)));
- connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
+ connect(task, &Task::started, this, &ProgressDialog::onTaskStarted);
+ connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed);
+ connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded);
+ connect(task, &Task::status, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::progress, this, &ProgressDialog::changeProgress);
connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); });
@@ -89,19 +89,15 @@ int ProgressDialog::execWithTask(Task* task)
ui->globalProgressBar->setHidden(true);
}
- // if this didn't connect to an already running task, invoke start
+ // It's a good idea to start the task after we entered the dialog's event loop :^)
if (!task->isRunning()) {
- task->start();
- }
- if (task->isRunning()) {
- changeProgress(task->getProgress(), task->getTotalProgress());
- changeStatus(task->getStatus());
- return QDialog::exec();
- } else if (handleImmediateResult(result)) {
- return result;
+ QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection);
} else {
- return QDialog::Rejected;
+ changeStatus(task->getStatus());
+ changeProgress(task->getProgress(), task->getTotalProgress());
}
+
+ return QDialog::exec();
}
// TODO: only provide the unique_ptr overloads
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index c92234a4..e664e566 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -40,7 +40,7 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
auto* item = ui->modTreeWidget->topLevelItem(0);
- for (int i = 0; item != nullptr; ++i) {
+ for (int i = 1; item != nullptr; ++i) {
if (item->checkState(0) == Qt::CheckState::Unchecked) {
list.append(item->text(0));
}
diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui
index 299d2ecc..e684185f 100644
--- a/launcher/ui/dialogs/ScrollMessageBox.ui
+++ b/launcher/ui/dialogs/ScrollMessageBox.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>400</width>
+ <width>500</width>
<height>455</height>
</rect>
</property>
diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp
index b5b78690..8180ac1f 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.cpp
+++ b/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -57,68 +57,72 @@ void SkinUploadDialog::on_buttonBox_accepted()
{
QString fileName;
QString input = ui->skinPathTextBox->text();
- QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
- bool isLocalFile = false;
- // it has an URL prefix -> it is an URL
- if(urlPrefixMatcher.match(input).hasMatch())
- {
- QUrl fileURL = input;
- if(fileURL.isValid())
+ ProgressDialog prog(this);
+ SequentialTask skinUpload;
+
+ if (!input.isEmpty()) {
+ QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
+ bool isLocalFile = false;
+ // it has an URL prefix -> it is an URL
+ if(urlPrefixMatcher.match(input).hasMatch())
{
- // local?
- if(fileURL.isLocalFile())
+ QUrl fileURL = input;
+ if(fileURL.isValid())
{
- isLocalFile = true;
- fileName = fileURL.toLocalFile();
+ // local?
+ if(fileURL.isLocalFile())
+ {
+ isLocalFile = true;
+ fileName = fileURL.toLocalFile();
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("Using remote URLs for setting skins is not implemented yet."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
}
else
{
CustomMessageBox::selectable(
this,
tr("Skin Upload"),
- tr("Using remote URLs for setting skins is not implemented yet."),
+ tr("You cannot use an invalid URL for uploading skins."),
QMessageBox::Warning
- )->exec();
+ )->exec();
close();
return;
}
}
else
{
- CustomMessageBox::selectable(
- this,
- tr("Skin Upload"),
- tr("You cannot use an invalid URL for uploading skins."),
- QMessageBox::Warning
- )->exec();
+ // just assume it's a path then
+ isLocalFile = true;
+ fileName = ui->skinPathTextBox->text();
+ }
+ if (isLocalFile && !QFile::exists(fileName))
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
close();
return;
}
+ SkinUpload::Model model = SkinUpload::STEVE;
+ if (ui->steveBtn->isChecked())
+ {
+ model = SkinUpload::STEVE;
+ }
+ else if (ui->alexBtn->isChecked())
+ {
+ model = SkinUpload::ALEX;
+ }
+ skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
}
- else
- {
- // just assume it's a path then
- isLocalFile = true;
- fileName = ui->skinPathTextBox->text();
- }
- if (isLocalFile && !QFile::exists(fileName))
- {
- CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
- close();
- return;
- }
- SkinUpload::Model model = SkinUpload::STEVE;
- if (ui->steveBtn->isChecked())
- {
- model = SkinUpload::STEVE;
- }
- else if (ui->alexBtn->isChecked())
- {
- model = SkinUpload::ALEX;
- }
- ProgressDialog prog(this);
- SequentialTask skinUpload;
- skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
+
auto selectedCape = ui->capeCombo->currentData().toString();
if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui
index f4b0ed0a..c7b16645 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.ui
+++ b/launcher/ui/dialogs/SkinUploadDialog.ui
@@ -21,7 +21,11 @@
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
- <widget class="QLineEdit" name="skinPathTextBox"/>
+ <widget class="QLineEdit" name="skinPathTextBox">
+ <property name="placeholderText">
+ <string>Leave empty to keep current skin</string>
+ </property>
+ </widget>
</item>
<item>
<widget class="QPushButton" name="skinBrowseBtn">