aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorflow <thiagodonato300@gmail.com>2022-05-14 20:17:05 -0300
committerflow <thiagodonato300@gmail.com>2022-05-14 20:19:26 -0300
commit4fda35b466e4e3f242955cf8cb692a10e8820f0b (patch)
tree3c7d832f4700c1814ff4b4167719beb9389c1c90
parent31988f0529f6c316d6a9ba3e66cf981a807ed710 (diff)
downloadPrismLauncher-4fda35b466e4e3f242955cf8cb692a10e8820f0b.tar.gz
PrismLauncher-4fda35b466e4e3f242955cf8cb692a10e8820f0b.tar.bz2
PrismLauncher-4fda35b466e4e3f242955cf8cb692a10e8820f0b.zip
feat: add modrinth pack downloading
Things that don't work / work poorly (there's more for sure but those are the evident ones): - Icons are broken in the import dialog - No way to search for private packs - Icons are not downloaded when downloading a mod - No support for multiple download URLs - Probably a lot more...
-rw-r--r--launcher/CMakeLists.txt2
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.cpp84
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.h50
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp274
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h81
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp207
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.h67
7 files changed, 729 insertions, 36 deletions
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 7984d3c9..8e75be20 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -778,6 +778,8 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h
+ ui/pages/modplatform/modrinth/ModrinthModel.cpp
+ ui/pages/modplatform/modrinth/ModrinthModel.h
ui/pages/modplatform/technic/TechnicModel.cpp
ui/pages/modplatform/technic/TechnicModel.h
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
index 2100aaf9..4dcd2fd4 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -14,3 +14,87 @@
*/
#include "ModrinthPackManifest.h"
+#include "Json.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+namespace Modrinth {
+
+void loadIndexedPack(Modpack& pack, QJsonObject& obj)
+{
+ pack.id = Json::ensureString(obj, "project_id");
+
+ pack.name = Json::ensureString(obj, "title");
+ pack.description = Json::ensureString(obj, "description");
+ pack.authors << Json::ensureString(obj, "author");
+ pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug"));
+ pack.iconUrl = Json::ensureString(obj, "icon_url");
+}
+
+void loadIndexedInfo(Modpack& pack, QJsonObject& obj)
+{
+ pack.extra.body = Json::ensureString(obj, "body");
+ pack.extra.sourceUrl = Json::ensureString(obj, "source_url");
+ pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url");
+
+ pack.extraInfoLoaded = true;
+}
+
+void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
+{
+ QVector<ModpackVersion> unsortedVersions;
+
+ auto arr = Json::requireArray(doc);
+
+ for (auto versionIter : arr) {
+ auto obj = Json::requireObject(versionIter);
+ auto file = loadIndexedVersion(obj);
+
+ if(!file.id.isEmpty()) // Heuristic to check if the returned value is valid
+ unsortedVersions.append(file);
+ }
+ auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+
+ pack.versions.swap(unsortedVersions);
+
+ pack.versionsLoaded = true;
+}
+
+auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
+{
+ ModpackVersion file;
+
+ file.name = Json::requireString(obj, "name");
+ file.version = Json::requireString(obj, "version_number");
+
+ file.id = Json::requireString(obj, "id");
+ file.project_id = Json::requireString(obj, "project_id");
+
+ file.date = Json::requireString(obj, "date_published");
+
+ auto files = Json::requireArray(obj, "files");
+
+ for (auto file_iter : files) {
+ File indexed_file;
+ auto parent = Json::requireObject(file_iter);
+ if (!Json::ensureBoolean(parent, "primary", false)) {
+ continue;
+ }
+
+ file.download_url = Json::requireString(parent, "url");
+ break;
+ }
+
+ if(file.download_url.isEmpty())
+ return {};
+
+ return file;
+}
+
+} // namespace Modrinth
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h
index 9742aeb2..7dab893c 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.h
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h
@@ -15,18 +15,68 @@
#pragma once
+#include <QMetaType>
+
#include <QByteArray>
#include <QCryptographicHash>
#include <QString>
#include <QUrl>
+class MinecraftInstance;
+
namespace Modrinth {
+
struct File
{
QString path;
+
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
// TODO: should this support multiple download URLs, like the JSON does?
QUrl download;
};
+
+struct ModpackExtra {
+ QString body;
+
+ QString sourceUrl;
+ QString wikiUrl;
+};
+
+struct ModpackVersion {
+ QString name;
+ QString version;
+
+ QString id;
+ QString project_id;
+
+ QString date;
+
+ QString download_url;
+};
+
+struct Modpack {
+ QString id;
+
+ QString name;
+ QString description;
+ QStringList authors;
+ QString iconName;
+ QUrl iconUrl;
+
+ bool versionsLoaded = false;
+ bool extraInfoLoaded = false;
+
+ ModpackExtra extra;
+ QVector<ModpackVersion> versions;
+};
+
+void loadIndexedPack(Modpack&, QJsonObject&);
+void loadIndexedInfo(Modpack&, QJsonObject&);
+void loadIndexedVersions(Modpack&, QJsonDocument&);
+auto loadIndexedVersion(QJsonObject&) -> ModpackVersion;
+
}
+
+Q_DECLARE_METATYPE(Modrinth::Modpack);
+Q_DECLARE_METATYPE(Modrinth::ModpackVersion);
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
new file mode 100644
index 00000000..2890e27d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -0,0 +1,274 @@
+#include "ModrinthModel.h"
+
+#include "BuildConfig.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+
+#include <QMessageBox>
+
+namespace Modrinth {
+
+ModpackListModel::ModpackListModel(ModrinthPage* parent) : QAbstractListModel(parent), m_parent(parent) {}
+
+auto ModpackListModel::debugName() const -> QString
+{
+ return m_parent->debugName();
+}
+
+/******** Make data requests ********/
+
+void ModpackListModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid())
+ return;
+ if (nextSearchOffset == 0) {
+ qWarning() << "fetchMore with 0 offset is wrong...";
+ return;
+ }
+ performPaginatedSearch();
+}
+
+auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant
+{
+ int pos = index.row();
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ Modrinth::Modpack pack = modpacks.at(pos);
+ if (role == Qt::DisplayRole) {
+ return pack.name;
+ } else if (role == Qt::ToolTipRole) {
+ if (pack.description.length() > 100) {
+ // some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+ }
+ return pack.description;
+ } else if (role == Qt::DecorationRole) {
+ // FIXME: help the icons dont have the same size ;-;
+ if (m_logoMap.contains(pack.iconName)) {
+ return (m_logoMap.value(pack.iconName));
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString());
+ return icon;
+ } else if (role == Qt::UserRole) {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return {};
+}
+
+/*
+void ModpackListModel::requestModVersions(ModPlatform::IndexedPack const& current)
+{
+ auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
+
+ m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
+}*/
+
+void ModpackListModel::performPaginatedSearch()
+{
+ // TODO: Move to standalone API
+ NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network());
+ auto searchAllUrl = QString(
+ "https://staging-api.modrinth.com/v2/search?"
+ "query=%1&"
+ "facets=[[\"project_type:modpack\"]]")
+ .arg(currentSearchTerm);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this] {
+ QJsonParseError parse_error_all {};
+
+ QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all);
+ if (parse_error_all.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset
+ << " reason: " << parse_error_all.errorString();
+ qWarning() << m_all_response;
+ return;
+ }
+
+ searchRequestFinished(doc_all);
+ });
+ QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
+
+ jobPtr = netJob;
+ jobPtr->start();
+}
+
+void ModpackListModel::refresh()
+{
+ if (jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ } else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void ModpackListModel::searchWithTerm(const QString& term, const int sort)
+{
+ if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
+ return;
+ }
+
+ currentSearchTerm = term;
+ currentSort = sort;
+
+ refresh();
+}
+
+void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
+{
+ if (m_logoMap.contains(logo)) {
+ callback(APPLICATION->metacache()
+ ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))
+ ->getFullPath());
+ } else {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ModpackListModel::requestLogo(QString logo, QString url)
+{
+ if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
+ return;
+ }
+
+ MetaEntryPtr entry =
+ APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
+ auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
+ job->deleteLater();
+ emit logoLoaded(logo, QIcon(fullPath));
+ if (waitingCallbacks.contains(logo)) {
+ waitingCallbacks.value(logo)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
+ job->deleteLater();
+ emit logoFailed(logo);
+ });
+
+ job->start();
+ m_loadingLogos.append(logo);
+}
+
+/******** Request callbacks ********/
+
+void ModpackListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ for (int i = 0; i < modpacks.size(); i++) {
+ if (modpacks[i].iconName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
+ }
+ }
+}
+
+void ModpackListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
+{
+ jobPtr.reset();
+
+ QList<Modrinth::Modpack> newList;
+
+ auto packs_all = doc_all.object().value("hits").toArray();
+ for (auto packRaw : packs_all) {
+ auto packObj = packRaw.toObject();
+
+ Modrinth::Modpack pack;
+ try {
+ Modrinth::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ } catch (const JSONValidationError& e) {
+ qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ if (packs_all.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ModpackListModel::searchRequestFailed(QString reason)
+{
+ if (!jobPtr->first()->m_reply) {
+ // Network error
+ QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
+ } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ // 409 Gone, notify user to update
+ QMessageBox::critical(nullptr, tr("Error"),
+ //: %1 refers to the launcher itself
+ QString("%1 %2")
+ .arg(m_parent->displayName())
+ .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
+ }
+ jobPtr.reset();
+
+ if (searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+void ModpackListModel::versionRequestSucceeded(QJsonDocument doc, QString id)
+{
+ auto& current = m_parent->getCurrent();
+ if (id != current.id) {
+ return;
+ }
+
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+
+ try {
+ // loadIndexedPackVersions(current, arr);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
+ }
+
+ // m_parent->updateModVersions();
+}
+
+} // namespace Modrinth
+
+/******** Helpers ********/
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
new file mode 100644
index 00000000..1fdbe278
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/modrinth/ModrinthPackManifest.h"
+#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
+
+class ModPage;
+class Version;
+
+namespace Modrinth {
+
+using LogoMap = QMap<QString, QIcon>;
+using LogoCallback = std::function<void (QString)>;
+
+class ModpackListModel : public QAbstractListModel {
+ Q_OBJECT
+
+ public:
+ ModpackListModel(ModrinthPage* parent);
+ ~ModpackListModel() override = default;
+
+ inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
+ inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
+ inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
+
+ auto debugName() const -> QString;
+
+ /* Retrieve information from the model at a given index with the given role */
+ auto data(const QModelIndex& index, int role) const -> QVariant override;
+
+ inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
+
+ /* Ask the API for more information */
+ void fetchMore(const QModelIndex& parent) override;
+ void refresh();
+ void searchWithTerm(const QString& term, const int sort);
+
+ void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
+
+ inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; };
+
+ public slots:
+ void searchRequestFinished(QJsonDocument& doc_all);
+ void searchRequestFailed(QString reason);
+
+ void versionRequestSucceeded(QJsonDocument doc, QString addonId);
+
+ protected slots:
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+ void performPaginatedSearch();
+
+ protected:
+ void requestLogo(QString file, QString url);
+
+ inline auto getMineVersions() const -> std::list<Version>;
+
+ protected:
+ ModrinthPage* m_parent;
+
+ QList<Modrinth::Modpack> modpacks;
+
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
+
+ NetJob::Ptr jobPtr;
+
+ QByteArray m_all_response;
+ QByteArray m_specific_response;
+};
+} // namespace ModPlatform
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 0d65ef16..68805316 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -34,14 +34,41 @@
*/
#include "ModrinthPage.h"
-
#include "ui_ModrinthPage.h"
+#include "ModrinthModel.h"
+
+#include "InstanceImportTask.h"
+#include "Json.h"
+
+#include <HoeDown.h>
+
+#include <QComboBox>
#include <QKeyEvent>
+#include <QPushButton>
-ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog)
+ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog)
{
ui->setupUi(this);
+
+ connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ m_model = new Modrinth::ModpackListModel(this);
+ ui->packView->setModel(m_model);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ ui->sortByBox->addItem(tr("Sort by Featured"));
+ ui->sortByBox->addItem(tr("Sort by Popularity"));
+ ui->sortByBox->addItem(tr("Sort by Last Updated"));
+ ui->sortByBox->addItem(tr("Sort by Name"));
+ ui->sortByBox->addItem(tr("Sort by Author"));
+ ui->sortByBox->addItem(tr("Sort by Total Downloads"));
+
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged);
}
ModrinthPage::~ModrinthPage()
@@ -60,10 +87,10 @@ void ModrinthPage::openedImpl()
triggerSearch();
}
-bool ModrinthPage::eventFilter(QObject *watched, QEvent *event)
+bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- auto *keyEvent = reinterpret_cast<QKeyEvent *>(event);
+ auto* keyEvent = reinterpret_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
this->triggerSearch();
keyEvent->accept();
@@ -73,6 +100,176 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event)
return QObject::eventFilter(watched, event);
}
-void ModrinthPage::triggerSearch() {
+void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if (!first.isValid()) {
+ if (isOpened) {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ current = m_model->data(first, Qt::UserRole).value<Modrinth::Modpack>();
+ auto name = current.name;
+
+ if (!current.extraInfoLoaded) {
+ qDebug() << "Loading modrinth modpack information";
+
+ auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network());
+ auto response = new QByteArray();
+
+ QString id = current.id;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1").arg(id), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] {
+ if (id != current.id) {
+ return; // wrong request?
+ }
+
+ 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;
+ return;
+ }
+
+ auto obj = Json::requireObject(doc);
+
+ try {
+ Modrinth::loadIndexedInfo(current, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << *response;
+ qWarning() << "Error while reading modrinth modpack version: " << e.cause();
+ }
+
+ updateUI();
+ suggestCurrent();
+ });
+ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
+ netJob->deleteLater();
+ delete response;
+ });
+ netJob->start();
+ } else
+ updateUI();
+
+ if (!current.versionsLoaded) {
+ qDebug() << "Loading modrinth modpack versions";
+
+ auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network());
+ auto response = new QByteArray();
+
+ QString id = current.id;
+
+ netJob->addNetAction(
+ Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1/version").arg(id), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] {
+ if (id != current.id) {
+ return; // wrong request?
+ }
+
+ 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;
+ return;
+ }
+
+ try {
+ Modrinth::loadIndexedVersions(current, doc);
+ } catch (const JSONValidationError& e) {
+ qDebug() << *response;
+ qWarning() << "Error while reading modrinth modpack version: " << e.cause();
+ }
+
+ for (auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.id));
+ }
+
+ updateVersionsUI();
+ suggestCurrent();
+ });
+ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
+ netJob->deleteLater();
+ delete response;
+ });
+ netJob->start();
+
+ } else {
+ for (auto version : current.versions) {
+ ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id));
+ }
+
+ suggestCurrent();
+ }
+}
+
+void ModrinthPage::updateUI()
+{
+ QString text = "";
+
+ if (current.extra.sourceUrl.isEmpty())
+ text = current.name;
+ else
+ text = "<a href=\"" + current.extra.sourceUrl + "\">" + current.name + "</a>";
+
+ if (!current.authors.empty()) {
+ // TODO: Implement multiple authors with links
+ text += "<br>" + tr(" by ") + current.authors.at(0);
+ }
+
+ text += "<br>";
+
+ HoeDown h;
+ text += h.process(current.extra.body.toUtf8());
+
+ ui->packDescription->setHtml(text + current.description);
+}
+
+void ModrinthPage::updateVersionsUI()
+{
+ // idk
+}
+
+void ModrinthPage::suggestCurrent()
+{
+ if (!isOpened) {
+ return;
+ }
+
+ if (selectedVersion.isEmpty()) {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ for (auto& ver : current.versions) {
+ if (ver.id == selectedVersion) {
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url));
+
+ break;
+ }
+ }
+}
+
+void ModrinthPage::triggerSearch()
+{
+ m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void ModrinthPage::onVersionSelectionChanged(QString data)
+{
+ if (data.isNull() || data.isEmpty()) {
+ selectedVersion = "";
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toString();
+ suggestCurrent();
}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
index 562049b4..f72a5071 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -39,48 +39,53 @@
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/pages/BasePage.h"
+#include "modplatform/modrinth/ModrinthPackManifest.h"
+
#include <QWidget>
-namespace Ui
-{
- class ModrinthPage;
+namespace Ui {
+class ModrinthPage;
+}
+
+namespace Modrinth {
+class ModpackListModel;
}
-class ModrinthPage : public QWidget, public BasePage
-{
+class ModrinthPage : public QWidget, public BasePage {
Q_OBJECT
-public:
- explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr);
+ public:
+ explicit ModrinthPage(NewInstanceDialog* dialog, QWidget* parent = nullptr);
~ModrinthPage() override;
- QString displayName() const override
- {
- return tr("Modrinth");
- }
- QIcon icon() const override
- {
- return APPLICATION->getThemedIcon("modrinth");
- }
- QString id() const override
- {
- return "modrinth";
- }
-
- virtual QString helpPage() const override
- {
- return "Modrinth-platform";
- }
- void retranslate() override;
+ QString displayName() const override { return tr("Modrinth"); }
+ QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); }
+ QString id() const override { return "modrinth"; }
+ QString helpPage() const override { return "Modrinth-platform"; }
- void openedImpl() override;
+ inline auto debugName() const -> QString { return "Modrinth"; }
+ inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; };
- bool eventFilter(QObject *watched, QEvent *event) override;
+ auto getCurrent() -> Modrinth::Modpack& { return current; }
+ void suggestCurrent();
-private slots:
+ void updateUI();
+ void updateVersionsUI();
+
+ void retranslate() override;
+ void openedImpl() override;
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+ private slots:
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
void triggerSearch();
-private:
- Ui::ModrinthPage *ui;
- NewInstanceDialog *dialog;
+ private:
+ Ui::ModrinthPage* ui;
+ NewInstanceDialog* dialog;
+ Modrinth::ModpackListModel* m_model;
+
+ Modrinth::Modpack current;
+ QString selectedVersion;
};