diff options
Diffstat (limited to 'launcher/ui/pages/instance/ManagedPackPage.cpp')
-rw-r--r-- | launcher/ui/pages/instance/ManagedPackPage.cpp | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp new file mode 100644 index 00000000..7a0d234c --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ManagedPackPage.h" +#include "ui_ManagedPackPage.h" + +#include <QListView> +#include <QProxyStyle> + +#include <HoeDown.h> + +#include "Application.h" +#include "BuildConfig.h" +#include "InstanceImportTask.h" +#include "InstanceList.h" +#include "InstanceTask.h" +#include "Json.h" + +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "ui/InstanceWindow.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" + +/** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen. + * ... thanks Qt. + */ +class NoBigComboBoxStyle : public QProxyStyle { + Q_OBJECT + + public: + NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} + + // clang-format off + int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override + { + if (hint == QStyle::SH_ComboBox_Popup) + return false; + + return QProxyStyle::styleHint(hint, option, widget, returnData); + } + // clang-format on +}; + +ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) +{ + if (type == "modrinth") + return new ModrinthManagedPackPage(inst, nullptr, parent); + if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame)) + return new FlameManagedPackPage(inst, nullptr, parent); + + return new GenericManagedPackPage(inst, nullptr, parent); +} + +ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst) +{ + Q_ASSERT(inst); + + ui->setupUi(this); + + ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style())); + + ui->reloadButton->setVisible(false); + connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ + ui->reloadButton->setVisible(false); + + m_loaded = false; + // Pretend we're opening the page again + openedImpl(); + }); +} + +ManagedPackPage::~ManagedPackPage() +{ + delete ui; +} + +void ManagedPackPage::openedImpl() +{ + ui->packName->setText(m_inst->getManagedPackName()); + ui->packVersion->setText(m_inst->getManagedPackVersionName()); + ui->packOrigin->setText(tr("Website: <a href=%1>%2</a> | Pack ID: %3 | Version ID: %4") + .arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID())); + + parseManagedPack(); +} + +QString ManagedPackPage::displayName() const +{ + auto type = m_inst->getManagedPackType(); + if (type.isEmpty()) + return {}; + if (type == "flame") + type = "CurseForge"; + return type.replace(0, 1, type[0].toUpper()); +} + +QIcon ManagedPackPage::icon() const +{ + return APPLICATION->getThemedIcon(m_inst->getManagedPackType()); +} + +QString ManagedPackPage::helpPage() const +{ + return {}; +} + +void ManagedPackPage::retranslate() +{ + ui->retranslateUi(this); +} + +bool ManagedPackPage::shouldDisplay() const +{ + return m_inst->isManagedPack(); +} + +bool ManagedPackPage::runUpdateTask(InstanceTask* task) +{ + Q_ASSERT(task); + + unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task)); + + connect(task, &Task::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task, &Task::succeeded, [this, task]() { + QStringList warnings = task->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + }); + connect(task, &Task::aborted, [this] { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + return task->wasSuccessful(); +} + +void ManagedPackPage::suggestVersion() +{ + ui->updateButton->setText(tr("Update pack")); + ui->updateButton->setDisabled(false); +} + +void ManagedPackPage::setFailState() +{ + qDebug() << "Setting fail state!"; + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {}); + ui->versionsComboBox->blockSignals(false); + + ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack.")); + + ui->updateButton->setText(tr("Cannot update!")); + ui->updateButton->setDisabled(true); + + ui->reloadButton->setVisible(true); +} + +ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update); +} + +// MODRINTH + +void ModrinthManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Modrinth pack"; + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + Modrinth::loadIndexedVersions(m_pack, doc); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + if (!version.name.contains(version.version)) + name = QString("%1 — %2").arg(version.name, version.version); + else + name = version.name; + + // NOTE: the id from version isn't the same id in the modpack format spec... + // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. + if (version.version == m_inst->getManagedPackVersionName()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.id)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + + ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); + + m_fetch_job->start(); +} + +QString ModrinthManagedPackPage::url() const +{ + return "https://modrinth.com/mod/" + m_inst->getManagedPackID(); +} + +void ModrinthManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + HoeDown md_parser; + ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8())); + + ManagedPackPage::suggestVersion(); +} + +void ModrinthManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", version.id); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info)); + + InstanceName inst_name(m_inst->getManagedPackName(), version.version); + inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); + extracted->setName(inst_name); + + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +// FLAME + +FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update); +} + +void FlameManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Flame pack"; + + // We need to tell the user to redownload the pack, since we didn't save the required info previously + if (m_inst->getManagedPackID().isEmpty()) { + setFailState(); + QString message = + tr("<h1>Hey there!</h1>" + "<h4>" + "It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.<br/>" + "Unfortunately, we can't do the proper API requests without this information.<br/>" + "<br/>" + "So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.<br/>" + "<br/>" + "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" + "</h4>"); + + ui->changelogTextBrowser->setHtml(message); + return; + } + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + auto obj = doc.object(); + auto data = Json::ensureArray(obj, "data"); + Flame::loadIndexedPackVersions(m_pack, data); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + name = version.version; + + if (version.fileId == m_inst->getManagedPackVersionID().toInt()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.fileId)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + + m_fetch_job->start(); +} + +QString FlameManagedPackPage::url() const +{ + // FIXME: We should display the websiteUrl field, but this requires doing the API request first :( + return {}; +} + +void FlameManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); + + ManagedPackPage::suggestVersion(); +} + +void FlameManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +#include "ManagedPackPage.moc" |