2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -14,6 +14,8 @@ CMakeLists.txt.user.*
 | 
			
		||||
/.project
 | 
			
		||||
/.settings
 | 
			
		||||
/.idea
 | 
			
		||||
/.vscode
 | 
			
		||||
.clang-format
 | 
			
		||||
cmake-build-*/
 | 
			
		||||
Debug
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL
 | 
			
		||||
######## Set version numbers ########
 | 
			
		||||
set(Launcher_VERSION_MAJOR    1)
 | 
			
		||||
set(Launcher_VERSION_MINOR    2)
 | 
			
		||||
set(Launcher_VERSION_HOTFIX   1)
 | 
			
		||||
set(Launcher_VERSION_HOTFIX   2)
 | 
			
		||||
 | 
			
		||||
# Build number
 | 
			
		||||
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
 | 
			
		||||
@@ -89,6 +89,10 @@ set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can g
 | 
			
		||||
# 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")
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,7 @@ Config::Config()
 | 
			
		||||
    HELP_URL = "@Launcher_HELP_URL@";
 | 
			
		||||
    IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
 | 
			
		||||
    MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
 | 
			
		||||
    CURSEFORGE_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
 | 
			
		||||
    META_URL = "@Launcher_META_URL@";
 | 
			
		||||
 | 
			
		||||
    BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@";
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,8 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \brief The Config class holds all the build-time information passed from the build system.
 | 
			
		||||
 */
 | 
			
		||||
class Config
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
class Config {
 | 
			
		||||
   public:
 | 
			
		||||
    Config();
 | 
			
		||||
    QString LAUNCHER_NAME;
 | 
			
		||||
    QString LAUNCHER_DISPLAYNAME;
 | 
			
		||||
@@ -75,7 +74,6 @@ public:
 | 
			
		||||
    /// URL for the updater's channel
 | 
			
		||||
    QString UPDATER_BASE;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// User-Agent to use.
 | 
			
		||||
    QString USER_AGENT;
 | 
			
		||||
 | 
			
		||||
@@ -117,6 +115,11 @@ public:
 | 
			
		||||
     */
 | 
			
		||||
    QString MSA_CLIENT_ID;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Client API key for CurseForge
 | 
			
		||||
     */
 | 
			
		||||
    QString CURSEFORGE_API_KEY;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Metadata repository URL prefix
 | 
			
		||||
     */
 | 
			
		||||
@@ -156,4 +159,3 @@ public:
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern const Config BuildConfig;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,9 @@
 | 
			
		||||
#include "FileResolvingTask.h"
 | 
			
		||||
#include "Json.h"
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
    const char * metabase = "https://cursemeta.dries007.net";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
 | 
			
		||||
    : m_network(network), m_toProcess(toProcess)
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
{}
 | 
			
		||||
 | 
			
		||||
void Flame::FileResolvingTask::executeTask()
 | 
			
		||||
{
 | 
			
		||||
@@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask()
 | 
			
		||||
    m_dljob = new NetJob("Mod id resolver", m_network);
 | 
			
		||||
    results.resize(m_toProcess.files.size());
 | 
			
		||||
    int index = 0;
 | 
			
		||||
    for(auto & file: m_toProcess.files)
 | 
			
		||||
    {
 | 
			
		||||
    for (auto& file : m_toProcess.files) {
 | 
			
		||||
        auto projectIdStr = QString::number(file.projectId);
 | 
			
		||||
        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]);
 | 
			
		||||
        m_dljob->addNetAction(dl);
 | 
			
		||||
        index ++;
 | 
			
		||||
        index++;
 | 
			
		||||
    }
 | 
			
		||||
    connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
 | 
			
		||||
    m_dljob->start();
 | 
			
		||||
@@ -34,16 +28,11 @@ void Flame::FileResolvingTask::netJobFinished()
 | 
			
