diff options
Diffstat (limited to 'launcher/ui')
-rw-r--r-- | launcher/ui/dialogs/ChooseProviderDialog.cpp | 96 | ||||
-rw-r--r-- | launcher/ui/dialogs/ChooseProviderDialog.h | 56 | ||||
-rw-r--r-- | launcher/ui/dialogs/ChooseProviderDialog.ui | 89 | ||||
-rw-r--r-- | launcher/ui/dialogs/ModUpdateDialog.cpp | 408 | ||||
-rw-r--r-- | launcher/ui/dialogs/ModUpdateDialog.h | 62 | ||||
-rw-r--r-- | launcher/ui/dialogs/ProgressDialog.cpp | 34 | ||||
-rw-r--r-- | launcher/ui/dialogs/ReviewMessageBox.cpp | 2 | ||||
-rw-r--r-- | launcher/ui/dialogs/ScrollMessageBox.ui | 2 | ||||
-rw-r--r-- | launcher/ui/dialogs/SkinUploadDialog.cpp | 90 | ||||
-rw-r--r-- | launcher/ui/dialogs/SkinUploadDialog.ui | 6 | ||||
-rw-r--r-- | launcher/ui/pages/instance/ExternalResourcesPage.ui | 11 | ||||
-rw-r--r-- | launcher/ui/pages/instance/ModFolderPage.cpp | 78 | ||||
-rw-r--r-- | launcher/ui/pages/instance/ModFolderPage.h | 1 | ||||
-rw-r--r-- | launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 2 | ||||
-rw-r--r-- | launcher/ui/widgets/WideBar.cpp | 49 | ||||
-rw-r--r-- | launcher/ui/widgets/WideBar.h | 31 |
16 files changed, 926 insertions, 91 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"> diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 17bf455a..8edcfd64 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -147,6 +147,17 @@ <string>Download a new resource</string> </property> </action> + <action name="actionUpdateItem"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Check for &Updates</string> + </property> + <property name="toolTip"> + <string>"Tries to find / update all selected resources (all resources if none is selected)"</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 4432ccc8..b190e51a 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -49,6 +49,7 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ModUpdateDialog.h" #include "DesktopServices.h" @@ -78,6 +79,23 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); + + ui->actionUpdateItem->setToolTip(tr("Tries to find / update all selected mods (all mods if none is selected)")); + ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); + + connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::rowsInserted, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] { + ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); + + // Prevent a weird crash when trying to open the mods page twice in a session o.O + disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); + }); } } @@ -107,7 +125,6 @@ bool CoreModFolderPage::shouldDisplay() const return false; if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) return true; - } return false; } @@ -118,7 +135,7 @@ void ModFolderPage::installMods() return; if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - + auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile(); if (profile->getModLoaders() == ModAPI::Unspecified) { QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); @@ -140,7 +157,7 @@ void ModFolderPage::installMods() QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - + tasks->deleteLater(); }); @@ -155,3 +172,58 @@ void ModFolderPage::installMods() m_model->update(); } } + +void ModFolderPage::updateMods() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedMods(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allMods(); + + ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + CustomMessageBox::selectable(this, tr("Update checker"), + (mods_list.size() == 1) + ? tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) + : tr("All %1mods are up-to-date! :)").arg(use_all ? "" : (tr("selected") + " "))) + ->exec(); + return; + } + + if (update_dialog.exec()) { + ConcurrentTask* tasks = new ConcurrentTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 19caa732..0a7fc9fa 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -56,6 +56,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void installMods(); + void updateMods(); }; class CoreModFolderPage : public ModFolderPage { diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 10d34218..772fd2e0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -64,7 +64,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); } // I don't know why, but doing this on the parent class makes it so that diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 8d5bd12d..79f1e0c9 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -76,13 +76,20 @@ void WideBar::addSeparator() m_entries.push_back(entry); } -void WideBar::insertActionBefore(QAction* before, QAction* action){ - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) { - return entry->wideAction == before; +auto WideBar::getMatching(QAction* act) -> QList<BarEntry*>::iterator +{ + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) { + return entry->wideAction == act; }); - if(iter == m_entries.end()) { + + return iter; +} + +void WideBar::insertActionBefore(QAction* before, QAction* action){ + auto iter = getMatching(before); + if(iter == m_entries.end()) return; - } + auto entry = new BarEntry(); entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this)); entry->wideAction = action; @@ -90,14 +97,24 @@ void WideBar::insertActionBefore(QAction* before, QAction* action){ m_entries.insert(iter, entry); } +void WideBar::insertActionAfter(QAction* after, QAction* action){ + auto iter = getMatching(after); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this)); + entry->wideAction = action; + entry->type = BarEntry::Action; + m_entries.insert(iter + 1, entry); +} + void WideBar::insertSpacer(QAction* action) { - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) { - return entry->wideAction == action; - }); - if(iter == m_entries.end()) { + auto iter = getMatching(action); + if(iter == m_entries.end()) return; - } + QWidget* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -107,6 +124,18 @@ void WideBar::insertSpacer(QAction* action) m_entries.insert(iter, entry); } +void WideBar::insertSeparator(QAction* before) +{ + auto iter = getMatching(before); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = QToolBar::insertSeparator(before); + entry->type = BarEntry::Separator; + m_entries.insert(iter, entry); +} + QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title) { QMenu *contextMenu = new QMenu(title, parent); diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 2b676a8c..8ff62ef2 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -1,27 +1,34 @@ #pragma once -#include <QToolBar> #include <QAction> #include <QMap> +#include <QToolBar> class QMenu; -class WideBar : public QToolBar -{ +class WideBar : public QToolBar { Q_OBJECT -public: - explicit WideBar(const QString &title, QWidget * parent = nullptr); - explicit WideBar(QWidget * parent = nullptr); + public: + explicit WideBar(const QString& title, QWidget* parent = nullptr); + explicit WideBar(QWidget* parent = nullptr); virtual ~WideBar(); - void addAction(QAction *action); + void addAction(QAction* action); void addSeparator(); - void insertSpacer(QAction *action); - void insertActionBefore(QAction *before, QAction *action); - QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString()); -private: + void insertSpacer(QAction* action); + void insertSeparator(QAction* before); + void insertActionBefore(QAction* before, QAction* action); + void insertActionAfter(QAction* after, QAction* action); + + QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); + + private: struct BarEntry; - QList<BarEntry *> m_entries; + + auto getMatching(QAction* act) -> QList<BarEntry*>::iterator; + + private: + QList<BarEntry*> m_entries; }; |