removed some duplicate code

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97
2025-03-30 00:32:59 +02:00
parent ab3bfb0f74
commit 29cff14fd6
52 changed files with 928 additions and 2318 deletions

View File

@@ -488,6 +488,7 @@ set(API_SOURCES
modplatform/ResourceType.cpp
modplatform/ResourceAPI.h
modplatform/ResourceAPI.cpp
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
@@ -498,8 +499,6 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkResourceAPI.h
modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
@@ -527,8 +526,6 @@ set(FTB_SOURCES
set(FLAME_SOURCES
# Flame
modplatform/flame/FlamePackIndex.cpp
modplatform/flame/FlamePackIndex.h
modplatform/flame/FlameModIndex.cpp
modplatform/flame/FlameModIndex.h
modplatform/flame/PackManifest.h
@@ -546,8 +543,6 @@ set(FLAME_SOURCES
set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthPackIndex.cpp
modplatform/modrinth/ModrinthPackIndex.h
modplatform/modrinth/ModrinthPackManifest.cpp
modplatform/modrinth/ModrinthPackManifest.h
modplatform/modrinth/ModrinthCheckUpdate.cpp
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -1035,8 +1030,6 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/OptionalModDialog.cpp
ui/pages/modplatform/OptionalModDialog.h
ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
ui/pages/modplatform/modrinth/ModrinthResourcePages.h

View File

@@ -27,12 +27,8 @@
#include "minecraft/mod/MetadataHandler.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "tasks/SequentialTask.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/flame/FlameResourceModels.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
static Version mcVersion(BaseInstance* inst)
{
@@ -55,14 +51,7 @@ static bool checkDependencies(std::shared_ptr<GetModDependenciesTask::PackDepend
GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance,
ModFolderModel* folder,
QList<std::shared_ptr<PackDependency>> selected)
: SequentialTask(tr("Get dependencies"))
, m_selected(selected)
, m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared<ResourceDownload::FlameModModel>(*instance),
std::make_shared<FlameAPI>() }
, m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared<ResourceDownload::ModrinthModModel>(*instance),
std::make_shared<ModrinthAPI>() }
, m_version(mcVersion(instance))
, m_loaderType(mcLoaders(instance))
: SequentialTask(tr("Get dependencies")), m_selected(selected), m_version(mcVersion(instance)), m_loaderType(mcLoaders(instance))
{
for (auto mod : folder->allMods()) {
m_mods_file_names << mod->fileinfo().fileName();
@@ -144,9 +133,9 @@ QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion
Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr<PackDependency> pDep)
{
auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
auto provider = pDep->pack->provider;
auto responseInfo = std::make_shared<QByteArray>();
auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo);
auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo);
connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error);
@@ -158,9 +147,10 @@ Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr<PackDepende
return;
}
try {
auto obj = provider.name == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data")
: Json::requireObject(doc);
provider.mod->loadIndexedPack(*pDep->pack, obj);
auto obj = provider == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data")
: Json::requireObject(doc);
getAPI(provider)->loadIndexedPack(*pDep->pack, obj);
} catch (const JSONValidationError& e) {
removePack(pDep->pack->addonId);
qDebug() << doc;
@@ -181,7 +171,8 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
pDep->pack->provider = providerName;
m_pack_dependencies.append(pDep);
auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider;
auto provider = providerName;
auto tasks = makeShared<SequentialTask>(
QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString()));
@@ -191,46 +182,30 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
}
ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
ResourceAPI::DependencySearchCallbacks callbacks;
ResourceAPI::Callback<ModPlatform::IndexedVersion> callbacks;
callbacks.on_fail = [](QString reason, int) {
qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason);
};
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) {
try {
QJsonArray arr;
if (dep.version.length() != 0 && doc.isObject()) {
arr.append(doc.object());
} else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
}
pDep->version = provider.mod->loadDependencyVersions(dep, arr);
if (!pDep->version.addonId.isValid()) {
if (m_loaderType & ModPlatform::Quilt) { // falback for quilt
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](const auto& o) {
return o.provider == provider.name && dep.addonId == o.quilt;
});
if (over != overide.cend()) {
removePack(dep.addonId);
addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level));
return;
}
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) {
pDep->version = pack;
if (!pDep->version.addonId.isValid()) {
if (m_loaderType & ModPlatform::Quilt) { // falback for quilt
auto overide = ModPlatform::getOverrideDeps();
auto over = std::find_if(overide.cbegin(), overide.cend(),
[dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; });
if (over != overide.cend()) {
removePack(dep.addonId);
addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level));
return;
}
removePack(dep.addonId);
qWarning() << "Error while reading mod version empty ";
qDebug() << doc;
return;
}
pDep->version.is_currently_selected = true;
pDep->pack->versions = { pDep->version };
pDep->pack->versionsLoaded = true;
} catch (const JSONValidationError& e) {
removePack(dep.addonId);
qDebug() << doc;
qWarning() << "Error while reading mod version: " << e.cause();
return;
}
pDep->version.is_currently_selected = true;
pDep->pack->versions = { pDep->version };
pDep->pack->versionsLoaded = true;
if (level == 0) {
removePack(dep.addonId);
qWarning() << "Dependency cycle exceeded";
@@ -238,10 +213,10 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
}
if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) {
pDep->pack->addonId = pDep->version.addonId;
auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name);
auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider);
if (dep_.addonId != pDep->version.addonId) {
removePack(pDep->version.addonId);
addTask(prepareDependencyTask(dep_, provider.name, level));
addTask(prepareDependencyTask(dep_, provider, level));
} else {
addTask(getProjectInfoTask(pDep));
}
@@ -250,12 +225,12 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
removePack(pDep->version.addonId);
return;
}
for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) {
addTask(prepareDependencyTask(dep_, provider.name, level - 1));
for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) {
addTask(prepareDependencyTask(dep_, provider, level - 1));
}
};
auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks));
auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks));
tasks->addTask(version);
return tasks;
}

View File

@@ -28,6 +28,8 @@
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "tasks/SequentialTask.h"
#include "tasks/Task.h"
#include "ui/pages/modplatform/ModModel.h"
@@ -54,17 +56,20 @@ class GetModDependenciesTask : public SequentialTask {
QStringList required_by;
};
struct Provider {
ModPlatform::ResourceProvider name;
std::shared_ptr<ResourceDownload::ModModel> mod;
std::shared_ptr<ResourceAPI> api;
};
explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList<std::shared_ptr<PackDependency>> selected);
auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
QHash<QString, PackDependencyExtraInfo> getExtraInfo();
private:
inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider)
{
if (provider == ModPlatform::ResourceProvider::FLAME)
return &m_flameAPI;
else
return &m_modrinthAPI;
}
protected slots:
Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int);
QList<ModPlatform::Dependency> getDependenciesForVersion(const ModPlatform::IndexedVersion&,
@@ -82,9 +87,10 @@ class GetModDependenciesTask : public SequentialTask {
QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
QList<std::shared_ptr<PackDependency>> m_selected;
QStringList m_mods_file_names;
Provider m_flame_provider;
Provider m_modrinth_provider;
Version m_version;
ModPlatform::ModLoaderTypes m_loaderType;
ModrinthAPI m_modrinthAPI;
FlameAPI m_flameAPI;
};

View File

@@ -128,6 +128,23 @@ struct IndexedVersion {
// For internal use, not provided by APIs
bool is_currently_selected = false;
QString getVersionDisplayString() const
{
auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : "";
auto versionStr = !version.contains(version_number) ? version_number : "";
QString gameVersion = "";
for (auto v : mcVersion) {
if (version.contains(v)) {
gameVersion = "";
break;
}
if (gameVersion.isEmpty()) {
gameVersion = QObject::tr(" for %1").arg(v);
}
}
return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type);
}
};
struct ExtraPackData {

View File

@@ -1,18 +1,14 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "NetworkResourceAPI.h"
#include <memory>
#include "modplatform/ResourceAPI.h"
#include "Application.h"
#include "Json.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const
Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback<QList<ModPlatform::IndexedPack::Ptr>>&& callbacks) const
{
auto search_url_optional = getSearchURL(args);
if (!search_url_optional.has_value()) {
@@ -40,7 +36,23 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
return;
}
callbacks.on_succeed(doc);
QList<ModPlatform::IndexedPack::Ptr> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
newList << pack;
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
}
}
callbacks.on_succeed(newList);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
@@ -60,29 +72,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
return netJob;
}
Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
{
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.pack);
});
QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); });
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return job;
}
Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const
Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
@@ -95,7 +85,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] {
QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -105,7 +95,32 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
return;
}
callbacks.on_succeed(doc, args.pack);
QVector<ModPlatform::IndexedVersion> unsortedVersions;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, args.resourceType);
if (!file.addonId.isValid())
file.addonId = args.pack.addonId;
if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
}
callbacks.on_succeed(unsortedVersions);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
@@ -120,11 +135,147 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return netJob;
}
Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack>&& callbacks) const
{
auto response = std::make_shared<QByteArray>();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto obj = Json::requireObject(doc);
if (obj.contains("data"))
obj = Json::requireObject(obj, "data");
loadIndexedPack(pack, obj);
loadExtraPackInfo(pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
}
callbacks.on_succeed(pack);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = job.toWeakRef();
QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto job = weak.lock()) {
if (auto netJob = qSharedPointerDynamicCast<NetJob>(job)) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) {
network_error_code = failed_action->replyStatusCode();
}
}
}
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });
return job;
}
Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback<ModPlatform::IndexedVersion>&& callbacks) const
{
auto versions_url_optional = getDependencyURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
QJsonArray arr;
if (args.dependency.version.length() != 0 && doc.isObject()) {
arr.append(doc.object());
} else {
arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
}
QVector<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod);
if (!file.addonId.isValid())
file.addonId = args.dependency.addonId;
if (file.fileId.isValid() &&
(!file.loaders || args.loader & file.loaders)) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion();
callbacks.on_succeed(bestMatch);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = netJob.toWeakRef();
QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto netJob = weak.lock()) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
}
callbacks.on_fail(reason, network_error_code);
});
return netJob;
}
QString ResourceAPI::getGameVersionsString(std::list<Version> mcVersions) const
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s;
}
QString ResourceAPI::mapMCVersionToModrinth(Version v) const
{
static const QString preString = " Pre-Release ";
auto verStr = v.toString();
if (verStr.contains(preString)) {
verStr.replace(preString, "-pre");
}
verStr.replace(" ", "-");
return verStr;
}
Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
@@ -138,44 +289,3 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr<QByteA
return netJob;
}
Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const
{
auto versions_url_optional = getDependencyURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.dependency);
});
// Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
// This prevents the lambda from extending the lifetime of the shared resource,
// as it only temporarily locks the resource when needed.
auto weak = netJob.toWeakRef();
QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) {
int network_error_code = -1;
if (auto netJob = weak.lock()) {
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
network_error_code = failed_action->replyStatusCode();
}
callbacks.on_fail(reason, network_error_code);
});
return netJob;
}

View File

