adopt changes from #497 remapped

This commit is contained in:
Ryan Cao 2022-05-08 15:22:50 +08:00
parent ae1aa6f63e
commit 22f5128e39
No known key found for this signature in database
GPG Key ID: 528A2C1B6656B97F
13 changed files with 261 additions and 378 deletions

View File

@ -89,6 +89,10 @@ set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can g
# MSA Client ID # MSA Client ID
set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application")
# CurseForge API Key
# CHANGE THIS IF YOU FORK THIS PROJECT!
set(Launcher_CURSEFORGE_API_KEY "$2a$10$iR1RdPDG95FWdILZbHuoMOlV4vL4eckBx7QPZR6SVZmliEb9ZQplu" CACHE STRING "CurseForge API Key")
# Bug tracker URL # Bug tracker URL
set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.")

View File

@ -90,6 +90,7 @@ Config::Config()
HELP_URL = "@Launcher_HELP_URL@"; HELP_URL = "@Launcher_HELP_URL@";
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
CURSEFORGE_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
META_URL = "@Launcher_META_URL@"; META_URL = "@Launcher_META_URL@";
BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@";

View File

@ -39,9 +39,8 @@
/** /**
* \brief The Config class holds all the build-time information passed from the build system. * \brief The Config class holds all the build-time information passed from the build system.
*/ */
class Config class Config {
{ public:
public:
Config(); Config();
QString LAUNCHER_NAME; QString LAUNCHER_NAME;
QString LAUNCHER_DISPLAYNAME; QString LAUNCHER_DISPLAYNAME;
@ -74,7 +73,6 @@ public:
/// URL for the updater's channel /// URL for the updater's channel
QString UPDATER_BASE; QString UPDATER_BASE;
/// User-Agent to use. /// User-Agent to use.
QString USER_AGENT; QString USER_AGENT;
@ -116,6 +114,11 @@ public:
*/ */
QString MSA_CLIENT_ID; QString MSA_CLIENT_ID;
/**
* Client API key for CurseForge
*/
QString CURSEFORGE_API_KEY;
/** /**
* Metadata repository URL prefix * Metadata repository URL prefix
*/ */
@ -154,4 +157,3 @@ public:
}; };
extern const Config BuildConfig; extern const Config BuildConfig;

View File

@ -1,14 +1,9 @@
#include "FileResolvingTask.h" #include "FileResolvingTask.h"
#include "Json.h" #include "Json.h"
namespace {
const char * metabase = "https://cursemeta.dries007.net";
}
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess) Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess) : m_network(network), m_toProcess(toProcess)
{ {}
}
void Flame::FileResolvingTask::executeTask() void Flame::FileResolvingTask::executeTask()
{ {
@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask()
m_dljob = new NetJob("Mod id resolver", m_network); m_dljob = new NetJob("Mod id resolver", m_network);
results.resize(m_toProcess.files.size()); results.resize(m_toProcess.files.size());
int index = 0; int index = 0;
for(auto & file: m_toProcess.files) for (auto& file : m_toProcess.files) {
{
auto projectIdStr = QString::number(file.projectId); auto projectIdStr = QString::number(file.projectId);
auto fileIdStr = QString::number(file.fileId); auto fileIdStr = QString::number(file.fileId);
QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr); QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]); auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
m_dljob->addNetAction(dl); m_dljob->addNetAction(dl);
index ++; index++;
} }
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
m_dljob->start(); m_dljob->start();
@ -34,16 +28,11 @@ void Flame::FileResolvingTask::netJobFinished()
{ {
bool failed = false; bool failed = false;
int index = 0; int index = 0;
for(auto & bytes: results) for (auto& bytes : results) {
{ auto& out = m_toProcess.files[index];
auto & out = m_toProcess.files[index]; try {
try
{
failed &= (!out.parseFromBytes(bytes)); failed &= (!out.parseFromBytes(bytes));
} } catch (const JSONValidationError& e) {
catch (const JSONValidationError &e)
{
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
qCritical() << e.cause(); qCritical() << e.cause();
qCritical() << "JSON:"; qCritical() << "JSON:";
@ -52,12 +41,9 @@ void Flame::FileResolvingTask::netJobFinished()
} }
index++; index++;
} }
if(!failed) if (!failed) {
{
emitSucceeded(); emitSucceeded();
} } else {
else
{
emitFailed(tr("Some mod ID resolving tasks failed.")); emitFailed(tr("Some mod ID resolving tasks failed."));
} }
} }

View File

