PrismLauncher/launcher/ui/dialogs/ResourceUpdateDialog.cpp
Trial97 a63048d7e2 chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-08 20:41:10 +03:00

535 lines
21 KiB
C++

#include "ResourceUpdateDialog.h"
#include "Application.h"
#include "ChooseProviderDialog.h"
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
#include "ScrollMessageBox.h"
#include "StringUtils.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/SequentialTask.h"
#include "ui_ReviewMessageBox.h"
#include "Markdown.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 <QClipboard>
#include <QShortcut>
#include <QTextBrowser>
#include <QTreeWidgetItem>
#include <optional>
namespace {
std::vector<Version> mcVersions(BaseInstance* inst)
{
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
}
} // namespace
ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
BaseInstance* instance,
ResourceFolderModel* resourceModel,
QList<Resource*>& searchFor,
bool includeDeps,
QList<ModPlatform::ModLoaderType> loadersList)
: ReviewMessageBox(parent, tr("Confirm resources to update"), "")
, m_parent(parent)
, m_resourceModel(resourceModel)
, m_candidates(searchFor)
, m_secondTryMetadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()))
, m_instance(instance)
, m_includeDeps(includeDeps)
, m_loadersList(std::move(loadersList))
{
ReviewMessageBox::setGeometry(0, 0, 800, 600);
ui->explainLabel->setText(tr("You're about to update the following resources:"));
ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!"));
}
void ResourceUpdateDialog::checkCandidates()
{
// Ensure mods have valid metadata
auto wentWell = ensureMetadata();
if (!wentWell) {
m_aborted = true;
return;
}
// Report failed metadata generation
if (!m_failedMetadata.empty()) {
QString text;
for (const auto& failed : m_failedMetadata) {
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 messageDialog(m_parent, tr("Metadata generation failed"),
tr("Could not generate metadata for the following resources:<br>"
"Do you wish to proceed without those resources?"),
text);
messageDialog.setModal(true);
if (messageDialog.exec() == QDialog::Rejected) {
m_aborted = true;
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return;
}
}
auto versions = mcVersions(m_instance);
SequentialTask checkTask(tr("Checking for updates"));
if (!m_modrinthToUpdate.empty()) {
m_modrinthCheckTask.reset(new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel));
connect(m_modrinthCheckTask.get(), &CheckUpdateTask::checkFailed, this,
[this](Resource* resource, const QString& reason, const QUrl& recoverUrl) {
m_failedCheckUpdate.append({ resource, reason, recoverUrl });
});
checkTask.addTask(m_modrinthCheckTask);
}
if (!m_flameToUpdate.empty()) {
m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel));
connect(m_flameCheckTask.get(), &CheckUpdateTask::checkFailed, this,
[this](Resource* resource, const QString& reason, const QUrl& recoverUrl) {
m_failedCheckUpdate.append({ resource, reason, recoverUrl });
});
checkTask.addTask(m_flameCheckTask);
}
connect(&checkTask, &Task::failed, this,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
connect(&checkTask, &Task::succeeded, this, [this, &checkTask]() {
QStringList warnings = checkTask.warnings();
if (warnings.count()) {
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
}
});
// Check for updates
ProgressDialog progressDialog(m_parent);
progressDialog.setSkipButton(true, tr("Abort"));
progressDialog.setWindowTitle(tr("Checking for updates..."));
auto ret = progressDialog.execWithTask(&checkTask);
// If the dialog was skipped / some download error happened
if (ret == QDialog::DialogCode::Rejected) {
m_aborted = true;
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return;
}
QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> selectedVers;
// Add found updates for Modrinth
if (m_modrinthCheckTask) {
auto modrinthUpdates = m_modrinthCheckTask->getUpdates();
for (auto& updatable : modrinthUpdates) {
qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
appendResource(updatable);
m_tasks.insert(updatable.name, updatable.download);
}
selectedVers.append(m_modrinthCheckTask->getDependencies());
}
// Add found updated for Flame
if (m_flameCheckTask) {
auto flameUpdates = m_flameCheckTask->getUpdates();
for (auto& updatable : flameUpdates) {
qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
appendResource(updatable);
m_tasks.insert(updatable.name, updatable.download);
}
selectedVers.append(m_flameCheckTask->getDependencies());
}
// Report failed update checking
if (!m_failedCheckUpdate.empty()) {
QString text;
for (const auto& failed : m_failedCheckUpdate) {
const auto& mod = std::get<0>(failed);
const auto& reason = std::get<1>(failed);
const auto& recoverUrl = 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 (!recoverUrl.isEmpty()) {
//: %1 is the link to download it manually
text += tr("Possible solution: Getting the latest version manually:<br>%1<br>")
.arg(QString("<a href='%1'>%1</a>").arg(recoverUrl.toString()));
}
text += "<br>";
}
ScrollMessageBox messageDialog(m_parent, tr("Failed to check for updates"),
tr("Could not check or get the following resources for updates:<br>"
"Do you wish to proceed without those resources?"),
text, "Disable unavailable mods");
messageDialog.setModal(true);
if (messageDialog.exec() == QDialog::Rejected) {
m_aborted = true;
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return;
}
// Disable unavailable mods
if (messageDialog.isOptionChecked()) {
for (const auto& failed : m_failedCheckUpdate) {
const auto& mod = std::get<0>(failed);
mod->enable(EnableAction::DISABLE);
}
}
}
if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies
auto* modModel = dynamic_cast<ModFolderModel*>(m_resourceModel);
if (modModel != nullptr) {
auto depTask = makeShared<GetModDependenciesTask>(m_instance, modModel, selectedVers);
connect(depTask.get(), &Task::failed, this, [this](const QString& reason) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
});
auto weak = depTask.toWeakRef();
connect(depTask.get(), &Task::succeeded, this, [this, weak]() {
QStringList warnings;
if (auto depTask = weak.lock()) {
warnings = depTask->warnings();
}
if (warnings.count()) {
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
}
});
ProgressDialog progressDialogDeps(m_parent);
progressDialogDeps.setSkipButton(true, tr("Abort"));
progressDialogDeps.setWindowTitle(tr("Checking for dependencies..."));
auto dret = progressDialogDeps.execWithTask(depTask.get());
// If the dialog was skipped / some download error happened
if (dret == QDialog::DialogCode::Rejected) {
m_aborted = true;
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return;
}
static FlameAPI s_api;
auto dependencyExtraInfo = depTask->getExtraInfo();
for (const auto& dep : depTask->getDependecies()) {
auto changelog = dep->version.changelog;
if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) {
changelog = s_api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt());
}
auto downloadTask = makeShared<ResourceDownloadTask>(dep->pack, dep->version, m_resourceModel, true, "dependency");
auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString());
CheckUpdateTask::Update updatable = {
dep->pack->name, dep->version.hash, tr("Not installed"), dep->version.version, dep->version.version_type,
changelog, dep->pack->provider, downloadTask, !extraInfo.maybe_installed
};
appendResource(updatable, extraInfo.required_by);
m_tasks.insert(updatable.name, updatable.download);
}
}
}
// If there's no resource to be updated
if (ui->modTreeWidget->topLevelItemCount() == 0) {
m_noUpdates = 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_noUpdates) {
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
}
}
// Part 1: Ensure we have a valid metadata
auto ResourceUpdateDialog::ensureMetadata() -> bool
{
auto indexDir2 = indexDir();
SequentialTask seq(tr("Looking for metadata"));
// A better use of data structures here could remove the need for this QHash
QHash<QString, bool> shouldTryOthers;
QList<Resource*> modrinthTmp;
QList<Resource*> flameTmp;
bool confirmRest = false;
bool tryOthersRest = false;
bool skipRest = false;
ModPlatform::ResourceProvider providerRest = ModPlatform::ResourceProvider::MODRINTH;
// adds resource to list based on provider
auto addToTmp = [&modrinthTmp, &flameTmp](Resource* resource, ModPlatform::ResourceProvider p) {
switch (p) {
case ModPlatform::ResourceProvider::MODRINTH:
modrinthTmp.push_back(resource);
break;
case ModPlatform::ResourceProvider::FLAME:
flameTmp.push_back(resource);
break;
}
};
// ask the user on what provider to seach for the mod first
for (auto* candidate : m_candidates) {
if (candidate->status() != ResourceStatus::NO_METADATA) {
onMetadataEnsured(candidate);
continue;
}
if (skipRest) {
continue;
}
if (candidate->type() == ResourceType::FOLDER) {
continue;
}
if (confirmRest) {
addToTmp(candidate, providerRest);
shouldTryOthers.insert(candidate->internal_id(), tryOthersRest);
continue;
}
ChooseProviderDialog chooser(this);
chooser.setDescription(tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant "
"information on how to update this mod. "
"To do this, please select a mod provider which we can use to check for updates for this mod.")
.arg(candidate->name()));
auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted;
auto response = chooser.getResponse();
if (response.skip_all) {
skipRest = true;
}
if (response.confirm_all) {
confirmRest = true;
providerRest = response.chosen;
tryOthersRest = response.try_others;
}
shouldTryOthers.insert(candidate->internal_id(), response.try_others);
if (confirmed) {
addToTmp(candidate, response.chosen);
}
}
// prepare task for the modrinth mods
if (!modrinthTmp.empty()) {
auto modrinthTask = makeShared<EnsureMetadataTask>(modrinthTmp, indexDir2, ModPlatform::ResourceProvider::MODRINTH);
connect(modrinthTask.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
connect(modrinthTask.get(), &EnsureMetadataTask::metadataFailed, [this, &shouldTryOthers](Resource* candidate) {
onMetadataFailed(candidate, shouldTryOthers.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH);
});
connect(modrinthTask.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
if (modrinthTask->getHashingTask()) {
seq.addTask(modrinthTask->getHashingTask());
}
seq.addTask(modrinthTask);
}
// prepare task for the flame mods
if (!flameTmp.empty()) {
auto flameTask = makeShared<EnsureMetadataTask>(flameTmp, indexDir2, ModPlatform::ResourceProvider::FLAME);
connect(flameTask.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
connect(flameTask.get(), &EnsureMetadataTask::metadataFailed, [this, &shouldTryOthers](Resource* candidate) {
onMetadataFailed(candidate, shouldTryOthers.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME);
});
connect(flameTask.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
if (flameTask->getHashingTask()) {
seq.addTask(flameTask->getHashingTask());
}
seq.addTask(flameTask);
}
seq.addTask(m_secondTryMetadata);
// execute all the tasks
ProgressDialog checkingDialog(m_parent);
checkingDialog.setSkipButton(true, tr("Abort"));
checkingDialog.setWindowTitle(tr("Generating metadata..."));
auto retMetadata = checkingDialog.execWithTask(&seq);
return (retMetadata != QDialog::DialogCode::Rejected);
}
void ResourceUpdateDialog::onMetadataEnsured(Resource* resource)
{
// When the mod is a folder, for instance
if (!resource->metadata()) {
return;
}
switch (resource->metadata()->provider) {
case ModPlatform::ResourceProvider::MODRINTH:
m_modrinthToUpdate.push_back(resource);
break;
case ModPlatform::ResourceProvider::FLAME:
m_flameToUpdate.push_back(resource);
break;
}
}
ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
{
switch (p) {
case ModPlatform::ResourceProvider::MODRINTH:
return ModPlatform::ResourceProvider::FLAME;
case ModPlatform::ResourceProvider::FLAME:
return ModPlatform::ResourceProvider::MODRINTH;
}
return ModPlatform::ResourceProvider::FLAME;
}
void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool tryOthers, ModPlatform::ResourceProvider firstChoice)
{
if (tryOthers) {
auto indexDir2 = indexDir();
auto task = makeShared<EnsureMetadataTask>(resource, indexDir2, next(firstChoice));
connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
connect(task.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
if (task->getHashingTask()) {
auto seq = makeShared<SequentialTask>();
seq->addTask(task->getHashingTask());
seq->addTask(task);
m_secondTryMetadata->addTask(seq);
} else {
m_secondTryMetadata->addTask(task);
}
} else {
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
m_failedMetadata.append({ resource, reason });
}
}
void ResourceUpdateDialog::appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy)
{
auto* itemTop = new QTreeWidgetItem(ui->modTreeWidget);
itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
if (!info.enabled) {
itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed."));
}
itemTop->setText(0, info.name);
itemTop->setExpanded(true);
auto* providerItem = new QTreeWidgetItem(itemTop);
QString providerName = ModPlatform::ProviderCapabilities::readableName(info.provider);
providerItem->setText(0, tr("Provider: %1").arg(providerName));
providerItem->setData(0, Qt::UserRole, providerName);
auto* oldVersionItem = new QTreeWidgetItem(itemTop);
oldVersionItem->setText(0, tr("Old version: %1").arg(info.old_version));
oldVersionItem->setData(0, Qt::UserRole, info.old_version);
auto* newVersionItem = new QTreeWidgetItem(itemTop);
newVersionItem->setText(0, tr("New version: %1").arg(info.new_version));
newVersionItem->setData(0, Qt::UserRole, info.new_version);
if (info.new_version_type.has_value()) {
auto* newVersionTypeItem = new QTreeWidgetItem(itemTop);
newVersionTypeItem->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString()));
newVersionTypeItem->setData(0, Qt::UserRole, info.new_version_type.value().toString());
}
if (!requiredBy.isEmpty()) {
auto* requiredByItem = new QTreeWidgetItem(itemTop);
if (requiredBy.length() == 1) {
requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back()));
requiredByItem->setData(0, Qt::UserRole, requiredBy.back());
} else {
requiredByItem->setText(0, tr("Required by:"));
for (const auto& req : requiredBy) {
auto* reqItem = new QTreeWidgetItem(requiredByItem);
reqItem->setText(0, req);
}
}
ui->toggleDepsButton->show();
m_deps << itemTop;
}
auto* changelogItem = new QTreeWidgetItem(itemTop);
changelogItem->setText(0, tr("Changelog of the latest version"));
auto* changelog = new QTreeWidgetItem(changelogItem);
auto* changelogArea = new QTextBrowser();
QString text = info.changelog;
changelog->setData(0, Qt::UserRole, text);
if (info.provider == ModPlatform::ResourceProvider::MODRINTH) {
text = markdownToHTML(info.changelog.toUtf8());
}
changelogArea->setHtml(StringUtils::htmlListPatch(text));
changelogArea->setOpenExternalLinks(true);
changelogArea->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
changelogArea->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
ui->modTreeWidget->setItemWidget(changelog, 0, changelogArea);
ui->modTreeWidget->addTopLevelItem(itemTop);
}
auto ResourceUpdateDialog::getTasks() -> const QList<ResourceDownloadTask::Ptr>
{
QList<ResourceDownloadTask::Ptr> 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;
}