@@ -4,7 +4,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* Copyright (c) 2023-2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -67,6 +67,13 @@ class ResourceAPI {
QString readable_name;
};
template <typename T>
struct Callback {
std::function<void(T&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
std::function<void()> on_abort;
};
struct SearchArgs {
ModPlatform::ResourceType type{};
int offset = 0;
@@ -79,31 +86,18 @@ class ResourceAPI {
std::optional<QStringList> categoryIds;
bool openSource;
};
struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
std::function<void()> on_abort;
};
struct VersionSearchArgs {
ModPlatform::IndexedPack pack;
std::optional<std::list<Version>> mcVersions;
std::optional<ModPlatform::ModLoaderTypes> loaders;
};
struct VersionSearchCallbacks {
std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
ModPlatform::ResourceType resourceType;
};
struct ProjectInfoArgs {
ModPlatform::IndexedPack pack;
};
struct ProjectInfoCallbacks {
std::function<void(QJsonDocument&, const ModPlatform::IndexedPack&)> on_succeed;
std::function<void(QString const& reason)> on_fail;
std::function<void()> on_abort;
};
struct DependencySearchArgs {
ModPlatform::Dependency dependency;
@@ -111,73 +105,52 @@ class ResourceAPI {
ModPlatform::ModLoaderTypes loader;
};
struct DependencySearchCallbacks {
std::function<void(QJsonDocument&, const ModPlatform::Dependency&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
};
public:
/** Gets a list of available sorting methods for this API. */
virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
public slots:
[[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::searchProjects";
return nullptr;
}
virtual Task::Ptr getProject([[maybe_unused]] QString addonId,
[[maybe_unused]] std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProject";
return nullptr;
}
virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds,
[[maybe_unused]] std::shared_ptr<QByteArray> response) const
{
qWarning() << "TODO: ResourceAPI::getProjects";
return nullptr;
}
virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::getProjectInfo";
return nullptr;
}
virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const
{
qWarning() << "TODO: ResourceAPI::getProjectVersions";
return nullptr;
}
virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const;
virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack>&&) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;
protected:
inline QString debugName() const { return "External resource API"; }
inline QString mapMCVersionToModrinth(Version v) const
{
static const QString preString = " Pre-Release ";
auto verStr = v.toString();
QString mapMCVersionToModrinth(Version v) const;
if (verStr.contains(preString)) {
verStr.replace(preString, "-pre");
}
verStr.replace(" ", "-");
return verStr;
}
QString getGameVersionsString(std::list<Version> mcVersions) const;
inline QString getGameVersionsString(std::list<Version> mcVersions) const
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
}
s.remove(s.length() - 1, 1); // remove last comma
return s;
}
public:
virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0;
/** Converts a JSON document to a common array format.
*
* This is needed so that different providers, with different JSON structures, can be parsed
* uniformally. You NEED to re-implement this if you intend on using the default callbacks.
*/
virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
};

View File

@@ -7,11 +7,13 @@
#include <QList>
#include <memory>
#include "BuildConfig.h"
#include "Json.h"
#include "Version.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
#include "modplatform/flame/FlameModIndex.h"
class FlameAPI : public NetworkResourceAPI {
class FlameAPI : public ResourceAPI {
public:
QString getModFileChangelog(int modId, int fileId);
QString getModDescription(int modId);
@@ -138,6 +140,25 @@ class FlameAPI : public NetworkResourceAPI {
return url;
}
QJsonArray documentToArray(QJsonDocument& obj) const override { return Json::ensureArray(obj.object(), "data"); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override
{
auto arr = FlameMod::loadIndexedPackVersion(obj);
if (resourceType != ModPlatform::ResourceType::TexturePack) {
return arr;
}
// FIXME: Client-side version filtering. This won't take into account any user-selected filtering.
auto const& mc_versions = arr.mcVersion;
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
[](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); })) {
return arr;
}
return {};
};
void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); }
private:
std::optional<QString> getInfoURL(QString const& id) const override
{

View File

@@ -58,7 +58,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.extraDataLoaded = true;
}
void FlameMod::loadBody(ModPlatform::IndexedPack& pack, [[maybe_unused]] QJsonObject& obj)
void FlameMod::loadBody(ModPlatform::IndexedPack& pack)
{
pack.extraData.body = api.getModDescription(pack.addonId.toInt());
@@ -204,31 +204,3 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
return file;
}
ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst)
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
QList<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (!file.addonId.isValid())
file.addonId = m.addonId;
if (file.fileId.isValid() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
if (versions.size() != 0)
return versions.front();
return {};
}

View File

@@ -12,8 +12,7 @@ namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false);
ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst);
} // namespace FlameMod

View File

@@ -1,150 +0,0 @@
#include "FlamePackIndex.h"
#include <QFileInfo>
#include <QUrl>
#include "Json.h"
#include "modplatform/ModIndex.h"
void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
pack.description = Json::ensureString(obj, "summary", "");
auto logo = Json::requireObject(obj, "logo");
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix();
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, "mainFileId");
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, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
found = true;
break;
}
if (!found) {
throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
}
loadIndexedInfo(pack, obj);
}
void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj)
{
auto links_obj = Json::ensureObject(obj, "links");
pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl");
if (pack.extra.websiteUrl.endsWith('/'))
pack.extra.websiteUrl.chop(1);
pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
if (pack.extra.issuesUrl.endsWith('/'))
pack.extra.issuesUrl.chop(1);
pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
if (pack.extra.sourceUrl.endsWith('/'))
pack.extra.sourceUrl.chop(1);
pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
if (pack.extra.wikiUrl.endsWith('/'))
pack.extra.wikiUrl.chop(1);
pack.extraInfoLoaded = true;
}
void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
{
QList<Flame::IndexedVersion> 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, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
for (auto mcVer : versionArray) {
auto str = mcVer.toString();
if (str.contains('.'))
file.mcVersion.append(str);
if (auto loader = str.toLower(); loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
}
// pick the latest version supported
file.version = Json::requireString(version, "displayName");
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(version, "releaseType")) {
case 1:
ver_type = ModPlatform::IndexedVersionType::VersionType::Release;
break;
case 2:
ver_type = ModPlatform::IndexedVersionType::VersionType::Beta;
break;
case 3:
ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha;
break;
default:
ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown;
}
file.version_type = ModPlatform::IndexedVersionType(ver_type);
file.downloadUrl = Json::ensureString(version, "downloadUrl");
// only add if we have a download URL (third party distribution is enabled)
if (!file.downloadUrl.isEmpty()) {
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;
}
auto Flame::getVersionDisplayString(const IndexedVersion& version) -> QString
{
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
auto mcVersion =
!version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) ? QObject::tr(" for %1").arg(version.mcVersion) : "";
return QString("%1%2%3").arg(version.version, mcVersion, release_type);
}

View File

@@ -1,55 +0,0 @@
#pragma once
#include <QList>
#include <QMetaType>
#include <QString>
#include "modplatform/ModIndex.h"
namespace Flame {
struct ModpackAuthor {
QString name;
QString url;
};
struct IndexedVersion {
int addonId;
int fileId;
QString version;
ModPlatform::IndexedVersionType version_type;
ModPlatform::ModLoaderTypes loaders = {};
QString mcVersion;
QString downloadUrl;
};
struct ModpackExtra {
QString websiteUrl;
QString wikiUrl;
QString issuesUrl;
QString sourceUrl;
};
struct IndexedPack {
int addonId;
QString name;
QString description;
QList<ModpackAuthor> authors;
QString logoName;
QString logoUrl;
bool versionsLoaded = false;
QList<IndexedVersion> versions;
bool extraInfoLoaded = false;
ModpackExtra extra;
};
void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
void loadIndexedInfo(IndexedPack&, QJsonObject&);
void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
auto getVersionDisplayString(const IndexedVersion&) -> QString;
} // namespace Flame
Q_DECLARE_METATYPE(Flame::IndexedPack)
Q_DECLARE_METATYPE(QList<Flame::IndexedVersion>)

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include "modplatform/ResourceAPI.h"
class NetworkResourceAPI : public ResourceAPI {
public:
Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const override;
Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override;
protected:
virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
};

View File

@@ -5,12 +5,14 @@
#pragma once
#include "BuildConfig.h"
#include "Json.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkResourceAPI.h"
#include "modplatform/ResourceAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include <QDebug>
class ModrinthAPI : public NetworkResourceAPI {
class ModrinthAPI : public ResourceAPI {
public:
Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response);
@@ -214,4 +216,12 @@ class ModrinthAPI : public NetworkResourceAPI {
.arg(mapMCVersionToModrinth(args.mcVersion))
.arg(getModLoaderStrings(args.loader).join("\",\""));
};
QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); }
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override
{
return Modrinth::loadIndexedPackVersion(obj);
};
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); }
};

View File

@@ -13,7 +13,6 @@
#include "modplatform/EnsureMetadataTask.h"
#include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/ChecksumValidator.h"
#include "net/ApiDownload.h"
@@ -85,7 +84,7 @@ bool ModrinthCreationTask::updateInstance()
QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json"));
QFileInfo old_index_file(old_index_path);
if (old_index_file.exists()) {
std::vector<Modrinth::File> old_files;
std::vector<File> old_files;
parseManifest(old_index_path, old_files, false, false);
// Let's remove all duplicated, identical resources!
@@ -356,7 +355,7 @@ bool ModrinthCreationTask::createInstance()
}
bool ModrinthCreationTask::parseManifest(const QString& index_path,
std::vector<Modrinth::File>& files,
std::vector<File>& files,
bool set_internal_data,
bool show_optional_dialog)
{
@@ -377,9 +376,9 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
std::vector<Modrinth::File> optionalFiles;
std::vector<File> optionalFiles;
for (const auto& modInfo : jsonFiles) {
Modrinth::File file;
File file;
file.path = Json::requireString(modInfo, "path").replace("\\", "/");
auto env = Json::ensureObject(modInfo, "env");

View File

@@ -1,13 +1,27 @@
#pragma once
#include <optional>
#include <QByteArray>
#include <QCryptographicHash>
#include <QQueue>
#include <QString>
#include <QUrl>
#include <QVector>
#include "BaseInstance.h"
#include "InstanceCreationTask.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
struct File {
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
public:
ModrinthCreationTask(QString staging_path,
@@ -30,7 +44,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
bool createInstance() override;
private:
bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_internal_data = true, bool show_optional_dialog = true);
bool parseManifest(const QString&, std::vector<File>&, bool set_internal_data = true, bool show_optional_dialog = true);
private:
QWidget* m_parent = nullptr;
@@ -38,7 +52,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version;
QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files;
std::vector<File> m_files;
Task::Ptr m_task;
std::optional<InstancePtr> m_instance;

View File

@@ -112,25 +112,6 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraDataLoaded = true;
}
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
{
QList<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions;
pack.versionsLoaded = true;
}
ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
{
ModPlatform::IndexedVersion file;
@@ -244,28 +225,3 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q
return {};
}
ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
QJsonArray& arr,
const BaseInstance* inst)
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
QList<ModPlatform::IndexedVersion> versions;
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
versions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;
};
std::sort(versions.begin(), versions.end(), orderSortPredicate);
return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion();
}

View File

@@ -25,8 +25,6 @@ namespace Modrinth {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;
} // namespace Modrinth

View File