		||||
{
 | 
			
		||||
    bool failed = false;
 | 
			
		||||
    int index = 0;
 | 
			
		||||
    for(auto & bytes: results)
 | 
			
		||||
    {
 | 
			
		||||
        auto & out = m_toProcess.files[index];
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
    for (auto& bytes : results) {
 | 
			
		||||
        auto& out = m_toProcess.files[index];
 | 
			
		||||
        try {
 | 
			
		||||
            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() << e.cause();
 | 
			
		||||
            qCritical() << "JSON:";
 | 
			
		||||
@@ -52,12 +41,9 @@ void Flame::FileResolvingTask::netJobFinished()
 | 
			
		||||
        }
 | 
			
		||||
        index++;
 | 
			
		||||
    }
 | 
			
		||||
    if(!failed)
 | 
			
		||||
    {
 | 
			
		||||
    if (!failed) {
 | 
			
		||||
        emitSucceeded();
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
    } else {
 | 
			
		||||
        emitFailed(tr("Some mod ID resolving tasks failed."));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,33 +3,53 @@
 | 
			
		||||
#include "modplatform/helpers/NetworkModAPI.h"
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
    inline auto getModSearchURL(SearchArgs& args) const -> QString override
 | 
			
		||||
    {
 | 
			
		||||
        auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
 | 
			
		||||
 | 
			
		||||
        return QString(
 | 
			
		||||
                   "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
 | 
			
		||||
                   "https://api.curseforge.com/v1/mods/search?"
 | 
			
		||||
                   "gameId=432&"
 | 
			
		||||
                   "categoryId=0&"
 | 
			
		||||
                   "sectionId=6&"
 | 
			
		||||
                   "classId=6&"
 | 
			
		||||
 | 
			
		||||
                   "index=%1&"
 | 
			
		||||
                   "pageSize=25&"
 | 
			
		||||
                   "searchFilter=%2&"
 | 
			
		||||
                   "sort=%3&"
 | 
			
		||||
                   "sortField=%3&"
 | 
			
		||||
                   "sortOrder=desc&"
 | 
			
		||||
                   "modLoaderType=%4&"
 | 
			
		||||
                   "%5")
 | 
			
		||||
            .arg(args.offset)
 | 
			
		||||
            .arg(args.search)
 | 
			
		||||
            .arg(args.sorting)
 | 
			
		||||
            .arg(getSortFieldInt(args.sorting))
 | 
			
		||||
            .arg(getMappedModLoader(args.mod_loader))
 | 
			
		||||
            .arg(gameVersionStr);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
 | 
			
		||||
{
 | 
			
		||||
    pack.addonId = Json::requireInteger(obj, "id");
 | 
			
		||||
    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", "");
 | 
			
		||||
 | 
			
		||||
    bool thumbnailFound = false;
 | 
			
		||||
    auto attachments = Json::requireArray(obj, "attachments");
 | 
			
		||||
    for (auto attachmentRaw : attachments) {
 | 
			
		||||
        auto attachmentObj = Json::requireObject(attachmentRaw);
 | 
			
		||||
        bool isDefault = attachmentObj.value("isDefault").toBool(false);
 | 
			
		||||
        if (isDefault) {
 | 
			
		||||
            thumbnailFound = true;
 | 
			
		||||
            pack.logoName = Json::requireString(attachmentObj, "title");
 | 
			
		||||
            pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
 | 
			
		||||
    QJsonObject logo = Json::requireObject(obj, "logo");
 | 
			
		||||
    pack.logoName = Json::requireString(logo, "title");
 | 
			
		||||
    pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
 | 
			
		||||
 | 
			
		||||
    auto authors = Json::requireArray(obj, "authors");
 | 
			
		||||
    for (auto authorIter : authors) {
 | 
			
		||||
@@ -45,18 +34,22 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
 | 
			
		||||
{
 | 
			
		||||
    QVector<ModPlatform::IndexedVersion> unsortedVersions;
 | 
			
		||||
    auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
 | 
			
		||||
    bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric;
 | 
			
		||||
    QString mcVersion = profile->getComponentVersion("net.minecraft");
 | 
			
		||||
 | 
			
		||||
    for (auto versionIter : arr) {
 | 
			
		||||
        auto obj = versionIter.toObject();
 | 
			
		||||
 | 
			
		||||
        auto versionArray = Json::requireArray(obj, "gameVersion");
 | 
			
		||||
        if (versionArray.isEmpty()) { continue; }
 | 
			
		||||
        auto versionArray = Json::requireArray(obj, "gameVersions");
 | 
			
		||||
        if (versionArray.isEmpty()) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ModPlatform::IndexedVersion file;
 | 
			
		||||
        for (auto mcVer : versionArray) {
 | 
			
		||||
            file.mcVersion.append(mcVer.toString());
 | 
			
		||||
            auto str = mcVer.toString();
 | 
			
		||||
 | 
			
		||||
            if (str.contains('.'))
 | 
			
		||||
                file.mcVersion.append(str);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        file.addonId = pack.addonId;
 | 
			
		||||
@@ -66,28 +59,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
 | 
			
		||||
        file.downloadUrl = Json::requireString(obj, "downloadUrl");
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
 | 
			
		||||
        // dates are in RFC 3339 format
 | 
			
		||||
        return a.date > b.date;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,76 +2,63 @@
 | 
			
		||||
 | 
			
		||||
#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.name = Json::requireString(obj, "name");
 | 
			
		||||
    pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
 | 
			
		||||
    pack.description = Json::ensureString(obj, "summary", "");
 | 
			
		||||
 | 
			
		||||
    bool thumbnailFound = false;
 | 
			
		||||
    auto attachments = Json::requireArray(obj, "attachments");
 | 
			
		||||
    for(auto attachmentRaw: attachments) {
 | 
			
		||||
        auto attachmentObj = Json::requireObject(attachmentRaw);
 | 
			
		||||
        bool isDefault = attachmentObj.value("isDefault").toBool(false);
 | 
			
		||||
        if(isDefault) {
 | 
			
		||||
            thumbnailFound = true;
 | 
			
		||||
            pack.logoName = Json::requireString(attachmentObj, "title");
 | 
			
		||||
            pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(!thumbnailFound) {
 | 
			
		||||
        throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
 | 
			
		||||
    }
 | 
			
		||||
    auto logo = Json::requireObject(obj, "logo");
 | 
			
		||||
    pack.logoName = Json::requireString(logo, "title");
 | 
			
		||||
    pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
 | 
			
		||||
 | 
			
		||||
    auto authors = Json::requireArray(obj, "authors");
 | 
			
		||||
    for(auto authorIter: authors) {
 | 
			
		||||
    for (auto authorIter : authors) {
 | 
			
		||||
        auto author = Json::requireObject(authorIter);
 | 
			
		||||
        Flame::ModpackAuthor packAuthor;
 | 
			
		||||
        packAuthor.name = Json::requireString(author, "name");
 | 
			
		||||
        packAuthor.url = Json::requireString(author, "url");
 | 
			
		||||
        pack.authors.append(packAuthor);
 | 
			
		||||
    }
 | 
			
		||||
    int defaultFileId = Json::requireInteger(obj, "defaultFileId");
 | 
			
		||||
    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) {
 | 
			
		||||
    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) {
 | 
			
		||||
        if (id != defaultFileId) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        auto versionArray = Json::requireArray(file, "gameVersion");
 | 
			
		||||
        if(versionArray.size() < 1) {
 | 
			
		||||
        auto versionArray = Json::requireArray(file, "gameVersions");
 | 
			
		||||
        if (versionArray.size() < 1) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        found = true;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    if(!found) {
 | 
			
		||||
    if (!found) {
 | 
			
		||||
        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;
 | 
			
		||||
    for(auto versionIter: arr) {
 | 
			
		||||
    for (auto versionIter : arr) {
 | 
			
		||||
        auto version = Json::requireObject(versionIter);
 | 
			
		||||
        Flame::IndexedVersion file; 
 | 
			
		||||
        Flame::IndexedVersion file;
 | 
			
		||||
 | 
			
		||||
        file.addonId = pack.addonId;
 | 
			
		||||
        file.fileId = Json::requireInteger(version, "id");
 | 
			
		||||
        auto versionArray = Json::requireArray(version, "gameVersion");
 | 
			
		||||
        if(versionArray.size() < 1) {
 | 
			
		||||
        auto versionArray = Json::requireArray(version, "gameVersions");
 | 
			
		||||
        if (versionArray.size() < 1) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
 | 
			
		||||
        unsortedVersions.append(file);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
 | 
			
		||||
    {
 | 
			
		||||
        return a.fileId > b.fileId;
 | 
			
		||||
    };
 | 
			
		||||
    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;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,27 @@
 | 
			
		||||
#include "PackManifest.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.fileId = Json::requireInteger(file, "fileID");
 | 
			
		||||
    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.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");
 | 
			
		||||
    // 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
 | 
			
		||||
    m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
 | 
			
		||||
    auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
 | 
			
		||||
    for (QJsonValueRef item : arr)
 | 
			
		||||
    {
 | 
			
		||||
    for (QJsonValueRef item : arr) {
 | 
			
		||||
        auto obj = Json::requireObject(item);
 | 
			
		||||
        Flame::Modloader loader;
 | 
			
		||||
        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");
 | 
			
		||||
    loadMinecraftV1(m.minecraft, mc);
 | 
			
		||||
    m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
 | 
			
		||||
    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());
 | 
			
		||||
    for (QJsonValueRef item : arr)
 | 
			
		||||
    {
 | 
			
		||||
    for (QJsonValueRef item : arr) {
 | 
			
		||||
        auto obj = Json::requireObject(item);
 | 
			
		||||
        Flame::File file;
 | 
			
		||||
        loadFileV1(file, obj);
 | 
			
		||||
@@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
 | 
			
		||||
    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 obj = Json::requireObject(doc);
 | 
			
		||||
    m.manifestType = Json::requireString(obj, "manifestType");
 | 
			
		||||
    if(m.manifestType != "minecraftModpack")
 | 
			
		||||
    {
 | 
			
		||||
    if (m.manifestType != "minecraftModpack") {
 | 
			
		||||
        throw JSONValidationError("Not a modpack manifest!");
 | 
			
		||||
    }
 | 
			
		||||
    m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
 | 
			
		||||
    if(m.manifestVersion != 1)
 | 
			
		||||
    {
 | 
			
		||||
    if (m.manifestVersion != 1) {
 | 
			
		||||
        throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
 | 
			
		||||
    }
 | 
			
		||||
    loadManifestV1(m, obj);
 | 
			
		||||
@@ -68,59 +64,30 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
 | 
			
		||||
bool Flame::File::parseFromBytes(const QByteArray& bytes)
 | 
			
		||||
{
 | 
			
		||||
    auto doc = Json::requireDocument(bytes);
 | 
			
		||||
    auto obj = Json::requireObject(doc);
 | 
			
		||||
    // result code signifies true failure.
 | 
			
		||||
    if(obj.contains("code"))
 | 
			
		||||
    {
 | 
			
		||||
        qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
 | 
			
		||||
        qCritical() << bytes;
 | 
			
		||||
        return false;
 | 
			
		||||
    if (!doc.isObject()) {
 | 
			
		||||
        throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
 | 
			
		||||
    }
 | 
			
		||||
    fileName = Json::requireString(obj, "FileNameOnDisk");
 | 
			
		||||
    QString rawUrl = Json::requireString(obj, "DownloadURL");
 | 
			
		||||
    auto obj = Json::ensureObject(doc.object(), "data");
 | 
			
		||||
 | 
			
		||||
    fileName = Json::requireString(obj, "fileName");
 | 
			
		||||
 | 
			
		||||
    QString rawUrl = Json::requireString(obj, "downloadUrl");
 | 
			
		||||
    url = QUrl(rawUrl, QUrl::TolerantMode);
 | 
			
		||||
    if(!url.isValid())
 | 
			
		||||
    {
 | 
			
		||||
    if (!url.isValid()) {
 | 
			
		||||
        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
 | 
			
		||||
    // It is also optional
 | 
			
		||||
    QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
 | 
			
		||||
    if(!projObj.isEmpty())
 | 
			
		||||
    {
 | 
			
		||||
        QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
 | 
			
		||||
        if(strType == "singlefile")
 | 
			
		||||
        {
 | 
			
		||||
            type = File::Type::SingleFile;
 | 
			
		||||
        }
 | 
			
		||||
        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");
 | 
			
		||||
    type = File::Type::SingleFile;
 | 
			
		||||
 | 
			
		||||
    if (fileName.endsWith(".zip")) {
 | 
			
		||||
        // this is probably a resource pack
 | 
			
		||||
        targetFolder = "resourcepacks";
 | 
			
		||||
    } else {
 | 
			
		||||
        // this is probably a mod, dunno what else could modpacks download
 | 
			
		||||
        targetFolder = "mods";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resolved = true;
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,27 +15,27 @@
 | 
			
		||||
 | 
			
		||||
#include "Download.h"
 | 
			
		||||
 | 
			
		||||
#include <QFileInfo>
 | 
			
		||||
#include <QDateTime>
 | 
			
		||||
#include <QDebug>
 | 
			
		||||
#include <QFileInfo>
 | 
			
		||||
 | 
			
		||||
#include "FileSystem.h"
 | 
			
		||||
#include "ChecksumValidator.h"
 | 
			
		||||
#include "MetaCacheSink.h"
 | 
			
		||||
#include "ByteArraySink.h"
 | 
			
		||||
#include "ChecksumValidator.h"
 | 
			
		||||
#include "FileSystem.h"
 | 
			
		||||
#include "MetaCacheSink.h"
 | 
			
		||||
 | 
			
		||||
#include "BuildConfig.h"
 | 
			
		||||
 | 
			
		||||
namespace Net {
 | 
			
		||||
 | 
			
		||||
Download::Download():NetAction()
 | 
			
		||||
Download::Download() : NetAction()
 | 
			
		||||
{
 | 
			
		||||
    m_status = Job_NotStarted;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options)
 | 
			
		||||
{
 | 
			
		||||
    Download * dl = new Download();
 | 
			
		||||
    Download* dl = new Download();
 | 
			
		||||
    dl->m_url = url;
 | 
			
		||||
    dl->m_options = options;
 | 
			
		||||
    auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
 | 
			
		||||
@@ -45,9 +45,9 @@ Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options
 | 
			
		||||
    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_options = options;
 | 
			
		||||
    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 * dl = new Download();
 | 
			
		||||
    Download* dl = new Download();
 | 
			
		||||
    dl->m_url = url;
 | 
			
		||||
    dl->m_options = options;
 | 
			
		||||
    dl->m_sink.reset(new FileSink(path));
 | 
			
		||||
    return dl;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Download::addValidator(Validator * v)
 | 
			
		||||
void Download::addValidator(Validator* v)
 | 
			
		||||
{
 | 
			
		||||
    m_sink->addValidator(v);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Download::startImpl()
 | 
			
		||||
{
 | 
			
		||||
    if(m_status == Job_Aborted)
 | 
			
		||||
    {
 | 
			
		||||
    if (m_status == Job_Aborted) {
 | 
			
		||||
        qWarning() << "Attempt to start an aborted Download:" << m_url.toString();
 | 
			
		||||
        emit aborted(m_index_within_job);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    QNetworkRequest request(m_url);
 | 
			
		||||
    m_status = m_sink->init(request);
 | 
			
		||||
    switch(m_status)
 | 
			
		||||
    {
 | 
			
		||||
    switch (m_status) {
 | 
			
		||||
        case Job_Finished:
 | 
			
		||||
            emit succeeded(m_index_within_job);
 | 
			
		||||
            qDebug() << "Download cache hit " << m_url.toString();
 | 
			
		||||
@@ -87,7 +85,7 @@ void Download::startImpl()
 | 
			
		||||
        case Job_InProgress:
 | 
			
		||||
            qDebug() << "Downloading " << m_url.toString();
 | 
			
		||||
            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_Failed:
 | 
			
		||||
            emit failed(m_index_within_job);
 | 
			
		||||
@@ -97,8 +95,11 @@ void Download::startImpl()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    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)
 | 
			
		||||
{
 | 
			
		||||
    if(error == QNetworkReply::OperationCanceledError)
 | 
			
		||||
    {
 | 
			
		||||
    if (error == QNetworkReply::OperationCanceledError) {
 | 
			
		||||
        qCritical() << "Aborted " << m_url.toString();
 | 
			
		||||
        m_status = Job_Aborted;
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
        if(m_options & Option::AcceptLocalFiles)
 | 
			
		||||
        {
 | 
			
		||||
            if(m_sink->hasLocalData())
 | 
			
		||||
            {
 | 
			
		||||
    } else {
 | 
			
		||||
        if (m_options & Option::AcceptLocalFiles) {
 | 
			
		||||
            if (m_sink->hasLocalData()) {
 | 
			
		||||
                m_status = Job_Failed_Proceed;
 | 
			
		||||
                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;
 | 
			
		||||
    for (auto error : errors)
 | 
			
		||||
    {
 | 
			
		||||
    for (auto error : errors) {
 | 
			
		||||
        qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
 | 
			
		||||
        auto cert = error.certificate();
 | 
			
		||||
        qCritical() << "Certificate in question:\n" << cert.toText();
 | 
			
		||||
@@ -153,33 +148,27 @@ void Download::sslErrors(const QList<QSslError> & errors)
 | 
			
		||||
bool Download::handleRedirect()
 | 
			
		||||
{
 | 
			
		||||
    QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
 | 
			
		||||
    if(!redirect.isValid())
 | 
			
		||||
    {
 | 
			
		||||
        if(!m_reply->hasRawHeader("Location"))
 | 
			
		||||
        {
 | 
			
		||||
    if (!redirect.isValid()) {
 | 
			
		||||
        if (!m_reply->hasRawHeader("Location")) {
 | 
			
		||||
            // no redirect -> it's fine to continue
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        // there is a Location header, but it's not correct. we need to apply some workarounds...
 | 
			
		||||
        QByteArray redirectBA = m_reply->rawHeader("Location");
 | 
			
		||||
        if(redirectBA.size() == 0)
 | 
			
		||||
        {
 | 
			
		||||
        if (redirectBA.size() == 0) {
 | 
			
		||||
            // empty, yet present redirect header? WTF?
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        QString redirectStr = QString::fromUtf8(redirectBA);
 | 
			
		||||
 | 
			
		||||
        if(redirectStr.startsWith("//"))
 | 
			
		||||
        {
 | 
			
		||||
        if (redirectStr.startsWith("//")) {
 | 
			
		||||
            /*
 | 
			
		||||
             * IF the URL begins with //, we need to insert the URL scheme.
 | 
			
		||||
             * See: https://bugreports.qt.io/browse/QTBUG-41061
 | 
			
		||||
             * See: http://tools.ietf.org/html/rfc3986#section-4.2
 | 
			
		||||
             */
 | 
			
		||||
            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
 | 
			
		||||
             */
 | 
			
		||||
@@ -193,16 +182,13 @@ bool Download::handleRedirect()
 | 
			
		||||
         * FIXME: report Qt bug for this
 | 
			
		||||
         */
 | 
			
		||||
        redirect = QUrl(redirectStr, QUrl::TolerantMode);
 | 
			
		||||
        if(!redirect.isValid())
 | 
			
		||||
        {
 | 
			
		||||
        if (!redirect.isValid()) {
 | 
			
		||||
            qWarning() << "Failed to parse redirect URL:" << redirectStr;
 | 
			
		||||
            downloadError(QNetworkReply::ProtocolFailure);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        qDebug() << "Fixed location header:" << redirect;
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
    } else {
 | 
			
		||||
        qDebug() << "Location header:" << redirect;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -212,35 +198,28 @@ bool Download::handleRedirect()
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
void Download::downloadFinished()
 | 
			
		||||
{
 | 
			
		||||
    // handle HTTP redirection first
 | 
			
		||||
    if(handleRedirect())
 | 
			
		||||
    {
 | 
			
		||||
    if (handleRedirect()) {
 | 
			
		||||
        qDebug() << "Download redirected:" << m_url.toString();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        m_sink->abort();
 | 
			
		||||
        m_reply.reset();
 | 
			
		||||
        emit succeeded(m_index_within_job);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    else if (m_status == Job_Failed)
 | 
			
		||||
    {
 | 
			
		||||
    } else if (m_status == Job_Failed) {
 | 
			
		||||
        qDebug() << "Download failed in previous step:" << m_url.toString();
 | 
			
		||||
        m_sink->abort();
 | 
			
		||||
        m_reply.reset();
 | 
			
		||||
        emit failed(m_index_within_job);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    else if(m_status == Job_Aborted)
 | 
			
		||||
    {
 | 
			
		||||
    } else if (m_status == Job_Aborted) {
 | 
			
		||||
        qDebug() << "Download aborted in previous step:" << m_url.toString();
 | 
			
		||||
        m_sink->abort();
 | 
			
		||||
        m_reply.reset();
 | 
			
		||||
@@ -250,16 +229,14 @@ void Download::downloadFinished()
 | 
			
		||||
 | 
			
		||||
    // make sure we got all the remaining data, if any
 | 
			
		||||
    auto data = m_reply->readAll();
 | 
			
		||||
    if(data.size())
 | 
			
		||||
    {
 | 
			
		||||
    if (data.size()) {
 | 
			
		||||
        qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path;
 | 
			
		||||
        m_status = m_sink->write(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // otherwise, finalize the whole graph
 | 
			
		||||
    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();
 | 
			
		||||
        m_sink->abort();
 | 
			
		||||
        m_reply.reset();
 | 
			
		||||
@@ -273,32 +250,25 @@ void Download::downloadFinished()
 | 
			
		||||
 | 
			
		||||
void Download::downloadReadyRead()
 | 
			
		||||
{
 | 
			
		||||
    if(m_status == Job_InProgress)
 | 
			
		||||
    {
 | 
			
		||||
    if (m_status == Job_InProgress) {
 | 
			
		||||
        auto data = m_reply->readAll();
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
        // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
    } else {
 | 
			
		||||
        qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}  // namespace Net
 | 
			
		||||
 | 
			
		||||
bool Net::Download::abort()
 | 
			
		||||
{
 | 
			
		||||
    if(m_reply)
 | 
			
		||||
    {
 | 
			
		||||
    if (m_reply) {
 | 
			
		||||
        m_reply->abort();
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
    } else {
 | 
			
		||||
        m_status = Job_Aborted;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,8 @@ auto ListModel::debugName() const -> QString
 | 
			
		||||
 | 
			
		||||
void ListModel::fetchMore(const QModelIndex& parent)
 | 
			
		||||
{
 | 
			
		||||
    if (parent.isValid()) return;
 | 
			
		||||
    if (parent.isValid())
 | 
			
		||||
        return;
 | 
			
		||||
    if (nextSearchOffset == 0) {
 | 
			
		||||
        qWarning() << "fetchMore with 0 offset is wrong...";
 | 
			
		||||
        return;
 | 
			
		||||
@@ -32,7 +33,9 @@ void ListModel::fetchMore(const QModelIndex& parent)
 | 
			
		||||
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
 | 
			
		||||
{
 | 
			
		||||
    int pos = index.row();
 | 
			
		||||
    if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); }
 | 
			
		||||
    if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
 | 
			
		||||
        return QString("INVALID INDEX %1").arg(pos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ModPlatform::IndexedPack pack = modpacks.at(pos);
 | 
			
		||||
    if (role == Qt::DisplayRole) {
 | 
			
		||||
@@ -46,7 +49,9 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
 | 
			
		||||
        }
 | 
			
		||||
        return pack.description;
 | 
			
		||||
    } 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");
 | 
			
		||||
        ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
    m_parent->apiProvider()->getVersions(this,
 | 
			
		||||
            { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
 | 
			
		||||
    m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ListModel::performPaginatedSearch()
 | 
			
		||||
{
 | 
			
		||||
    auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
 | 
			
		||||
 | 
			
		||||
    m_parent->apiProvider()->searchMods(this,
 | 
			
		||||
            { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
 | 
			
		||||
    m_parent->apiProvider()->searchMods(
 | 
			
		||||
        this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ListModel::refresh()
 | 
			
		||||
@@ -93,11 +97,9 @@ void ListModel::refresh()
 | 
			
		||||
 | 
			
		||||
void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
 | 
			
		||||
{
 | 
			
		||||
    if (currentSearchTerm == term 
 | 
			
		||||
            && currentSearchTerm.isNull() == term.isNull() 
 | 
			
		||||
            && currentSort == sort 
 | 
			
		||||
            && !filter_changed) 
 | 
			
		||||
        { return; }
 | 
			
		||||
    if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    currentSearchTerm = term;
 | 
			
		||||
    currentSort = sort;
 | 
			
		||||
@@ -118,7 +120,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac
 | 
			
		||||
 | 
			
		||||
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 =
 | 
			
		||||
        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] {
 | 
			
		||||
        job->deleteLater();
 | 
			
		||||
        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] {
 | 
			
		||||
@@ -148,7 +154,9 @@ 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) { 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
 | 
			
		||||
        QMessageBox::critical(nullptr, tr("Error"),
 | 
			
		||||
                              //: %1 refers to the launcher itself
 | 
			
		||||
                              QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
 | 
			
		||||
                              QString("%1 %2")
 | 
			
		||||
                                  .arg(m_parent->displayName())
 | 
			
		||||
                                  .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
 | 
			
		||||
    }
 | 
			
		||||
    jobPtr.reset();
 | 
			
		||||
 | 
			
		||||
@@ -218,9 +228,12 @@ void ListModel::searchRequestFailed(QString reason)
 | 
			
		||||
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
 | 
			
		||||
{
 | 
			
		||||
    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 {
 | 
			
		||||
        loadIndexedPackVersions(current, arr);
 | 
			
		||||
    } catch (const JSONValidationError& e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
#include "FlameModModel.h"
 | 
			
		||||
 | 
			
		||||
#include "Json.h"
 | 
			
		||||
#include "modplatform/flame/FlameModIndex.h"
 | 
			
		||||
 | 
			
		||||
namespace FlameMod {
 | 
			
		||||
@@ -19,7 +19,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray&
 | 
			
		||||
 | 
			
		||||
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
 | 
			
		||||
{
 | 
			
		||||
    return obj.array();
 | 
			
		||||
    return Json::ensureArray(obj.object(), "data");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace FlameMod
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#include "FlameModel.h"
 | 
			
		||||
#include "Application.h"
 | 
			
		||||
#include <Json.h>
 | 
			
		||||
#include "Application.h"
 | 
			
		||||
 | 
			
		||||
#include <MMCStrings.h>
 | 
			
		||||
#include <Version.h>
 | 
			
		||||
@@ -9,61 +9,46 @@
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int ListModel::columnCount(const QModelIndex &parent) const
 | 
			
		||||
int ListModel::columnCount(const QModelIndex& parent) const
 | 
			
		||||
{
 | 
			
		||||
    return 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QVariant ListModel::data(const QModelIndex &index, int role) const
 | 
			
		||||
QVariant ListModel::data(const QModelIndex& index, int role) const
 | 
			
		||||
{
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    IndexedPack pack = modpacks.at(pos);
 | 
			
		||||
    if(role == Qt::DisplayRole)
 | 
			
		||||
    {
 | 
			
		||||
    if (role == Qt::DisplayRole) {
 | 
			
		||||
        return pack.name;
 | 
			
		||||
    }
 | 
			
		||||
    else if (role == Qt::ToolTipRole)
 | 
			
		||||
    {
 | 
			
		||||
        if(pack.description.length() > 100)
 | 
			
		||||
        {
 | 
			
		||||
            //some magic to prevent to long tooltips and replace html linebreaks
 | 
			
		||||
    } else if (role == Qt::ToolTipRole) {
 | 
			
		||||
        if (pack.description.length() > 100) {
 | 
			
		||||
            // some magic to prevent to long tooltips and replace html linebreaks
 | 
			
		||||
            QString edit = pack.description.left(97);
 | 
			
		||||
            edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
 | 
			
		||||
            return edit;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        return pack.description;
 | 
			
		||||
    }
 | 
			
		||||
    else if(role == Qt::DecorationRole)
 | 
			
		||||
    {
 | 
			
		||||
        if(m_logoMap.contains(pack.logoName))
 | 
			
		||||
        {
 | 
			
		||||
    } else if (role == Qt::DecorationRole) {
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
    else if(role == Qt::UserRole)
 | 
			
		||||
    {
 | 
			
		||||
    } else if (role == Qt::UserRole) {
 | 
			
		||||
        QVariant v;
 | 
			
		||||
        v.setValue(pack);
 | 
			
		||||
        return v;
 | 
			
		||||
@@ -76,9 +61,9 @@ 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) {
 | 
			
		||||
            emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
 | 
			
		||||
    for (int i = 0; i < modpacks.size(); i++) {
 | 
			
		||||
        if (modpacks[i].logoName == logo) {
 | 
			
		||||
            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)
 | 
			
		||||
{
 | 
			
		||||
    if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
 | 
			
		||||
    {
 | 
			
		||||
    if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -101,18 +85,15 @@ void ListModel::requestLogo(QString logo, QString url)
 | 
			
		||||
    job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
        emit logoLoaded(logo, QIcon(fullPath));
 | 
			
		||||
        if(waitingCallbacks.contains(logo))
 | 
			
		||||
        {
 | 
			
		||||
        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] {
 | 
			
		||||
        job->deleteLater();
 | 
			
		||||
        emit logoFailed(logo);
 | 
			
		||||
    });
 | 
			
		||||
@@ -122,19 +103,16 @@ void ListModel::requestLogo(QString logo, QString url)
 | 
			
		||||
    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());
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
    } else {
 | 
			
		||||
        requestLogo(logo, logoUrl);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
 | 
			
		||||
Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
 | 
			
		||||
{
 | 
			
		||||
    return QAbstractListModel::flags(index);
 | 
			
		||||
}
 | 
			
		||||
@@ -148,7 +126,7 @@ void ListModel::fetchMore(const QModelIndex& parent)
 | 
			
		||||
{
 | 
			
		||||
    if (parent.isValid())
 | 
			
		||||
        return;
 | 
			
		||||
    if(nextSearchOffset == 0) {
 | 
			
		||||
    if (nextSearchOffset == 0) {
 | 
			
		||||
        qWarning() << "fetchMore with 0 offset is wrong...";
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -157,17 +135,20 @@ void ListModel::fetchMore(const QModelIndex& parent)
 | 
			
		||||
 | 
			
		||||
void ListModel::performPaginatedSearch()
 | 
			
		||||
{
 | 
			
		||||
    NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network());
 | 
			
		||||
    NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network());
 | 
			
		||||
    auto searchUrl = QString(
 | 
			
		||||
        "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
 | 
			
		||||
        "categoryId=0&"
 | 
			
		||||
        "gameId=432&"
 | 
			
		||||
        "index=%1&"
 | 
			
		||||
        "pageSize=25&"
 | 
			
		||||
        "searchFilter=%2&"
 | 
			
		||||
        "sectionId=4471&"
 | 
			
		||||
        "sort=%3"
 | 
			
		||||
    ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort);
 | 
			
		||||
                         "https://api.curseforge.com/v1/mods/search?"
 | 
			
		||||
                         "gameId=432&"
 | 
			
		||||
                         "classId=4471&"
 | 
			
		||||
                         "index=%1&"
 | 
			
		||||
                         "pageSize=25&"
 | 
			
		||||
                         "searchFilter=%2&"
 | 
			
		||||
                         "sortField=%3&"
 | 
			
		||||
                         "sortOrder=desc")
 | 
			
		||||
                         .arg(nextSearchOffset)
 | 
			
		||||
                         .arg(currentSearchTerm)
 | 
			
		||||
                         .arg(currentSort + 1);
 | 
			
		||||
 | 
			
		||||
    netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
 | 
			
		||||
    jobPtr = netJob;
 | 
			
		||||
    jobPtr->start();
 | 
			
		||||
@@ -177,17 +158,16 @@ void ListModel::performPaginatedSearch()
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
    currentSearchTerm = term;
 | 
			
		||||
    currentSort = sort;
 | 
			
		||||
    if(jobPtr) {
 | 
			
		||||
    if (jobPtr) {
 | 
			
		||||
        jobPtr->abort();
 | 
			
		||||
        searchState = ResetRequested;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
    } else {
 | 
			
		||||
        beginResetModel();
 | 
			
		||||
        modpacks.clear();
 | 
			
		||||
        endResetModel();
 | 
			
		||||
@@ -203,30 +183,28 @@ void Flame::ListModel::searchRequestFinished()
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
    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 = doc.array();
 | 
			
		||||
    for(auto packRaw : packs) {
 | 
			
		||||
    auto packs = Json::ensureArray(doc.object(), "data");
 | 
			
		||||
    for (auto packRaw : packs) {
 | 
			
		||||
        auto packObj = packRaw.toObject();
 | 
			
		||||
 | 
			
		||||
        Flame::IndexedPack pack;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
        try {
 | 
			
		||||
            Flame::loadIndexedPack(pack, packObj);
 | 
			
		||||
            newList.append(pack);
 | 
			
		||||
        }
 | 
			
		||||
        catch(const JSONValidationError &e)
 | 
			
		||||
        {
 | 
			
		||||
        } catch (const JSONValidationError& e) {
 | 
			
		||||
            qWarning() << "Error while loading pack from CurseForge: " << e.cause();
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if(packs.size() < 25) {
 | 
			
		||||
    if (packs.size() < 25) {
 | 
			
		||||
        searchState = Finished;
 | 
			
		||||
    } else {
 | 
			
		||||
        nextSearchOffset += 25;
 | 
			
		||||
@@ -241,7 +219,7 @@ void Flame::ListModel::searchRequestFailed(QString reason)
 | 
			
		||||
{
 | 
			
		||||
    jobPtr.reset();
 | 
			
		||||
 | 
			
		||||
    if(searchState == ResetRequested) {
 | 
			
		||||
    if (searchState == ResetRequested) {
 | 
			
		||||
        beginResetModel();
 | 
			
		||||
        modpacks.clear();
 | 
			
		||||
        endResetModel();
 | 
			
		||||
@@ -253,5 +231,4 @@ void Flame::ListModel::searchRequestFailed(QString reason)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace Flame
 | 
			
		||||
 
 | 
			
		||||
@@ -39,13 +39,12 @@
 | 
			
		||||
#include <QKeyEvent>
 | 
			
		||||
 | 
			
		||||
#include "Application.h"
 | 
			
		||||
#include "FlameModel.h"
 | 
			
		||||
#include "InstanceImportTask.h"
 | 
			
		||||
#include "Json.h"
 | 
			
		||||
#include "ui/dialogs/NewInstanceDialog.h"
 | 
			
		||||
#include "InstanceImportTask.h"
 | 
			
		||||
#include "FlameModel.h"
 | 
			
		||||
 | 
			
		||||
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
 | 
			
		||||
    : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
 | 
			
		||||
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
 | 
			
		||||
{
 | 
			
		||||
    ui->setupUi(this);
 | 
			
		||||
    connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
 | 
			
		||||
@@ -112,10 +111,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
 | 
			
		||||
{
 | 
			
		||||
    ui->versionSelectionBox->clear();
 | 
			
		||||
 | 
			
		||||
    if(!first.isValid())
 | 
			
		||||
    {
 | 
			
		||||
        if(isOpened)
 | 
			
		||||
        {
 | 
			
		||||
    if (!first.isValid()) {
 | 
			
		||||
        if (isOpened) {
 | 
			
		||||
            dialog->setSuggestedPack();
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
@@ -130,14 +127,14 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
 | 
			
		||||
    else
 | 
			
		||||
        text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
 | 
			
		||||
    if (!current.authors.empty()) {
 | 
			
		||||
        auto authorToStr = [](Flame::ModpackAuthor & author) {
 | 
			
		||||
            if(author.url.isEmpty()) {
 | 
			
		||||
        auto authorToStr = [](Flame::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 : current.authors) {
 | 
			
		||||
            authorStrs.push_back(authorToStr(author));
 | 
			
		||||
        }
 | 
			
		||||
        text += "<br>" + tr(" by ") + authorStrs.join(", ");
 | 
			
		||||
@@ -146,53 +143,46 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
 | 
			
		||||
 | 
			
		||||
    ui->packDescription->setHtml(text + current.description);
 | 
			
		||||
 | 
			
		||||
    if (current.versionsLoaded == false)
 | 
			
		||||
    {
 | 
			
		||||
    if (current.versionsLoaded == false) {
 | 
			
		||||
        qDebug() << "Loading flame modpack versions";
 | 
			
		||||
        auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
 | 
			
		||||
        auto response = new QByteArray();
 | 
			
		||||
        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]
 | 
			
		||||
        {
 | 
			
		||||
            if(addonId != current.addonId){
 | 
			
		||||
                return; //wrong request
 | 
			
		||||
        QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
 | 
			
		||||
            if (addonId != 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();
 | 
			
		||||
            if (parse_error.error != QJsonParseError::NoError) {
 | 
			
		||||
                qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
 | 
			
		||||
                           << " reason: " << parse_error.errorString();
 | 
			
		||||
                qWarning() << *response;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            QJsonArray arr = doc.array();
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
            auto arr = Json::ensureArray(doc.object(), "data");
 | 
			
		||||
            try {
 | 
			
		||||
                Flame::loadIndexedPackVersions(current, arr);
 | 
			
		||||
            }
 | 
			
		||||
            catch(const JSONValidationError &e)
 | 
			
		||||
            {
 | 
			
		||||
            } catch (const JSONValidationError& e) {
 | 
			
		||||
                qDebug() << *response;
 | 
			
		||||
                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));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            suggestCurrent();
 | 
			
		||||
        });
 | 
			
		||||
        QObject::connect(netJob, &NetJob::finished, this, [response, netJob]
 | 
			
		||||
        {
 | 
			
		||||
        QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
 | 
			
		||||
            netJob->deleteLater();
 | 
			
		||||
            delete response;
 | 
			
		||||
        });
 | 
			
		||||
        netJob->start();
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
        for(auto version : current.versions) {
 | 
			
		||||
    } else {
 | 
			
		||||
        for (auto version : current.versions) {
 | 
			
		||||
            ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -202,13 +192,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
 | 
			
		||||
 | 
			
		||||
void FlamePage::suggestCurrent()
 | 
			
		||||
{
 | 
			
		||||
    if(!isOpened)
 | 
			
		||||
    {
 | 
			
		||||
    if (!isOpened) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (selectedVersion.isEmpty())
 | 
			
		||||
    {
 | 
			
		||||
    if (selectedVersion.isEmpty()) {
 | 
			
		||||
        dialog->setSuggestedPack();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -216,16 +204,13 @@ void FlamePage::suggestCurrent()
 | 
			
		||||
    dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
 | 
			
		||||
    QString editedLogoName;
 | 
			
		||||
    editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
 | 
			
		||||
    listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
 | 
			
		||||
    {
 | 
			
		||||
        dialog->setSuggestedIconFromFile(logo, editedLogoName);
 | 
			
		||||
    });
 | 
			
		||||
    listModel->getLogo(current.logoName, current.logoUrl,
 | 
			
		||||
                       [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FlamePage::onVersionSelectionChanged(QString data)
 | 
			
		||||
{
 | 
			
		||||
    if(data.isNull() || data.isEmpty())
 | 
			
		||||
    {
 | 
			
		||||
    if (data.isNull() || data.isEmpty()) {
 | 
			
		||||
        selectedVersion = "";
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user