Start working on search

This commit is contained in:
kb1000
2022-05-12 00:20:35 +02:00
parent 831b76bb78
commit acc637cfcb
7 changed files with 412 additions and 2 deletions

1
.gitignore vendored
View File

@@ -16,6 +16,7 @@ CMakeLists.txt.user.*
/.idea /.idea
cmake-build-*/ cmake-build-*/
Debug Debug
.cache
# Build dirs # Build dirs
build build

View File

@@ -750,6 +750,9 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h ui/pages/modplatform/flame/FlamePage.h
ui/pages/modplatform/modrinth/ModrinthData.h
ui/pages/modplatform/modrinth/ModrinthModel.cpp
ui/pages/modplatform/modrinth/ModrinthModel.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h ui/pages/modplatform/modrinth/ModrinthPage.h

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 kb1000
*
* This source is subject to the Microsoft Permissive License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include <QString>
#include <QMetaType>
#include <QUrl>
namespace Modrinth {
struct Modpack {
QString id;
QString name;
QUrl iconUrl;
QString author;
QString description;
bool metadataLoaded = false;
QString wikiUrl;
QString body;
};
}
Q_DECLARE_METATYPE(Modrinth::Modpack)

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2013-2022 MultiMC Contributors
* Copyright 2022 kb1000
*
* This source is subject to the Microsoft Permissive License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include "ModrinthModel.h"
#include "Application.h"
#include "Json.h"
#include <QIcon>
Modrinth::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
Modrinth::ListModel::~ListModel() = default;
QVariant Modrinth::ListModel::data(const QModelIndex &index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
{
return QString("INVALID INDEX %1").arg(pos);
}
auto pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
return pack.name;
}
else if(role == Qt::DecorationRole)
{
if(m_logoMap.contains(pack.id))
{
return (m_logoMap.value(pack.id));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel *)this)->requestLogo(pack.id, pack.iconUrl);
return icon;
}
else if (role == Qt::ToolTipRole)
{
return pack.description;
}
else if(role == Qt::UserRole)
{
QVariant v;
v.setValue(pack);
return v;
}
return QVariant();
}
bool Modrinth::ListModel::canFetchMore(const QModelIndex& parent) const
{
return searchState == CanPossiblyFetchMore;
}
void Modrinth::ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if(nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
performPaginatedSearch();
}
int Modrinth::ListModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
int Modrinth::ListModel::rowCount(const QModelIndex &parent) const
{
return modpacks.size();
}
void Modrinth::ListModel::searchWithTerm(const QString& term, const QString &sort)
{
if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
return;
}
currentSearchTerm = term;
currentSort = sort;
if(jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
}
else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
}
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);
}
else
{
searchUrl = QString(
"https://staging-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));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void Modrinth::ListModel::searchRequestFinished()
{
jobPtr.reset();
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;
}
QList<Modrinth::Modpack> newList;
QJsonArray hits;
int total_hits;
try
{
auto obj = Json::requireObject(doc);
hits = Json::requireArray(obj, "hits");
total_hits = Json::requireInteger(obj, "total_hits");
}
catch(const JSONValidationError &e)
{
qWarning() << "Error while parsing response from Modrinth: " << e.cause();
}
for (auto packRaw : hits)
{
auto packObj = packRaw.toObject();
Modrinth::Modpack pack;
try
{
if (Json::ensureString(packObj, "client_side", "required") == QStringLiteral("unsupported"))
continue;
pack.id = Json::requireString(packObj, "project_id");
pack.name = Json::requireString(packObj, "title");
pack.iconUrl = Json::requireUrl(packObj, "icon_url");
pack.author = Json::requireString(packObj, "author");
pack.description = Json::requireString(packObj, "description");
newList.append(pack);
}
catch(const JSONValidationError &e)
{
qWarning() << "Error while loading pack from Modrinth: " << e.cause();
continue;
}
}
if ((total_hits - nextSearchOffset) <= 25)
searchState = Finished;
else
{
nextSearchOffset += 25;
searchState = CanPossiblyFetchMore;
}
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
}
void Modrinth::ListModel::searchRequestFailed()
{
jobPtr.reset();
if(searchState == ResetRequested)
{
beginResetModel();
modpacks.clear();
endResetModel();
nextSearchOffset = 0;
performPaginatedSearch();
}
else
{
searchState = Finished;
}
}
void Modrinth::ListModel::logoLoaded(const QString &logo, const QIcon &out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out);
for(int i = 0; i < modpacks.size(); i++) {
if(modpacks[i].id == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
}
}
}
void Modrinth::ListModel::logoFailed(const QString &logo)
{
m_failedLogos.append(logo);
m_loadingLogos.removeAll(logo);
}
void Modrinth::ListModel::requestLogo(const QString &logo, const QUrl &url)
{
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
{
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());
job->addNetAction(Net::Download::makeCached(url, entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
{
QIcon icon(fullPath);
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);
}
logoLoaded(logo, icon);
if(waitingCallbacks.contains(logo))
{
waitingCallbacks.value(logo)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, logo]
{
logoFailed(logo);
});
job->start();
m_loadingLogos.append(logo);
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2022 MultiMC Contributors
* Copyright 2022 kb1000
*
* This source is subject to the Microsoft Permissive License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include "ModrinthData.h"
#include "net/NetJob.h"
#include <QAbstractListModel>
namespace Modrinth {
using LogoCallback = std::function<void(QString)>;
class ListModel : public QAbstractListModel {
Q_OBJECT
public:
explicit ListModel(QObject *parent);
~ListModel() override;
QVariant data(const QModelIndex &index, int role) const override;
int columnCount(const QModelIndex &parent) const override;
int rowCount(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
void searchWithTerm(const QString &term, const QString &sort);
private slots:
void searchRequestFinished();
void searchRequestFailed();
private:
void performPaginatedSearch();
void logoFailed(const QString &logo);
void logoLoaded(const QString &logo, const QIcon &out);
void requestLogo(const QString &logo, const QUrl &url);
QList<Modpack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
QMap<QString, QIcon> m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QString currentSearchTerm;
QString currentSort = QStringLiteral("relevance");
int nextSearchOffset = 0;
enum SearchState {
None,
CanPossiblyFetchMore,
ResetRequested,
Finished
} searchState = None;
NetJob::Ptr jobPtr;
QByteArray response;
};
}

View File

@@ -15,7 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
#include "ModrinthModel.h"
#include "ModrinthPage.h" #include "ModrinthPage.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui_ModrinthPage.h" #include "ui_ModrinthPage.h"
@@ -24,6 +26,23 @@
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); ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
model = new Modrinth::ListModel(this);
ui->packView->setModel(model);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
ui->sortByBox->addItem(tr("Sort by relevance"), QStringLiteral("relevance"));
ui->sortByBox->addItem(tr("Sort by total downloads"), QStringLiteral("downloads"));
ui->sortByBox->addItem(tr("Sort by follow count"), QStringLiteral("follows"));
ui->sortByBox->addItem(tr("Sort by creation date"), QStringLiteral("newest"));
ui->sortByBox->addItem(tr("Sort by last updated"), QStringLiteral("updated"));
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() ModrinthPage::~ModrinthPage()
@@ -51,5 +70,24 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event)
} }
void ModrinthPage::triggerSearch() { void ModrinthPage::triggerSearch() {
model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->itemData(ui->sortByBox->currentIndex()).toString());
}
void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) {
if(!first.isValid())
{
if(isOpened)
{
dialog->setSuggestedPack();
}
//ui->frame->clear();
return;
}
current = model->data(first, Qt::UserRole).value<Modrinth::Modpack>();
suggestCurrent();
}
void ModrinthPage::suggestCurrent() {
} }

View File

@@ -18,8 +18,8 @@
#pragma once #pragma once
#include "Application.h" #include "Application.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
#include "ModrinthData.h"
#include <QWidget> #include <QWidget>
@@ -28,6 +28,12 @@ namespace Ui
class ModrinthPage; class ModrinthPage;
} }
class NewInstanceDialog;
namespace Modrinth {
class ListModel;
}
class ModrinthPage : public QWidget, public BasePage class ModrinthPage : public QWidget, public BasePage
{ {
Q_OBJECT Q_OBJECT
@@ -55,8 +61,13 @@ public:
private slots: private slots:
void triggerSearch(); void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
private: private:
void suggestCurrent();
Ui::ModrinthPage *ui; Ui::ModrinthPage *ui;
NewInstanceDialog *dialog; NewInstanceDialog *dialog;
Modrinth::ListModel *model = nullptr;
Modrinth::Modpack current;
}; };