@@ -1,196 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ModrinthPackManifest.h"
#include <QFileInfo>
#include "Json.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include <QSet>
static ModrinthAPI api;
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");
auto temp_author_name = Json::ensureString(obj, "author");
pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name));
pack.iconUrl = Json::ensureString(obj, "icon_url");
pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix());
}
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.issuesUrl = Json::ensureString(obj, "issues_url");
if (pack.extra.issuesUrl.endsWith('/'))
pack.extra.issuesUrl.chop(1);
pack.extra.sourceUrl = Json::ensureString(obj, "source_url");
if (pack.extra.sourceUrl.endsWith('/'))
pack.extra.sourceUrl.chop(1);
pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url");
if (pack.extra.wikiUrl.endsWith('/'))
pack.extra.wikiUrl.chop(1);
pack.extra.discordUrl = Json::ensureString(obj, "discord_url");
if (pack.extra.discordUrl.endsWith('/'))
pack.extra.discordUrl.chop(1);
auto donate_arr = Json::ensureArray(obj, "donation_urls");
for (auto d : donate_arr) {
auto d_obj = Json::requireObject(d);
DonationData donate;
donate.id = Json::ensureString(d_obj, "id");
donate.platform = Json::ensureString(d_obj, "platform");
donate.url = Json::ensureString(d_obj, "url");
pack.extra.donate.append(donate);
}
pack.extra.status = Json::ensureString(obj, "status");
pack.extraInfoLoaded = true;
}
void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
{
QList<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");
auto gameVersions = Json::ensureArray(obj, "game_versions");
if (!gameVersions.isEmpty()) {
file.gameVersion = Json::ensureString(gameVersions[0]);
file.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion);
}
auto loaders = Json::requireArray(obj, "loaders");
for (auto loader : loaders) {
if (loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
}
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::ensureString(obj, "changelog");
file.id = Json::requireString(obj, "id");
file.project_id = Json::requireString(obj, "project_id");
file.date = Json::requireString(obj, "date_published");
auto files = Json::requireArray(obj, "files");
for (auto file_iter : files) {
File indexed_file;
auto parent = Json::requireObject(file_iter);
auto is_primary = Json::ensureBoolean(parent, (const QString)QStringLiteral("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;
}
auto url = Json::requireString(parent, "url");
file.download_url = url;
if (is_primary)
break;
}
if (file.download_url.isEmpty())
return {};
return file;
}
auto getVersionDisplayString(const ModpackVersion& version) -> QString
{
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion)
? QObject::tr(" for %1").arg(version.gameVersion)
: "";
auto versionStr = !version.name.contains(version.version) ? version.version : "";
return QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type);
}
} // namespace Modrinth

View File

@@ -1,129 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QMetaType>
#include <QByteArray>
#include <QCryptographicHash>
#include <QList>
#include <QQueue>
#include <QString>
#include <QUrl>
#include "modplatform/ModIndex.h"
class MinecraftInstance;
namespace Modrinth {
struct File {
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
QQueue<QUrl> downloads;
bool required = true;
};
struct DonationData {
QString id;
QString platform;
QString url;
};
struct ModpackExtra {
QString body;
QString projectUrl;
QString issuesUrl;
QString sourceUrl;
QString wikiUrl;
QString discordUrl;
QList<DonationData> donate;
QString status;
};
struct ModpackVersion {
QString name;
QString version;
QString gameVersion;
ModPlatform::IndexedVersionType version_type;
QString changelog;
ModPlatform::ModLoaderTypes loaders = {};
QString id;
QString project_id;
QString date;
QString download_url;
};
struct Modpack {
QString id;
QString name;
QString description;
std::tuple<QString, QUrl> author;
QString iconName;
QUrl iconUrl;
bool versionsLoaded = false;
bool extraInfoLoaded = false;
ModpackExtra extra;
QList<ModpackVersion> versions;
};
void loadIndexedPack(Modpack&, QJsonObject&);
void loadIndexedInfo(Modpack&, QJsonObject&);
void loadIndexedVersions(Modpack&, QJsonDocument&);
auto loadIndexedVersion(QJsonObject&) -> ModpackVersion;
auto validateDownloadUrl(QUrl) -> bool;
auto getVersionDisplayString(const ModpackVersion&) -> QString;
} // namespace Modrinth
Q_DECLARE_METATYPE(Modrinth::Modpack)
Q_DECLARE_METATYPE(Modrinth::ModpackVersion)
Q_DECLARE_METATYPE(QList<Modrinth::ModpackVersion>)

View File

@@ -22,8 +22,6 @@
#include "Markdown.h"
#include "StringUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
@@ -256,36 +254,13 @@ void ModrinthManagedPackPage::parseManagedPack()
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
m_pack = { m_inst->getManagedPackID() };
QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(
Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
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;
setFailState();
return;
}
try {
Modrinth::loadIndexedVersions(m_pack, doc);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading modrinth modpack version: " << e.cause();
setFailState();
return;
}
// Use default if no callbacks are set
callbacks.on_succeed = [this](auto& doc) {
m_pack.versions = doc;
m_pack.versionsLoaded = true;
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
@@ -293,22 +268,23 @@ void ModrinthManagedPackPage::parseManagedPack()
ui->versionsComboBox->blockSignals(false);
for (const auto& version : m_pack.versions) {
QString name = Modrinth::getVersionDisplayString(version);
QString name = version.getVersionDisplayString();
// NOTE: the id from version isn't the same id in the modpack format spec...
// e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index..............
if (version.version == m_inst->getManagedPackVersionName())
name = tr("%1 (Current)").arg(name);
ui->versionsComboBox->addItem(name, QVariant(version.id));
ui->versionsComboBox->addItem(name, version.fileId);
}
suggestVersion();
m_loaded = true;
});
connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState);
connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState);
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
@@ -370,10 +346,10 @@ void ModrinthManagedPackPage::update()
QMap<QString, QString> extra_info;
// NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field.
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", version.id);
extra_info.insert("pack_version_id", version.fileId.toString());
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info));
auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info));
InstanceName inst_name(m_inst->getManagedPackName(), version.version);
inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version));
@@ -449,37 +425,15 @@ void FlameManagedPackPage::parseManagedPack()
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
QString id = m_inst->getManagedPackID();
m_pack = { id };
m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response));
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
setFailState();
return;
}
try {
auto obj = doc.object();
auto data = Json::ensureArray(obj, "data");
Flame::loadIndexedPackVersions(m_pack, data);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause();
setFailState();
return;
}
// Use default if no callbacks are set
callbacks.on_succeed = [this](auto& doc) {
m_pack.versions = doc;
m_pack.versionsLoaded = true;
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
@@ -487,7 +441,7 @@ void FlameManagedPackPage::parseManagedPack()
ui->versionsComboBox->blockSignals(false);
for (const auto& version : m_pack.versions) {
QString name = Flame::getVersionDisplayString(version);
QString name = version.getVersionDisplayString();
if (version.fileId == m_inst->getManagedPackVersionID().toInt())
name = tr("%1 (Current)").arg(name);
@@ -498,9 +452,10 @@ void FlameManagedPackPage::parseManagedPack()
suggestVersion();
m_loaded = true;
});
connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState);
connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState);
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job->start();
}
@@ -521,7 +476,7 @@ void FlameManagedPackPage::suggestVersion()
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(
StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)));
StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId.toInt())));
ManagedPackPage::suggestVersion();
}
@@ -537,7 +492,7 @@ void FlameManagedPackPage::update()
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", QString::number(version.fileId));
extra_info.insert("pack_version_id", version.fileId.toString());
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info));

View File

@@ -6,11 +6,10 @@
#include "BaseInstance.h"
#include "modplatform/ModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
#include "net/NetJob.h"
@@ -130,9 +129,9 @@ class ModrinthManagedPackPage final : public ManagedPackPage {
void updateFromFile() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Task::Ptr m_fetch_job = nullptr;
Modrinth::Modpack m_pack;
ModPlatform::IndexedPack m_pack;
ModrinthAPI m_api;
};
@@ -154,8 +153,8 @@ class FlameManagedPackPage final : public ManagedPackPage {
void updateFromFile() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Task::Ptr m_fetch_job = nullptr;
Flame::IndexedPack m_pack;
ModPlatform::IndexedPack m_pack;
FlameAPI m_api;
};

View File

@@ -9,8 +9,8 @@
namespace ResourceDownload {
DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst)
DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase)
: ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase)
{}
/******** Make data requests ********/

View File

@@ -21,14 +21,13 @@ class DataPackResourceModel : public ResourceModel {
Q_OBJECT
public:
DataPackResourceModel(BaseInstance const&, ResourceAPI*);
DataPackResourceModel(BaseInstance const&, ResourceAPI*, QString, QString);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
[[nodiscard]] QString debugName() const override { return m_debugName; }
[[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; }
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
@@ -38,7 +37,9 @@ class DataPackResourceModel : public ResourceModel {
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
private:
QString m_debugName;
QString m_metaEntryBase;
};
} // namespace ResourceDownload

View File

@@ -14,7 +14,9 @@
namespace ResourceDownload {
ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase)
: ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase)
{}
/******** Make data requests ********/
@@ -60,7 +62,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelInd
if (m_filter->loaders)
loaders = m_filter->loaders;
return { pack, versions, loaders };
return { pack, versions, loaders, ModPlatform::ResourceType::Mod };
}
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry)

View File

