Merge branch 'develop' of https://github.com/PolyMC/PolyMC into flatpak_properly

This commit is contained in:
dada513 2022-03-28 20:55:06 +02:00
commit 341eb16a4c
No known key found for this signature in database
GPG Key ID: 403448C14FA4B33E
33 changed files with 1083 additions and 1428 deletions

View File

@ -9,12 +9,16 @@ on:
- '**/LICENSE' - '**/LICENSE'
- 'flake.lock' - 'flake.lock'
- '**.nix' - '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- '**/LICENSE' - '**/LICENSE'
- 'flake.lock' - 'flake.lock'
- '**.nix' - '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -24,9 +28,3 @@ jobs:
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
with: with:
build_type: Debug build_type: Debug
build_release:
name: Build Release
uses: ./.github/workflows/build.yml
with:
build_type: Release

View File

@ -727,6 +727,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("PastebinURL", "https://0x0.st"); m_settings->registerSetting("PastebinURL", "https://0x0.st");
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);
// Custom MSA credentials // Custom MSA credentials
m_settings->registerSetting("MSAClientIDOverride", ""); m_settings->registerSetting("MSAClientIDOverride", "");

View File

@ -144,6 +144,8 @@ set(LAUNCH_SOURCES
launch/steps/TextPrint.h launch/steps/TextPrint.h
launch/steps/Update.cpp launch/steps/Update.cpp
launch/steps/Update.h launch/steps/Update.h
launch/steps/QuitAfterGameStop.cpp
launch/steps/QuitAfterGameStop.h
launch/LaunchStep.cpp launch/LaunchStep.cpp
launch/LaunchStep.h launch/LaunchStep.h
launch/LaunchTask.cpp launch/LaunchTask.cpp
@ -485,6 +487,16 @@ set(META_SOURCES
meta/Index.h 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 set(FTB_SOURCES
modplatform/legacy_ftb/PackFetchTask.h modplatform/legacy_ftb/PackFetchTask.h
modplatform/legacy_ftb/PackFetchTask.cpp modplatform/legacy_ftb/PackFetchTask.cpp
@ -564,6 +576,7 @@ set(LOGIC_SOURCES
${TOOLS_SOURCES} ${TOOLS_SOURCES}
${META_SOURCES} ${META_SOURCES}
${ICONS_SOURCES} ${ICONS_SOURCES}
${API_SOURCES}
${FTB_SOURCES} ${FTB_SOURCES}
${FLAME_SOURCES} ${FLAME_SOURCES}
${MODRINTH_SOURCES} ${MODRINTH_SOURCES}
@ -713,6 +726,11 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h 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.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlFilterModel.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp 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/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui ui/pages/modplatform/VanillaPage.ui
ui/pages/modplatform/ModPage.ui
ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/flame/FlameModPage.ui
ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/ftb/FtbPage.ui ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/technic/TechnicPage.ui ui/pages/modplatform/technic/TechnicPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/widgets/InstanceCardWidget.ui ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui ui/widgets/CustomCommands.ui
ui/widgets/MCModInfoFrame.ui ui/widgets/MCModInfoFrame.ui

View File

@ -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();
}

View File

@ -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;
}
};

View File

@ -20,6 +20,7 @@
#include "launch/steps/PreLaunchCommand.h" #include "launch/steps/PreLaunchCommand.h"
#include "launch/steps/TextPrint.h" #include "launch/steps/TextPrint.h"
#include "launch/steps/CheckJava.h" #include "launch/steps/CheckJava.h"
#include "launch/steps/QuitAfterGameStop.h"
#include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/LauncherPartLaunch.h"
#include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/DirectJavaLaunch.h"
@ -935,6 +936,11 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
{ {
process->setCensorFilter(createCensorFilterFromSession(session)); process->setCensorFilter(createCensorFilterFromSession(session));
} }
if(APPLICATION->settings()->get("QuitAfterGameStop").toBool())
{
auto step = new QuitAfterGameStop(pptr);
process->appendStep(step);
}
m_launchProcess = process; m_launchProcess = process;
emit launchTaskChanged(m_launchProcess); emit launchTaskChanged(m_launchProcess);
return m_launchProcess; return m_launchProcess;

View File

@ -170,6 +170,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
{ {
if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) if (APPLICATION->settings()->get("CloseAfterLaunch").toBool())
APPLICATION->showMainWindow(); APPLICATION->showMainWindow();
m_parent->setPid(-1); m_parent->setPid(-1);
// if the exit code wasn't 0, report this as a crash // if the exit code wasn't 0, report this as a crash
auto exitCode = m_process.exitCode(); auto exitCode = m_process.exitCode();

View File

@ -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;
};

View File

@ -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)

