From 5c1026bd12004d7ffa5e27b3eec9883c77fc8d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 16 May 2022 00:25:36 +0200 Subject: [PATCH] NOISSUE Working import from Modrinth, license update to accomodate it --- CMakeLists.txt | 2 +- COPYING.md | 63 ++++ launcher/Application.cpp | 1 + launcher/InstanceImportTask.cpp | 86 +++-- .../pages/modplatform/modrinth/ModrinthData.h | 39 ++- .../modplatform/modrinth/ModrinthModel.cpp | 327 +++++++++++++++++- .../modplatform/modrinth/ModrinthModel.h | 30 +- .../modplatform/modrinth/ModrinthPage.cpp | 112 +++++- .../pages/modplatform/modrinth/ModrinthPage.h | 6 +- 9 files changed, 617 insertions(+), 49 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f566f9d4..add8dbcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,7 @@ set(CMAKE_CXX_FLAGS_RELEASE " -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE} if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type -O0") # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") diff --git a/COPYING.md b/COPYING.md index 4c19bbc2..0cbe6ed6 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,5 +1,7 @@ # MultiMC +Portions are licensed under Apache 2.0 License: + Copyright 2012-2021 MultiMC Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,6 +15,67 @@ See the License for the specific language governing permissions and limitations under the License. +Portions are licensed under MS-PL: + + This license governs use of the accompanying software. If you use the + software, you accept this license. If you do not accept the license, + do not use the software. + + 1. Definitions + The terms "reproduce," "reproduction," "derivative works," and + "distribution" have the same meaning here as under U.S. copyright law. + + A "contribution" is the original software, or any additions or + changes to the software. + + A "contributor" is any person that distributes its contribution + under this license. + + "Licensed patents" are a contributor's patent claims that read + directly on its contribution. + + 2. Grant of Rights + + (A) Copyright Grant- Subject to the terms of this license, + including the license conditions and limitations in section 3, + each contributor grants you a non-exclusive, worldwide, royalty-free + copyright license to reproduce its contribution, prepare derivative + works of its contribution, and distribute its contribution or any + derivative works that you create. + + (B) Patent Grant- Subject to the terms of this license, including + the license conditions and limitations in section 3, each contributor + grants you a non-exclusive, worldwide, royalty-free license under its + licensed patents to make, have made, use, sell, offer for sale, import, + and/or otherwise dispose of its contribution in the software or derivative + works of the contribution in the software. + + 3. Conditions and Limitations + + (A) No Trademark License- This license does not grant you rights to + use any contributors' name, logo, or trademarks. + + (B) If you bring a patent claim against any contributor over patents + that you claim are infringed by the software, your patent license + from such contributor to the software ends automatically. + + (C) If you distribute any portion of the software, you must retain all + copyright, patent, trademark, and attribution notices that are present + in the software. + + (D) If you distribute any portion of the software in source code form, + you may do so only under this license by including a complete copy of + this license with your distribution. If you distribute any portion of + the software in compiled or object code form, you may only do so under + a license that complies with this license. + + (E) The software is licensed "as-is." You bear the risk of using it. + The contributors give no express warranties, guarantees or conditions. + You may have additional consumer rights under your local laws which + this license cannot change. To the extent permitted under your local + laws, the contributors exclude the implied warranties of merchantability, + fitness for a particular purpose and non-infringement. + # MinGW runtime (Windows) Copyright (c) 2012 MinGW.org project diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0575f00a..97e9d225 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -859,6 +859,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); + m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index bfc628f8..6fa8b2b6 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -474,7 +474,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { std::vector files; - QString minecraftVersion, fabricVersion, forgeVersion; + QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -490,40 +490,53 @@ void InstanceImportTask::processModrinth() { } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) + for(auto & obj: jsonFiles) { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + + // env doesn't have to be present, in that case mod is required + auto env = Json::ensureObject(obj, "env"); + auto clientEnv = Json::ensureString(env, "client", "required"); + + if(clientEnv == "required") { + // NOOP + } + else if(clientEnv == "optional") { + file.path += ".disabled"; + } + else if(clientEnv == "unsupported") { + continue; + } + + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) { - Modrinth::File file; - file.path = Json::requireString(obj, "path"); - QString supported = Json::ensureString(Json::ensureObject(obj, "env")); - QJsonObject hashes = Json::requireObject(obj, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) - { - throw JSONValidationError("No hash found for: " + file.path); - } + throw JSONValidationError("No hash found for: " + file.path); } } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) - file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid()) - { - throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); - } - return file; - }); + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid()) + { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + files.push_back(file); + } auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) @@ -541,6 +554,12 @@ void InstanceImportTask::processModrinth() { throw JSONValidationError("Duplicate Fabric Loader version"); fabricVersion = Json::requireString(*it, "Fabric Loader version"); } + else if (name == "quilt-loader") + { + if (!quiltVersion.isEmpty()) + throw JSONValidationError("Duplicate Quilt Loader version"); + quiltVersion = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { if (!forgeVersion.isEmpty()) @@ -583,6 +602,8 @@ void InstanceImportTask::processModrinth() { components->setComponentVersion("net.minecraft", minecraftVersion, true); if (!fabricVersion.isEmpty()) components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!quiltVersion.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true); if (!forgeVersion.isEmpty()) components->setComponentVersion("net.minecraftforge", forgeVersion, true); if (m_instIcon != "default") @@ -602,11 +623,10 @@ void InstanceImportTask::processModrinth() { m_filesNetJob->addNetAction(dl); } connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() - { - m_filesNetJob.reset(); - emitSucceeded(); - } - ); + { + m_filesNetJob.reset(); + emitSucceeded(); + }); connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) { m_filesNetJob.reset(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthData.h b/launcher/ui/pages/modplatform/modrinth/ModrinthData.h index 7c4e7154..509351ca 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthData.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthData.h @@ -10,8 +10,40 @@ #include #include #include +#include +#include namespace Modrinth { +enum class LoadState { + NotLoaded = 0, + Loaded = 1, + Errored = 2 +}; + +enum class VersionType { + Alpha, + Beta, + Release, + Unknown +}; + +struct Download { + bool valid = false; + QString filename; + QString url; + QString sha1; + uint64_t size = 0; + bool primary = false; +}; + +struct Version { + QString name; + Download download; + QDateTime released; + VersionType type = VersionType::Unknown; + bool featured = false; +}; + struct Modpack { QString id; @@ -20,10 +52,15 @@ struct Modpack { QString author; QString description; - bool metadataLoaded = false; + LoadState detailsLoaded = LoadState::NotLoaded; QString wikiUrl; QString body; + + LoadState versionsLoaded = LoadState::NotLoaded; + QVector versions; }; } +Q_DECLARE_METATYPE(Modrinth::Download) +Q_DECLARE_METATYPE(Modrinth::Version) Q_DECLARE_METATYPE(Modrinth::Modpack) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 457b2ffe..8b741f31 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -108,12 +108,12 @@ void Modrinth::ListModel::performPaginatedSearch() auto *netJob = new NetJob("Modrinth::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = QString("https://staging-api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2").arg(currentSort).arg(nextSearchOffset); + searchUrl = QString("https://api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2").arg(currentSort).arg(nextSearchOffset); } else { searchUrl = QString( - "https://staging-api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2&query=%3" + "https://api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2&query=%3" ).arg(currentSort).arg(nextSearchOffset).arg(currentSearchTerm); } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); @@ -136,7 +136,7 @@ void Modrinth::ListModel::searchRequestFinished() return; } - QList newList; + QVector newList; QJsonArray hits; int total_hits; @@ -228,8 +228,8 @@ void Modrinth::ListModel::requestLogo(const QString &logo, const QUrl &url) return; } - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto *job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + auto *job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(url, entry)); auto fullPath = entry->getFullPath(); @@ -239,10 +239,7 @@ void Modrinth::ListModel::requestLogo(const QString &logo, const QUrl &url) QSize size = icon.actualSize(QSize(48, 48)); if (size.width() < 48 && size.height() < 48) { - /*while (size.width() < 48 && size.height() < 48) - size *= 2; - icon = icon.pixmap(48, 48).scaled(size);*/ - icon = icon.pixmap(48,48).scaled(48,48, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + icon = icon.pixmap(48, 48).scaled(48,48, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); } logoLoaded(logo, icon); if(waitingCallbacks.contains(logo)) @@ -260,3 +257,315 @@ void Modrinth::ListModel::requestLogo(const QString &logo, const QUrl &url) m_loadingLogos.append(logo); } + +void Modrinth::ListModel::getPackDetails(const QString& id) +{ + auto index = getIndexFromId(id); + if(!index) { + return; + } + + if(isPackDetailInProgress()) { + queuedPackDetailRequest = id; + cancelPackDetail(); + return; + } + + currentPackDetailRequest = id; + + QString detailsUrl = "https://api.modrinth.com/v2/project/" + id; + + auto & modpack = modpacks[*index]; + if(modpack.detailsLoaded != LoadState::Loaded) + { + auto *netJob = new NetJob("Modrinth::PackDetails", APPLICATION->network()); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(detailsUrl), &detailsResponse)); + detailsPtr = netJob; + detailsPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::detailsRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::detailsRequestFailed); + } + + QString versionsUrl = detailsUrl + "/version"; + if(modpack.versionsLoaded != LoadState::Loaded) + { + auto *netJob = new NetJob("Modrinth::PackVersions", APPLICATION->network()); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(versionsUrl), &versionsResponse)); + versionsPtr = netJob; + versionsPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::versionsRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::versionsRequestFailed); + } +} + +bool Modrinth::ListModel::isPackDetailInProgress() +{ + return detailsPtr || versionsPtr; +} + +void Modrinth::ListModel::cancelPackDetail() +{ + if(detailsPtr) { + detailsPtr->abort(); + } + if(versionsPtr) { + versionsPtr->abort(); + } +} + +nonstd::optional Modrinth::ListModel::getIndexFromId(const QString& id) +{ + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].id == id) { + return i; + } + } + return nonstd::nullopt; +} + +nonstd::optional Modrinth::ListModel::getModpackById(const QString& id) +{ + auto index = getIndexFromId(id); + if(!index) { + return nonstd::nullopt; + } + return modpacks[*index]; +} + +namespace { +bool parseDetailsInto(QByteArray & input, Modrinth::Modpack& output) { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(input, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing pack details response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << input; + return false; + } + + try + { + auto obj = Json::requireObject(doc); + QString body = Json::requireString(obj, "body"); + output.body = body; + return true; + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while parsing response from Modrinth: " << e.cause(); + return false; + } +} +} + +void Modrinth::ListModel::detailsRequestFinished() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + + if(parseDetailsInto(detailsResponse, modpack)) { + modpack.detailsLoaded = LoadState::Loaded; + } + else { + modpack.detailsLoaded = LoadState::Errored; + } + emit packDataChanged(currentPackDetailRequest); + } + detailsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::detailsRequestFailed() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + if(modpack.detailsLoaded == LoadState::NotLoaded) { + modpack.detailsLoaded = LoadState::Errored; + emit packDataChanged(currentPackDetailRequest); + } + } + detailsPtr.reset(); + checkDetailsDone(); +} + +/* + { + "id": "8mMRnfwS", + "project_id": "WCJmvhgU", + "author_id": "akScBBW1", + "featured": true, + "name": "Third Release", + "version_number": "2022.1.12", + "changelog": "This is the third release!", + "changelog_url": null, + "date_published": "2022-01-12T20:41:27+00:00", + "downloads": 22, + "version_type": "release", + "files": [ + { + "hashes": { + "sha1": "0fe87efacfd25c4c5e011cd1433e9be494b23b1c", + "sha512": "d43e148d35d0267b49ed14a7bb4bb5879aa3ebbf5805c2fe125f0f0f19fd84994242bdcd568399c9ff80ecfbe0e975ab632d8c71773bc09d54cfc857f9a6f716" + }, + "url": "https://cdn.modrinth.com/data/WCJmvhgU/versions/2022.1.12/waffles_Modpack-2022.1.12no-hydrogen.mrpack", + "filename": "waffles_Modpack-2022.1.12no-hydrogen.mrpack", + "primary": false, + "size": 0 + } + ], + "dependencies": [], + "game_versions": [ + "1.18.1" + ], + "loaders": [ + "fabric" + ] + } + */ + +bool parseFile(QJsonObject & fileObj, Modrinth::Download & out) { + out.primary = Json::requireBoolean(fileObj, "primary"); + out.size = Json::requireInteger(fileObj, "size"); + out.url = Json::requireString(fileObj, "url"); + out.filename = Json::requireString(fileObj, "filename"); + + auto hashesObj = fileObj["hashes"].toObject(); + out.sha1 = Json::requireString(hashesObj, "sha1"); + + if(!out.filename.endsWith(".mrpack")) { + out.valid = false; + return false; + } + else { + out.valid = true; + return true; + } +} + +namespace { + +bool parseVersionsInto(QByteArray & input, Modrinth::Modpack& output) { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(input, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing pack versions response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << input; + return false; + } + + qDebug() << input; + + try + { + QVector newList; + QJsonArray versions = Json::requireArray(doc); + for (auto obj : versions) + { + auto packObj = obj.toObject(); + Modrinth::Version version; + try + { + if (Json::ensureString(packObj, "client_side", "required") == QStringLiteral("unsupported")) + continue; + version.name = Json::requireString(packObj, "version_number"); + version.released = Json::requireDateTime(packObj, "date_published"); + version.featured = Json::requireBoolean(packObj, "featured"); + auto versionTypeString = Json::requireString(packObj, "version_type"); + if(versionTypeString == "alpha") { + version.type = Modrinth::VersionType::Alpha; + } + else if(versionTypeString == "beta") { + version.type = Modrinth::VersionType::Beta; + } + else if (versionTypeString == "release") { + version.type = Modrinth::VersionType::Release; + } + else { + qWarning() << "Unknown version type of Modrinth modpack: " << versionTypeString; + version.type = Modrinth::VersionType::Unknown; + } + Modrinth::Download fallbackOut = {}; + auto filesArray = Json::requireArray(packObj, "files"); + for(int i = 0; i < filesArray.size(); i++) { + Modrinth::Download maybeFileOut = {}; + QJsonObject fileObj = filesArray[i].toObject(); + parseFile(fileObj, maybeFileOut); + if(i == 0) { + fallbackOut = maybeFileOut; + } + if(maybeFileOut.valid && maybeFileOut.primary) { + version.download = maybeFileOut; + break; + } + } + + if(!version.download.valid) { + version.download = fallbackOut; + } + + if(version.download.valid) { + newList.append(version); + } + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from Modrinth: " << e.cause(); + continue; + } + } + output.versions = newList; + return true; + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while parsing response from Modrinth: " << e.cause(); + return false; + } +} +} + +void Modrinth::ListModel::versionsRequestFinished() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + parseVersionsInto(versionsResponse, modpack); + modpack.versionsLoaded = LoadState::Loaded; + emit packDataChanged(currentPackDetailRequest); + } + versionsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::versionsRequestFailed() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + if(modpack.versionsLoaded == LoadState::NotLoaded) { + modpack.versionsLoaded = LoadState::Errored; + emit packDataChanged(currentPackDetailRequest); + } + } + versionsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::checkDetailsDone() +{ + if(isPackDetailInProgress()) { + return; + } + + // all detail requests are finished + currentPackDetailRequest.clear(); + + // is there a new one queued? + if(!queuedPackDetailRequest.isNull()) { + getPackDetails(queuedPackDetailRequest); + queuedPackDetailRequest.clear(); + } +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 99d1adba..1c4fd1a9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -12,6 +12,8 @@ #include "net/NetJob.h" #include +#include +#include namespace Modrinth { @@ -31,11 +33,22 @@ public: void fetchMore(const QModelIndex &parent) override; void searchWithTerm(const QString &term, const QString &sort); + void getPackDetails(const QString &id); + nonstd::optional getModpackById(const QString &id); + +signals: + void packDataChanged(const QString &id); private slots: void searchRequestFinished(); void searchRequestFailed(); + void detailsRequestFinished(); + void detailsRequestFailed(); + + void versionsRequestFinished(); + void versionsRequestFailed(); + private: void performPaginatedSearch(); @@ -44,7 +57,13 @@ private: void requestLogo(const QString &logo, const QUrl &url); - QList modpacks; + nonstd::optional getIndexFromId(const QString &id); + + bool isPackDetailInProgress(); + void cancelPackDetail(); + void checkDetailsDone(); + + QVector modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; QMap m_logoMap; @@ -59,8 +78,17 @@ private: ResetRequested, Finished } searchState = None; + + QString queuedPackDetailRequest; + QString currentPackDetailRequest; NetJob::Ptr jobPtr; QByteArray response; + + NetJob::Ptr detailsPtr; + QByteArray detailsResponse; + + NetJob::Ptr versionsPtr; + QByteArray versionsResponse; }; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index a4a6d3d9..f5aff42a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -22,6 +22,8 @@ #include "ui_ModrinthPage.h" #include +#include +#include ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) { @@ -42,7 +44,8 @@ ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget 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); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(model, &Modrinth::ListModel::packDataChanged, this, &ModrinthPage::onPackDataChanged); } ModrinthPage::~ModrinthPage() @@ -85,9 +88,112 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { } current = model->data(first, Qt::UserRole).value(); + model->getPackDetails(current.id); + updateCurrentPackUI(); suggestCurrent(); } -void ModrinthPage::suggestCurrent() { +void ModrinthPage::onVersionSelectionChanged(const QString& version) { + if(version.isEmpty() || ui->versionSelectionBox->count() == 0) { + currentVersion = Modrinth::Version(); + } + else { + currentVersion = ui->versionSelectionBox->currentData().value(); + } +} -} \ No newline at end of file +void ModrinthPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (!currentVersion.name.size()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(current.name + " " + currentVersion.name, new InstanceImportTask(currentVersion.download.url)); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(current.id)); + dialog->setSuggestedIconFromFile(entry->getFullPath(), QString("modrinth-%1").arg(current.id)); +} + +void ModrinthPage::onPackDataChanged(const QString& id) +{ + if(id != current.id) { + return; + } + auto newData = model->getModpackById(id); + if(newData) { + current = *newData; + updateCurrentPackUI(); + } +} + +namespace { +QString processMarkdown(QString input) +{ + HoeDown hoedown; + return hoedown.process(input.toUtf8()); +} +} + +QString versionToString(const Modrinth::Version& version) { + switch(version.type) { + case Modrinth::VersionType::Alpha: { + return QString("%1 (Alpha)").arg(version.name); + } + case Modrinth::VersionType::Beta: { + return QString("%1 (Beta)").arg(version.name); + } + case Modrinth::VersionType::Release: { + return version.name; + } + case Modrinth::VersionType::Unknown: { + return QString("%1 (?)").arg(version.name); + } + } +} + +void ModrinthPage::updateCurrentPackUI() +{ + switch(current.detailsLoaded) { + case Modrinth::LoadState::Errored: { + ui->packDescription->setText(tr("Failed to get Modrinth modpack details...")); + break; + } + case Modrinth::LoadState::NotLoaded: { + ui->packDescription->setText(tr("Loading...")); + break; + } + case Modrinth::LoadState::Loaded: { + ui->packDescription->setText(processMarkdown(current.body)); + break; + } + } + if(current.versions.size() == 0) { + ui->versionSelectionBox->clear(); + } + else { + ui->versionSelectionBox->clear(); + int releaseFound = -1; + int i = 0; + for(auto & version: current.versions) { + ui->versionSelectionBox->addItem(versionToString(version), QVariant::fromValue(version)); + if(releaseFound == -1 && version.type == Modrinth::VersionType::Release) { + releaseFound = i; + } + i++; + } + if(releaseFound != -1) { + ui->versionSelectionBox->setCurrentIndex(releaseFound); + } + else if(current.versions.size() != 0) { + ui->versionSelectionBox->setCurrentIndex(0); + } + // select first release found from the top + } + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 9900d0fe..82581e9c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -44,7 +44,7 @@ public: QString displayName() const override { - return tr("Modrinth"); + return tr("Modrinth (WIP)"); } QIcon icon() const override { @@ -62,12 +62,16 @@ public: private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(const QString & version); + void onPackDataChanged(const QString &id); private: + void updateCurrentPackUI(); void suggestCurrent(); Ui::ModrinthPage *ui; NewInstanceDialog *dialog; Modrinth::ListModel *model = nullptr; Modrinth::Modpack current; + Modrinth::Version currentVersion; };