@@ -24,26 +24,23 @@ class ModModel : public ResourceModel {
Q_OBJECT
public:
ModModel(BaseInstance&, ResourceAPI* api);
ModModel(BaseInstance&, ResourceAPI* api, QString debugName, QString metaEntryBase);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0;
virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0;
void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override;
[[nodiscard]] QString debugName() const override { return m_debugName; }
[[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; }
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override;
ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override;
protected:
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override;
virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override;
@@ -53,6 +50,10 @@ class ModModel : public ResourceModel {
BaseInstance& m_base_instance;
std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
private:
QString m_debugName;
QString m_metaEntryBase;
};
} // namespace ResourceDownload

View File

@@ -15,8 +15,8 @@
#include "Application.h"
#include "BuildConfig.h"
#include "Json.h"
#include "modplatform/ResourceAPI.h"
#include "net/ApiDownload.h"
#include "net/NetJob.h"
@@ -141,9 +141,9 @@ void ResourceModel::search()
if (m_search_term.startsWith("#")) {
auto projectId = m_search_term.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::ProjectInfoCallbacks callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
callbacks.on_fail = [this](QString reason) {
callbacks.on_fail = [this](QString reason, int) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, -1);
@@ -154,10 +154,10 @@ void ResourceModel::search()
searchRequestAborted();
};
callbacks.on_succeed = [this](auto& doc, auto& pack) {
callbacks.on_succeed = [this](auto& pack) {
if (!s_running_models.constFind(this).value())
return;
searchRequestForOneSucceeded(doc);
searchRequestForOneSucceeded(pack);
};
if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job)
runSearchJob(job);
@@ -166,27 +166,23 @@ void ResourceModel::search()
}
auto args{ createSearchArguments() };
auto callbacks{ createSearchCallbacks() };
ResourceAPI::Callback<QList<ModPlatform::IndexedPack::Ptr>> callbacks{};
// Use defaults if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this](auto& doc) {
if (!s_running_models.constFind(this).value())
return;
searchRequestSucceeded(doc);
};
if (!callbacks.on_fail)
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, network_error_code);
};
if (!callbacks.on_abort)
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
searchRequestAborted();
};
callbacks.on_succeed = [this](auto& doc) {
if (!s_running_models.constFind(this).value())
return;
searchRequestSucceeded(doc);
};
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, network_error_code);
};
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
searchRequestAborted();
};
if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
runSearchJob(job);
@@ -201,14 +197,15 @@ void ResourceModel::loadEntry(const QModelIndex& entry)
if (!pack->versionsLoaded) {
auto args{ createVersionsArguments(entry) };
auto callbacks{ createVersionsCallbacks(entry) };
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
auto addonId = pack->addonId;
// Use default if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
callbacks.on_succeed = [this, entry, addonId](auto& doc) {
if (!s_running_models.constFind(this).value())
return;
versionRequestSucceeded(doc, pack, entry);
versionRequestSucceeded(doc, addonId, entry);
};
if (!callbacks.on_fail)
callbacks.on_fail = [](QString reason, int) {
@@ -222,28 +219,23 @@ void ResourceModel::loadEntry(const QModelIndex& entry)
if (!pack->extraDataLoaded) {
auto args{ createInfoArguments(entry) };
auto callbacks{ createInfoCallbacks(entry) };
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks{};
// Use default if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) {
if (!s_running_models.constFind(this).value())
return;
auto pack = newpack;
infoRequestSucceeded(doc, pack, entry);
};
if (!callbacks.on_fail)
callbacks.on_fail = [this](QString reason) {
if (!s_running_models.constFind(this).value())
return;
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason));
};
if (!callbacks.on_abort)
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
qCritical() << tr("The request was aborted for an unknown reason");
};
callbacks.on_succeed = [this, entry](auto& newpack) {
if (!s_running_models.constFind(this).value())
return;
infoRequestSucceeded(newpack, entry);
};
callbacks.on_fail = [this](QString reason, int) {
if (!s_running_models.constFind(this).value())
return;
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason));
};
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
qCritical() << tr("The request was aborted for an unknown reason");
};
if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
runInfoJob(job);
@@ -358,68 +350,35 @@ std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
return {};
}
// No 'forgor to implement' shall pass here :blobfox_knife:
#define NEED_FOR_CALLBACK_ASSERT(name) \
Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
QJsonArray ResourceModel::documentToArray([[maybe_unused]] QJsonDocument& doc) const
{
NEED_FOR_CALLBACK_ASSERT("documentToArray");
return {};
}
void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
{
NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
}
void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
{
NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
}
void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
{
NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
}
/* Default callbacks */
void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
void ResourceModel::searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>& newList)
{
QList<ModPlatform::IndexedPack::Ptr> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
[&pack](const DownloadTaskPtr i) {
const auto ipack = i->getPack();
return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
});
sel != m_selected.end()) {
newList.append(sel->get()->getPack());
} else
newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
QList<ModPlatform::IndexedPack::Ptr> filteredNewList;
for (auto pack : newList) {
ModPlatform::IndexedPack::Ptr p;
if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
[&pack](const DownloadTaskPtr i) {
const auto ipack = i->getPack();
return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
});
sel != m_selected.end()) {
p = sel->get()->getPack();
} else {
p = pack;
}
if (checkFilters(p)) {
filteredNewList << p;
}
}
if (packs.size() < 25) {
if (newList.size() < 25) {
m_search_state = SearchState::Finished;
} else {
m_next_search_offset += 25;
m_search_state = SearchState::CanFetchMore;
}
QList<ModPlatform::IndexedPack::Ptr> filteredNewList;
for (auto p : newList)
if (checkFilters(p))
filteredNewList << p;
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (filteredNewList.size() == 0)
return;
@@ -429,24 +388,12 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
endInsertRows();
}
void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc)
void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
{
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
auto obj = Json::requireObject(doc);
if (obj.contains("data"))
obj = Json::requireObject(obj, "data");
loadIndexedPack(*pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
}
m_search_state = SearchState::Finished;
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1);
m_packs.append(pack);
m_packs.append(std::make_shared<ModPlatform::IndexedPack>(pack));
endInsertRows();
}
@@ -479,21 +426,16 @@ void ResourceModel::searchRequestAborted()
search();
}
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
void ResourceModel::versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>& doc, QVariant pack, const QModelIndex& index)
{
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
if (pack.addonId != current_pack->addonId)
if (pack != current_pack->addonId)
return;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
loadIndexedPackVersions(*current_pack, arr);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
}
current_pack->versions = doc;
current_pack->versionsLoaded = true;
// Cache info :^)
QVariant new_pack;
@@ -506,7 +448,7 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind
emit versionListUpdated(index);
}
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
@@ -514,14 +456,7 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe
if (pack.addonId != current_pack->addonId)
return;
try {
auto obj = Json::requireObject(doc);
loadExtraPackInfo(*current_pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
}
*current_pack = pack;
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current_pack);

View File

@@ -43,10 +43,7 @@ class ResourceModel : public QAbstractListModel {
virtual auto debugName() const -> QString;
virtual auto metaEntryBase() const -> QString = 0;
inline int rowCount(const QModelIndex& parent) const override
{
return parent.isValid() ? 0 : static_cast<int>(m_packs.size());
}
inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast<int>(m_packs.size()); }
inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }
inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }
@@ -77,13 +74,10 @@ class ResourceModel : public QAbstractListModel {
void setSearchTerm(QString term) { m_search_term = term; }
virtual ResourceAPI::SearchArgs createSearchArguments() = 0;
virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; }
virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0;
virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(const QModelIndex&) { return {}; }
virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0;
virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(const QModelIndex&) { return {}; }
/** Requests the API for more entries. */
virtual void search();
@@ -114,22 +108,6 @@ class ResourceModel : public QAbstractListModel {
auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>;
/** Converts a JSON document to a common array format.
*
* This is needed so that different providers, with different JSON structures, can be parsed
* uniformally. You NEED to re-implement this if you intend on using the default callbacks.
*/
virtual auto documentToArray(QJsonDocument&) const -> QJsonArray;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; }
protected:
@@ -159,14 +137,14 @@ class ResourceModel : public QAbstractListModel {
private:
/* Default search request callbacks */
void searchRequestSucceeded(QJsonDocument&);
void searchRequestForOneSucceeded(QJsonDocument&);
void searchRequestSucceeded(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
void searchRequestFailed(QString reason, int network_error_code);
void searchRequestAborted();
void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
void versionRequestSucceeded(QVector<ModPlatform::IndexedVersion>&, QVariant, const QModelIndex&);
void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
void infoRequestSucceeded(ModPlatform::IndexedPack&, const QModelIndex&);
signals:
void versionListUpdated(const QModelIndex& index);

View File

@@ -8,8 +8,11 @@
namespace ResourceDownload {
ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst)
ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst,
ResourceAPI* api,
QString debugName,
QString metaEntryBase)
: ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase)
{}
/******** Make data requests ********/
@@ -23,7 +26,7 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
return { *pack, {}, {}, ModPlatform::ResourceType::ResourcePack };
}
ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry)

View File

@@ -20,14 +20,13 @@ class ResourcePackResourceModel : public ResourceModel {
Q_OBJECT
public:
ResourcePackResourceModel(BaseInstance const&, ResourceAPI*);
ResourcePackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
[[nodiscard]] QString debugName() const override { return m_debugName; }
[[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; }
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
@@ -37,7 +36,9 @@ class ResourcePackResourceModel : public ResourceModel {
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
private:
QString m_debugName;
QString m_metaEntryBase;
};
} // namespace ResourceDownload

View File

@@ -8,8 +8,8 @@
namespace ResourceDownload {
ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst)
ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase)
: ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase)
{}
/******** Make data requests ********/
@@ -23,7 +23,7 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
return { *pack, {}, {}, ModPlatform::ResourceType::ShaderPack };
}
ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry)

View File