View File

@ -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);
};
};

View File

@ -1,13 +1,11 @@
#include <QObject>
#include "FlameModIndex.h" #include "FlameModIndex.h"
#include "Json.h" #include "Json.h"
#include "net/NetJob.h"
#include "BaseInstance.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "net/NetJob.h"
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
{ {
pack.addonId = Json::requireInteger(obj, "id"); pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name"); pack.name = Json::requireString(obj, "name");
@ -16,10 +14,10 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
bool thumbnailFound = false; bool thumbnailFound = false;
auto attachments = Json::requireArray(obj, "attachments"); auto attachments = Json::requireArray(obj, "attachments");
for(auto attachmentRaw: attachments) { for (auto attachmentRaw : attachments) {
auto attachmentObj = Json::requireObject(attachmentRaw); auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false); bool isDefault = attachmentObj.value("isDefault").toBool(false);
if(isDefault) { if (isDefault) {
thumbnailFound = true; thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title"); pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
@ -27,37 +25,35 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj)
} }
} }
if(!thumbnailFound) { if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
}
auto authors = Json::requireArray(obj, "authors"); auto authors = Json::requireArray(obj, "authors");
for(auto authorIter: authors) { for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter); auto author = Json::requireObject(authorIter);
FlameMod::ModpackAuthor packAuthor; ModPlatform::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name"); packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url"); packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor); pack.authors.append(packAuthor);
} }
} }
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; QVector<ModPlatform::IndexedVersion> unsortedVersions;
bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); bool hasFabric = !(dynamic_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); QString mcVersion = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
for(auto versionIter: arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
auto versionArray = Json::requireArray(obj, "gameVersion"); auto versionArray = Json::requireArray(obj, "gameVersion");
if (versionArray.isEmpty()) { if (versionArray.isEmpty()) { continue; }
continue;
}
FlameMod::IndexedVersion file; ModPlatform::IndexedVersion file;
for(auto mcVer : versionArray){ for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString()); file.mcVersion.append(mcVer.toString());
} }
@ -70,29 +66,27 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray
auto modules = Json::requireArray(obj, "modules"); auto modules = Json::requireArray(obj, "modules");
bool is_valid_fabric_version = false; bool is_valid_fabric_version = false;
for(auto m : modules){ for (auto m : modules) {
auto fname = Json::requireString(m.toObject(),"foldername"); auto fname = Json::requireString(m.toObject(), "foldername");
// FIXME: This does not work properly when a mod supports more than one mod loader, since // FIXME: This 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 // they bundle the meta files for all of them in the same arquive, even when that version
// doesn't support the given mod loader. // doesn't support the given mod loader.
if(hasFabric){ if (hasFabric) {
if(fname == "fabric.mod.json"){ if (fname == "fabric.mod.json") {
is_valid_fabric_version = true; is_valid_fabric_version = true;
break; break;
} }
} } else
else break; break;
// NOTE: Since we're not validating forge versions, we can just skip this loop. // NOTE: Since we're not validating forge versions, we can just skip this loop.
} }
if(hasFabric && !is_valid_fabric_version) if (hasFabric && !is_valid_fabric_version) continue;
continue;
unsortedVersions.append(file); unsortedVersions.append(file);
} }
auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
{ // dates are in RFC 3339 format
//dates are in RFC 3339 format
return a.date > b.date; return a.date > b.date;
}; };
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);

View File

