// SPDX-FileCopyrightText: 2022 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ManagedPackPage.h" #include "ui_ManagedPackPage.h" #include #include #include #include "Application.h" #include "BuildConfig.h" #include "InstanceImportTask.h" #include "InstanceList.h" #include "InstanceTask.h" #include "Json.h" #include "Markdown.h" #include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/InstanceWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "net/ApiDownload.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: // 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 /** * Something about QProxyStyle and QStyle objects means they can't be free'd just * because all the widgets using them are gone. * They seems to be tied to the QApplicaiton lifecycle. * So make singletons tied to the lifetime of the application to clean them up and ensure they aren't * being remade over and over again, thus leaking memory. */ public: static NoBigComboBoxStyle* getInstance(QStyle* style) { static QHash s_singleton_instances_ = {}; static std::mutex s_singleton_instances_mutex_; std::lock_guard lock(s_singleton_instances_mutex_); auto inst_iter = s_singleton_instances_.constFind(style); NoBigComboBoxStyle* inst = nullptr; if (inst_iter == s_singleton_instances_.constEnd() || *inst_iter == nullptr) { inst = new NoBigComboBoxStyle(style); inst->setParent(APPLICATION); s_singleton_instances_.insert(style, inst); qDebug() << "QProxyStyle NoBigComboBox created for" << style->objectName() << style; } else { inst = *inst_iter; } return inst; } private: NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} }; 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); // NOTE: GTK2 themes crash with the proxy style. // This seems like an upstream bug, so there's not much else that can be done. if (!QStyleFactory::keys().contains("gtk2")) { auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style()); ui->versionsComboBox->setStyle(comboStyle); } 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: %2 | 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 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::clicked, 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(); QString id = m_inst->getManagedPackID(); m_fetch_job->addNetAction( Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); 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 = version.version; if (!version.name.contains(version.version)) name = QString("%1 — %2").arg(version.name, version.version); // 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 = tr("%1 (Current)").arg(name); 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(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); ManagedPackPage::suggestVersion(); } void ModrinthManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); QMap 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::clicked, 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("

Hey there!

" "

" "It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.
" "Unfortunately, we can't do the proper API requests without this information.
" "
" "So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.
" "
" "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" "

"); 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(); QString id = m_inst->getManagedPackID(); m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); 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 = version.version; if (version.fileId == m_inst->getManagedPackVersionID().toInt()) name = tr("%1 (Current)").arg(name); 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(); if (m_pack.versions.length() == 0) { setFailState(); return; } 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(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); QMap 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"