@@ -20,14 +20,13 @@ class ShaderPackResourceModel : public ResourceModel {
Q_OBJECT
public:
ShaderPackResourceModel(BaseInstance const&, ResourceAPI*);
ShaderPackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
[[nodiscard]] QString debugName() const override { return m_debugName; }
[[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; }
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
@@ -37,7 +36,9 @@ class ShaderPackResourceModel : public ResourceModel {
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
private:
QString m_debugName;
QString m_metaEntryBase;
};
} // namespace ResourceDownload

View File

@@ -12,8 +12,8 @@
static std::list<Version> s_availableVersions = {};
namespace ResourceDownload {
TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api)
: ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft"))
TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase)
: ResourcePackResourceModel(inst, api, debugName, metaEntryBase), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft"))
{
if (!m_version_list->isLoaded()) {
qDebug() << "Loading version list...";
@@ -73,6 +73,7 @@ ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments()
ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry)
{
auto args = ResourcePackResourceModel::createVersionsArguments(entry);
args.resourceType = ModPlatform::ResourceType::TexturePack;
if (!m_version_list->isLoaded()) {
qCritical() << "The version list could not be loaded. Falling back to showing all entries.";
return args;

View File

@@ -13,7 +13,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel {
Q_OBJECT
public:
TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api);
TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase);
inline ::Version maximumTexturePackVersion() const { return { "1.6" }; }

View File

@@ -20,7 +20,7 @@ ListModel::~ListModel() {}
int ListModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : modpacks.size();
return parent.isValid() ? 0 : m_modpacks.size();
}
int ListModel::columnCount(const QModelIndex& parent) const
@@ -31,27 +31,27 @@ int ListModel::columnCount(const QModelIndex& parent) const
QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
IndexedPack pack = modpacks.at(pos);
auto pack = m_modpacks.at(pos);
switch (role) {
case Qt::ToolTipRole: {
if (pack.description.length() > 100) {
if (pack->description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
QString edit = pack.description.left(97);
QString edit = pack->description.left(97);
edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
return edit;
}
return pack.description;
return pack->description;
}
case Qt::DecorationRole: {
if (m_logoMap.contains(pack.logoName)) {
return (m_logoMap.value(pack.logoName));
if (m_logoMap.contains(pack->logoName)) {
return (m_logoMap.value(pack->logoName));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
((ListModel*)this)->requestLogo(pack->logoName, pack->logoUrl);
return icon;
}
case Qt::UserRole: {
@@ -62,9 +62,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
case Qt::SizeHintRole:
return QSize(0, 58);
case UserDataTypes::TITLE:
return pack.name;
return pack->name;
case UserDataTypes::DESCRIPTION:
return pack.description;
return pack->description;
case UserDataTypes::INSTALLED:
return false;
default:
@@ -76,11 +76,10 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role)
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid())
if (pos >= m_modpacks.size() || pos < 0 || !index.isValid())
return false;
Q_ASSERT(value.canConvert<Flame::IndexedPack>());
modpacks[pos] = value.value<Flame::IndexedPack>();
m_modpacks[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
return true;
}
@@ -89,8 +88,8 @@ void ListModel::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].logoName == logo) {
for (int i = 0; i < m_modpacks.size(); i++) {
if (m_modpacks[i]->logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
@@ -117,8 +116,8 @@ void ListModel::requestLogo(QString logo, QString url)
connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
if (m_waitingCallbacks.contains(logo)) {
m_waitingCallbacks.value(logo)(fullPath);
}
});
@@ -148,14 +147,14 @@ Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
bool ListModel::canFetchMore([[maybe_unused]] const QModelIndex& parent) const
{
return searchState == CanPossiblyFetchMore;
return m_searchState == CanPossiblyFetchMore;
}
void ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if (nextSearchOffset == 0) {
if (m_nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
@@ -164,138 +163,106 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch()
{
if (currentSearchTerm.startsWith("#")) {
auto projectId = currentSearchTerm.mid(1);
static const FlameAPI api;
if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::ProjectInfoCallbacks callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
static const FlameAPI api;
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) {
jobPtr = job;
jobPtr->start();
if (auto job = api.getProjectInfo({ { projectId } }, std::move(callbacks)); job) {
m_jobPtr = job;
m_jobPtr->start();
}
return;
}
}
ResourceAPI::SortingMethod sort{};
sort.index = currentSort + 1;
sort.index = m_currentSort + 1;
auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network());
auto searchUrl =
FlameAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource });
ResourceAPI::Callback<QList<ModPlatform::IndexedPack::Ptr>> callbacks{};
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response));
jobPtr = netJob;
jobPtr->start();
connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed);
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
std::move(callbacks));
m_jobPtr = netJob;
m_jobPtr->start();
}
void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr<ModFilterWidget::Filter> filter, bool filterChanged)
{
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) {
if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort && !filterChanged) {
return;
}
currentSearchTerm = term;
currentSort = sort;
m_currentSearchTerm = term;
m_currentSort = sort;
m_filter = filter;
if (hasActiveSearchJob()) {
jobPtr->abort();
searchState = ResetRequested;
m_jobPtr->abort();
m_searchState = ResetRequested;
return;
}
beginResetModel();
modpacks.clear();
m_modpacks.clear();
endResetModel();
searchState = None;
m_searchState = None;
nextSearchOffset = 0;
m_nextSearchOffset = 0;
performPaginatedSearch();
}
void Flame::ListModel::searchRequestFinished()
void Flame::ListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& newList)
{
if (hasActiveSearchJob())
return;
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;
}
QList<Flame::IndexedPack> newList;
auto packs = Json::ensureArray(doc.object(), "data");
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
Flame::IndexedPack pack;
try {
Flame::loadIndexedPack(pack, packObj);
newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading pack from CurseForge: " << e.cause();
continue;
}
}
if (packs.size() < 25) {
searchState = Finished;
if (newList.size() < 25) {
m_searchState = Finished;
} else {
nextSearchOffset += 25;
searchState = CanPossiblyFetchMore;
m_nextSearchOffset += 25;
m_searchState = CanPossiblyFetchMore;
}
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (newList.size() == 0)
return;
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1);
m_modpacks.append(newList);
endInsertRows();
}
void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc)
void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
{
jobPtr.reset();
m_jobPtr.reset();
auto packObj = Json::ensureObject(doc.object(), "data");
Flame::IndexedPack pack;
try {
Flame::loadIndexedPack(pack, packObj);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading pack from CurseForge: " << e.cause();
return;
}
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1);
modpacks.append({ pack });
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack));
endInsertRows();
}
void Flame::ListModel::searchRequestFailed(QString reason)
{
jobPtr.reset();
m_jobPtr.reset();
if (searchState == ResetRequested) {
if (m_searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
m_modpacks.clear();
endResetModel();
nextSearchOffset = 0;
m_nextSearchOffset = 0;
performPaginatedSearch();
} else {
searchState = Finished;
m_searchState = Finished;
}
}

View File

@@ -16,8 +16,6 @@
#include <functional>
#include "ui/widgets/ModFilterWidget.h"
#include <modplatform/flame/FlamePackIndex.h>
namespace Flame {
using LogoMap = QMap<QString, QIcon>;
@@ -41,8 +39,8 @@ class ListModel : public QAbstractListModel {
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
void searchWithTerm(const QString& term, int sort, std::shared_ptr<ModFilterWidget::Filter> filter, bool filterChanged);
bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); }
Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; }
bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); }
Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; }
private slots:
void performPaginatedSearch();
@@ -50,27 +48,26 @@ class ListModel : public QAbstractListModel {
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
void searchRequestFinished();
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>&);
void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(QJsonDocument&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
private:
void requestLogo(QString file, QString url);
private:
QList<IndexedPack> modpacks;
QList<ModPlatform::IndexedPack::Ptr> m_modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QMap<QString, LogoCallback> m_waitingCallbacks;
QString currentSearchTerm;
int currentSort = 0;
QString m_currentSearchTerm;
int m_currentSort = 0;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
int nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
Task::Ptr jobPtr;
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
int m_nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None;
Task::Ptr m_jobPtr;
};
} // namespace Flame

View File

@@ -35,7 +35,8 @@
#include "FlamePage.h"
#include "Version.h"
#include "modplatform/flame/FlamePackIndex.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/widgets/ModFilterWidget.h"
#include "ui_FlamePage.h"
@@ -43,29 +44,25 @@
#include <QKeyEvent>
#include <memory>
#include "Application.h"
#include "FlameModel.h"
#include "InstanceImportTask.h"
#include "Json.h"
#include "StringUtils.h"
#include "modplatform/flame/FlameAPI.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/widgets/ProjectItem.h"
#include "net/ApiDownload.h"
static FlameAPI api;
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent)
: QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false)
: QWidget(parent), m_ui(new Ui::FlamePage), m_dialog(dialog), m_fetch_progress(this, false)
{
ui->setupUi(this);
ui->searchEdit->installEventFilter(this);
listModel = new Flame::ListModel(this);
ui->packView->setModel(listModel);
m_ui->setupUi(this);
m_ui->searchEdit->installEventFilter(this);
m_listModel = new Flame::ListModel(this);
m_ui->packView->setModel(m_listModel);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
@@ -76,33 +73,33 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent)
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->verticalLayout->insertWidget(2, &m_fetch_progress);
m_ui->verticalLayout->insertWidget(2, &m_fetch_progress);
// 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"));
m_ui->sortByBox->addItem(tr("Sort by Featured"));
m_ui->sortByBox->addItem(tr("Sort by Popularity"));
m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
m_ui->sortByBox->addItem(tr("Sort by Name"));
m_ui->sortByBox->addItem(tr("Sort by Author"));
m_ui->sortByBox->addItem(tr("Sort by Total Downloads"));
connect(ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch);
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged);
connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch);
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged);
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packDescription->setMetaEntry("FlamePacks");
m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
m_ui->packDescription->setMetaEntry("FlamePacks");
createFilterWidget();
}
FlamePage::~FlamePage()
{
delete ui;
delete m_ui;
}
bool FlamePage::eventFilter(QObject* watched, QEvent* event)
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
@@ -125,7 +122,7 @@ bool FlamePage::shouldDisplay() const
void FlamePage::retranslate()
{
ui->retranslateUi(this);
m_ui->retranslateUi(this);
}
void FlamePage::openedImpl()
@@ -136,109 +133,91 @@ void FlamePage::openedImpl()
void FlamePage::triggerSearch()
{
ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
ui->packView->clearSelection();
ui->packDescription->clear();
ui->versionSelectionBox->clear();
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
bool filterChanged = m_filterWidget->changed();
listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
m_fetch_progress.watch(listModel->activeSearchJob().get());
}
bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr<ModFilterWidget::Filter> filter)
{
if (!filter)
return true;
return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders
(filter->releases.empty() || // releases
std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) &&
filter->checkMcVersions({ v.mcVersion })); // mcVersions}
m_listModel->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
m_fetch_progress.watch(m_listModel->activeSearchJob().get());
}
void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev)
{
ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->clear();
if (!curr.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
m_dialog->setSuggestedPack();
}
return;
}
QVariant raw = listModel->data(curr, Qt::UserRole);
Q_ASSERT(raw.canConvert<Flame::IndexedPack>());
current = raw.value<Flame::IndexedPack>();
m_current = m_listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
if (!current.versionsLoaded || m_filterWidget->changed()) {
if (!m_current->versionsLoaded || m_filterWidget->changed()) {
qDebug() << "Loading flame modpack versions";
auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
int addonId = current.addonId;
netJob->addNetAction(
Net::ApiDownload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files").arg(addonId), response));
connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] {
if (addonId != current.addonId) {
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion> > callbacks{};
auto addonId = m_current->addonId;
// Use default if no callbacks are set
callbacks.on_succeed = [this, curr, addonId](auto& doc) {
if (addonId != m_current->addonId) {
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 CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
auto arr = Json::ensureArray(doc.object(), "data");
try {
Flame::loadIndexedPackVersions(current, arr);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause();
}
auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); };
m_current->versions = doc;
m_current->versionsLoaded = true;
auto pred = [this](const ModPlatform::IndexedVersion& v) {
if (auto filter = m_filterWidget->getFilter())
return !filter->checkModpackFilters(v);
return false;
};
#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
current.versions.removeIf(pred);
m_current->versions.removeIf(pred);
#else
for (auto it = current.versions.begin(); it != current.versions.end();)
if (pred(*it))
it = current.versions.erase(it);
else
++it;
for (auto it = m_current->versions.begin(); it != m_current->versions.end();)
if (pred(*it))
it = m_current->versions.erase(it);
else
++it;
#endif
for (const auto& version : current.versions) {
ui->versionSelectionBox->addItem(Flame::getVersionDisplayString(version), QVariant(version.downloadUrl));
for (auto version : m_current->versions) {
m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.downloadUrl));
}
QVariant current_updated;
current_updated.setValue(current);
current_updated.setValue(m_current);
if (!listModel->setData(curr, current_updated, Qt::UserRole))
if (!m_listModel->setData(curr, current_updated, Qt::UserRole))
qWarning() << "Failed to cache versions for the current pack!";
// TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution.
if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) {
ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) {
m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
}
suggestCurrent();
});
connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
connect(netJob, &NetJob::failed,
[this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
};
callbacks.on_fail = [this](QString reason, int) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
};
auto netJob = api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job = netJob;
netJob->start();
} else {
for (auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
for (auto version : m_current->versions) {
m_ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
}
suggestCurrent();
}
// TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution.
if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) {
ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) {
m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
}
updateUi();
@@ -251,26 +230,26 @@ void FlamePage::suggestCurrent()
}
if (m_selected_version_index == -1) {
dialog->setSuggestedPack();
m_dialog->setSuggestedPack();
return;
}
auto version = current.versions.at(m_selected_version_index);
auto version = m_current->versions.at(m_selected_version_index);
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", QString::number(current.addonId));
extra_info.insert("pack_version_id", QString::number(version.fileId));
extra_info.insert("pack_id", m_current->addonId.toString());
extra_info.insert("pack_version_id", version.fileId.toString());
dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)));
QString editedLogoName = "curseforge_" + current.logoName;
listModel->getLogo(current.logoName, current.logoUrl,
[this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
m_dialog->setSuggestedPack(m_current->name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)));
QString editedLogoName = "curseforge_" + m_current->logoName;
m_listModel->getLogo(m_current->logoName, m_current->logoUrl,
[this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); });
}
void FlamePage::onVersionSelectionChanged(int index)
{
bool is_blocked = false;
ui->versionSelectionBox->itemData(index).toInt(&is_blocked);
m_ui->versionSelectionBox->itemData(index).toInt(&is_blocked);
if (index == -1 || is_blocked) {
m_selected_version_index = -1;
@@ -279,7 +258,7 @@ void FlamePage::onVersionSelectionChanged(int index)
m_selected_version_index = index;
Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString());
Q_ASSERT(m_current->versions.at(m_selected_version_index).downloadUrl == m_ui->versionSelectionBox->currentData().toString());
suggestCurrent();
}
@@ -287,66 +266,67 @@ void FlamePage::onVersionSelectionChanged(int index)
void FlamePage::updateUi()
{
QString text = "";
QString name = current.name;
QString name = m_current->name;
if (current.extra.websiteUrl.isEmpty())
if (m_current->websiteUrl.isEmpty())
text = name;
else
text = "<a href=\"" + current.extra.websiteUrl + "\">" + name + "</a>";
if (!current.authors.empty()) {
auto authorToStr = [](Flame::ModpackAuthor& author) {
text = "<a href=\"" + m_current->websiteUrl + "\">" + name + "</a>";
if (!m_current->authors.empty()) {
auto authorToStr = [](ModPlatform::ModpackAuthor& author) {
if (author.url.isEmpty()) {
return author.name;
}
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
for (auto& author : current.authors) {
for (auto& author : m_current->authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
}
if (current.extraInfoLoaded) {
if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty()) {
if (m_current->extraDataLoaded) {
if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() ||
!m_current->extraData.wikiUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
if (!current.extra.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>";
if (!current.extra.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>";
if (!current.extra.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>";
if (!m_current->extraData.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(m_current->extraData.issuesUrl) + "<br>";
if (!m_current->extraData.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(m_current->extraData.wikiUrl) + "<br>";
if (!m_current->extraData.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(m_current->extraData.sourceUrl) + "<br>";
}
text += "<hr>";
text += api.getModDescription(current.addonId).toUtf8();
text += api.getModDescription(m_current->addonId.toInt()).toUtf8();
ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush();
m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description));
m_ui->packDescription->flush();
}
QString FlamePage::getSerachTerm() const
{
return ui->searchEdit->text();
return m_ui->searchEdit->text();
}
void FlamePage::setSearchTerm(QString term)
{
ui->searchEdit->setText(term);
m_ui->searchEdit->setText(term);
}
void FlamePage::createFilterWidget()
{
auto widget = ModFilterWidget::create(nullptr, false);
m_filterWidget.swap(widget);
auto old = ui->splitter->replaceWidget(0, m_filterWidget.get());
auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get());
// because we replaced the widget we also need to delete it
if (old) {
delete old;
}
connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); });
connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); });
connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch);
auto response = std::make_shared<QByteArray>();

View File

@@ -38,8 +38,8 @@
#include <QWidget>
#include <Application.h>
#include <modplatform/flame/FlamePackIndex.h>
#include <QTimer>
#include "modplatform/ModIndex.h"
#include "ui/pages/modplatform/ModpackProviderBasePage.h"
#include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
@@ -88,10 +88,10 @@ class FlamePage : public QWidget, public ModpackProviderBasePage {
void createFilterWidget();
private:
Ui::FlamePage* ui = nullptr;
NewInstanceDialog* dialog = nullptr;
Flame::ListModel* listModel = nullptr;
Flame::IndexedPack current;
Ui::FlamePage* m_ui = nullptr;
NewInstanceDialog* m_dialog = nullptr;
Flame::ListModel* m_listModel = nullptr;
ModPlatform::IndexedPack::Ptr m_current;
int m_selected_version_index = -1;
@@ -102,4 +102,5 @@ class FlamePage : public QWidget, public ModpackProviderBasePage {
std::unique_ptr<ModFilterWidget> m_filterWidget;
Task::Ptr m_categoriesTask;
Task::Ptr m_job;
};

View File

@@ -8,7 +8,7 @@
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "ui/pages/modplatform/flame/FlameResourcePages.h"
namespace ResourceDownload {
@@ -17,97 +17,9 @@ static bool isOptedOut(const ModPlatform::IndexedVersion& ver)
return ver.downloadUrl.isEmpty();
}
FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {}
void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr);
}
auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
{
return FlameMod::loadDependencyVersions(m, arr, &m_base_instance);
}
bool FlameModModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameResourcePackModel::FlameResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new FlameAPI) {}
void FlameResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr);
}
bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {}
void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr);
QList<ModPlatform::IndexedVersion> filtered_versions(m.versions.size());
// FIXME: Client-side version filtering. This won't take into account any user-selected filtering.
for (auto const& version : m.versions) {
auto const& mc_versions = version.mcVersion;
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
[this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); }))
filtered_versions.push_back(version);
}
m.versions = filtered_versions;
}
FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base)
: TexturePackResourceModel(base, new FlameAPI, Flame::debugName(), Flame::metaEntryBase())
{}
ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments()
{
@@ -137,65 +49,4 @@ bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) con
return isOptedOut(ver);
}
auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameShaderPackModel::FlameShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new FlameAPI) {}
void FlameShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr);
}
bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameDataPackModel::FlameDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new FlameAPI) {}
void FlameDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr);
}
bool FlameDataPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
} // namespace ResourceDownload