@ -3,48 +3,18 @@
// //
#pragma once #pragma once
#include <QList>
#include <QMetaType> #include "modplatform/ModIndex.h"
#include <QString>
#include <QVector>
#include <QNetworkAccessManager>
#include <QObjectPtr.h>
#include "net/NetJob.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include <QNetworkAccessManager>
namespace FlameMod { namespace FlameMod {
struct ModpackAuthor {
QString name;
QString url;
};
struct IndexedVersion { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
int addonId; void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
int fileId; QJsonArray& arr,
QString version; const shared_qobject_ptr<QNetworkAccessManager>& network,
QVector<QString> mcVersion; BaseInstance* inst);
QString downloadUrl;
QString date;
QString fileName;
};
struct IndexedPack } // namespace FlameMod
{
int addonId;
QString name;
QString description;
QList<ModpackAuthor> authors;
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);
}
Q_DECLARE_METATYPE(FlameMod::IndexedPack)

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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;
}
};

View File

@ -1,14 +1,14 @@
#include <QObject>
#include "ModrinthPackIndex.h" #include "ModrinthPackIndex.h"
#include "ModrinthAPI.h"
#include "Json.h" #include "Json.h"
#include "net/NetJob.h"
#include "BaseInstance.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.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.addonId = Json::requireString(obj, "project_id");
pack.name = Json::requireString(obj, "title"); 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.description = Json::ensureString(obj, "description", "");
pack.logoUrl = Json::requireString(obj, "icon_url"); 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.name = Json::requireString(obj, "author");
modAuthor.url = "https://modrinth.com/user/"+modAuthor.name; modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.author = modAuthor; 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; QVector<ModPlatform::IndexedVersion> unsortedVersions;
bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); bool hasFabric = !(static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); QString mcVersion = (static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
for(auto versionIter: arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
Modrinth::IndexedVersion file; ModPlatform::IndexedVersion file;
file.addonId = Json::requireString(obj,"project_id") ; file.addonId = Json::requireString(obj, "project_id");
file.fileId = Json::requireString(obj, "id"); file.fileId = Json::requireString(obj, "id");
file.date = Json::requireString(obj, "date_published"); file.date = Json::requireString(obj, "date_published");
auto versionArray = Json::requireArray(obj, "game_versions"); auto versionArray = Json::requireArray(obj, "game_versions");
if (versionArray.empty()) { if (versionArray.empty()) { continue; }
continue; for (auto mcVer : versionArray) {
}
for(auto mcVer : versionArray){
file.mcVersion.append(mcVer.toString()); file.mcVersion.append(mcVer.toString());
} }
auto loaders = Json::requireArray(obj,"loaders"); auto loaders = Json::requireArray(obj, "loaders");
for(auto loader : loaders){ for (auto loader : loaders) {
file.loaders.append(loader.toString()); file.loaders.append(loader.toString());
} }
file.version = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "name");
@ -78,18 +79,16 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray
i++; i++;
} }
auto parent = files[i].toObject(); auto parent = files[i].toObject();
if(parent.contains("url")) { if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url"); file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename"); file.fileName = Json::requireString(parent, "filename");
unsortedVersions.append(file); unsortedVersions.append(file);
} }
} }
auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
{ // dates are in RFC 3339 format
//dates are in RFC 3339 format
return a.date > b.date; return a.date > b.date;
}; };
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);

View File

@ -1,48 +1,16 @@
#pragma once #pragma once
#include <QList> #include "modplatform/ModIndex.h"
#include <QMetaType>
#include <QString>
#include <QVector>
#include <QNetworkAccessManager>
#include <QObjectPtr.h>
#include "net/NetJob.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include <QNetworkAccessManager>
namespace Modrinth { namespace Modrinth {
struct ModpackAuthor { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
QString name; void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QString url; QJsonArray& arr,
}; const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
struct IndexedVersion { } // namespace Modrinth
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);
}
Q_DECLARE_METATYPE(Modrinth::IndexedPack)

View File

@ -94,6 +94,7 @@ void MinecraftPage::applySettings()
// Miscellaneous // Miscellaneous
s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked());
s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked());
} }
void MinecraftPage::loadSettings() void MinecraftPage::loadSettings()
@ -113,6 +114,7 @@ void MinecraftPage::loadSettings()
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool());
ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool());
} }
void MinecraftPage::retranslate() void MinecraftPage::retranslate()

View File

@ -180,6 +180,16 @@
</property> </property>
</widget> </widget>
</item> </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> </layout>
</widget> </widget>
</item> </item>

View File

@ -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

View File

@ -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

View File

