adopt changes from #497 remapped
This commit is contained in:
		| @@ -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@"; | ||||
|   | ||||
| @@ -39,9 +39,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; | ||||
| @@ -74,7 +73,6 @@ public: | ||||
|     /// URL for the updater's channel | ||||
|     QString UPDATER_BASE; | ||||
|  | ||||
|  | ||||
|     /// User-Agent to use. | ||||
|     QString USER_AGENT; | ||||
|  | ||||
| @@ -116,6 +114,11 @@ public: | ||||
|      */ | ||||
|     QString MSA_CLIENT_ID; | ||||
|  | ||||
|     /** | ||||
|      * Client API key for CurseForge | ||||
|      */ | ||||
|     QString CURSEFORGE_API_KEY; | ||||
|  | ||||
|     /** | ||||
|      * Metadata repository URL prefix | ||||
|      */ | ||||
| @@ -154,4 +157,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.indexOf('.') > -1) | ||||
|                 file.mcVersion.append(mcVer.toString()); | ||||
|         } | ||||
|  | ||||
|         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