@ -3,33 +3,53 @@
#include "modplatform/helpers/NetworkModAPI.h" #include "modplatform/helpers/NetworkModAPI.h"
class FlameAPI : public NetworkModAPI { class FlameAPI : public NetworkModAPI {
private:
inline auto getSortFieldInt(QString sortString) const -> int
{
return sortString == "Featured" ? 1
: sortString == "Popularity" ? 2
: sortString == "LastUpdated" ? 3
: sortString == "Name" ? 4
: sortString == "Author" ? 5
: sortString == "TotalDownloads" ? 6
: sortString == "Category" ? 7
: sortString == "GameVersion" ? 8
: 1;
}
private: private:
inline auto getModSearchURL(SearchArgs& args) const -> QString override inline auto getModSearchURL(SearchArgs& args) const -> QString override
{ {
auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
return QString( return QString(
"https://addons-ecs.forgesvc.net/api/v2/addon/search?" "https://api.curseforge.com/v1/mods/search?"
"gameId=432&" "gameId=432&"
"categoryId=0&" "classId=6&"
"sectionId=6&"
"index=%1&" "index=%1&"
"pageSize=25&" "pageSize=25&"
"searchFilter=%2&" "searchFilter=%2&"
"sort=%3&" "sortField=%3&"
"sortOrder=desc&"
"modLoaderType=%4&" "modLoaderType=%4&"
"%5") "%5")
.arg(args.offset) .arg(args.offset)
.arg(args.search) .arg(args.search)
.arg(args.sorting) .arg(getSortFieldInt(args.sorting))
.arg(getMappedModLoader(args.mod_loader)) .arg(getMappedModLoader(args.mod_loader))
.arg(gameVersionStr); .arg(gameVersionStr);
}; };
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{ {
return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId); QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader));
return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
.arg(args.addonId)
.arg(gameVersionQuery)
.arg(modLoaderQuery);
}; };
public: public:

View File