@ -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"));
}
}

View File

@ -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;
};

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ModrinthPage</class> <class>ModPage</class>
<widget class="QWidget" name="ModrinthPage"> <widget class="QWidget" name="ModPage">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>

View File

@ -1,273 +1,25 @@
#include "FlameModModel.h" #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 { namespace FlameMod {
ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) // NOLINTNEXTLINE(modernize-avoid-c-arrays)
const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{ {
FlameMod::loadIndexedPack(m, obj);
} }
ListModel::~ListModel() void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
} }
int ListModel::rowCount(const QModelIndex &parent) const auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{ {
return modpacks.size(); return obj.array();
}
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);
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
requestLogo(logo, logoUrl);
}
}
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
{
return QAbstractListModel::flags(index);
}
bool ListModel::canFetchMore(const QModelIndex& parent) const
{
return searchState == CanPossiblyFetchMore;
}
void ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if(nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
performPaginatedSearch();
}
const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"};
void ListModel::performPaginatedSearch()
{
QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
auto netJob = new NetJob("Flame::Search", APPLICATION->network());
auto searchUrl = QString(
"https://addons-ecs.forgesvc.net/api/v2/addon/search?"
"gameId=432&"
"categoryId=0&"
"sectionId=6&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
"sort=%3&"
"modLoaderType=%4&"
"gameVersion=%5"
)
.arg(nextSearchOffset)
.arg(currentSearchTerm)
.arg(sorts[currentSort])
.arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType
.arg(mcVersion);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void ListModel::searchWithTerm(const QString &term, const int sort)
{
if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
return;
}
currentSearchTerm = term;
currentSort = sort;
if(jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
}
else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
}
void ListModel::searchRequestFinished()
{
jobPtr.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
QList<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;
}
}
} }
} // namespace FlameMod

View File

@ -1,79 +1,25 @@
#pragma once #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" #include "FlameModPage.h"
namespace FlameMod { namespace FlameMod {
class ListModel : public ModPlatform::ListModel {
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
Q_OBJECT Q_OBJECT
public: public:
ListModel(FlameModPage *parent); ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
virtual ~ListModel(); ~ListModel() override = default;
int rowCount(const QModelIndex &parent) const override; private:
int columnCount(const QModelIndex &parent) const override; void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
QVariant data(const QModelIndex &index, int role) const override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) 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); auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
void searchWithTerm(const QString &term, const int sort);
private slots: // NOLINTNEXTLINE(modernize-avoid-c-arrays)
void performPaginatedSearch(); static const char* sorts[6];
inline auto getSorts() const -> const char** override { return sorts; };
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
void searchRequestFinished();
void searchRequestFailed(QString reason);
private:
void requestLogo(QString file, QString url);
private:
QList<IndexedPack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QString currentSearchTerm;
int currentSort = 0;
int nextSearchOffset = 0;
enum SearchState {
None,
CanPossiblyFetchMore,
ResetRequested,
Finished
} searchState = None;
NetJob::Ptr jobPtr;
QByteArray response;
}; };
} } // namespace FlameMod

View File

