diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 3193d813..beaa0c00 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -466,6 +466,8 @@ set(FTB_SOURCES set(FLAME_SOURCES # Flame + modplatform/flame/FlamePackIndex.cpp + modplatform/flame/FlamePackIndex.h modplatform/flame/PackManifest.h modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h diff --git a/api/logic/modplatform/flame/FlamePackIndex.cpp b/api/logic/modplatform/flame/FlamePackIndex.cpp new file mode 100644 index 00000000..3d8ea22a --- /dev/null +++ b/api/logic/modplatform/flame/FlamePackIndex.cpp @@ -0,0 +1,92 @@ +#include "FlamePackIndex.h" + +#include "Json.h" + +void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) +{ + pack.addonId = Json::requireInteger(obj, "id"); + pack.name = Json::requireString(obj, "name"); + pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.description = Json::ensureString(obj, "summary", ""); + + bool thumbnailFound = false; + auto attachments = Json::requireArray(obj, "attachments"); + for(auto attachmentRaw: attachments) { + auto attachmentObj = Json::requireObject(attachmentRaw); + bool isDefault = attachmentObj.value("isDefault").toBool(false); + if(isDefault) { + thumbnailFound = true; + pack.logoName = Json::requireString(attachmentObj, "title"); + pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); + break; + } + } + + if(!thumbnailFound) { + throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); + } + + auto authors = Json::requireArray(obj, "authors"); + for(auto authorIter: authors) { + auto author = Json::requireObject(authorIter); + Flame::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } + int defaultFileId = Json::requireInteger(obj, "defaultFileId"); + + bool found = false; + // check if there are some files before adding the pack + auto files = Json::requireArray(obj, "latestFiles"); + for(auto fileIter: files) { + auto file = Json::requireObject(fileIter); + int id = Json::requireInteger(file, "id"); + + // NOTE: for now, ignore everything that's not the default... + if(id != defaultFileId) { + continue; + } + + auto versionArray = Json::requireArray(file, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + found = true; + break; + } + if(!found) { + throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); + } +} + +void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) +{ + QVector unsortedVersions; + for(auto versionIter: arr) { + auto version = Json::requireObject(versionIter); + Flame::IndexedVersion file; + + file.addonId = pack.addonId; + file.fileId = Json::requireInteger(version, "id"); + auto versionArray = Json::requireArray(version, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + // pick the latest version supported + file.mcVersion = versionArray[0].toString(); + file.version = Json::requireString(version, "displayName"); + file.downloadUrl = Json::requireString(version, "downloadUrl"); + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool + { + return a.fileId > b.fileId; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} diff --git a/api/logic/modplatform/flame/FlamePackIndex.h b/api/logic/modplatform/flame/FlamePackIndex.h new file mode 100644 index 00000000..cdeb2c13 --- /dev/null +++ b/api/logic/modplatform/flame/FlamePackIndex.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +namespace Flame { + +struct ModpackAuthor { + QString name; + QString url; +}; + +struct IndexedVersion { + int addonId; + int fileId; + QString version; + QString mcVersion; + QString downloadUrl; +}; + +struct IndexedPack +{ + int addonId; + QString name; + QString description; + QList authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector versions; +}; + +MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +MULTIMC_LOGIC_EXPORT void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); +} + +Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index afd13574..c240baf2 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -145,7 +145,6 @@ SET(MULTIMC_SOURCES pages/modplatform/legacy_ftb/ListModel.h pages/modplatform/legacy_ftb/ListModel.cpp - pages/modplatform/flame/FlameData.h pages/modplatform/flame/FlameModel.cpp pages/modplatform/flame/FlameModel.h pages/modplatform/flame/FlamePage.cpp diff --git a/application/pages/modplatform/flame/FlameData.h b/application/pages/modplatform/flame/FlameData.h deleted file mode 100644 index 9245ba8a..00000000 --- a/application/pages/modplatform/flame/FlameData.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include - -namespace Flame { - -struct ModpackAuthor { - QString name; - QString url; -}; - -struct ModpackFile { - int addonId; - int fileId; - QString version; - QString mcVersion; - QString downloadUrl; -}; - -struct Modpack -{ - bool broken = true; - int addonId = 0; - - QString name; - QString description; - QList authors; - QString mcVersion; - QString logoName; - QString logoUrl; - QString websiteUrl; - - ModpackFile latestFile; -}; -} - -Q_DECLARE_METATYPE(Flame::Modpack) diff --git a/application/pages/modplatform/flame/FlameModel.cpp b/application/pages/modplatform/flame/FlameModel.cpp index 6d9dbda7..228a88c5 100644 --- a/application/pages/modplatform/flame/FlameModel.cpp +++ b/application/pages/modplatform/flame/FlameModel.cpp @@ -1,5 +1,6 @@ #include "FlameModel.h" #include "MultiMC.h" +#include #include #include @@ -38,7 +39,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const return QString("INVALID INDEX %1").arg(pos); } - Modpack pack = modpacks.at(pos); + IndexedPack pack = modpacks.at(pos); if(role == Qt::DisplayRole) { return pack.name; @@ -163,13 +164,12 @@ void ListModel::performPaginatedSearch() "https://addons-ecs.forgesvc.net/api/v2/addon/search?" "categoryId=0&" "gameId=432&" - //"gameVersion=1.12.2&" "index=%1&" "pageSize=25&" "searchFilter=%2&" "sectionId=4471&" - "sort=0" - ).arg(nextSearchOffset).arg(currentSearchTerm); + "sort=%3" + ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -177,12 +177,13 @@ void ListModel::performPaginatedSearch() QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString& term) +void ListModel::searchWithTerm(const QString& term, int sort) { - if(currentSearchTerm == term) { + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } currentSearchTerm = term; + currentSort = sort; if(jobPtr) { jobPtr->abort(); searchState = ResetRequested; @@ -210,79 +211,24 @@ void Flame::ListModel::searchRequestFinished() return; } - QList newList; - auto objs = doc.array(); - for(auto projectIter: objs) { - Modpack pack; - auto project = projectIter.toObject(); - pack.addonId = project.value("id").toInt(0); - if (pack.addonId == 0) { - qWarning() << "Pack without an ID, skipping: " << pack.name; - continue; - } - pack.name = project.value("name").toString(); - pack.websiteUrl = project.value("websiteUrl").toString(); - pack.description = project.value("summary").toString(); - bool thumbnailFound = false; - auto attachments = project.value("attachments").toArray(); - for(auto attachmentIter: attachments) { - auto attachment = attachmentIter.toObject(); - bool isDefault = attachment.value("isDefault").toBool(false); - if(isDefault) { - thumbnailFound = true; - pack.logoName = attachment.value("title").toString(); - pack.logoUrl = attachment.value("thumbnailUrl").toString(); - break; - } - } - if(!thumbnailFound) { - qWarning() << "Pack without an icon, skipping: " << pack.name; - continue; - } - auto authors = project.value("authors").toArray(); - for(auto authorIter: authors) { - auto author = authorIter.toObject(); - ModpackAuthor packAuthor; - packAuthor.name = author.value("name").toString(); - packAuthor.url = author.value("url").toString(); - pack.authors.append(packAuthor); - } - int defaultFileId = project.value("defaultFileId").toInt(0); - if(defaultFileId == 0) { - qWarning() << "Pack without default file, skipping: " << pack.name; - continue; - } - bool found = false; - auto files = project.value("latestFiles").toArray(); - for(auto fileIter: files) { - auto file = fileIter.toObject(); - int id = file.value("id").toInt(0); - // NOTE: for now, ignore everything that's not the default... - if(id != defaultFileId) { - continue; - } - pack.latestFile.addonId = pack.addonId; - pack.latestFile.fileId = id; - auto versionArray = file.value("gameVersion").toArray(); - if(versionArray.size() < 1) { - continue; - } + QList newList; + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); - // pick the latest version supported - pack.latestFile.mcVersion = versionArray[0].toString(); - pack.latestFile.version = file.value("displayName").toString(); - pack.latestFile.downloadUrl = file.value("downloadUrl").toString(); - found = true; - break; + Flame::IndexedPack pack; + try + { + Flame::loadIndexedPack(pack, packObj); + newList.append(pack); } - if(!found) { - qWarning() << "Pack with no good file, skipping: " << pack.name; + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); continue; } - pack.broken = false; - newList.append(pack); } - if(objs.size() < 25) { + if(packs.size() < 25) { searchState = Finished; } else { nextSearchOffset += 25; diff --git a/application/pages/modplatform/flame/FlameModel.h b/application/pages/modplatform/flame/FlameModel.h index b4dded76..24383db0 100644 --- a/application/pages/modplatform/flame/FlameModel.h +++ b/application/pages/modplatform/flame/FlameModel.h @@ -15,7 +15,7 @@ #include #include -#include "FlameData.h" +#include namespace Flame { @@ -39,7 +39,7 @@ public: void fetchMore(const QModelIndex & parent) override; void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term); + void searchWithTerm(const QString & term, const int sort); private slots: void performPaginatedSearch(); @@ -54,13 +54,14 @@ private: void requestLogo(QString file, QString url); private: - QList modpacks; + QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; QMap waitingCallbacks; QString currentSearchTerm; + int currentSort = 0; int nextSearchOffset = 0; enum SearchState { None, diff --git a/application/pages/modplatform/flame/FlamePage.cpp b/application/pages/modplatform/flame/FlamePage.cpp index 3889f15a..2b7d9004 100644 --- a/application/pages/modplatform/flame/FlamePage.cpp +++ b/application/pages/modplatform/flame/FlamePage.cpp @@ -2,6 +2,7 @@ #include "ui_FlamePage.h" #include "MultiMC.h" +#include #include "dialogs/NewInstanceDialog.h" #include #include "FlameModel.h" @@ -13,9 +14,20 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); ui->searchEdit->installEventFilter(this); - model = new Flame::ListModel(this); - ui->packView->setModel(model); + listModel = new Flame::ListModel(this); + ui->packView->setModel(listModel); + + // index is used to set the sorting with the curseforge api + 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, QOverload::of(&QComboBox::currentIndexChanged), this, &FlamePage::triggerSearch); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); } FlamePage::~FlamePage() @@ -44,26 +56,28 @@ bool FlamePage::shouldDisplay() const void FlamePage::openedImpl() { suggestCurrent(); + triggerSearch(); } void FlamePage::triggerSearch() { - model->searchWithTerm(ui->searchEdit->text()); + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); } void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) { + ui->versionSelectionBox->clear(); + if(!first.isValid()) { if(isOpened) { dialog->setSuggestedPack(); } - ui->frame->clear(); return; } - current = model->data(first, Qt::UserRole).value(); + current = listModel->data(first, Qt::UserRole).value(); QString text = ""; QString name = current.name; @@ -82,12 +96,56 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) for(auto & author: current.authors) { authorStrs.push_back(authorToStr(author)); } - text += tr(" by ") + authorStrs.join(", "); + text += "
" + tr(" by ") + authorStrs.join(", "); } + text += "

"; - ui->frame->setModText(text); - ui->frame->setModDescription(current.description); - suggestCurrent(); + ui->packDescription->setHtml(text + current.description); + + if (current.versionsLoaded == false) + { + qDebug() << "Loading flame modpack versions"; + NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name)); + std::shared_ptr response = std::make_shared(); + int addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] + { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + Flame::loadIndexedPackVersions(current, arr); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + } + + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + }); + netJob->start(); + } + else + { + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + } } void FlamePage::suggestCurrent() @@ -96,16 +154,23 @@ void FlamePage::suggestCurrent() { return; } - if(current.broken) - { - dialog->setSuggestedPack(); - } - dialog->setSuggestedPack(current.name, new InstanceImportTask(current.latestFile.downloadUrl)); + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); - model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } + +void FlamePage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); +} \ No newline at end of file diff --git a/application/pages/modplatform/flame/FlamePage.h b/application/pages/modplatform/flame/FlamePage.h index e50186f5..467bb44b 100644 --- a/application/pages/modplatform/flame/FlamePage.h +++ b/application/pages/modplatform/flame/FlamePage.h @@ -20,7 +20,7 @@ #include "pages/BasePage.h" #include #include "tasks/Task.h" -#include "FlameData.h" +#include namespace Ui { @@ -68,10 +68,13 @@ private: private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); private: Ui::FlamePage *ui = nullptr; NewInstanceDialog* dialog = nullptr; - Flame::ListModel* model = nullptr; - Flame::Modpack current; + Flame::ListModel* listModel = nullptr; + Flame::IndexedPack current; + + QString selectedVersion; }; diff --git a/application/pages/modplatform/flame/FlamePage.ui b/application/pages/modplatform/flame/FlamePage.ui index 21e23f1f..9723815a 100644 --- a/application/pages/modplatform/flame/FlamePage.ui +++ b/application/pages/modplatform/flame/FlamePage.ui @@ -1,91 +1,90 @@ - FlamePage - - - - 0 - 0 - 875 - 745 - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - + FlamePage + + + + 0 + 0 + 837 + 685 + + + + + + + + + + 48 + 48 + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + + + true + + + true + + + + - - - - Search - - + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - MCModInfoFrame - QFrame -
widgets/MCModInfoFrame.h
- 1 -
-
- - searchEdit - searchButton - packView - - - + + + + Search + + + + + + + Search and filter ... + + + +
+
+ + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + +