Merge pull request #4 from flowln/modrinth_pack
This commit is contained in:
commit
8e764fc8fb
@ -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
|
||||
|
@ -504,16 +504,16 @@ void InstanceImportTask::processModrinth() {
|
||||
QJsonObject hashes = Json::requireObject(obj, "hashes");
|
||||
QString hash;
|
||||
QCryptographicHash::Algorithm hashAlgorithm;
|
||||
hash = Json::ensureString(hashes, "sha256");
|
||||
hashAlgorithm = QCryptographicHash::Sha256;
|
||||
hash = Json::ensureString(hashes, "sha1");
|
||||
hashAlgorithm = QCryptographicHash::Sha1;
|
||||
if (hash.isEmpty())
|
||||
{
|
||||
hash = Json::ensureString(hashes, "sha512");
|
||||
hashAlgorithm = QCryptographicHash::Sha512;
|
||||
if (hash.isEmpty())
|
||||
{
|
||||
hash = Json::ensureString(hashes, "sha1");
|
||||
hashAlgorithm = QCryptographicHash::Sha1;
|
||||
hash = Json::ensureString(hashes, "sha256");
|
||||
hashAlgorithm = QCryptographicHash::Sha256;
|
||||
if (hash.isEmpty())
|
||||
{
|
||||
throw JSONValidationError("No hash found for: " + file.path);
|
||||
|
@ -14,3 +14,96 @@
|
||||
*/
|
||||
|
||||
#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.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug"));
|
||||
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");
|
||||
|
||||
qWarning() << files;
|
||||
|
||||
for (auto file_iter : files) {
|
||||
File indexed_file;
|
||||
auto parent = Json::requireObject(file_iter);
|
||||
auto is_primary = Json::ensureBoolean(parent, "primary", false);
|
||||
if (!is_primary) {
|
||||
auto filename = Json::ensureString(parent, "filename");
|
||||
// Checking suffix here is fine because it's the response from Modrinth,
|
||||
// so one would assume it will always be in English.
|
||||
if(!filename.endsWith("mrpack") && !filename.endsWith("zip"))
|
||||
continue;
|
||||
}
|
||||
|
||||
file.download_url = Json::requireString(parent, "url");
|
||||
if(is_primary)
|
||||
break;
|
||||
}
|
||||
|
||||
if(file.download_url.isEmpty())
|
||||
return {};
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
} // namespace Modrinth
|
||||
|
@ -15,18 +15,69 @@
|
||||
|
||||
#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 projectUrl;
|
||||
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)
|
||||
|
@ -110,7 +110,10 @@ void ImportPage::updateState()
|
||||
// FIXME: actually do some validation of what's inside here... this is fake AF
|
||||
QFileInfo fi(input);
|
||||
// mrpack is a modrinth pack
|
||||
if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack"))
|
||||
|
||||
// Allow non-latin people to use ZIP files!
|
||||
auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip");
|
||||
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
|
||||
{
|
||||
QFileInfo fi(url.fileName());
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
||||
|
@ -11,28 +11,75 @@
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="modpackBtn">
|
||||
<property name="text">
|
||||
<string>Browse</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLineEdit" name="modpackEdit">
|
||||
<property name="placeholderText">
|
||||
<string notr="true">http://</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="modpackLabel">
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="modpackBtn">
|
||||
<property name="text">
|
||||
<string>Local file or link to a direct download:</string>
|
||||
<string>Browse</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<item row="3" column="0" rowspan="2" colspan="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>The following file types are implemented (both for local files and URLs):</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>- Curseforge modpacks (ZIP)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>- Modrinth modpacks (ZIP and mrpack)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>- PolyMC / MultiMC exported instances (ZIP)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>- Technic modpacks (ZIP)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -45,6 +92,13 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="modpackLabel">
|
||||
<property name="text">
|
||||
<string>Local file or link to a direct download:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
267
launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
Normal file
267
launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
Normal file
@ -0,0 +1,267 @@
|
||||
#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) {
|
||||
if (m_logoMap.contains(pack.iconName)) {
|
||||
return (m_logoMap.value(pack.iconName)
|
||||
.pixmap(48, 48)
|
||||
.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation));
|
||||
}
|
||||
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::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 ********/
|
81
launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
Normal file
81
launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
Normal file
@ -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
|
@ -34,14 +34,41 @@
|
||||
*/
|
||||
|
||||
#include "ModrinthPage.h"
|
||||
|
||||
#include "ui_ModrinthPage.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include "ModrinthModel.h"
|
||||
|
||||
ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog)
|
||||
#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)
|
||||
{
|
||||
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,179 @@ 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.projectUrl + "\">" + 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));
|
||||
auto iconName = current.iconName;
|
||||
m_model->getLogo(iconName, current.iconUrl.toString(),
|
||||
[this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); });
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
class ModrinthPage : public QWidget, public BasePage
|
||||
{
|
||||
namespace Modrinth {
|
||||
class ModpackListModel;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
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"; }
|
||||
|
||||
inline auto debugName() const -> QString { return "Modrinth"; }
|
||||
inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; };
|
||||
|
||||
auto getCurrent() -> Modrinth::Modpack& { return current; }
|
||||
void suggestCurrent();
|
||||
|
||||
void updateUI();
|
||||
void updateVersionsUI();
|
||||
|
||||
virtual QString helpPage() const override
|
||||
{
|
||||
return "Modrinth-platform";
|
||||
}
|
||||
void retranslate() override;
|
||||
|
||||
void openedImpl() override;
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
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;
|
||||
};
|
||||
|
@ -45,6 +45,9 @@
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="uniformItemSizes">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
Loading…
Reference in New Issue
Block a user