@ -34,224 +34,40 @@
*/ */
#include "FlameModPage.h" #include "FlameModPage.h"
#include "ui_FlameModPage.h" #include "ui_ModPage.h"
#include <QKeyEvent>
#include "Application.h"
#include "FlameModModel.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" #include "ui/dialogs/ModDownloadDialog.h"
FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
: QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), : ModPage(dialog, instance, new FlameAPI())
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()
{ {
ui->retranslateUi(this); listModel = new FlameMod::ListModel(this);
ui->packView->setModel(listModel);
// index is used to set the sorting with the flame api
ui->sortByBox->addItem(tr("Sort by Featured"));
ui->sortByBox->addItem(tr("Sort by Popularity"));
ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by Name"));
ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Downloads"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
} }
void FlameModPage::openedImpl() { auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
updateSelectionButton(); {
triggerSearch(); (void) loaderVer;
return ver.mcVersion.contains(mineVer);
} }
void FlameModPage::triggerSearch() { // I don't know why, but doing this on the parent class makes it so that
listModel->searchWithTerm(ui->searchEdit->text(), // other mod providers start loading before being selected, at least with
ui->sortByBox->currentIndex()); // my Qt, so we need to implement this in every derived class...
} auto FlameModPage::shouldDisplay() const -> bool { return true; }
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"));
}
}
void FlameModPage::onVersionSelectionChanged(QString data) {
if (data.isNull() || data.isEmpty()) {
selectedVersion = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void FlameModPage::onModSelected() {
auto &version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
dialog->addSelectedMod(current.name,
new ModDownloadTask(version.downloadUrl,
version.fileName, dialog->mods));
}
updateSelectionButton();
}

View File

@ -35,70 +35,26 @@
#pragma once #pragma once
#include <QWidget> #include "ui/pages/modplatform/ModPage.h"
#include "ui/pages/BasePage.h" #include "modplatform/flame/FlameAPI.h"
#include <Application.h>
#include "tasks/Task.h"
#include "modplatform/flame/FlameModIndex.h"
namespace Ui class FlameModPage : public ModPage {
{
class FlameModPage;
}
class ModDownloadDialog;
namespace FlameMod {
class ListModel;
}
class FlameModPage : public QWidget, public BasePage
{
Q_OBJECT Q_OBJECT
public: public:
explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
virtual ~FlameModPage(); ~FlameModPage() override = default;
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; inline auto displayName() const -> QString override { return "CurseForge"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
inline auto id() const -> QString override { return "curseforge"; }
inline auto helpPage() const -> QString override { return "Flame-platform"; }
bool eventFilter(QObject * watched, QEvent * event) override; inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
BaseInstance *m_instance; auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
private: auto shouldDisplay() const -> bool override;
void updateSelectionButton();
private slots:
void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onModSelected();
private:
Ui::FlameModPage *ui = nullptr;
ModDownloadDialog* dialog = nullptr;
FlameMod::ListModel* listModel = nullptr;
FlameMod::IndexedPack current;
int selectedVersion = -1;
}; };

View File

@ -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>

View File

@ -1,276 +1,25 @@
#include "ModrinthModel.h" #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 { namespace Modrinth {
ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) // NOLINTNEXTLINE(modernize-avoid-c-arrays)
const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{ {
Modrinth::loadIndexedPack(m, obj);
} }
ListModel::~ListModel() void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
} }
int ListModel::rowCount(const QModelIndex &parent) const auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{ {
return modpacks.size(); return obj.object().value("hits").toArray();
}
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));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job]
{
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo))
{
waitingCallbacks.value(logo)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, logo, job]
{
job->deleteLater();
emit logoFailed(logo);
});
job->start();
m_loadingLogos.append(logo);
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
requestLogo(logo, logoUrl);
}
}
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
{
return QAbstractListModel::flags(index);
}
bool ListModel::canFetchMore(const QModelIndex& parent) const
{
return searchState == CanPossiblyFetchMore;
}
void ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if(nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
performPaginatedSearch();
}
const char* sorts[5]{"relevance","downloads","follows","updated","newest"};
void ListModel::performPaginatedSearch()
{
QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
auto netJob = new NetJob("Modrinth::Search", APPLICATION->network());
auto searchUrl = QString(
"https://api.modrinth.com/v2/search?"
"offset=%1&"
"limit=25&"
"query=%2&"
"index=%3&"
"facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]"
)
.arg(nextSearchOffset)
.arg(currentSearchTerm)
.arg(sorts[currentSort])
.arg(hasFabric ? "fabric" : "forge")
.arg(mcVersion);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void ListModel::searchWithTerm(const QString &term, const int sort)
{
if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
return;
}
currentSearchTerm = term;
currentSort = sort;
if(jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
}
else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
}
void Modrinth::ListModel::searchRequestFinished()
{
jobPtr.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
QList<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();
}
void Modrinth::ListModel::searchRequestFailed(QString reason)
{
if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){
//409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!"));
//self-destruct
((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject();
}
jobPtr.reset();
if(searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
endResetModel();
nextSearchOffset = 0;
performPaginatedSearch();
} else {
searchState = Finished;
}
}
} }
} // namespace Modrinth

View File

