diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 1561b9d68..3ec6bb958 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -9,12 +9,16 @@ on: - '**/LICENSE' - 'flake.lock' - '**.nix' + - 'packages/**' + - '.github/ISSUE_TEMPLATE/**' pull_request: paths-ignore: - '**.md' - '**/LICENSE' - 'flake.lock' - '**.nix' + - 'packages/**' + - '.github/ISSUE_TEMPLATE/**' workflow_dispatch: jobs: @@ -24,9 +28,3 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Debug - - build_release: - name: Build Release - uses: ./.github/workflows/build.yml - with: - build_type: Release diff --git a/launcher/Application.cpp b/launcher/Application.cpp index abdfc06d4..af1cfe85d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -727,6 +727,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("PastebinURL", "https://0x0.st"); m_settings->registerSetting("CloseAfterLaunch", false); + m_settings->registerSetting("QuitAfterGameStop", false); // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 98cb0a3b2..32c7ea713 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -144,6 +144,8 @@ set(LAUNCH_SOURCES launch/steps/TextPrint.h launch/steps/Update.cpp launch/steps/Update.h + launch/steps/QuitAfterGameStop.cpp + launch/steps/QuitAfterGameStop.h launch/LaunchStep.cpp launch/LaunchStep.h launch/LaunchTask.cpp @@ -485,6 +487,16 @@ set(META_SOURCES meta/Index.h ) +set(API_SOURCES + modplatform/ModAPI.h + + modplatform/flame/FlameAPI.h + modplatform/modrinth/ModrinthAPI.h + + modplatform/helpers/NetworkModAPI.h + modplatform/helpers/NetworkModAPI.cpp +) + set(FTB_SOURCES modplatform/legacy_ftb/PackFetchTask.h modplatform/legacy_ftb/PackFetchTask.cpp @@ -564,6 +576,7 @@ set(LOGIC_SOURCES ${TOOLS_SOURCES} ${META_SOURCES} ${ICONS_SOURCES} + ${API_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} ${MODRINTH_SOURCES} @@ -713,6 +726,11 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/VanillaPage.cpp ui/pages/modplatform/VanillaPage.h + ui/pages/modplatform/ModPage.cpp + ui/pages/modplatform/ModPage.h + ui/pages/modplatform/ModModel.cpp + ui/pages/modplatform/ModModel.h + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -871,13 +889,12 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/VanillaPage.ui + ui/pages/modplatform/ModPage.ui ui/pages/modplatform/flame/FlamePage.ui - ui/pages/modplatform/flame/FlameModPage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui ui/pages/modplatform/technic/TechnicPage.ui - ui/pages/modplatform/modrinth/ModrinthPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/MCModInfoFrame.ui diff --git a/launcher/launch/steps/QuitAfterGameStop.cpp b/launcher/launch/steps/QuitAfterGameStop.cpp new file mode 100644 index 000000000..f9eced993 --- /dev/null +++ b/launcher/launch/steps/QuitAfterGameStop.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "QuitAfterGameStop.h" +#include +#include "Application.h" + +void QuitAfterGameStop::executeTask() +{ + APPLICATION->quit(); +} diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h new file mode 100644 index 000000000..1ce14da95 --- /dev/null +++ b/launcher/launch/steps/QuitAfterGameStop.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +class QuitAfterGameStop: public LaunchStep +{ + Q_OBJECT +public: + explicit QuitAfterGameStop(LaunchTask *parent) :LaunchStep(parent){}; + virtual ~QuitAfterGameStop() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +}; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 6db12c42b..90bb92a17 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -20,6 +20,7 @@ #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/TextPrint.h" #include "launch/steps/CheckJava.h" +#include "launch/steps/QuitAfterGameStop.h" #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/DirectJavaLaunch.h" @@ -935,6 +936,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->setCensorFilter(createCensorFilterFromSession(session)); } + if(APPLICATION->settings()->get("QuitAfterGameStop").toBool()) + { + auto step = new QuitAfterGameStop(pptr); + process->appendStep(step); + } m_launchProcess = process; emit launchTaskChanged(m_launchProcess); return m_launchProcess; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d15d7e9db..173f29b50 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -170,6 +170,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) { if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); + m_parent->setPid(-1); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h new file mode 100644 index 000000000..5c7c63490 --- /dev/null +++ b/launcher/modplatform/ModAPI.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace ModPlatform { +class ListModel; +} + +class ModAPI { + protected: + using CallerType = ModPlatform::ListModel; + + public: + virtual ~ModAPI() = default; + + // https://docs.curseforge.com/?http#tocS_ModLoaderType + enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4 }; + + struct SearchArgs { + int offset; + QString search; + QString sorting; + ModLoaderType mod_loader; + QString version; + }; + + virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; + virtual void getVersions(CallerType* caller, const QString& addonId) const = 0; +}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h new file mode 100644 index 000000000..7e1cf254e --- /dev/null +++ b/launcher/modplatform/ModIndex.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ModPlatform { + +struct ModpackAuthor { + QString name; + QString url; +}; + +struct IndexedVersion { + QVariant addonId; + QVariant fileId; + QString version; + QVector mcVersion; + QString downloadUrl; + QString date; + QString fileName; + QVector loaders = {}; +}; + +struct IndexedPack { + QVariant addonId; + QString name; + QString description; + QList authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector versions; +}; + +} // namespace ModPlatform + +Q_DECLARE_METATYPE(ModPlatform::IndexedPack) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h new file mode 100644 index 000000000..62accfa4d --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.h @@ -0,0 +1,32 @@ +#pragma once + +#include "modplatform/helpers/NetworkModAPI.h" + +class FlameAPI : public NetworkModAPI { + private: + inline auto getModSearchURL(SearchArgs& args) const -> QString override + { + return QString( + "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "gameId=432&" + "categoryId=0&" + "sectionId=6&" + + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sort=%3&" + "modLoaderType=%4&" + "gameVersion=%5") + .arg(args.offset) + .arg(args.search) + .arg(args.sorting) + .arg(args.mod_loader) + .arg(args.version); + }; + + inline auto getVersionsURL(const QString& addonId) const -> QString override + { + return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); + }; +}; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 4adaf5f16..2c3adee43 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -1,13 +1,11 @@ -#include #include "FlameModIndex.h" + #include "Json.h" -#include "net/NetJob.h" -#include "BaseInstance.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/NetJob.h" - -void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) +void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); @@ -16,10 +14,10 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) bool thumbnailFound = false; auto attachments = Json::requireArray(obj, "attachments"); - for(auto attachmentRaw: attachments) { + for (auto attachmentRaw : attachments) { auto attachmentObj = Json::requireObject(attachmentRaw); bool isDefault = attachmentObj.value("isDefault").toBool(false); - if(isDefault) { + if (isDefault) { thumbnailFound = true; pack.logoName = Json::requireString(attachmentObj, "title"); pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); @@ -27,37 +25,35 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) } } - if(!thumbnailFound) { - throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); - } - + if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); } auto authors = Json::requireArray(obj, "authors"); - for(auto authorIter: authors) { + for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); - FlameMod::ModpackAuthor packAuthor; + ModPlatform::ModpackAuthor packAuthor; packAuthor.name = Json::requireString(author, "name"); packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } } -void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr& network, BaseInstance * inst) +void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst) { - QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + QVector unsortedVersions; + bool hasFabric = !(dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = (dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto versionArray = Json::requireArray(obj, "gameVersion"); - if (versionArray.isEmpty()) { - continue; - } + if (versionArray.isEmpty()) { continue; } - FlameMod::IndexedVersion file; - for(auto mcVer : versionArray){ + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) { file.mcVersion.append(mcVer.toString()); } @@ -70,29 +66,27 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray auto modules = Json::requireArray(obj, "modules"); bool is_valid_fabric_version = false; - for(auto m : modules){ - auto fname = Json::requireString(m.toObject(),"foldername"); + 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 // 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"){ + if (hasFabric) { + if (fname == "fabric.mod.json") { is_valid_fabric_version = true; break; } - } - else break; + } else + break; // NOTE: Since we're not validating forge versions, we can just skip this loop. } - if(hasFabric && !is_valid_fabric_version) - continue; + if (hasFabric && !is_valid_fabric_version) continue; unsortedVersions.append(file); } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - //dates are in RFC 3339 format + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 0293bb230..d3171d943 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -3,48 +3,18 @@ // #pragma once -#include -#include -#include -#include -#include -#include -#include "net/NetJob.h" + +#include "modplatform/ModIndex.h" + #include "BaseInstance.h" +#include namespace FlameMod { - struct ModpackAuthor { - QString name; - QString url; - }; - struct IndexedVersion { - int addonId; - int fileId; - QString version; - QVector mcVersion; - QString downloadUrl; - QString date; - QString fileName; - }; +void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst); - struct IndexedPack - { - int addonId; - QString name; - QString description; - QList authors; - QString logoName; - QString logoUrl; - QString websiteUrl; - - bool versionsLoaded = false; - QVector versions; - }; - - void loadIndexedPack(IndexedPack & m, QJsonObject & obj); - void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr &network, BaseInstance *inst); - -} - -Q_DECLARE_METATYPE(FlameMod::IndexedPack) +} // namespace FlameMod diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp new file mode 100644 index 000000000..25c7b9fd2 --- /dev/null +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -0,0 +1,60 @@ +#include "NetworkModAPI.h" + +#include "ui/pages/modplatform/ModModel.h" + +#include "Application.h" +#include "net/NetJob.h" + +void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const +{ + auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network()); + auto searchUrl = getModSearchURL(args); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); }); + QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); + QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->searchRequestFinished(doc); + }); + + netJob->start(); +} + +void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) const +{ + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(addonId), APPLICATION->network()); + auto response = new QByteArray(); + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, addonId] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->versionRequestSucceeded(doc, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); +} diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h new file mode 100644 index 000000000..4d3f70057 --- /dev/null +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -0,0 +1,13 @@ +#pragma once + +#include "modplatform/ModAPI.h" + +class NetworkModAPI : public ModAPI { + public: + void searchMods(CallerType* caller, SearchArgs&& args) const override; + void getVersions(CallerType* caller, const QString& addonId) const override; + + protected: + virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; + virtual auto getVersionsURL(const QString& addonId) const -> QString = 0; +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h new file mode 100644 index 000000000..cf4dec1a8 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -0,0 +1,57 @@ +#pragma once + +#include "modplatform/helpers/NetworkModAPI.h" + +#include + +class ModrinthAPI : public NetworkModAPI { + public: + inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; + + private: + inline auto getModSearchURL(SearchArgs& args) const -> QString override + { + if (!validateModLoader(args.mod_loader)) { + qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; + return ""; + } + + return QString( + "https://api.modrinth.com/v2/search?" + "offset=%1&" + "limit=25&" + "query=%2&" + "index=%3&" + "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") + .arg(args.offset) + .arg(args.search) + .arg(args.sorting) + .arg(getModLoaderString(args.mod_loader)) + .arg(args.version); + }; + + inline auto getVersionsURL(const QString& addonId) const -> QString override + { + return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); + }; + + inline auto getModLoaderString(ModLoaderType modLoader) const -> QString + { + switch (modLoader) { + case Any: + return "fabric, forge"; + case Forge: + return "forge"; + case Fabric: + return "fabric"; + default: + return ""; + } + } + + inline auto validateModLoader(ModLoaderType modLoader) const -> bool + { + return modLoader == Any || modLoader == Forge || modLoader == Fabric; + } + +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 992d66571..82988cf67 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,14 +1,14 @@ -#include #include "ModrinthPackIndex.h" +#include "ModrinthAPI.h" #include "Json.h" -#include "net/NetJob.h" -#include "BaseInstance.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/NetJob.h" +static ModrinthAPI api; -void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj) +void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); pack.name = Json::requireString(obj, "title"); @@ -16,35 +16,36 @@ void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj) pack.description = Json::ensureString(obj, "description", ""); pack.logoUrl = Json::requireString(obj, "icon_url"); - pack.logoName = pack.addonId; + pack.logoName = pack.addonId.toString(); - Modrinth::ModpackAuthor modAuthor; + ModPlatform::ModpackAuthor modAuthor; modAuthor.name = Json::requireString(obj, "author"); - modAuthor.url = "https://modrinth.com/user/"+modAuthor.name; - pack.author = modAuthor; + modAuthor.url = api.getAuthorURL(modAuthor.name); + pack.authors.append(modAuthor); } -void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr& network, BaseInstance * inst) +void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst) { - QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + QVector unsortedVersions; + bool hasFabric = !(static_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto obj = versionIter.toObject(); - Modrinth::IndexedVersion file; - file.addonId = Json::requireString(obj,"project_id") ; + ModPlatform::IndexedVersion file; + file.addonId = Json::requireString(obj, "project_id"); file.fileId = Json::requireString(obj, "id"); file.date = Json::requireString(obj, "date_published"); auto versionArray = Json::requireArray(obj, "game_versions"); - if (versionArray.empty()) { - continue; - } - for(auto mcVer : versionArray){ + if (versionArray.empty()) { continue; } + for (auto mcVer : versionArray) { file.mcVersion.append(mcVer.toString()); } - auto loaders = Json::requireArray(obj,"loaders"); - for(auto loader : loaders){ + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); @@ -78,18 +79,16 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray i++; } - auto parent = files[i].toObject(); - if(parent.contains("url")) { + if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); unsortedVersions.append(file); } } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - //dates are in RFC 3339 format + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 3a4cd2707..fd17847af 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -1,48 +1,16 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include "net/NetJob.h" +#include "modplatform/ModIndex.h" + #include "BaseInstance.h" +#include namespace Modrinth { -struct ModpackAuthor { - QString name; - QString url; -}; +void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst); -struct IndexedVersion { - QString addonId; - QString fileId; - QString version; - QVector mcVersion; - QString downloadUrl; - QString date; - QString fileName; - QVector loaders; -}; - -struct IndexedPack -{ - QString addonId; - QString name; - QString description; - ModpackAuthor author; - QString logoName; - QString logoUrl; - QString websiteUrl; - - bool versionsLoaded = false; - QVector versions; -}; - -void loadIndexedPack(IndexedPack & m, QJsonObject & obj); -void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr &network, BaseInstance *inst); -} - -Q_DECLARE_METATYPE(Modrinth::IndexedPack) +} // namespace Modrinth diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 9abae425a..f49f5a920 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -94,6 +94,7 @@ void MinecraftPage::applySettings() // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); + s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); } void MinecraftPage::loadSettings() @@ -113,6 +114,7 @@ void MinecraftPage::loadSettings() ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); + ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index a28b1f596..decc9b8b1 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -180,6 +180,16 @@ + + + + <html><head/><body><p>PolyMC will automatically exit if the game crashes or exists.</p></body></html> + + + Quit PolyMC after game window stops + + + diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp new file mode 100644 index 000000000..cc3c5326b --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -0,0 +1,223 @@ +#include "ModModel.h" + +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +#include + +namespace ModPlatform { + +ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} + +auto ListModel::debugName() const -> QString +{ + return m_parent->debugName(); +} + + +/******** Make data requests ********/ + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +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); } + + ModPlatform::IndexedPack pack = modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } 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); + return icon; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return {}; +} + +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) +{ + m_parent->apiProvider()->getVersions(this, current.addonId.toString()); +} + +void ListModel::performPaginatedSearch() +{ + QString mcVersion = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !(dynamic_cast((dynamic_cast(parent()))->m_instance)) + ->getPackProfile() + ->getComponentVersion("net.fabricmc.fabric-loader") + .isEmpty(); + + m_parent->apiProvider()->searchMods( + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric ? ModAPI::Fabric : ModAPI::Forge, mcVersion }); +} + +void ListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } + currentSearchTerm = term; + currentSort = sort; + if (jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + + +/******** Request callbacks ********/ + +void 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 }); } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::searchRequestFinished(QJsonDocument& doc) +{ + jobPtr.reset(); + + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack pack; + try { + loadIndexedPack(pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + continue; + } + } + + if (packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::searchRequestFailed(QString reason) +{ + if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update PolyMC!"))); + // self-destruct + (dynamic_cast((dynamic_cast(parent()))->parentWidget()))->reject(); + } + jobPtr.reset(); + + if (searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) +{ + auto& current = m_parent->getCurrent(); + if (addonId != current.addonId) { return; } + + QJsonArray arr = doc.array(); + try { + loadIndexedPackVersions(current, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); + } + + m_parent->updateModVersions(); +} + +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h new file mode 100644 index 000000000..02be60491 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "net/NetJob.h" + +class ModPage; + +namespace ModPlatform { + +using LogoMap = QMap; +using LogoCallback = std::function; + +class ListModel : public QAbstractListModel { + Q_OBJECT + + public: + ListModel(ModPage* parent); + ~ListModel() override = default; + + inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + + auto debugName() const -> QString; + + /* Retrieve information from the model at a given index with the given role */ + auto data(const QModelIndex& index, int role) const -> QVariant override; + + inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + + /* Ask the API for more information */ + void fetchMore(const QModelIndex& parent) override; + void searchWithTerm(const QString& term, const int sort); + void requestModVersions(const ModPlatform::IndexedPack& current); + + virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + + public slots: + void searchRequestFinished(QJsonDocument& doc); + void searchRequestFailed(QString reason); + + void versionRequestSucceeded(QJsonDocument doc, QString addonId); + + protected slots: + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void performPaginatedSearch(); + + protected: + virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; + virtual auto getSorts() const -> const char** = 0; + + void requestLogo(QString file, QString url); + + protected: + ModPage* m_parent; + + QList modpacks; + + LogoMap m_logoMap; + QMap waitingCallbacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + + NetJob::Ptr jobPtr; +}; +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp new file mode 100644 index 000000000..3a116d3c7 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -0,0 +1,170 @@ +#include "ModPage.h" +#include "ui_ModPage.h" + +#include + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) + : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog), api(api) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + +} + +ModPage::~ModPage() +{ + delete ui; +} + + +/******** Qt things ********/ + +void ModPage::openedImpl() +{ + updateSelectionButton(); + triggerSearch(); +} + +auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto* keyEvent = dynamic_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void ModPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if (!first.isValid()) { return; } + + current = listModel->data(first, Qt::UserRole).value(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + + if (!current.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { return author.name; } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
" + tr(" by ") + authorStrs.join(", "); + } + text += "