View File

@@ -5,52 +5,10 @@
#pragma once
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
#include "ui/pages/modplatform/flame/FlameResourcePages.h"
namespace ResourceDownload {
class FlameModModel : public ModModel {
Q_OBJECT
public:
FlameModModel(BaseInstance&);
~FlameModModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
QString debugName() const override { return Flame::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class FlameResourcePackModel : public ResourcePackResourceModel {
Q_OBJECT
public:
FlameResourcePackModel(const BaseInstance&);
~FlameResourcePackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
QString debugName() const override { return Flame::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class FlameTexturePackModel : public TexturePackResourceModel {
Q_OBJECT
@@ -64,52 +22,8 @@ class FlameTexturePackModel : public TexturePackResourceModel {
QString debugName() const override { return Flame::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class FlameShaderPackModel : public ShaderPackResourceModel {
Q_OBJECT
public:
FlameShaderPackModel(const BaseInstance&);
~FlameShaderPackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
QString debugName() const override { return Flame::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class FlameDataPackModel : public DataPackResourceModel {
Q_OBJECT
public:
FlameDataPackModel(const BaseInstance&);
~FlameDataPackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
QString debugName() const override { return Flame::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
} // namespace ResourceDownload

View File

@@ -40,7 +40,6 @@
#include "FlameResourcePages.h"
#include <QList>
#include <memory>
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "ui_ResourcePage.h"
@@ -51,7 +50,7 @@ namespace ResourceDownload {
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance)
{
m_model = new FlameModModel(instance);
m_model = new ModModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -85,7 +84,7 @@ void FlameModPage::openUrl(const QUrl& url)
FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
: ResourcePackResourcePage(dialog, instance)
{
m_model = new FlameResourcePackModel(instance);
m_model = new ResourcePackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -169,7 +168,7 @@ void FlameDataPackPage::openUrl(const QUrl& url)
FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
: ShaderPackResourcePage(dialog, instance)
{
m_model = new FlameShaderPackModel(instance);
m_model = new ShaderPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -184,10 +183,9 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance)
: DataPackResourcePage(dialog, instance)
FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance)
{
m_model = new FlameDataPackModel(instance);
m_model = new DataPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();

View File

@@ -62,7 +62,7 @@ void ModpackListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if (nextSearchOffset == 0) {
if (m_nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
@@ -72,27 +72,27 @@ void ModpackListModel::fetchMore(const QModelIndex& parent)
auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
Modrinth::Modpack pack = modpacks.at(pos);
auto pack = m_modpacks.at(pos);
switch (role) {
case Qt::ToolTipRole: {
if (pack.description.length() > 100) {
if (pack->description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
QString edit = pack.description.left(97);
QString edit = pack->description.left(97);
edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
return edit;
}
return pack.description;
return pack->description;
}
case Qt::DecorationRole: {
if (m_logoMap.contains(pack.iconName))
return m_logoMap.value(pack.iconName);
if (m_logoMap.contains(pack->logoName))
return m_logoMap.value(pack->logoName);
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString());
((ModpackListModel*)this)->requestLogo(pack->logoName, pack->logoUrl);
return icon;
}
case Qt::UserRole: {
@@ -104,9 +104,9 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
return QSize(0, 58);
// Custom data
case UserDataTypes::TITLE:
return pack.name;
return pack->name;
case UserDataTypes::DESCRIPTION:
return pack.description;
return pack->description;
case UserDataTypes::INSTALLED:
return false;
default:
@@ -119,11 +119,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role)
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid())
if (pos >= m_modpacks.size() || pos < 0 || !index.isValid())
return false;
Q_ASSERT(value.canConvert<Modrinth::Modpack>());
modpacks[pos] = value.value<Modrinth::Modpack>();
m_modpacks[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
return true;
}
@@ -132,68 +131,56 @@ void ModpackListModel::performPaginatedSearch()
{
if (hasActiveSearchJob())
return;
static const ModrinthAPI api;
if (currentSearchTerm.startsWith("#")) {
auto projectId = currentSearchTerm.mid(1);
if (m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::ProjectInfoCallbacks callbacks;
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& doc, auto&) { searchRequestForOneSucceeded(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
};
static const ModrinthAPI api;
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) {
jobPtr = job;
jobPtr->start();
m_jobPtr = job;
m_jobPtr->start();
}
return;
}
} // TODO: Move to standalone API
ResourceAPI::SortingMethod sort{};
sort.name = currentSort;
auto searchUrl =
ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource });
sort.name = m_currentSort;
auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse));
ResourceAPI::Callback<QList<ModPlatform::IndexedPack::Ptr>> callbacks{};
connect(netJob.get(), &NetJob::succeeded, this, [this] {
QJsonParseError parseError{};
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *m_allResponse;
return;
}
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource },
std::move(callbacks));
searchRequestFinished(doc);
});
connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
jobPtr = netJob;
jobPtr->start();
m_jobPtr = netJob;
m_jobPtr->start();
}
void ModpackListModel::refresh()
{
if (hasActiveSearchJob()) {
jobPtr->abort();
searchState = ResetRequested;
m_jobPtr->abort();
m_searchState = ResetRequested;
return;
}
beginResetModel();
modpacks.clear();
m_modpacks.clear();
endResetModel();
searchState = None;
m_searchState = None;
nextSearchOffset = 0;
m_nextSearchOffset = 0;
performPaginatedSearch();
}
@@ -224,12 +211,12 @@ void ModpackListModel::searchWithTerm(const QString& term,
auto sort_str = sortFromIndex(sort);
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) {
if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort_str && !filterChanged) {
return;
}
currentSearchTerm = term;
currentSort = sort_str;
m_currentSearchTerm = term;
m_currentSort = sort_str;
m_filter = filter;
refresh();
@@ -259,8 +246,8 @@ void ModpackListModel::requestLogo(QString logo, QString url)
connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
if (m_waitingCallbacks.contains(logo)) {
m_waitingCallbacks.value(logo)(fullPath);
}
});
@@ -279,8 +266,8 @@ 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) {
for (int i = 0; i < m_modpacks.size(); i++) {
if (m_modpacks[i]->logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
@@ -292,65 +279,38 @@ void ModpackListModel::logoFailed(QString logo)
m_loadingLogos.removeAll(logo);
}
void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
void ModpackListModel::searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& newList)
{
jobPtr.reset();
m_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() < m_modpacks_per_page) {
searchState = Finished;
if (newList.size() < m_modpacks_per_page) {
m_searchState = Finished;
} else {
nextSearchOffset += m_modpacks_per_page;
searchState = CanPossiblyFetchMore;
m_nextSearchOffset += m_modpacks_per_page;
m_searchState = CanPossiblyFetchMore;
}
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (newList.size() == 0)
return;
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1);
m_modpacks.append(newList);
endInsertRows();
}
void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc)
void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack)
{
jobPtr.reset();
m_jobPtr.reset();
auto packObj = doc.object();
Modrinth::Modpack pack;
try {
Modrinth::loadIndexedPack(pack, packObj);
pack.id = Json::ensureString(packObj, "id", pack.id);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
return;
}
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1);
modpacks.append({ pack });
beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1);
m_modpacks.append(std::make_shared<ModPlatform::IndexedPack>(pack));
endInsertRows();
}
void ModpackListModel::searchRequestFailed(QString)
{
auto failed_action = dynamic_cast<NetJob*>(jobPtr.get())->getFailedActions().at(0);
auto failed_action = dynamic_cast<NetJob*>(m_jobPtr.get())->getFailedActions().at(0);
if (failed_action->replyStatusCode() == -1) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
@@ -362,17 +322,17 @@ void ModpackListModel::searchRequestFailed(QString)
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
}
jobPtr.reset();
m_jobPtr.reset();
if (searchState == ResetRequested) {
if (m_searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
m_modpacks.clear();
endResetModel();
nextSearchOffset = 0;
m_nextSearchOffset = 0;
performPaginatedSearch();
} else {
searchState = Finished;
m_searchState = Finished;
}
}