@ -1,79 +1,25 @@
#pragma once #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" #include "ModrinthPage.h"
namespace Modrinth { namespace Modrinth {
class ListModel : public ModPlatform::ListModel {
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
Q_OBJECT Q_OBJECT
public: public:
ListModel(ModrinthPage *parent); ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){};
virtual ~ListModel(); ~ListModel() override = default;
int rowCount(const QModelIndex &parent) const override; private:
int columnCount(const QModelIndex &parent) const override; void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
QVariant data(const QModelIndex &index, int role) const override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool canFetchMore(const QModelIndex & parent) const override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
void fetchMore(const QModelIndex & parent) override;
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); // NOLINTNEXTLINE(modernize-avoid-c-arrays)
void searchWithTerm(const QString &term, const int sort); static const char* sorts[5];
inline auto getSorts() const -> const char** override { return sorts; };
private slots:
void performPaginatedSearch();
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
void searchRequestFinished();
void searchRequestFailed(QString reason);
private:
void requestLogo(QString file, QString url);
private:
QList<IndexedPack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QString currentSearchTerm;
int currentSort = 0;
int nextSearchOffset = 0;
enum SearchState {
None,
CanPossiblyFetchMore,
ResetRequested,
Finished
} searchState = None;
NetJob::Ptr jobPtr;
QByteArray response;
}; };
} } // namespace Modrinth

View File

@ -34,211 +34,38 @@
*/ */
#include "ModrinthPage.h" #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 "ModrinthModel.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance)
: QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), : ModPage(dialog, instance, new ModrinthAPI())
dialog(dialog) { {
ui->setupUi(this); listModel = new Modrinth::ListModel(this);
connect(ui->searchButton, &QPushButton::clicked, this, ui->packView->setModel(listModel);
&ModrinthPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
listModel = new Modrinth::ListModel(this);
ui->packView->setModel(listModel);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( // index is used to set the sorting with the modrinth api
Qt::ScrollBarAsNeeded); ui->sortByBox->addItem(tr("Sort by Relevence"));
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by newest"));
// index is used to set the sorting with the modrinth api // sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
ui->sortByBox->addItem(tr("Sort by Relevance")); // so it's best not to connect them in the parent's contructor...
ui->sortByBox->addItem(tr("Sort by Downloads")); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
ui->sortByBox->addItem(tr("Sort by Follows")); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
ui->sortByBox->addItem(tr("Sort by last updated")); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged);
ui->sortByBox->addItem(tr("Sort by newest")); connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected);
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; } auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
{
bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer);
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; } // 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
void ModrinthPage::retranslate() { // my Qt, so we need to implement this in every derived class...
ui->retranslateUi(this); auto ModrinthPage::shouldDisplay() const -> bool { return true; }
}
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"));
}
}
void ModrinthPage::onVersionSelectionChanged(QString data) {
if (data.isNull() || data.isEmpty()) {
selectedVersion = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ModrinthPage::onModSelected() {
auto &version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
dialog->addSelectedMod(current.name,
new ModDownloadTask(version.downloadUrl,
version.fileName, dialog->mods));
}
updateSelectionButton();
}

View File

@ -35,70 +35,26 @@
#pragma once #pragma once
#include <QWidget> #include "ui/pages/modplatform/ModPage.h"
#include "ui/pages/BasePage.h" #include "modplatform/modrinth/ModrinthAPI.h"
#include <Application.h>
#include "tasks/Task.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
namespace Ui class ModrinthPage : public ModPage {
{
class ModrinthPage;
}
class ModDownloadDialog;
namespace Modrinth {
class ListModel;
}
class ModrinthPage : public QWidget, public BasePage
{
Q_OBJECT Q_OBJECT
public: public:
explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance);
virtual ~ModrinthPage(); ~ModrinthPage() override = default;
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; inline auto displayName() const -> QString override { return "Modrinth"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; }
inline auto helpPage() const -> QString override { return "Modrinth-platform"; }
bool eventFilter(QObject * watched, QEvent * event) override; inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
BaseInstance *m_instance; auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
private: auto shouldDisplay() const -> bool override;
void updateSelectionButton();
private slots:
void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onModSelected();
private:
Ui::ModrinthPage *ui = nullptr;
ModDownloadDialog* dialog = nullptr;
Modrinth::ListModel* listModel = nullptr;
Modrinth::IndexedPack current;
int selectedVersion = -1;
}; };