@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
pack.addonId = Json::requireInteger(obj, "id"); pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name"); pack.name = Json::requireString(obj, "name");
pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", ""); pack.description = Json::ensureString(obj, "summary", "");
bool thumbnailFound = false; QJsonObject logo = Json::requireObject(obj, "logo");
auto attachments = Json::requireArray(obj, "attachments"); pack.logoName = Json::requireString(logo, "title");
for (auto attachmentRaw : attachments) { pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false);
if (isDefault) {
thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
break;
}
}
if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
auto authors = Json::requireArray(obj, "authors"); auto authors = Json::requireArray(obj, "authors");
for (auto authorIter : authors) { for (auto authorIter : authors) {
@ -45,18 +34,22 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
{ {
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile(); auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric;
QString mcVersion = profile->getComponentVersion("net.minecraft"); QString mcVersion = profile->getComponentVersion("net.minecraft");
for (auto versionIter : arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
auto versionArray = Json::requireArray(obj, "gameVersion"); auto versionArray = Json::requireArray(obj, "gameVersions");
if (versionArray.isEmpty()) { continue; } if (versionArray.isEmpty()) {
continue;
}
ModPlatform::IndexedVersion file; ModPlatform::IndexedVersion file;
for (auto mcVer : versionArray) { for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString()); auto str = mcVer.toString();
if (str.indexOf('.') > -1)
file.mcVersion.append(mcVer.toString());
} }
file.addonId = pack.addonId; file.addonId = pack.addonId;
@ -66,28 +59,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.downloadUrl = Json::requireString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName"); file.fileName = Json::requireString(obj, "fileName");
auto modules = Json::requireArray(obj, "modules");
bool is_valid_fabric_version = false;
for (auto m : modules) {
auto fname = Json::requireString(m.toObject(), "foldername");
// FIXME: This does not work properly when a mod supports more than one mod loader, since
// FIXME: This also doesn't deal with Quilt mods at the moment
// they bundle the meta files for all of them in the same arquive, even when that version
// doesn't support the given mod loader.
if (hasFabric) {
if (fname == "fabric.mod.json") {
is_valid_fabric_version = true;
break;
}
} else
break;
// NOTE: Since we're not validating forge versions, we can just skip this loop.
}
if (hasFabric && !is_valid_fabric_version) continue;
unsortedVersions.append(file); unsortedVersions.append(file);
} }
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format // dates are in RFC 3339 format
return a.date > b.date; return a.date > b.date;

View File

@ -2,76 +2,63 @@
#include "Json.h" #include "Json.h"
void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
{ {
pack.addonId = Json::requireInteger(obj, "id"); pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name"); pack.name = Json::requireString(obj, "name");
pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", ""); pack.description = Json::ensureString(obj, "summary", "");
bool thumbnailFound = false; auto logo = Json::requireObject(obj, "logo");
auto attachments = Json::requireArray(obj, "attachments"); pack.logoName = Json::requireString(logo, "title");
for(auto attachmentRaw: attachments) { pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false);
if(isDefault) {
thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
break;
}
}
if(!thumbnailFound) {
throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
}
auto authors = Json::requireArray(obj, "authors"); auto authors = Json::requireArray(obj, "authors");
for(auto authorIter: authors) { for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter); auto author = Json::requireObject(authorIter);
Flame::ModpackAuthor packAuthor; Flame::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name"); packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url"); packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor); pack.authors.append(packAuthor);
} }
int defaultFileId = Json::requireInteger(obj, "defaultFileId"); int defaultFileId = Json::requireInteger(obj, "mainFileId");
bool found = false; bool found = false;
// check if there are some files before adding the pack // check if there are some files before adding the pack
auto files = Json::requireArray(obj, "latestFiles"); auto files = Json::requireArray(obj, "latestFiles");
for(auto fileIter: files) { for (auto fileIter : files) {
auto file = Json::requireObject(fileIter); auto file = Json::requireObject(fileIter);
int id = Json::requireInteger(file, "id"); int id = Json::requireInteger(file, "id");
// NOTE: for now, ignore everything that's not the default... // NOTE: for now, ignore everything that's not the default...
if(id != defaultFileId) { if (id != defaultFileId) {
continue; continue;
} }
auto versionArray = Json::requireArray(file, "gameVersion"); auto versionArray = Json::requireArray(file, "gameVersions");
if(versionArray.size() < 1) { if (versionArray.size() < 1) {
continue; continue;
} }
found = true; found = true;
break; break;
} }
if(!found) { if (!found) {
throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
} }
} }
void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
{ {
QVector<Flame::IndexedVersion> unsortedVersions; QVector<Flame::IndexedVersion> unsortedVersions;
for(auto versionIter: arr) { for (auto versionIter : arr) {
auto version = Json::requireObject(versionIter); auto version = Json::requireObject(versionIter);
Flame::IndexedVersion file; Flame::IndexedVersion file;
file.addonId = pack.addonId; file.addonId = pack.addonId;
file.fileId = Json::requireInteger(version, "id"); file.fileId = Json::requireInteger(version, "id");
auto versionArray = Json::requireArray(version, "gameVersion"); auto versionArray = Json::requireArray(version, "gameVersions");
if(versionArray.size() < 1) { if (versionArray.size() < 1) {
continue; continue;
} }
@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
unsortedVersions.append(file); unsortedVersions.append(file);
} }
auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; };
{
return a.fileId > b.fileId;
};
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions; pack.versions = unsortedVersions;
pack.versionsLoaded = true; pack.versionsLoaded = true;

View File

@ -1,28 +1,27 @@
#include "PackManifest.h" #include "PackManifest.h"
#include "Json.h" #include "Json.h"
static void loadFileV1(Flame::File & f, QJsonObject & file) static void loadFileV1(Flame::File& f, QJsonObject& file)
{ {
f.projectId = Json::requireInteger(file, "projectID"); f.projectId = Json::requireInteger(file, "projectID");
f.fileId = Json::requireInteger(file, "fileID"); f.fileId = Json::requireInteger(file, "fileID");
f.required = Json::ensureBoolean(file, QString("required"), true); f.required = Json::ensureBoolean(file, QString("required"), true);
} }
static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader) static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
{ {
m.id = Json::requireString(modLoader, "id"); m.id = Json::requireString(modLoader, "id");
m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
} }
static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
{ {
m.version = Json::requireString(minecraft, "version"); m.version = Json::requireString(minecraft, "version");
// extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
// intended use is likely hardcoded in the 'Flame' client, the manifest says nothing // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
for (QJsonValueRef item : arr) for (QJsonValueRef item : arr) {
{
auto obj = Json::requireObject(item); auto obj = Json::requireObject(item);
Flame::Modloader loader; Flame::Modloader loader;
loadModloaderV1(loader, obj); loadModloaderV1(loader, obj);
@ -30,16 +29,15 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
} }
} }
static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
{ {
auto mc = Json::requireObject(manifest, "minecraft"); auto mc = Json::requireObject(manifest, "minecraft");
loadMinecraftV1(m.minecraft, mc); loadMinecraftV1(m.minecraft, mc);
m.name = Json::ensureString(manifest, QString("name"), "Unnamed"); m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
m.version = Json::ensureString(manifest, QString("version"), QString()); m.version = Json::ensureString(manifest, QString("version"), QString());
m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward"); m.author = Json::ensureString(manifest, QString("author"), "Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray()); auto arr = Json::ensureArray(manifest, "files", QJsonArray());
for (QJsonValueRef item : arr) for (QJsonValueRef item : arr) {
{
auto obj = Json::requireObject(item); auto obj = Json::requireObject(item);
Flame::File file; Flame::File file;
loadFileV1(file, obj); loadFileV1(file, obj);
@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
m.overrides = Json::ensureString(manifest, "overrides", "overrides"); m.overrides = Json::ensureString(manifest, "overrides", "overrides");
} }
void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
{ {
auto doc = Json::requireDocument(filepath); auto doc = Json::requireDocument(filepath);
auto obj = Json::requireObject(doc); auto obj = Json::requireObject(doc);
m.manifestType = Json::requireString(obj, "manifestType"); m.manifestType = Json::requireString(obj, "manifestType");
if(m.manifestType != "minecraftModpack") if (m.manifestType != "minecraftModpack") {
{
throw JSONValidationError("Not a modpack manifest!"); throw JSONValidationError("Not a modpack manifest!");
} }
m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
if(m.manifestVersion != 1) if (m.manifestVersion != 1) {
{
throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
} }
loadManifestV1(m, obj); loadManifestV1(m, obj);
@ -68,59 +64,30 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
bool Flame::File::parseFromBytes(const QByteArray& bytes) bool Flame::File::parseFromBytes(const QByteArray& bytes)
{ {
auto doc = Json::requireDocument(bytes); auto doc = Json::requireDocument(bytes);
auto obj = Json::requireObject(doc); if (!doc.isObject()) {
// result code signifies true failure. throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
if(obj.contains("code"))
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
qCritical() << bytes;
return false;
} }
fileName = Json::requireString(obj, "FileNameOnDisk"); auto obj = Json::ensureObject(doc.object(), "data");
QString rawUrl = Json::requireString(obj, "DownloadURL");
fileName = Json::requireString(obj, "fileName");
QString rawUrl = Json::requireString(obj, "downloadUrl");
url = QUrl(rawUrl, QUrl::TolerantMode); url = QUrl(rawUrl, QUrl::TolerantMode);
if(!url.isValid()) if (!url.isValid()) {
{
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
} }
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional // It is also optional
QJsonObject projObj = Json::ensureObject(obj, "_Project", {}); type = File::Type::SingleFile;
if(!projObj.isEmpty())
{ if (fileName.endsWith(".zip")) {
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower(); // this is probably a resource pack
if(strType == "singlefile") targetFolder = "resourcepacks";
{ } else {
type = File::Type::SingleFile; // this is probably a mod, dunno what else could modpacks download
} targetFolder = "mods";
else if(strType == "ctoc")
{
type = File::Type::Ctoc;
}
else if(strType == "cmod2")
{
type = File::Type::Cmod2;
}
else if(strType == "mod")
{
type = File::Type::Mod;
}
else if(strType == "folder")
{
type = File::Type::Folder;
}
else if(strType == "modpack")
{
type = File::Type::Modpack;
}
else
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
type = File::Type::Unknown;
return false;
}
targetFolder = Json::ensureString(projObj, "Path", "mods");
} }
resolved = true; resolved = true;
return true; return true;
} }

View File

@ -15,27 +15,27 @@
#include "Download.h" #include "Download.h"
#include <QFileInfo>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include "FileSystem.h"
#include "ChecksumValidator.h"
#include "MetaCacheSink.h"
#include "ByteArraySink.h" #include "ByteArraySink.h"
#include "ChecksumValidator.h"
#include "FileSystem.h"
#include "MetaCacheSink.h"
#include "BuildConfig.h" #include "BuildConfig.h"
namespace Net { namespace Net {
Download::Download():NetAction() Download::Download() : NetAction()
{ {
m_status = Job_NotStarted; m_status = Job_NotStarted;
} }
Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options)
{ {
Download * dl = new Download(); Download* dl = new Download();
dl->m_url = url; dl->m_url = url;
dl->m_options = options; dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
@ -45,9 +45,9 @@ Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options
return dl; return dl;
} }
Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options) Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options)
{ {
Download * dl = new Download(); Download* dl = new Download();
dl->m_url = url; dl->m_url = url;
dl->m_options = options; dl->m_options = options;
dl->m_sink.reset(new ByteArraySink(output)); dl->m_sink.reset(new ByteArraySink(output));
@ -56,30 +56,28 @@ Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options opti
Download::Ptr Download::makeFile(QUrl url, QString path, Options options) Download::Ptr Download::makeFile(QUrl url, QString path, Options options)
{ {
Download * dl = new Download(); Download* dl = new Download();
dl->m_url = url; dl->m_url = url;
dl->m_options = options; dl->m_options = options;
dl->m_sink.reset(new FileSink(path)); dl->m_sink.reset(new FileSink(path));
return dl; return dl;
} }
void Download::addValidator(Validator * v) void Download::addValidator(Validator* v)
{ {
m_sink->addValidator(v); m_sink->addValidator(v);
} }
void Download::startImpl() void Download::startImpl()
{ {
if(m_status == Job_Aborted) if (m_status == Job_Aborted) {
{
qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); qWarning() << "Attempt to start an aborted Download:" << m_url.toString();
emit aborted(m_index_within_job); emit aborted(m_index_within_job);
return; return;
} }
QNetworkRequest request(m_url); QNetworkRequest request(m_url);
m_status = m_sink->init(request); m_status = m_sink->init(request);
switch(m_status) switch (m_status) {
{
case Job_Finished: case Job_Finished:
emit succeeded(m_index_within_job); emit succeeded(m_index_within_job);
qDebug() << "Download cache hit " << m_url.toString(); qDebug() << "Download cache hit " << m_url.toString();
@ -87,7 +85,7 @@ void Download::startImpl()
case Job_InProgress: case Job_InProgress:
qDebug() << "Downloading " << m_url.toString(); qDebug() << "Downloading " << m_url.toString();
break; break;
case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink.
case Job_NotStarted: case Job_NotStarted:
case Job_Failed: case Job_Failed:
emit failed(m_index_within_job); emit failed(m_index_within_job);
@ -97,8 +95,11 @@ void Download::startImpl()
} }
request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
if (request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8());
};
QNetworkReply *rep = m_network->get(request); QNetworkReply* rep = m_network->get(request);
m_reply.reset(rep); m_reply.reset(rep);
connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
@ -117,17 +118,12 @@ void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
void Download::downloadError(QNetworkReply::NetworkError error) void Download::downloadError(QNetworkReply::NetworkError error)
{ {
if(error == QNetworkReply::OperationCanceledError) if (error == QNetworkReply::OperationCanceledError) {
{
qCritical() << "Aborted " << m_url.toString(); qCritical() << "Aborted " << m_url.toString();
m_status = Job_Aborted; m_status = Job_Aborted;
} } else {
else if (m_options & Option::AcceptLocalFiles) {
{ if (m_sink->hasLocalData()) {
if(m_options & Option::AcceptLocalFiles)
{
if(m_sink->hasLocalData())
{
m_status = Job_Failed_Proceed; m_status = Job_Failed_Proceed;
return; return;
} }
@ -138,11 +134,10 @@ void Download::downloadError(QNetworkReply::NetworkError error)
} }
} }
void Download::sslErrors(const QList<QSslError> & errors) void Download::sslErrors(const QList<QSslError>& errors)
{ {
int i = 1; int i = 1;
for (auto error : errors) for (auto error : errors) {
{
qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate(); auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText(); qCritical() << "Certificate in question:\n" << cert.toText();
@ -153,33 +148,27 @@ void Download::sslErrors(const QList<QSslError> & errors)
bool Download::handleRedirect() bool Download::handleRedirect()
{ {
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
if(!redirect.isValid()) if (!redirect.isValid()) {
{ if (!m_reply->hasRawHeader("Location")) {
if(!m_reply->hasRawHeader("Location"))
{
// no redirect -> it's fine to continue // no redirect -> it's fine to continue
return false; return false;
} }
// there is a Location header, but it's not correct. we need to apply some workarounds... // there is a Location header, but it's not correct. we need to apply some workarounds...
QByteArray redirectBA = m_reply->rawHeader("Location"); QByteArray redirectBA = m_reply->rawHeader("Location");
if(redirectBA.size() == 0) if (redirectBA.size() == 0) {
{
// empty, yet present redirect header? WTF? // empty, yet present redirect header? WTF?
return false; return false;
} }
QString redirectStr = QString::fromUtf8(redirectBA); QString redirectStr = QString::fromUtf8(redirectBA);
if(redirectStr.startsWith("//")) if (redirectStr.startsWith("//")) {
{
/* /*
* IF the URL begins with //, we need to insert the URL scheme. * IF the URL begins with //, we need to insert the URL scheme.
* See: https://bugreports.qt.io/browse/QTBUG-41061 * See: https://bugreports.qt.io/browse/QTBUG-41061
* See: http://tools.ietf.org/html/rfc3986#section-4.2 * See: http://tools.ietf.org/html/rfc3986#section-4.2
*/ */
redirectStr = m_reply->url().scheme() + ":" + redirectStr; redirectStr = m_reply->url().scheme() + ":" + redirectStr;
} } else if (redirectStr.startsWith("/")) {
else if(redirectStr.startsWith("/"))
{
/* /*
* IF the URL begins with /, we need to process it as a relative URL * IF the URL begins with /, we need to process it as a relative URL
*/ */
@ -193,16 +182,13 @@ bool Download::handleRedirect()
* FIXME: report Qt bug for this * FIXME: report Qt bug for this
*/ */
redirect = QUrl(redirectStr, QUrl::TolerantMode); redirect = QUrl(redirectStr, QUrl::TolerantMode);
if(!redirect.isValid()) if (!redirect.isValid()) {
{
qWarning() << "Failed to parse redirect URL:" << redirectStr; qWarning() << "Failed to parse redirect URL:" << redirectStr;
downloadError(QNetworkReply::ProtocolFailure); downloadError(QNetworkReply::ProtocolFailure);
return false; return false;
} }
qDebug() << "Fixed location header:" << redirect; qDebug() << "Fixed location header:" << redirect;
} } else {
else
{
qDebug() << "Location header:" << redirect; qDebug() << "Location header:" << redirect;
} }
@ -212,35 +198,28 @@ bool Download::handleRedirect()
return true; return true;
} }
void Download::downloadFinished() void Download::downloadFinished()
{ {
// handle HTTP redirection first // handle HTTP redirection first
if(handleRedirect()) if (handleRedirect()) {
{
qDebug() << "Download redirected:" << m_url.toString(); qDebug() << "Download redirected:" << m_url.toString();
return; return;
} }
// if the download failed before this point ... // if the download failed before this point ...
if (m_status == Job_Failed_Proceed) if (m_status == Job_Failed_Proceed) {
{
qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString();
m_sink->abort(); m_sink->abort();
m_reply.reset(); m_reply.reset();
emit succeeded(m_index_within_job); emit succeeded(m_index_within_job);
return; return;
} } else if (m_status == Job_Failed) {
else if (m_status == Job_Failed)
{
qDebug() << "Download failed in previous step:" << m_url.toString(); qDebug() << "Download failed in previous step:" << m_url.toString();
m_sink->abort(); m_sink->abort();
m_reply.reset(); m_reply.reset();
emit failed(m_index_within_job); emit failed(m_index_within_job);
return; return;
} } else if (m_status == Job_Aborted) {
else if(m_status == Job_Aborted)
{
qDebug() << "Download aborted in previous step:" << m_url.toString(); qDebug() << "Download aborted in previous step:" << m_url.toString();
m_sink->abort(); m_sink->abort();
m_reply.reset(); m_reply.reset();
@ -250,16 +229,14 @@ void Download::downloadFinished()
// make sure we got all the remaining data, if any // make sure we got all the remaining data, if any
auto data = m_reply->readAll(); auto data = m_reply->readAll();
if(data.size()) if (data.size()) {
{
qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path;
m_status = m_sink->write(data); m_status = m_sink->write(data);
} }
// otherwise, finalize the whole graph // otherwise, finalize the whole graph
m_status = m_sink->finalize(*m_reply.get()); m_status = m_sink->finalize(*m_reply.get());
if (m_status != Job_Finished) if (m_status != Job_Finished) {
{
qDebug() << "Download failed to finalize:" << m_url.toString(); qDebug() << "Download failed to finalize:" << m_url.toString();
m_sink->abort(); m_sink->abort();
m_reply.reset(); m_reply.reset();
@ -273,32 +250,25 @@ void Download::downloadFinished()
void Download::downloadReadyRead() void Download::downloadReadyRead()
{ {
if(m_status == Job_InProgress) if (m_status == Job_InProgress) {
{
auto data = m_reply->readAll(); auto data = m_reply->readAll();
m_status = m_sink->write(data); m_status = m_sink->write(data);
if(m_status == Job_Failed) if (m_status == Job_Failed) {
{
qCritical() << "Failed to process response chunk for " << m_target_path; qCritical() << "Failed to process response chunk for " << m_target_path;
} }
// qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
} } else {
else
{
qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status;
} }
} }
} } // namespace Net
bool Net::Download::abort() bool Net::Download::abort()
{ {
if(m_reply) if (m_reply) {
{
m_reply->abort(); m_reply->abort();
} } else {
else
{
m_status = Job_Aborted; m_status = Job_Aborted;
} }
return true; return true;

View File

@ -21,7 +21,8 @@ auto ListModel::debugName() const -> QString
void ListModel::fetchMore(const QModelIndex& parent) void ListModel::fetchMore(const QModelIndex& parent)
{ {
if (parent.isValid()) return; if (parent.isValid())
return;
if (nextSearchOffset == 0) { if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong..."; qWarning() << "fetchMore with 0 offset is wrong...";
return; return;
@ -32,7 +33,9 @@ void ListModel::fetchMore(const QModelIndex& parent)
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
{ {
int pos = index.row(); int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
ModPlatform::IndexedPack pack = modpacks.at(pos); ModPlatform::IndexedPack pack = modpacks.at(pos);
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
@ -46,7 +49,9 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
} }
return pack.description; return pack.description;
} else if (role == Qt::DecorationRole) { } else if (role == 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"); QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon; return icon;
@ -63,16 +68,15 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
{ {
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->getVersions(this, m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
{ current.addonId.toString(), getMineVersions(), profile->getModLoader() });
} }
void ListModel::performPaginatedSearch() void ListModel::performPaginatedSearch()
{ {
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->searchMods(this, m_parent->apiProvider()->searchMods(
{ nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
} }
void ListModel::refresh() void ListModel::refresh()
@ -93,11 +97,9 @@ void ListModel::refresh()
void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
{ {
if (currentSearchTerm == term if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
&& currentSearchTerm.isNull() == term.isNull() return;
&& currentSort == sort }
&& !filter_changed)
{ return; }
currentSearchTerm = term; currentSearchTerm = term;
currentSort = sort; currentSort = sort;
@ -118,7 +120,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac
void ListModel::requestLogo(QString logo, QString url) void ListModel::requestLogo(QString logo, QString url)
{ {
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
return;
}
MetaEntryPtr entry = MetaEntryPtr entry =
APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
@ -129,7 +133,9 @@ void ListModel::requestLogo(QString logo, QString url)
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater(); job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath)); emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
}
}); });
QObject::connect(job, &NetJob::failed, this, [this, logo, job] { QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
@ -148,7 +154,9 @@ void ListModel::logoLoaded(QString logo, QIcon out)
m_loadingLogos.removeAll(logo); m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out); m_logoMap.insert(logo, out);
for (int i = 0; i < modpacks.size(); i++) { for (int i = 0; i < modpacks.size(); i++) {
if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } if (modpacks[i].logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
} }
} }
@ -199,7 +207,9 @@ void ListModel::searchRequestFailed(QString reason)
// 409 Gone, notify user to update // 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"), QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself //: %1 refers to the launcher itself
QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); QString("%1 %2")
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
} }
jobPtr.reset(); jobPtr.reset();
@ -218,9 +228,12 @@ void ListModel::searchRequestFailed(QString reason)
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
{ {
auto& current = m_parent->getCurrent(); auto& current = m_parent->getCurrent();
if (addonId != current.addonId) { return; } if (addonId != current.addonId) {
return;
}
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
QJsonArray arr = doc.array();
try { try {
loadIndexedPackVersions(current, arr); loadIndexedPackVersions(current, arr);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {

View File

@ -1,5 +1,5 @@
#include "FlameModModel.h" #include "FlameModModel.h"
#include "Json.h"
#include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/FlameModIndex.h"
namespace FlameMod { namespace FlameMod {
@ -19,7 +19,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray&
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{ {
return obj.array(); return Json::ensureArray(obj.object(), "data");
} }
} // namespace FlameMod } // namespace FlameMod

View File

@ -1,6 +1,6 @@
#include "FlameModel.h" #include "FlameModel.h"
#include "Application.h"
#include <Json.h> #include <Json.h>
#include "Application.h"
#include <MMCStrings.h> #include <MMCStrings.h>
#include <Version.h> #include <Version.h>
@ -9,61 +9,46 @@
namespace Flame { namespace Flame {
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
{
}
ListModel::~ListModel() ListModel::~ListModel() {}
{
}
int ListModel::rowCount(const QModelIndex &parent) const int ListModel::rowCount(const QModelIndex& parent) const
{ {
return modpacks.size(); return modpacks.size();
} }
int ListModel::columnCount(const QModelIndex &parent) const int ListModel::columnCount(const QModelIndex& parent) const
{ {
return 1; return 1;
} }
QVariant ListModel::data(const QModelIndex &index, int role) const QVariant ListModel::data(const QModelIndex& index, int role) const
{ {
int pos = index.row(); int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid()) if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
{
return QString("INVALID INDEX %1").arg(pos); return QString("INVALID INDEX %1").arg(pos);
} }
IndexedPack pack = modpacks.at(pos); IndexedPack pack = modpacks.at(pos);
if(role == Qt::DisplayRole) if (role == Qt::DisplayRole) {
{
return pack.name; return pack.name;
} } else if (role == Qt::ToolTipRole) {
else if (role == Qt::ToolTipRole) if (pack.description.length() > 100) {
{ // some magic to prevent to long tooltips and replace html linebreaks
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("..."); edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
return edit; return edit;
} }
return pack.description; return pack.description;
} } else if (role == Qt::DecorationRole) {
else if(role == Qt::DecorationRole) if (m_logoMap.contains(pack.logoName)) {
{
if(m_logoMap.contains(pack.logoName))
{
return (m_logoMap.value(pack.logoName)); return (m_logoMap.value(pack.logoName));
} }
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon; return icon;
} } else if (role == Qt::UserRole) {
else if(role == Qt::UserRole)
{
QVariant v; QVariant v;
v.setValue(pack); v.setValue(pack);
return v; return v;
@ -76,9 +61,9 @@ void ListModel::logoLoaded(QString logo, QIcon out)
{ {
m_loadingLogos.removeAll(logo); m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out); m_logoMap.insert(logo, out);
for(int i = 0; i < modpacks.size(); i++) { for (int i = 0; i < modpacks.size(); i++) {
if(modpacks[i].logoName == logo) { if (modpacks[i].logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
} }
} }
} }
@ -91,8 +76,7 @@ void ListModel::logoFailed(QString logo)
void ListModel::requestLogo(QString logo, QString url) void ListModel::requestLogo(QString logo, QString url)
{ {
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
{
return; return;
} }
@ -101,18 +85,15 @@ void ListModel::requestLogo(QString logo, QString url)
job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
{
job->deleteLater(); job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath)); emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo)) if (waitingCallbacks.contains(logo)) {
{
waitingCallbacks.value(logo)(fullPath); waitingCallbacks.value(logo)(fullPath);
} }
}); });
QObject::connect(job, &NetJob::failed, this, [this, logo, job] QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
{
job->deleteLater(); job->deleteLater();
emit logoFailed(logo); emit logoFailed(logo);
}); });
@ -122,19 +103,16 @@ void ListModel::requestLogo(QString logo, QString url)
m_loadingLogos.append(logo); m_loadingLogos.append(logo);
} }
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
{ {
if(m_logoMap.contains(logo)) if (m_logoMap.contains(logo)) {
{
callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
} } else {
else
{
requestLogo(logo, logoUrl); requestLogo(logo, logoUrl);
} }
} }
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
{ {
return QAbstractListModel::flags(index); return QAbstractListModel::flags(index);
} }
@ -148,7 +126,7 @@ void ListModel::fetchMore(const QModelIndex& parent)
{ {
if (parent.isValid()) if (parent.isValid())
return; return;
if(nextSearchOffset == 0) { if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong..."; qWarning() << "fetchMore with 0 offset is wrong...";
return; return;
} }
@ -157,17 +135,20 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch() void ListModel::performPaginatedSearch()
{ {
NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network()); NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network());
auto searchUrl = QString( auto searchUrl = QString(
"https://addons-ecs.forgesvc.net/api/v2/addon/search?" "https://api.curseforge.com/v1/mods/search?"
"categoryId=0&" "gameId=432&"
"gameId=432&" "classId=4471&"
"index=%1&" "index=%1&"
"pageSize=25&" "pageSize=25&"
"searchFilter=%2&" "searchFilter=%2&"
"sectionId=4471&" "sortField=%3&"
"sort=%3" "sortOrder=desc")
).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); .arg(nextSearchOffset)
.arg(currentSearchTerm)
.arg(currentSort + 1);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob; jobPtr = netJob;
jobPtr->start(); jobPtr->start();
@ -177,17 +158,16 @@ void ListModel::performPaginatedSearch()
void ListModel::searchWithTerm(const QString& term, int sort) void ListModel::searchWithTerm(const QString& term, int sort)
{ {
if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
return; return;
} }
currentSearchTerm = term; currentSearchTerm = term;
currentSort = sort; currentSort = sort;
if(jobPtr) { if (jobPtr) {
jobPtr->abort(); jobPtr->abort();
searchState = ResetRequested; searchState = ResetRequested;
return; return;
} } else {
else {
beginResetModel(); beginResetModel();
modpacks.clear(); modpacks.clear();
endResetModel(); endResetModel();
@ -203,30 +183,28 @@ void Flame::ListModel::searchRequestFinished()
QJsonParseError parse_error; QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) { if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << response; qWarning() << response;
return; return;
} }
QList<Flame::IndexedPack> newList; QList<Flame::IndexedPack> newList;
auto packs = doc.array(); auto packs = Json::ensureArray(doc.object(), "data");
for(auto packRaw : packs) { for (auto packRaw : packs) {
auto packObj = packRaw.toObject(); auto packObj = packRaw.toObject();
Flame::IndexedPack pack; Flame::IndexedPack pack;
try try {
{
Flame::loadIndexedPack(pack, packObj); Flame::loadIndexedPack(pack, packObj);
newList.append(pack); newList.append(pack);
} } catch (const JSONValidationError& e) {
catch(const JSONValidationError &e)
{
qWarning() << "Error while loading pack from CurseForge: " << e.cause(); qWarning() << "Error while loading pack from CurseForge: " << e.cause();
continue; continue;
} }
} }
if(packs.size() < 25) { if (packs.size() < 25) {
searchState = Finished; searchState = Finished;
} else { } else {
nextSearchOffset += 25; nextSearchOffset += 25;
@ -241,7 +219,7 @@ void Flame::ListModel::searchRequestFailed(QString reason)
{ {
jobPtr.reset(); jobPtr.reset();
if(searchState == ResetRequested) { if (searchState == ResetRequested) {
beginResetModel(); beginResetModel();
modpacks.clear(); modpacks.clear();
endResetModel(); endResetModel();
@ -253,5 +231,4 @@ void Flame::ListModel::searchRequestFailed(QString reason)
} }
} }
} } // namespace Flame

View File

@ -39,13 +39,12 @@
#include <QKeyEvent> #include <QKeyEvent>
#include "Application.h" #include "Application.h"
#include "FlameModel.h"
#include "InstanceImportTask.h"
#include "Json.h" #include "Json.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
#include "InstanceImportTask.h"
#include "FlameModel.h"
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
: QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
{ {
ui->setupUi(this); ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
@ -112,10 +111,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
{ {
ui->versionSelectionBox->clear(); ui->versionSelectionBox->clear();
if(!first.isValid()) if (!first.isValid()) {
{ if (isOpened) {
if(isOpened)
{
dialog->setSuggestedPack(); dialog->setSuggestedPack();
} }
return; return;
@ -130,14 +127,14 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
else else
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
if (!current.authors.empty()) { if (!current.authors.empty()) {
auto authorToStr = [](Flame::ModpackAuthor & author) { auto authorToStr = [](Flame::ModpackAuthor& author) {
if(author.url.isEmpty()) { if (author.url.isEmpty()) {
return author.name; return author.name;
} }
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
}; };
QStringList authorStrs; QStringList authorStrs;
for(auto & author: current.authors) { for (auto& author : current.authors) {
authorStrs.push_back(authorToStr(author)); authorStrs.push_back(authorToStr(author));
} }
text += "<br>" + tr(" by ") + authorStrs.join(", "); text += "<br>" + tr(" by ") + authorStrs.join(", ");
@ -146,53 +143,46 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
ui->packDescription->setHtml(text + current.description); ui->packDescription->setHtml(text + current.description);
if (current.versionsLoaded == false) if (current.versionsLoaded == false) {
{
qDebug() << "Loading flame modpack versions"; qDebug() << "Loading flame modpack versions";
auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = new QByteArray(); auto response = new QByteArray();
int addonId = current.addonId; int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
{ if (addonId != current.addonId) {
if(addonId != current.addonId){ return; // wrong request
return; //wrong request
} }
QJsonParseError parse_error; QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) { if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response; qWarning() << *response;
return; return;
} }
QJsonArray arr = doc.array(); auto arr = Json::ensureArray(doc.object(), "data");
try try {
{
Flame::loadIndexedPackVersions(current, arr); Flame::loadIndexedPackVersions(current, arr);
} } catch (const JSONValidationError& e) {
catch(const JSONValidationError &e)
{
qDebug() << *response; qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause(); qWarning() << "Error while reading flame modpack version: " << e.cause();
} }
for(auto version : current.versions) { for (auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
} }
suggestCurrent(); suggestCurrent();
}); });
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
{
netJob->deleteLater(); netJob->deleteLater();
delete response; delete response;
}); });
netJob->start(); netJob->start();
} } else {
else for (auto version : current.versions) {
{
for(auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
} }
@ -202,13 +192,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
void FlamePage::suggestCurrent() void FlamePage::suggestCurrent()
{ {
if(!isOpened) if (!isOpened) {
{
return; return;
} }
if (selectedVersion.isEmpty()) if (selectedVersion.isEmpty()) {
{
dialog->setSuggestedPack(); dialog->setSuggestedPack();
return; return;
} }
@ -216,16 +204,13 @@ void FlamePage::suggestCurrent()
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
QString editedLogoName; QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) listModel->getLogo(current.logoName, current.logoUrl,
{ [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
} }
void FlamePage::onVersionSelectionChanged(QString data) void FlamePage::onVersionSelectionChanged(QString data)
{ {
if(data.isNull() || data.isEmpty()) if (data.isNull() || data.isEmpty()) {
{
selectedVersion = ""; selectedVersion = "";
return; return;
} }