aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
authordada513 <dada513@protonmail.com>2022-03-28 20:55:06 +0200
committerdada513 <dada513@protonmail.com>2022-03-28 20:55:06 +0200
commit341eb16a4c0571cc66068c0bdd85d8d8955869b7 (patch)
treea2214b34b0b6a49add241d044aaeda5151396733 /launcher
parent3a7eeff135f92c807fdc066cb06d835f295b66d3 (diff)
parent87cf38a3770dd7f55fbae79ca3a4958155289af7 (diff)
downloadPrismLauncher-341eb16a4c0571cc66068c0bdd85d8d8955869b7.tar.gz
PrismLauncher-341eb16a4c0571cc66068c0bdd85d8d8955869b7.tar.bz2
PrismLauncher-341eb16a4c0571cc66068c0bdd85d8d8955869b7.zip
Merge branch 'develop' of https://github.com/PolyMC/PolyMC into flatpak_properly
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp1
-rw-r--r--launcher/CMakeLists.txt21
-rw-r--r--launcher/launch/steps/QuitAfterGameStop.cpp26
-rw-r--r--launcher/launch/steps/QuitAfterGameStop.h35
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp6
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp1
-rw-r--r--launcher/modplatform/ModAPI.h29
-rw-r--r--launcher/modplatform/ModIndex.h42
-rw-r--r--launcher/modplatform/flame/FlameAPI.h32
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp62
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h50
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp60
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h13
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h57
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp51
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h50
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp2
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui10
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp223
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h82
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp170
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h69
-rw-r--r--launcher/ui/pages/modplatform/ModPage.ui (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui)4
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp268
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h78
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.cpp240
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.h72
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.ui97
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp271
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h80
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp227
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.h72
32 files changed, 1079 insertions, 1422 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index abdfc06d..af1cfe85 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 98cb0a3b..32c7ea71 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 00000000..f9eced99
--- /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 <dada513@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "QuitAfterGameStop.h"
+#include <launch/LaunchTask.h>
+#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 00000000..1ce14da9
--- /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 <dada513@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+
+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 6db12c42..90bb92a1 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<LaunchTask> 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 d15d7e9d..173f29b5 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 00000000..5c7c6349
--- /dev/null
+++ b/launcher/modplatform/ModAPI.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QString>
+
+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 00000000..7e1cf254
--- /dev/null
+++ b/launcher/modplatform/ModIndex.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVariant>
+#include <QVector>
+
+namespace ModPlatform {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct IndexedVersion {
+ QVariant addonId;
+ QVariant fileId;
+ QString version;
+ QVector<QString> mcVersion;
+ QString downloadUrl;
+ QString date;
+ QString fileName;
+ QVector<QString> loaders = {};
+};
+
+struct IndexedPack {
+ QVariant addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> 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 00000000..62accfa4
--- /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 4adaf5f1..2c3adee4 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -1,13 +1,11 @@
-#include <QObject>
#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<QNetworkAccessManager>& network, BaseInstance * inst)
+void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst)
{
- QVector<FlameMod::IndexedVersion> unsortedVersions;
- bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft");
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ bool hasFabric = !(dynamic_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ QString mcVersion = (dynamic_cast<MinecraftInstance*>(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 0293bb23..d3171d94 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -3,48 +3,18 @@
//
#pragma once
-#include <QList>
-#include <QMetaType>
-#include <QString>
-#include <QVector>
-#include <QNetworkAccessManager>
-#include <QObjectPtr.h>
-#include "net/NetJob.h"
-#include "BaseInstance.h"
-
-namespace FlameMod {
- struct ModpackAuthor {
- QString name;
- QString url;
- };
- struct IndexedVersion {
- int addonId;
- int fileId;
- QString version;
- QVector<QString> mcVersion;
- QString downloadUrl;
- QString date;
- QString fileName;
- };
+#include "modplatform/ModIndex.h"
- struct IndexedPack
- {
- int addonId;
- QString name;
- QString description;
- QList<ModpackAuthor> authors;
- QString logoName;
- QString logoUrl;
- QString websiteUrl;
-
- bool versionsLoaded = false;
- QVector<IndexedVersion> versions;
- };
+#include "BaseInstance.h"
+#include <QNetworkAccessManager>
- void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
- void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst);
+namespace FlameMod {
-}
+void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& 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 00000000..25c7b9fd
--- /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 00000000..4d3f7005
--- /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 00000000..cf4dec1a
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "modplatform/helpers/NetworkModAPI.h"
+
+#include <QDebug>
+
+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 992d6657..82988cf6 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -1,14 +1,14 @@
-#include <QObject>
#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<QNetworkAccessManager>& network, BaseInstance * inst)
+void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& network,
+ BaseInstance* inst)
{
- QVector<Modrinth::IndexedVersion> unsortedVersions;
- bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
- QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft");
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ bool hasFabric = !(static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ QString mcVersion = (static_cast<MinecraftInstance*>(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 3a4cd270..fd17847a 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -1,48 +1,16 @@
#pragma once
-#include <QList>
-#include <QMetaType>
-#include <QString>
-#include <QVector>
-#include <QNetworkAccessManager>
-#include <QObjectPtr.h>
-#include "net/NetJob.h"
+#include "modplatform/ModIndex.h"
+
#include "BaseInstance.h"
+#include <QNetworkAccessManager>
namespace Modrinth {
-struct ModpackAuthor {
- QString name;
- QString url;
-};
-
-struct IndexedVersion {
- QString addonId;
- QString fileId;
- QString version;
- QVector<QString> mcVersion;
- QString downloadUrl;
- QString date;
- QString fileName;
- QVector<QString> loaders;
-};
-
-struct IndexedPack
-{
- QString addonId;
- QString name;
- QString description;
- ModpackAuthor author;
- QString logoName;
- QString logoUrl;
- QString websiteUrl;
-
- bool versionsLoaded = false;
- QVector<IndexedVersion> versions;
-};
-
-void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
-void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr<QNetworkAccessManager> &network, BaseInstance *inst);
-}
+void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
+ QJsonArray& arr,
+ const shared_qobject_ptr<QNetworkAccessManager>& 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 9abae425..f49f5a92 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 a28b1f59..decc9b8b 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -180,6 +180,16 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QCheckBox" name="quitAfterGameStopCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;PolyMC will automatically exit if the game crashes or exists.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Quit PolyMC after game window stops</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
new file mode 100644
index 00000000..cc3c5326
--- /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 <QMessageBox>
+
+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("<br>")).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<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile()->getComponentVersion("net.minecraft");
+ bool hasFabric = !(dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(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<ModPlatform::IndexedPack> 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<ModDownloadDialog*>((dynamic_cast<ModPage*>(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 00000000..02be6049
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
+#include "net/NetJob.h"
+
+class ModPage;
+
+namespace ModPlatform {
+
+using LogoMap = QMap<QString, QIcon>;
+using LogoCallback = std::function<void (QString)>;
+
+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<ModPlatform::IndexedPack> modpacks;
+
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> 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 00000000..3a116d3c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -0,0 +1,170 @@
+#include "ModPage.h"
+#include "ui_ModPage.h"
+
+#include <QKeyEvent>
+
+#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<QKeyEvent*>(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<ModPlatform::IndexedPack>();
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+
+ if (!current.authors.empty()) {
+ auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
+ 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) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+ text += "<br><br>";
+
+ 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<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];
+ //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 00000000..0cd13f37
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QWidget>
+
+#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<ModAPI> api;
+
+ int selectedVersion = -1;
+};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/ModPage.ui
index 6c709825..508f1bac 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
+++ b/launcher/ui/pages/modplatform/ModPage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ModrinthPage</class>
- <widget class="QWidget" name="ModrinthPage">
+ <class>ModPage</class>
+ <widget class="QWidget" name="ModPage">
<property name="geometry">
<rect>
<x>0</x>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
index e8afba5a..905fb2dd 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 <Json.h>
-
-#include <MMCStrings.h>
-#include <Version.h>
-
-#include <QtMath>
+#include "modplatform/flame/FlameModIndex.h"
namespace FlameMod {
-ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent)
-{
-}
-
-ListModel::~ListModel()
-{
-}
+// NOLINTNEXTLINE(modernize-avoid-c-arrays)
+const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
-int ListModel::rowCount(const QModelIndex &parent) const
+void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- 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("<br>")).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);
+ FlameMod::loadIndexedPack(m, obj);
}
-void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- if(m_logoMap.contains(logo))
- {
- callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
- }
- else
- {
- requestLogo(logo, logoUrl);
- }
+ FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
-Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
- 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<FlameMod::IndexedPack> 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 0c1cb95e..707c1bb1 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h
@@ -1,79 +1,25 @@
#pragma once
-#include <RWStorage.h>
-
-#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QThreadPool>
-#include <QIcon>
-#include <QStyledItemDelegate>
-#include <QList>
-#include <QString>
-#include <QStringList>
-#include <QMetaType>
-
-#include <functional>
-#include <net/NetJob.h>
-
-#include <modplatform/flame/FlamePackIndex.h>
-#include "modplatform/flame/FlameModIndex.h"
-#include "BaseInstance.h"
#include "FlameModPage.h"
namespace FlameMod {
-
-typedef QMap<QString, QIcon> LogoMap;
-typedef std::function<void(QString)> LogoCallback;
-
-class ListModel : public QAbstractListModel
-{
+class ListModel : public ModPlatform::ListModel {
Q_OBJECT
-public:
- ListModel(FlameModPage *parent);
- virtual ~ListModel();
-
- 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;
-
- 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);
+ public:
+ ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
+ ~ListModel() override = default;
-private:
- void requestLogo(QString file, QString url);
+ private:
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-private:
- QList<IndexedPack> modpacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- 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 d1641729..864ae8e6 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 <QKeyEvent>
-
-#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<QKeyEvent *>(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);
-}
-
-void FlameModPage::openedImpl() {
- updateSelectionButton();
- triggerSearch();
-}
-
-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<FlameMod::IndexedPack>();
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
- if (!current.authors.empty()) {
- auto authorToStr = [](FlameMod::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) {
- authorStrs.push_back(authorToStr(author));
- }
- text += "<br>" + tr(" by ") + authorStrs.join(", ");
- }
- text += "<br><br>";
-
- 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"));
- }
+ 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::onVersionSelectionChanged(QString data) {
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
+auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
+{
+ (void) loaderVer;
+ return ver.mcVersion.contains(mineVer);
}
-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 2a6ade85..dc58fd7f 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h
@@ -35,70 +35,26 @@
#pragma once
-#include <QWidget>
+#include "ui/pages/modplatform/ModPage.h"
-#include "ui/pages/BasePage.h"
-#include <Application.h>
-#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;
-
- void openedImpl() override;
-
- bool eventFilter(QObject * watched, QEvent * event) override;
-
- BaseInstance *m_instance;
+ public:
+ explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ~FlameModPage() override = default;
-private:
- void updateSelectionButton();
+ 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"; }
-private slots:
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
+ inline auto debugName() const -> QString override { return "Flame"; }
+ inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
-private:
- Ui::FlameModPage *ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
- FlameMod::ListModel* listModel = nullptr;
- FlameMod::IndexedPack current;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
- 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 25cb2571..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>FlameModPage</class>
- <widget class="QWidget" name="FlameModPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>837</width>
- <height>685</height>
- </rect>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="2" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0,0,0" columnminimumwidth="0,0,0">
- <item row="1" column="2">
- <widget class="QComboBox" name="versionSelectionBox"/>
- </item>
- <item row="1" column="0">
- <widget class="QComboBox" name="sortByBox"/>
- </item>
- <item row="1" column="1">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>Version selected:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <widget class="QPushButton" name="modSelectionButton">
- <property name="text">
- <string>Select mod for download</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search and filter...</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="1" column="0">
- <widget class="QListView" name="packView">
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOff</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="iconSize">
- <size>
- <width>48</width>
- <height>48</height>
- </size>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QTextBrowser" name="packDescription">
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- <property name="openLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="0" column="1">
- <widget class="QPushButton" name="searchButton">
- <property name="text">
- <string>Search</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>searchEdit</tabstop>
- <tabstop>searchButton</tabstop>
- <tabstop>packView</tabstop>
- <tabstop>packDescription</tabstop>
- <tabstop>sortByBox</tabstop>
- <tabstop>versionSelectionBox</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 5a18830a..daa43e26 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 <Json.h>
-
-#include <MMCStrings.h>
-#include <Version.h>
-
-#include <QtMath>
-#include <QMessageBox>
+#include "modplatform/modrinth/ModrinthPackIndex.h"
namespace Modrinth {
-ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent)
-{
-}
-
-ListModel::~ListModel()
-{
-}
-
-int ListModel::rowCount(const QModelIndex &parent) const
-{
- 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("<br>")).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));
+// NOLINTNEXTLINE(modernize-avoid-c-arrays)
+const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
- 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)
+void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- 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();
+ Modrinth::loadIndexedPack(m, obj);
}
-void Modrinth::ListModel::searchRequestFinished()
+void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- 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<Modrinth::IndexedPack> 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();
+ Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
-void Modrinth::ListModel::searchRequestFailed(QString reason)
+auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
- 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 53f1f134..45a6090a 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -1,79 +1,25 @@
#pragma once
-#include <RWStorage.h>
-
-#include <QAbstractListModel>
-#include <QSortFilterProxyModel>
-#include <QThreadPool>
-#include <QIcon>
-#include <QStyledItemDelegate>
-#include <QList>
-#include <QString>
-#include <QStringList>
-#include <QMetaType>
-
-#include <functional>
-#include <net/NetJob.h>
-
-#include <modplatform/flame/FlamePackIndex.h>
-#include "modplatform/modrinth/ModrinthPackIndex.h"
-#include "BaseInstance.h"
#include "ModrinthPage.h"
namespace Modrinth {
-
-typedef QMap<QString, QIcon> LogoMap;
-typedef std::function<void(QString)> LogoCallback;
-
-class ListModel : public QAbstractListModel
-{
+class ListModel : public ModPlatform::ListModel {
Q_OBJECT
-public:
- ListModel(ModrinthPage *parent);
- virtual ~ListModel();
-
- 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;
-
- 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);
+ public:
+ ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){};
+ ~ListModel() override = default;
-private:
- QList<IndexedPack> modpacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
+ private:
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
+
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- 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 82340448..ddaf96e2 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 <QKeyEvent>
-
-#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);
-
- 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 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);
-}
-
-ModrinthPage::~ModrinthPage() { delete ui; }
-
-bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) {
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-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<Modrinth::IndexedPack>();
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
- text += "<br>" + tr(" by ") + "<a href=\"" + current.author.url + "\">" +
- current.author.name + "</a><br><br>";
- 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"));
- }
+ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance)
+ : ModPage(dialog, instance, new ModrinthAPI())
+{
+ listModel = new Modrinth::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ // 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"));
+
+ // 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);
}
-void ModrinthPage::onVersionSelectionChanged(QString data) {
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
+auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
+{
+ return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer);
}
-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 92955d62..aa5ed793 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -35,70 +35,26 @@
#pragma once
-#include <QWidget>
+#include "ui/pages/modplatform/ModPage.h"
-#include "ui/pages/BasePage.h"
-#include <Application.h>
-#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;
-
- void openedImpl() override;
-
- bool eventFilter(QObject * watched, QEvent * event) override;
-
- BaseInstance *m_instance;
+ public:
+ explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ~ModrinthPage() override = default;
-private:
- void updateSelectionButton();
+ 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"; }
-private slots:
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
+ inline auto debugName() const -> QString override { return "Modrinth"; }
+ inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
-private:
- Ui::ModrinthPage *ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
- Modrinth::ListModel* listModel = nullptr;
- Modrinth::IndexedPack current;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
- int selectedVersion = -1;
+ auto shouldDisplay() const -> bool override;
};