View File

@@ -37,7 +37,7 @@
#include <QAbstractListModel>
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/ModIndex.h"
#include "net/NetJob.h"
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
@@ -56,7 +56,7 @@ class ModpackListModel : public QAbstractListModel {
ModpackListModel(ModrinthPage* parent);
~ModpackListModel() override = default;
inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); };
inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : m_modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; };
inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
@@ -66,27 +66,27 @@ class ModpackListModel : public QAbstractListModel {
auto data(const QModelIndex& index, int role) const -> QVariant override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
inline void setActiveJob(NetJob::Ptr ptr) { m_jobPtr = ptr; }
/* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, int sort, std::shared_ptr<ModFilterWidget::Filter> filter, bool filterChanged);
bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); }
Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; }
bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); }
Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; }
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
inline auto canFetchMore(const QModelIndex& parent) const -> bool override
{
return parent.isValid() ? false : searchState == CanPossiblyFetchMore;
return parent.isValid() ? false : m_searchState == CanPossiblyFetchMore;
};
public slots:
void searchRequestFinished(QJsonDocument& doc_all);
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& doc_all);
void searchRequestFailed(QString reason);
void searchRequestForOneSucceeded(QJsonDocument&);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack&);
protected slots:
@@ -103,20 +103,20 @@ class ModpackListModel : public QAbstractListModel {
protected:
ModrinthPage* m_parent;
QList<Modrinth::Modpack> modpacks;
QList<ModPlatform::IndexedPack::Ptr> m_modpacks;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QMap<QString, LogoCallback> m_waitingCallbacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
QString currentSearchTerm;
QString currentSort;
QString m_currentSearchTerm;
QString m_currentSort;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
int nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
int m_nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None;
Task::Ptr jobPtr;
Task::Ptr m_jobPtr;
std::shared_ptr<QByteArray> m_allResponse = std::make_shared<QByteArray>();
QByteArray m_specific_response;

View File

@@ -36,6 +36,7 @@
#include "ModrinthPage.h"
#include "Version.h"
#include "modplatform/ModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ModrinthPage.h"
@@ -57,17 +58,17 @@
#include <QPushButton>
ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent)
: QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false)
: QWidget(parent), m_ui(new Ui::ModrinthPage), m_dialog(dialog), m_fetch_progress(this, false)
{
ui->setupUi(this);
m_ui->setupUi(this);
createFilterWidget();
ui->searchEdit->installEventFilter(this);
m_ui->searchEdit->installEventFilter(this);
m_model = new Modrinth::ModpackListModel(this);
ui->packView->setModel(m_model);
m_ui->packView->setModel(m_model);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
@@ -78,30 +79,30 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent)
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->verticalLayout->insertWidget(1, &m_fetch_progress);
m_ui->verticalLayout->insertWidget(1, &m_fetch_progress);
ui->sortByBox->addItem(tr("Sort by Relevance"));
ui->sortByBox->addItem(tr("Sort by Total Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by Newest"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
m_ui->sortByBox->addItem(tr("Sort by Relevance"));
m_ui->sortByBox->addItem(tr("Sort by Total Downloads"));
m_ui->sortByBox->addItem(tr("Sort by Follows"));
m_ui->sortByBox->addItem(tr("Sort by Newest"));
m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
connect(ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch);
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged);
connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch);
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged);
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packDescription->setMetaEntry(metaEntryBase());
m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
ModrinthPage::~ModrinthPage()
{
delete ui;
delete m_ui;
}
void ModrinthPage::retranslate()
{
ui->retranslateUi(this);
m_ui->retranslateUi(this);
}
void ModrinthPage::openedImpl()
@@ -113,7 +114,7 @@ void ModrinthPage::openedImpl()
bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) {
auto* keyEvent = reinterpret_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
this->triggerSearch();
@@ -129,146 +130,108 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)
return QObject::eventFilter(watched, event);
}
bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr<ModFilterWidget::Filter> filter)
{
if (!filter)
return true;
return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders
(filter->releases.empty() || // releases
std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) &&
filter->checkMcVersions({ v.gameVersion })); // gameVersion}
}
void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev)
{
ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->clear();
if (!curr.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
m_dialog->setSuggestedPack();
}
return;
}
QVariant raw = m_model->data(curr, Qt::UserRole);
Q_ASSERT(raw.canConvert<Modrinth::Modpack>());
current = raw.value<Modrinth::Modpack>();
auto name = current.name;
m_current = m_model->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
auto name = m_current->name;
if (!current.extraInfoLoaded) {
if (!m_current->extraDataLoaded) {
qDebug() << "Loading modrinth modpack information";
ResourceAPI::Callback<ModPlatform::IndexedPack> callbacks;
auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
QString id = current.id;
netJob->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] {
if (id != current.id) {
auto id = m_current->addonId;
callbacks.on_fail = [this](QString reason, int) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
};
callbacks.on_succeed = [this, id, curr](auto& pack) {
if (id != m_current->addonId) {
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();
*m_current = pack;
QVariant current_updated;
current_updated.setValue(current);
current_updated.setValue(m_current);
if (!m_model->setData(curr, current_updated, Qt::UserRole))
qWarning() << "Failed to cache extra info for the current pack!";
suggestCurrent();
});
connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
connect(netJob, &NetJob::failed,
[this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
netJob->start();
updateUI();
};
if (auto netJob = m_api.getProjectInfo({ { m_current->addonId } }, std::move(callbacks)); netJob) {
m_job = netJob;
m_job->start();
}
} else
updateUI();
if (!current.versionsLoaded || m_filterWidget->changed()) {
if (!m_current->versionsLoaded || m_filterWidget->changed()) {
qDebug() << "Loading modrinth modpack versions";
auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
QString id = current.id;
netJob->addNetAction(
Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] {
if (id != current.id) {
return; // wrong request?
auto addonId = m_current->addonId;
// Use default if no callbacks are set
callbacks.on_succeed = [this, curr, addonId](auto& doc) {
if (addonId != m_current->addonId) {
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();
}
auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); };
m_current->versions = doc;
m_current->versionsLoaded = true;
auto pred = [this](const ModPlatform::IndexedVersion& v) {
if (auto filter = m_filterWidget->getFilter())
return !filter->checkModpackFilters(v);
return false;
};
#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
current.versions.removeIf(pred);
m_current->versions.removeIf(pred);
#else
for (auto it = current.versions.begin(); it != current.versions.end();)
if (pred(*it))
it = current.versions.erase(it);
else
++it;
for (auto it = m_current->versions.begin(); it != m_current->versions.end();)
if (pred(*it))
it = m_current->versions.erase(it);
else
++it;
#endif
for (const auto& version : current.versions) {
ui->versionSelectionBox->addItem(Modrinth::getVersionDisplayString(version), QVariant(version.id));
for (const auto& version : m_current->versions) {
m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.addonId));
}
QVariant current_updated;
current_updated.setValue(current);
current_updated.setValue(m_current);
if (!m_model->setData(curr, current_updated, Qt::UserRole))
qWarning() << "Failed to cache versions for the current pack!";
suggestCurrent();
});
connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
connect(netJob, &NetJob::failed,
[this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
netJob->start();
};
callbacks.on_fail = [this](QString reason, int) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec();
};
auto netJob = m_api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_job2 = netJob;
m_job2->start();
} else {
for (auto version : current.versions) {
if (!version.name.contains(version.version))
ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id));
for (auto version : m_current->versions) {
if (!version.version.contains(version.version))
m_ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.version, version.version_number),
QVariant(version.addonId));
else
ui->versionSelectionBox->addItem(version.name, QVariant(version.id));
m_ui->versionSelectionBox->addItem(version.version, QVariant(version.addonId));
}
suggestCurrent();
@@ -279,53 +242,64 @@ void ModrinthPage::updateUI()
{
QString text = "";
if (current.extra.projectUrl.isEmpty())
text = current.name;
if (m_current->websiteUrl.isEmpty())
text = m_current->name;
else
text = "<a href=\"" + current.extra.projectUrl + "\">" + current.name + "</a>";
text = "<a href=\"" + m_current->websiteUrl + "\">" + m_current->name + "</a>";
// TODO: Implement multiple authors with links
text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author));
if (!m_current->authors.empty()) {
auto authorToStr = [](ModPlatform::ModpackAuthor& author) {
if (author.url.isEmpty()) {
return author.name;
}
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
for (auto& author : m_current->authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
}
if (current.extraInfoLoaded) {
if (current.extra.status == "archived") {
if (m_current->extraDataLoaded) {
if (m_current->extraData.status == "archived") {
text += "<br><br>" + tr("<b>This project has been archived. It will not receive any further updates unless the author decides "
"to unarchive the project.</b>");
}
if (!current.extra.donate.isEmpty()) {
if (!m_current->extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](Modrinth::DonationData& donate) -> QString {
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
};
QStringList donates;
for (auto& donate : current.extra.donate) {
for (auto& donate : m_current->extraData.donate) {
donates.append(donateToStr(donate));
}
text += donates.join(", ");
}
if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty() ||
!current.extra.discordUrl.isEmpty()) {
if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() ||
!m_current->extraData.wikiUrl.isEmpty() || !m_current->extraData.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
if (!current.extra.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>";
if (!current.extra.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>";
if (!current.extra.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>";
if (!current.extra.discordUrl.isEmpty())
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extra.discordUrl) + "<br>";
if (!m_current->extraData.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(m_current->extraData.issuesUrl) + "<br>";
if (!m_current->extraData.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(m_current->extraData.wikiUrl) + "<br>";
if (!m_current->extraData.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(m_current->extraData.sourceUrl) + "<br>";
if (!m_current->extraData.discordUrl.isEmpty())
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(m_current->extraData.discordUrl) + "<br>";
}
text += "<hr>";
text += markdownToHTML(current.extra.body.toUtf8());
text += markdownToHTML(m_current->extraData.body.toUtf8());
ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description));
ui->packDescription->flush();
m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description));
m_ui->packDescription->flush();
}
void ModrinthPage::suggestCurrent()
@@ -334,21 +308,21 @@ void ModrinthPage::suggestCurrent()
return;
}
if (selectedVersion.isEmpty()) {
dialog->setSuggestedPack();
if (m_selectedVersion.isEmpty()) {
m_dialog->setSuggestedPack();
return;
}
for (auto& ver : current.versions) {
if (ver.id == selectedVersion) {
for (auto& ver : m_current->versions) {
if (ver.addonId == m_selectedVersion) {
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", current.id);
extra_info.insert("pack_version_id", ver.id);
extra_info.insert("pack_id", m_current->addonId.toString());
extra_info.insert("pack_version_id", ver.fileId.toString());
dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info)));
auto iconName = current.iconName;
m_model->getLogo(iconName, current.iconUrl.toString(),
[this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); });
m_dialog->setSuggestedPack(m_current->name, ver.version, new InstanceImportTask(ver.downloadUrl, this, std::move(extra_info)));
auto iconName = m_current->logoName;
m_model->getLogo(iconName, m_current->logoUrl,
[this, iconName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, iconName); });
break;
}
@@ -357,46 +331,46 @@ void ModrinthPage::suggestCurrent()
void ModrinthPage::triggerSearch()
{
ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
ui->packView->clearSelection();
ui->packDescription->clear();
ui->versionSelectionBox->clear();
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
bool filterChanged = m_filterWidget->changed();
m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
m_model->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged);
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
void ModrinthPage::onVersionSelectionChanged(int index)
{
if (index == -1) {
selectedVersion = "";
m_selectedVersion = "";
return;
}
selectedVersion = ui->versionSelectionBox->itemData(index).toString();
m_selectedVersion = m_ui->versionSelectionBox->itemData(index).toString();
suggestCurrent();
}
void ModrinthPage::setSearchTerm(QString term)
{
ui->searchEdit->setText(term);
m_ui->searchEdit->setText(term);
}
QString ModrinthPage::getSerachTerm() const
{
return ui->searchEdit->text();
return m_ui->searchEdit->text();
}
void ModrinthPage::createFilterWidget()
{
auto widget = ModFilterWidget::create(nullptr, true);
m_filterWidget.swap(widget);
auto old = ui->splitter->replaceWidget(0, m_filterWidget.get());
auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get());
// because we replaced the widget we also need to delete it
if (old) {
delete old;
}
connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); });
connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); });
connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch);
auto response = std::make_shared<QByteArray>();

View File

@@ -37,9 +37,10 @@
#pragma once
#include "Application.h"
#include "modplatform/ModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "ui/pages/modplatform/ModpackProviderBasePage.h"
#include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
@@ -67,10 +68,10 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
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"; };
inline QString debugName() const { return "Modrinth"; }
inline QString metaEntryBase() const { return "ModrinthModpacks"; };
auto getCurrent() -> Modrinth::Modpack& { return current; }
ModPlatform::IndexedPack::Ptr getCurrent() { return m_current; }
void suggestCurrent();
void updateUI();
@@ -91,12 +92,12 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
void createFilterWidget();
private:
Ui::ModrinthPage* ui;
NewInstanceDialog* dialog;
Ui::ModrinthPage* m_ui;
NewInstanceDialog* m_dialog;
Modrinth::ModpackListModel* m_model;
Modrinth::Modpack current;
QString selectedVersion;
ModPlatform::IndexedPack::Ptr m_current;
QString m_selectedVersion;
ProgressWidget m_fetch_progress;
@@ -105,4 +106,8 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
std::unique_ptr<ModFilterWidget> m_filterWidget;
Task::Ptr m_categoriesTask;
ModrinthAPI m_api;
Task::Ptr m_job;
Task::Ptr m_job2;
};

View File

@@ -1,144 +0,0 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ModrinthResourceModels.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
namespace ResourceDownload {
ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {}
void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr);
}
auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
{
return ::Modrinth::loadDependencyVersions(m, arr, &m_base_instance);
}
auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {}
void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr);
}
auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {}
void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr);
}
auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {}
void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr);
}
auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
ModrinthDataPackModel::ModrinthDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new ModrinthAPI) {}
void ModrinthDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr);
}
auto ModrinthDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
} // namespace ResourceDownload

View File

@@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "ui/pages/modplatform/DataPackModel.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
namespace ResourceDownload {
class ModrinthModModel : public ModModel {
Q_OBJECT
public:
ModrinthModModel(BaseInstance&);
~ModrinthModModel() override = default;
private:
QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class ModrinthResourcePackModel : public ResourcePackResourceModel {
Q_OBJECT
public:
ModrinthResourcePackModel(const BaseInstance&);
~ModrinthResourcePackModel() override = default;
private:
QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class ModrinthTexturePackModel : public TexturePackResourceModel {
Q_OBJECT
public:
ModrinthTexturePackModel(const BaseInstance&);
~ModrinthTexturePackModel() override = default;
private:
QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class ModrinthShaderPackModel : public ShaderPackResourceModel {
Q_OBJECT
public:
ModrinthShaderPackModel(const BaseInstance&);
~ModrinthShaderPackModel() override = default;
private:
QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class ModrinthDataPackModel : public DataPackResourceModel {
Q_OBJECT
public:
ModrinthDataPackModel(const BaseInstance&);
~ModrinthDataPackModel() override = default;
private:
QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
} // namespace ResourceDownload

View File

@@ -37,19 +37,18 @@
*/
#include "ModrinthResourcePages.h"
#include "ui/pages/modplatform/DataPackModel.h"
#include "ui_ResourcePage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
namespace ResourceDownload {
ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance)
{
m_model = new ModrinthModModel(instance);
m_model = new ModModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -67,7 +66,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan
ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
: ResourcePackResourcePage(dialog, instance)
{
m_model = new ModrinthResourcePackModel(instance);
m_model = new ResourcePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -85,7 +84,7 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d
ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
: TexturePackResourcePage(dialog, instance)
{
m_model = new ModrinthTexturePackModel(instance);
m_model = new TexturePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -103,7 +102,7 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial
ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
: ShaderPackResourcePage(dialog, instance)
{
m_model = new ModrinthShaderPackModel(instance);
m_model = new ShaderPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();
@@ -118,10 +117,9 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog,
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance)
: DataPackResourcePage(dialog, instance)
ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance)
{
m_model = new ModrinthDataPackModel(instance);
m_model = new DataPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase());
m_ui->packView->setModel(m_model);
addSortings();

View File

@@ -36,6 +36,7 @@
*/
#include "MinecraftSettingsWidget.h"
#include "modplatform/ModIndex.h"
#include "ui_MinecraftSettingsWidget.h"
#include <QFileDialog>

View File

@@ -81,6 +81,14 @@ class ModFilterWidget : public QTabWidget {
return versions.empty();
}
bool checkModpackFilters(const ModPlatform::IndexedVersion& v)
{
return ((!loaders || !v.loaders || loaders & v.loaders) && // loaders
(releases.empty() || // releases
std::find(releases.cbegin(), releases.cend(), v.version_type) != releases.cend()) &&
checkMcVersions({ v.mcVersion })); // gameVersion}
}
};
static std::unique_ptr<ModFilterWidget> create(MinecraftInstance* instance, bool extended);

View File

@@ -21,9 +21,6 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V
ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourcePackParse)
ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourceModel)
ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME TexturePackParse)