"; + + ui->packDescription->setHtml(text + current.description); + + if (!current.versionsLoaded) { + qDebug() << QString("Loading %1 mod versions").arg(debugName()); + + ui->modSelectionButton->setText(tr("Loading versions...")); + ui->modSelectionButton->setEnabled(false); + + listModel->requestModVersions(current); + } else { + for (int i = 0; i < current.versions.size(); i++) { + ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } + + updateSelectionButton(); + } +} + +void ModPage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + selectedVersion = -1; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toInt(); + updateSelectionButton(); +} + +void ModPage::onModSelected() +{ + auto& version = current.versions[selectedVersion]; + if (dialog->isModSelected(current.name, version.fileName)) { + dialog->removeSelectedMod(current.name); + } else { + dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + } + + updateSelectionButton(); +} + + +/******** Make changes to the UI ********/ + +void ModPage::retranslate() +{ + ui->retranslateUi(this); +} + +void ModPage::updateModVersions() +{ + auto packProfile = (dynamic_cast(m_instance))->getPackProfile(); + + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; + + for (int i = 0; i < current.versions.size(); i++) { + auto version = current.versions[i]; + //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. + if (!validateVersion(version, mcVersion, loaderString)) { + continue; + } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } + + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + updateSelectionButton(); +} + + +void ModPage::updateSelectionButton() +{ + if (!isOpened || selectedVersion < 0) { + ui->modSelectionButton->setEnabled(false); + return; + } + + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if (!dialog->isModSelected(current.name, version.fileName)) { + ui->modSelectionButton->setText(tr("Select mod for download")); + } else { + ui->modSelectionButton->setText(tr("Deselect mod for download")); + } +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h new file mode 100644 index 000000000..0cd13f379 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include "Application.h" +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModModel.h" + +class ModDownloadDialog; + +namespace Ui { +class ModPage; +} + +/* This page handles most logic related to browsing and selecting mods to download. */ +class ModPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); + ~ModPage() override; + + /* Affects what the user sees */ + auto displayName() const -> QString override = 0; + auto icon() const -> QIcon override = 0; + auto id() const -> QString override = 0; + auto helpPage() const -> QString override = 0; + + /* Used internally */ + virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString = 0; + + + void retranslate() override; + + auto shouldDisplay() const -> bool override = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; + + auto apiProvider() const -> const ModAPI* { return api.get(); }; + + auto getCurrent() -> ModPlatform::IndexedPack& { return current; } + void updateModVersions(); + + void openedImpl() override; + auto eventFilter(QObject* watched, QEvent* event) -> bool override; + + BaseInstance* m_instance; + + protected: + void updateSelectionButton(); + + protected slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + void onModSelected(); + + protected: + Ui::ModPage* ui = nullptr; + ModDownloadDialog* dialog = nullptr; + ModPlatform::ListModel* listModel = nullptr; + ModPlatform::IndexedPack current; + + std::unique_ptr api; + + int selectedVersion = -1; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/ModPage.ui similarity index 97% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui rename to launcher/ui/pages/modplatform/ModPage.ui index 6c7098254..508f1bac8 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -1,7 +1,7 @@ - ModrinthPage - + ModPage + 0 diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index e8afba5a1..905fb2dd1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,273 +1,25 @@ #include "FlameModModel.h" -#include "Application.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "FlameModPage.h" -#include - -#include -#include - -#include +#include "modplatform/flame/FlameModIndex.h" namespace FlameMod { -ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; + +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { + FlameMod::loadIndexedPack(m, obj); } -ListModel::~ListModel() +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -int ListModel::rowCount(const QModelIndex &parent) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - 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); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -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}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"}; - -void ListModel::performPaginatedSearch() -{ - - QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - auto netJob = new NetJob("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "gameId=432&" - "categoryId=0&" - "sectionId=6&" - - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sort=%3&" - "modLoaderType=%4&" - "gameVersion=%5" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} - -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} - -void ListModel::searchRequestFinished() -{ - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - QList newList; - auto packs = doc.array(); - for(auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - FlameMod::IndexedPack pack; - try - { - FlameMod::loadIndexedPack(pack, packObj); - newList.append(pack); - } - catch(const JSONValidationError &e) - { - qWarning() << "Error while loading mod from Flame: " << e.cause(); - continue; - } - } - if(packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -void ListModel::searchRequestFailed(QString reason) -{ - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - + return obj.array(); } +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 0c1cb95e4..707c1bb15 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -1,79 +1,25 @@ #pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include "modplatform/flame/FlameModIndex.h" -#include "BaseInstance.h" #include "FlameModPage.h" namespace FlameMod { - -typedef QMap LogoMap; -typedef std::function LogoCallback; - -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(FlameModPage *parent); - virtual ~ListModel(); + public: + ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} + ~ListModel() override = default; - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; + private: + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); - -private: - void requestLogo(QString file, QString url); - -private: - QList modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap waitingCallbacks; - - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + static const char* sorts[6]; + inline auto getSorts() const -> const char** override { return sorts; }; }; -} +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index d16417298..864ae8e67 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -34,224 +34,40 @@ */ #include "FlameModPage.h" -#include "ui_FlameModPage.h" +#include "ui_ModPage.h" -#include - -#include "Application.h" #include "FlameModModel.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &FlameModPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new FlameMod::ListModel(this); - ui->packView->setModel(listModel); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - // index is used to set the sorting with the flame api - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &FlameModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &FlameModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &FlameModPage::onModSelected); -} - -FlameModPage::~FlameModPage() { delete ui; } - -bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - -bool FlameModPage::shouldDisplay() const { return true; } - -void FlameModPage::retranslate() +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance, new FlameAPI()) { - ui->retranslateUi(this); + listModel = new FlameMod::ListModel(this); + ui->packView->setModel(listModel); + + // index is used to set the sorting with the flame api + ui->sortByBox->addItem(tr("Sort by Featured")); + ui->sortByBox->addItem(tr("Sort by Popularity")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by Name")); + ui->sortByBox->addItem(tr("Sort by Author")); + ui->sortByBox->addItem(tr("Sort by Downloads")); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -void FlameModPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +{ + (void) loaderVer; + return ver.mcVersion.contains(mineVer); } -void FlameModPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](FlameMod::ModpackAuthor &author) { - if (author.url.isEmpty()) { - return author.name; - } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto &author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "
" + tr(" by ") + authorStrs.join(", "); - } - text += "

"; - - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading flame mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Flame::ModVersions(%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)); - - 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 Flame at " - << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - QJsonArray arr = doc.array(); - try { - FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { - qDebug() << *response; - qWarning() << "Error while reading Flame mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - netJob->start(); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - updateSelectionButton(); - } -} - -void FlameModPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} - -void FlameModPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void FlameModPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); -} +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto FlameModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 2a6ade853..dc58fd7f5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -35,70 +35,26 @@ #pragma once -#include +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include -#include "tasks/Task.h" -#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/flame/FlameAPI.h" -namespace Ui -{ -class FlameModPage; -} - -class ModDownloadDialog; - -namespace FlameMod { - class ListModel; -} - -class FlameModPage : public QWidget, public BasePage -{ +class FlameModPage : public ModPage { Q_OBJECT -public: - explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~FlameModPage(); - virtual QString displayName() const override - { - return "CurseForge"; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("flame"); - } - virtual QString id() const override - { - return "curseforge"; - } - virtual QString helpPage() const override - { - return "Flame-platform"; - } - virtual bool shouldDisplay() const override; - void retranslate() override; + public: + explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~FlameModPage() override = default; - void openedImpl() override; + inline auto displayName() const -> QString override { return "CurseForge"; } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } + inline auto id() const -> QString override { return "curseforge"; } + inline auto helpPage() const -> QString override { return "Flame-platform"; } - bool eventFilter(QObject * watched, QEvent * event) override; + inline auto debugName() const -> QString override { return "Flame"; } + inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - BaseInstance *m_instance; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; -private: - void updateSelectionButton(); - -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - -private: - Ui::FlameModPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - FlameMod::ListModel* listModel = nullptr; - FlameMod::IndexedPack current; - - int selectedVersion = -1; + auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui deleted file mode 100644 index 25cb2571c..000000000 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ /dev/null @@ -1,97 +0,0 @@ - - - FlameModPage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Select mod for download - - - - - - - - - Search and filter... - - - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - true - - - true - - - - - - - - - Search - - - - - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - - diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 5a18830a6..daa43e26e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,276 +1,25 @@ #include "ModrinthModel.h" -#include "Application.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "ModrinthPage.h" -#include "ui/dialogs/ModDownloadDialog.h" -#include - -#include -#include - -#include -#include +#include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { -ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; + +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { + Modrinth::loadIndexedPack(m, obj); } -ListModel::~ListModel() +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { + Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -int ListModel::rowCount(const QModelIndex &parent) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - 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); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -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}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[5]{"relevance","downloads","follows","updated","newest"}; - -void ListModel::performPaginatedSearch() -{ - - QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.modrinth.com/v2/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? "fabric" : "forge") - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} - -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} - -void Modrinth::ListModel::searchRequestFinished() -{ - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - QList newList; - auto packs = doc.object().value("hits").toArray(); - for(auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - Modrinth::IndexedPack pack; - try - { - Modrinth::loadIndexedPack(pack, packObj); - newList.append(pack); - } - catch(const JSONValidationError &e) - { - qWarning() << "Error while loading mod from Modrinth: " << e.cause(); - continue; - } - } - if(packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -void Modrinth::ListModel::searchRequestFailed(QString reason) -{ - if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){ - //409 Gone, notify user to update - QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!")); - //self-destruct - ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject(); - } - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - + return obj.object().value("hits").toArray(); } +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 53f1f134e..45a6090a7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,79 +1,25 @@ #pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include "modplatform/modrinth/ModrinthPackIndex.h" -#include "BaseInstance.h" #include "ModrinthPage.h" namespace Modrinth { - -typedef QMap LogoMap; -typedef std::function LogoCallback; - -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(ModrinthPage *parent); - virtual ~ListModel(); + public: + ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; + ~ListModel() override = default; - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; + private: + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); - -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); - -private: - void requestLogo(QString file, QString url); - -private: - QList modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap waitingCallbacks; - - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + static const char* sorts[5]; + inline auto getSorts() const -> const char** override { return sorts; }; }; -} +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 823404489..ddaf96e2c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,211 +34,38 @@ */ #include "ModrinthPage.h" -#include "ui_ModrinthPage.h" +#include "ui_ModPage.h" -#include - -#include "Application.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" #include "ModrinthModel.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &ModrinthPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new Modrinth::ListModel(this); - ui->packView->setModel(listModel); +ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance, new ModrinthAPI()) +{ + listModel = new Modrinth::ListModel(this); + ui->packView->setModel(listModel); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + // index is used to set the sorting with the modrinth api + ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Downloads")); + ui->sortByBox->addItem(tr("Sort by Follows")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by newest")); - // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevance")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by newest")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &ModrinthPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &ModrinthPage::onModSelected); + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } -ModrinthPage::~ModrinthPage() { delete ui; } - -bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); +auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +{ + return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer); } -bool ModrinthPage::shouldDisplay() const { return true; } - -void ModrinthPage::retranslate() { - ui->retranslateUi(this); -} - -void ModrinthPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); -} - -void ModrinthPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - text += "
" + tr(" by ") + "" + - current.author.name + "

"; - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading Modrinth mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), - APPLICATION->network()); - auto response = new QByteArray(); - QString addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), - response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { - if(addonId != current.addonId){ - return; - } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " - << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - QJsonArray arr = doc.array(); - try { - Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { - qDebug() << *response; - qWarning() << "Error while reading Modrinth mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion) || - !version.loaders.contains(loaderString)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - updateSelectionButton(); - } -} - -void ModrinthPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} - -void ModrinthPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void ModrinthPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); -} +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto ModrinthPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 92955d62d..aa5ed793b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -35,70 +35,26 @@ #pragma once -#include +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include -#include "tasks/Task.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" -namespace Ui -{ -class ModrinthPage; -} - -class ModDownloadDialog; - -namespace Modrinth { - class ListModel; -} - -class ModrinthPage : public QWidget, public BasePage -{ +class ModrinthPage : public ModPage { Q_OBJECT -public: - explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~ModrinthPage(); - virtual QString displayName() const override - { - return "Modrinth"; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("modrinth"); - } - virtual QString id() const override - { - return "modrinth"; - } - virtual QString helpPage() const override - { - return "Modrinth-platform"; - } - virtual bool shouldDisplay() const override; - void retranslate() override; + public: + explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~ModrinthPage() override = default; - void openedImpl() override; + inline auto displayName() const -> QString override { return "Modrinth"; } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } + inline auto id() const -> QString override { return "modrinth"; } + inline auto helpPage() const -> QString override { return "Modrinth-platform"; } - bool eventFilter(QObject * watched, QEvent * event) override; + inline auto debugName() const -> QString override { return "Modrinth"; } + inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - BaseInstance *m_instance; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; -private: - void updateSelectionButton(); - -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - -private: - Ui::ModrinthPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - Modrinth::ListModel* listModel = nullptr; - Modrinth::IndexedPack current; - - int selectedVersion = -1; + auto shouldDisplay() const -> bool override; };