View File

@@ -1,46 +0,0 @@
#pragma once
#include <QJsonDocument>
#include <modplatform/ResourceAPI.h>
class SearchTask : public Task {
Q_OBJECT
public:
void executeTask() override { emitSucceeded(); }
};
class DummyResourceAPI : public ResourceAPI {
public:
static auto searchRequestResult()
{
static QByteArray json_response =
"{\"hits\":["
"{"
"\"author\":\"flowln\","
"\"description\":\"the bestest mod\","
"\"project_id\":\"something\","
"\"project_type\":\"mod\","
"\"slug\":\"bip_bop\","
"\"title\":\"AAAAAAAA\","
"\"versions\":[\"2.71\"]"
"}"
"]}";
return QJsonDocument::fromJson(json_response);
}
DummyResourceAPI() : ResourceAPI() {}
auto getSortingMethods() const -> QList<SortingMethod> override { return {}; }
Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override
{
auto task = makeShared<SearchTask>();
QObject::connect(task.get(), &Task::succeeded, [callbacks] {
auto json = searchRequestResult();
callbacks.on_succeed(json);
});
return task;
}
};

View File

@@ -1,95 +0,0 @@
#include <QAbstractItemModelTester>
#include <QTest>
#include <QTimer>
#include <Json.h>
#include <ui/pages/modplatform/ResourceModel.h>
#include "DummyResourceAPI.h"
using ResourceDownload::ResourceModel;
#define EXEC_TASK(EXEC) \
QEventLoop loop; \
\
connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \
\
QTimer expire_timer; \
expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
expire_timer.setSingleShot(true); \
expire_timer.start(4000); \
\
EXEC; \
if (model->hasActiveSearchJob()) \
loop.exec(); \
\
QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \
expire_timer.stop(); \
\
disconnect(model, nullptr, &loop, nullptr)
class ResourceModelTest;
class DummyResourceModel : public ResourceModel {
Q_OBJECT
friend class ResourceModelTest;
public:
DummyResourceModel() : ResourceModel(new DummyResourceAPI) {}
~DummyResourceModel() {}
auto metaEntryBase() const -> QString override { return ""; }
ResourceAPI::SearchArgs createSearchArguments() override { return {}; }
ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override { return {}; }
ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override { return {}; }
QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); }
void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override
{
pack.authors.append({ Json::requireString(obj, "author"), "" });
pack.description = Json::requireString(obj, "description");
pack.addonId = Json::requireString(obj, "project_id");
}
};
class ResourceModelTest : public QObject {
Q_OBJECT
private slots:
void test_abstract_item_model()
{
auto dummy = DummyResourceModel();
auto tester = QAbstractItemModelTester(&dummy);
}
void test_search()
{
auto model = new DummyResourceModel;
QVERIFY(model->m_packs.isEmpty());
EXEC_TASK(model->search());
QVERIFY(model->m_packs.size() == 1);
QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished);
auto processed_pack = model->m_packs.at(0);
auto search_json = DummyResourceAPI::searchRequestResult();
auto processed_response = model->documentToArray(search_json).first().toObject();
QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id"));
QVERIFY(processed_pack->description == Json::requireString(processed_response, "description"));
QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author"));
delete model;
}
};
QTEST_GUILESS_MAIN(ResourceModelTest)
#include "ResourceModel_test.moc"
#include "moc_DummyResourceAPI.cpp"