refactor: generalize mod models and APIs to resources

Firstly, this abstract away behavior in the mod download models that can
also be applied to other types of resources into a superclass, allowing
other resource types to be implemented without so much code duplication.

For that, this also generalizes the APIs used (currently, ModrinthAPI
and FlameAPI) to be able to make requests to other types of resources.

It also does a general cleanup of both of those. In particular, this
makes use of std::optional instead of invalid values for errors and,
well, optional values :p

This is a squash of some commits that were becoming too interlaced
together to be cleanly separated.

Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
flow 2022-11-25 09:23:46 -03:00
parent b937d33436
commit 6a18079953
No known key found for this signature in database
GPG Key ID: 8D0F221F0A59F469
68 changed files with 1965 additions and 1520 deletions

View File

@ -38,9 +38,9 @@ set(CORE_SOURCES
InstanceImportTask.h InstanceImportTask.h
InstanceImportTask.cpp InstanceImportTask.cpp
# Mod downloading task # Resource downloading task
ModDownloadTask.h ResourceDownloadTask.h
ModDownloadTask.cpp ResourceDownloadTask.cpp
# Use tracking separate from memory management # Use tracking separate from memory management
Usable.h Usable.h
@ -473,7 +473,7 @@ set(API_SOURCES
modplatform/ModIndex.h modplatform/ModIndex.h
modplatform/ModIndex.cpp modplatform/ModIndex.cpp
modplatform/ModAPI.h modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp modplatform/EnsureMetadataTask.cpp
@ -484,8 +484,8 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkResourceAPI.h
modplatform/helpers/NetworkModAPI.cpp modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.h
@ -771,6 +771,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/ResourcePage.cpp
ui/pages/modplatform/ResourcePage.h
ui/pages/modplatform/ResourceModel.cpp
ui/pages/modplatform/ResourceModel.h
ui/pages/modplatform/ModPage.cpp ui/pages/modplatform/ModPage.cpp
ui/pages/modplatform/ModPage.h ui/pages/modplatform/ModPage.h
ui/pages/modplatform/ModModel.cpp ui/pages/modplatform/ModModel.cpp
@ -803,10 +808,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlameModel.h ui/pages/modplatform/flame/FlameModel.h
ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h ui/pages/modplatform/flame/FlamePage.h
ui/pages/modplatform/flame/FlameModModel.cpp ui/pages/modplatform/flame/FlameResourceModels.cpp
ui/pages/modplatform/flame/FlameModModel.h ui/pages/modplatform/flame/FlameResourceModels.h
ui/pages/modplatform/flame/FlameModPage.cpp ui/pages/modplatform/flame/FlameResourcePages.cpp
ui/pages/modplatform/flame/FlameModPage.h ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h ui/pages/modplatform/modrinth/ModrinthPage.h
@ -821,10 +826,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h ui/pages/modplatform/ImportPage.h
ui/pages/modplatform/modrinth/ModrinthModModel.cpp ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthModModel.h ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthModPage.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
ui/pages/modplatform/modrinth/ModrinthModPage.h ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs # GUI - dialogs
ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.cpp
@ -869,6 +874,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/VersionSelectDialog.h ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h ui/dialogs/SkinUploadDialog.h
ui/dialogs/ResourceDownloadDialog.cpp
ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.cpp
ui/dialogs/ModDownloadDialog.h ui/dialogs/ModDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.cpp
@ -965,7 +972,7 @@ qt_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/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/flame/FlamePage.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

View File

@ -1,72 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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 "ModDownloadTask.h"
#include "Application.h"
#include "minecraft/mod/ModFolderModel.h"
ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed)
: m_mod(mod), m_mod_version(version), mods(mods)
{
if (is_indexed) {
m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
addTask(m_update_task);
}
m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl));
m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename())));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
addTask(m_filesNetJob);
}
void ModDownloadTask::downloadSucceeded()
{
m_filesNetJob.reset();
auto name = std::get<0>(to_delete);
auto filename = std::get<1>(to_delete);
if (!name.isEmpty() && filename != m_mod_version.fileName) {
mods->uninstallMod(filename, true);
}
}
void ModDownloadTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
{
emit progress(current, total);
}
// This indirection is done so that we don't delete a mod before being sure it was
// downloaded successfully!
void ModDownloadTask::hasOldMod(QString name, QString filename)
{
to_delete = {name, filename};
}

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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 "ResourceDownloadTask.h"
#include "Application.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
{
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
addTask(m_update_task);
}
m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
addTask(m_filesNetJob);
}
void ResourceDownloadTask::downloadSucceeded()
{
m_filesNetJob.reset();
auto name = std::get<0>(to_delete);
auto filename = std::get<1>(to_delete);
if (!name.isEmpty() && filename != m_pack_version.fileName) {
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model)
model->uninstallMod(filename, true);
else
m_pack_model->uninstallResource(filename);
}
}
void ResourceDownloadTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
{
emit progress(current, total);
}
// This indirection is done so that we don't delete a mod before being sure it was
// downloaded successfully!
void ResourceDownloadTask::hasOldResource(QString name, QString filename)
{
to_delete = { name, filename };
}

View File

@ -25,18 +25,18 @@
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h"
class ModFolderModel; class ResourceFolderModel;
class ModDownloadTask : public SequentialTask { class ResourceDownloadTask : public SequentialTask {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true); explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
const QString& getFilename() const { return m_mod_version.fileName; } const QString& getFilename() const { return m_pack_version.fileName; }
private: private:
ModPlatform::IndexedPack m_mod; ModPlatform::IndexedPack m_pack;
ModPlatform::IndexedVersion m_mod_version; ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ModFolderModel> mods; const std::shared_ptr<ResourceFolderModel> m_pack_model;
NetJob::Ptr m_filesNetJob; NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task; LocalModUpdateTask::Ptr m_update_task;
@ -50,7 +50,7 @@ private:
std::tuple<QString, QString> to_delete {"", ""}; std::tuple<QString, QString> to_delete {"", ""};
private slots: private slots:
void hasOldMod(QString name, QString filename); void hasOldResource(QString name, QString filename);
}; };

View File

@ -55,12 +55,13 @@
#include "PackProfile_p.h" #include "PackProfile_p.h"
#include "ComponentUpdateTask.h" #include "ComponentUpdateTask.h"
#include "modplatform/ModAPI.h" #include "Application.h"
#include "modplatform/ResourceAPI.h"
static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{ static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ModAPI::Forge}, {"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ModAPI::Fabric}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ModAPI::Quilt} {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
}; };
PackProfile::PackProfile(MinecraftInstance * instance) PackProfile::PackProfile(MinecraftInstance * instance)
@ -1066,19 +1067,22 @@ void PackProfile::disableInteraction(bool disable)
} }
} }
ModAPI::ModLoaderTypes PackProfile::getModLoaders() std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
{ {
ModAPI::ModLoaderTypes result = ModAPI::Unspecified; ResourceAPI::ModLoaderTypes result;
bool has_any_loader = false;
QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping); QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping);
while (i.hasNext()) while (i.hasNext()) {
{
i.next(); i.next();
Component* c = getComponent(i.key()); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) {
if (c != nullptr && c->isEnabled()) {
result |= i.value(); result |= i.value();
has_any_loader = true;
} }
} }
if (!has_any_loader)
return {};
return result; return result;
} }

View File

@ -49,7 +49,7 @@
#include "BaseVersion.h" #include "BaseVersion.h"
#include "MojangDownloadInfo.h" #include "MojangDownloadInfo.h"
#include "net/Mode.h" #include "net/Mode.h"
#include "modplatform/ModAPI.h" #include "modplatform/ResourceAPI.h"
class MinecraftInstance; class MinecraftInstance;
struct PackProfileData; struct PackProfileData;
@ -145,7 +145,7 @@ public:
// todo(merged): is this the best approach // todo(merged): is this the best approach
void appendComponent(ComponentPtr component); void appendComponent(ComponentPtr component);
ModAPI::ModLoaderTypes getModLoaders(); std::optional<ResourceAPI::ModLoaderTypes> getModLoaders();
private: private:
void scheduleSave(); void scheduleSave();

View File

@ -1,18 +1,18 @@
#pragma once #pragma once
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "modplatform/ModAPI.h" #include "modplatform/ResourceAPI.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "tasks/Task.h" #include "tasks/Task.h"
class ModDownloadTask; class ResourceDownloadTask;
class ModFolderModel; class ModFolderModel;
class CheckUpdateTask : public Task { class CheckUpdateTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
struct UpdatableMod { struct UpdatableMod {
@ -21,11 +21,11 @@ class CheckUpdateTask : public Task {
QString old_version; QString old_version;
QString new_version; QString new_version;
QString changelog; QString changelog;
ModPlatform::Provider provider; ModPlatform::ResourceProvider provider;
ModDownloadTask* download; ResourceDownloadTask* download;
public: public:
UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t) UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, ResourceDownloadTask* t)
: name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
{} {}
}; };
@ -44,7 +44,7 @@ class CheckUpdateTask : public Task {
protected: protected:
QList<Mod*>& m_mods; QList<Mod*>& m_mods;
std::list<Version>& m_game_versions; std::list<Version>& m_game_versions;
ModAPI::ModLoaderTypes m_loaders; std::optional<ResourceAPI::ModLoaderTypes> m_loaders;
std::shared_ptr<ModFolderModel> m_mods_folder; std::shared_ptr<ModFolderModel> m_mods_folder;
std::vector<UpdatableMod> m_updatable; std::vector<UpdatableMod> m_updatable;

View File

@ -20,7 +20,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api; static ModrinthAPI modrinth_api;
static FlameAPI flame_api; static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr) : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
{ {
auto hash_task = createNewHash(mod); auto hash_task = createNewHash(mod);
@ -31,7 +31,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider
hash_task->start(); hash_task->start();
} }
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov) EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{ {
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10); m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
@ -110,10 +110,10 @@ void EnsureMetadataTask::executeTask()
NetJob::Ptr version_task; NetJob::Ptr version_task;
switch (m_provider) { switch (m_provider) {
case (ModPlatform::Provider::MODRINTH): case (ModPlatform::ResourceProvider::MODRINTH):
version_task = modrinthVersionsTask(); version_task = modrinthVersionsTask();
break; break;
case (ModPlatform::Provider::FLAME): case (ModPlatform::ResourceProvider::FLAME):
version_task = flameVersionsTask(); version_task = flameVersionsTask();
break; break;
} }
@ -130,10 +130,10 @@ void EnsureMetadataTask::executeTask()
NetJob::Ptr project_task; NetJob::Ptr project_task;
switch (m_provider) { switch (m_provider) {
case (ModPlatform::Provider::MODRINTH): case (ModPlatform::ResourceProvider::MODRINTH):
project_task = modrinthProjectsTask(); project_task = modrinthProjectsTask();
break; break;
case (ModPlatform::Provider::FLAME): case (ModPlatform::ResourceProvider::FLAME):
project_task = flameProjectsTask(); project_task = flameProjectsTask();
break; break;
} }
@ -212,7 +212,7 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
{ {
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto* response = new QByteArray(); auto* response = new QByteArray();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);

View File

@ -14,8 +14,8 @@ class EnsureMetadataTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default; ~EnsureMetadataTask() = default;
@ -57,7 +57,7 @@ class EnsureMetadataTask : public Task {
private: private:
QHash<QString, Mod*> m_mods; QHash<QString, Mod*> m_mods;
QDir m_index_dir; QDir m_index_dir;
ModPlatform::Provider m_provider; ModPlatform::ResourceProvider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions; QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
ConcurrentTask* m_hashing_task; ConcurrentTask* m_hashing_task;

View File

@ -1,118 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QString>
#include <QList>
#include <list>
#include "../Version.h"
#include "net/NetJob.h"
namespace ModPlatform {
class ListModel;
struct IndexedPack;
}
class ModAPI {
protected:
using CallerType = ModPlatform::ListModel;
public:
virtual ~ModAPI() = default;
enum ModLoaderType {
Unspecified = 0,
Forge = 1 << 0,
Cauldron = 1 << 1,
LiteLoader = 1 << 2,
Fabric = 1 << 3,
Quilt = 1 << 4
};
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
struct SearchArgs {
int offset;
QString search;
QString sorting;
ModLoaderTypes loaders;
std::list<Version> versions;
};
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0;
virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
struct VersionSearchArgs {
QString addonId;
std::list<Version> mcVersions;
ModLoaderTypes loaders;
};
virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0;
static auto getModLoaderString(ModLoaderType type) -> const QString {
switch (type) {
case Unspecified:
break;
case Forge:
return "forge";
case Cauldron:
return "cauldron";
case LiteLoader:
return "liteloader";
case Fabric:
return "fabric";
case Quilt:
return "quilt";
}
return "";
}
protected:
inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
{
QString s;
for(auto& ver : mcVersions){
s += QString("\"%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); //remove last comma
return s;
}
};

View File

@ -24,47 +24,47 @@
namespace ModPlatform { namespace ModPlatform {
auto ProviderCapabilities::name(Provider p) -> const char* auto ProviderCapabilities::name(ResourceProvider p) -> const char*
{ {
switch (p) { switch (p) {
case Provider::MODRINTH: case ResourceProvider::MODRINTH:
return "modrinth"; return "modrinth";
case Provider::FLAME: case ResourceProvider::FLAME:
return "curseforge"; return "curseforge";
} }
return {}; return {};
} }
auto ProviderCapabilities::readableName(Provider p) -> QString auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
{ {
switch (p) { switch (p) {
case Provider::MODRINTH: case ResourceProvider::MODRINTH:
return "Modrinth"; return "Modrinth";
case Provider::FLAME: case ResourceProvider::FLAME:
return "CurseForge"; return "CurseForge";
} }
return {}; return {};
} }
auto ProviderCapabilities::hashType(Provider p) -> QStringList auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
{ {
switch (p) { switch (p) {
case Provider::MODRINTH: case ResourceProvider::MODRINTH:
return { "sha512", "sha1" }; return { "sha512", "sha1" };
case Provider::FLAME: case ResourceProvider::FLAME:
// Try newer formats first, fall back to old format // Try newer formats first, fall back to old format
return { "sha1", "md5", "murmur2" }; return { "sha1", "md5", "murmur2" };
} }
return {}; return {};
} }
auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString
{ {
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1; QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) { switch (p) {
case Provider::MODRINTH: { case ResourceProvider::MODRINTH: {
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512; algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break; break;
} }
case Provider::FLAME: case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5; algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break; break;
} }

View File

@ -28,17 +28,16 @@ class QIODevice;
namespace ModPlatform { namespace ModPlatform {
enum class Provider { enum class ResourceProvider { MODRINTH, FLAME };
MODRINTH,
FLAME enum class ResourceType { MOD, RESOURCE_PACK };
};
class ProviderCapabilities { class ProviderCapabilities {
public: public:
auto name(Provider) -> const char*; auto name(ResourceProvider) -> const char*;
auto readableName(Provider) -> QString; auto readableName(ResourceProvider) -> QString;
auto hashType(Provider) -> QStringList; auto hashType(ResourceProvider) -> QStringList;
auto hash(Provider, QIODevice*, QString type = "") -> QString; auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
}; };
struct ModpackAuthor { struct ModpackAuthor {
@ -81,7 +80,7 @@ struct ExtraPackData {
struct IndexedPack { struct IndexedPack {
QVariant addonId; QVariant addonId;
Provider provider; ResourceProvider provider;
QString name; QString name;
QString slug; QString slug;
QString description; QString description;
@ -101,4 +100,4 @@ struct IndexedPack {
} // namespace ModPlatform } // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack) Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
Q_DECLARE_METATYPE(ModPlatform::Provider) Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)

View File

@ -0,0 +1,149 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QList>
#include <QString>
#include <list>
#include "../Version.h"
#include "modplatform/ModIndex.h"
#include "net/NetJob.h"
/* Simple class with a common interface for interacting with APIs */
class ResourceAPI {
public:
virtual ~ResourceAPI() = default;
enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 };
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
struct SearchArgs {
ModPlatform::ResourceType type{};
int offset = 0;
std::optional<QString> search;
std::optional<QString> sorting;
std::optional<ModLoaderTypes> loaders;
std::optional<std::list<Version> > versions;
};
struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed;
std::function<void(QString const& reason, int network_error_code)> on_fail;
std::function<void()> on_abort;
};
struct VersionSearchArgs {
QString addonId;
std::optional<std::list<Version> > mcVersions;
std::optional<ModLoaderTypes> loaders;
};
struct VersionSearchCallbacks {
std::function<void(QJsonDocument&, QString)> on_succeed;
};
struct ProjectInfoArgs {
ModPlatform::IndexedPack& pack;
void operator=(ProjectInfoArgs other) { pack = other.pack; }
};
struct ProjectInfoCallbacks {
std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> on_succeed;
};
public slots:
[[nodiscard]] virtual NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
[[nodiscard]] virtual NetJob::Ptr getProject(QString addonId, QByteArray* response) const
{
qWarning() << "TODO";
return nullptr;
}
[[nodiscard]] virtual NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const
{
qWarning() << "TODO";
return nullptr;
}
[[nodiscard]] virtual NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
[[nodiscard]] virtual NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const
{
qWarning() << "TODO";
return nullptr;
}
static auto getModLoaderString(ModLoaderType type) -> const QString
{
switch (type) {
case Forge:
return "forge";
case Cauldron:
return "cauldron";
case LiteLoader:
return "liteloader";
case Fabric:
return "fabric";
case Quilt:
return "quilt";
default:
break;
}
return "";
}
protected:
[[nodiscard]] inline QString debugName() const { return "External resource API"; }
[[nodiscard]] inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
{
QString s;
for (auto& ver : mcVersions) {
s += QString("\"%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); // remove last comma
return s;
}
};

View File

@ -106,13 +106,19 @@ auto FlameAPI::getModDescription(int modId) -> QString
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{ {
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
return {};
auto versions_url = versions_url_optional.value();
QEventLoop loop; QEventLoop loop;
auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
auto response = new QByteArray(); auto response = new QByteArray();
ModPlatform::IndexedVersion ver; ModPlatform::IndexedVersion ver;
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] { QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{}; QJsonParseError parse_error{};
@ -161,7 +167,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver; return ver;
} }
auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* NetJob::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
{ {
auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
@ -178,13 +184,13 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const ->
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob; return netJob;
} }
auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob* NetJob::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
{ {
auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network()); auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network());
@ -201,7 +207,7 @@ auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob; return netJob;

View File

@ -1,21 +1,21 @@
#pragma once #pragma once
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkModAPI.h" #include "modplatform/helpers/NetworkResourceAPI.h"
class FlameAPI : public NetworkModAPI { class FlameAPI : public NetworkResourceAPI {
public: public:
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString; auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString; auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*; NetJob::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
NetJob::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
private: private:
inline auto getSortFieldInt(QString sortString) const -> int static int getSortFieldInt(QString const& sortString)
{ {
return sortString == "Featured" ? 1 return sortString == "Featured" ? 1
: sortString == "Popularity" ? 2 : sortString == "Popularity" ? 2
@ -28,48 +28,16 @@ class FlameAPI : public NetworkModAPI {
: 1; : 1;
} }
private: static int getClassId(ModPlatform::ResourceType type)
inline auto getModSearchURL(SearchArgs& args) const -> QString override
{ {
auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); switch (type) {
default:
case ModPlatform::ResourceType::MOD:
return 6;
}
}
return QString( static int getMappedModLoader(ModLoaderTypes loaders)
"https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
"classId=6&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
"sortField=%3&"
"sortOrder=desc&"
"modLoaderType=%4&"
"%5")
.arg(args.offset)
.arg(args.search)
.arg(getSortFieldInt(args.sorting))
.arg(getMappedModLoader(args.loaders))
.arg(gameVersionStr);
};
inline auto getModInfoURL(QString& id) const -> QString override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
};
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders));
return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
.arg(args.addonId)
.arg(gameVersionQuery)
.arg(modLoaderQuery);
};
public:
static auto getMappedModLoader(const ModLoaderTypes loaders) -> int
{ {
// https://docs.curseforge.com/?http#tocS_ModLoaderType // https://docs.curseforge.com/?http#tocS_ModLoaderType
if (loaders & Forge) if (loaders & Forge)
@ -81,4 +49,43 @@ class FlameAPI : public NetworkModAPI {
return 4; // Quilt would probably be 5 return 4; // Quilt would probably be 5
return 0; return 0;
} }
private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
QStringList get_arguments;
get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
get_arguments.append(QString("index=%1").arg(args.offset));
get_arguments.append("pageSize=25");
if (args.search.has_value())
get_arguments.append(QString("searchFilter=%1").arg(args.search.value()));
if (args.sorting.has_value())
get_arguments.append(QString("sortField=%1").arg(getSortFieldInt(args.sorting.value())));
get_arguments.append("sortOrder=desc");
if (args.loaders.has_value())
get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
get_arguments.append(gameVersionStr);
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');
};
[[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
};
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.addonId)};
QStringList get_parameters;
if (args.mcVersions.has_value())
get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString()));
if (args.loaders.has_value())
get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
return url + get_parameters.join('&');
};
}; };

View File

@ -7,7 +7,10 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "ModDownloadTask.h" #include "ResourceDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
static FlameAPI api; static FlameAPI api;
@ -160,7 +163,7 @@ void FlameCheckUpdate::executeTask()
for (auto& author : mod->authors()) for (auto& author : mod->authors())
pack.authors.append({ author }); pack.authors.append({ author });
pack.description = mod->description(); pack.description = mod->description();
pack.provider = ModPlatform::Provider::FLAME; pack.provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version(); auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@ -168,10 +171,10 @@ void FlameCheckUpdate::executeTask()
old_version = current_ver.version; old_version = current_ver.version;
} }
auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); auto download_task = new ResourceDownloadTask(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version, m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::Provider::FLAME, download_task); ModPlatform::ResourceProvider::FLAME, download_task);
} }
} }

View File

@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask {
Q_OBJECT Q_OBJECT
public: public:
FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{} {}

View File

@ -183,7 +183,7 @@ bool FlameCreationTask::updateInstance()
QEventLoop loop; QEventLoop loop;
connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { connect(job.get(), &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
// Parse the API response // Parse the API response
QJsonParseError parse_error{}; QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); auto doc = QJsonDocument::fromJson(*raw_response, &parse_error);
@ -225,7 +225,7 @@ bool FlameCreationTask::updateInstance()
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
} }
}); });
connect(job, &NetJob::finished, &loop, &QEventLoop::quit); connect(job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job; m_process_update_file_info_job = job;
job->start(); job->start();

View File

@ -86,7 +86,7 @@ class FlameCreationTask final : public InstanceCreationTask {
Flame::Manifest m_pack; Flame::Manifest m_pack;
// Handle to allow aborting // Handle to allow aborting
NetJob* m_process_update_file_info_job = nullptr; NetJob::Ptr m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr; NetJob::Ptr m_files_job = nullptr;
QString m_managed_id, m_managed_version_id; QString m_managed_id, m_managed_version_id;

View File

@ -11,7 +11,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
pack.addonId = Json::requireInteger(obj, "id"); pack.addonId = Json::requireInteger(obj, "id");
pack.provider = ModPlatform::Provider::FLAME; pack.provider = ModPlatform::ResourceProvider::FLAME;
pack.name = Json::requireString(obj, "name"); pack.name = Json::requireString(obj, "name");
pack.slug = Json::requireString(obj, "slug"); pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
@ -127,7 +127,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
auto hash_list = Json::ensureArray(obj, "hashes"); auto hash_list = Json::ensureArray(obj, "hashes");
for (auto h : hash_list) { for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h); auto hash_entry = Json::ensureObject(h);
auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
if (hash_types.contains(hash_algo)) { if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value"); file.hash = Json::requireString(hash_entry, "value");

View File

@ -12,12 +12,12 @@ namespace Hashing {
static ModPlatform::ProviderCapabilities ProviderCaps; static ModPlatform::ProviderCapabilities ProviderCaps;
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider) Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
{ {
switch (provider) { switch (provider) {
case ModPlatform::Provider::MODRINTH: case ModPlatform::ResourceProvider::MODRINTH:
return createModrinthHasher(file_path); return createModrinthHasher(file_path);
case ModPlatform::Provider::FLAME: case ModPlatform::ResourceProvider::FLAME:
return createFlameHasher(file_path); return createFlameHasher(file_path);
default: default:
qCritical() << "[Hashing]" qCritical() << "[Hashing]"
@ -36,12 +36,12 @@ Hasher::Ptr createFlameHasher(QString file_path)
return new FlameHasher(file_path); return new FlameHasher(file_path);
} }
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider) Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
{ {
return new BlockedModHasher(file_path, provider); return new BlockedModHasher(file_path, provider);
} }
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type) Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type)
{ {
auto hasher = new BlockedModHasher(file_path, provider); auto hasher = new BlockedModHasher(file_path, provider);
hasher->useHashType(type); hasher->useHashType(type);
@ -62,8 +62,8 @@ void ModrinthHasher::executeTask()
return; return;
} }
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type); m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
file.close(); file.close();
@ -92,7 +92,7 @@ void FlameHasher::executeTask()
} }
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider) BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
: Hasher(file_path), provider(provider) { : Hasher(file_path), provider(provider) {
setObjectName(QString("BlockedModHasher: %1").arg(file_path)); setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first(); hash_type = ProviderCaps.hashType(provider).first();

View File

@ -42,21 +42,21 @@ class ModrinthHasher : public Hasher {
class BlockedModHasher : public Hasher { class BlockedModHasher : public Hasher {
public: public:
BlockedModHasher(QString file_path, ModPlatform::Provider provider); BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
void executeTask() override; void executeTask() override;
QStringList getHashTypes(); QStringList getHashTypes();
bool useHashType(QString type); bool useHashType(QString type);
private: private:
ModPlatform::Provider provider; ModPlatform::ResourceProvider provider;
QString hash_type; QString hash_type;
}; };
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider); Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createFlameHasher(QString file_path); Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path); Hasher::Ptr createModrinthHasher(QString file_path);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider); Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type); Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
} // namespace Hashing } // namespace Hashing

View File

@ -1,97 +0,0 @@
#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::aborted, caller, &CallerType::searchRequestAborted);
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::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback)
{
auto response = new QByteArray();
auto job = getProject(pack.addonId.toString(), response);
QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callback(doc, pack);
});
job->start();
}
void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const
{
auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network());
auto response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callback(doc, args.addonId);
});
QObject::connect(netJob, &NetJob::finished, [response, netJob] {
netJob->deleteLater();
delete response;
});
netJob->start();
}
auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
{
auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
auto searchUrl = getModInfoURL(addonId);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
QObject::connect(netJob, &NetJob::finished, [response, netJob] {
netJob->deleteLater();
delete response;
});
return netJob;
}

View File

@ -1,17 +0,0 @@
#pragma once
#include "modplatform/ModAPI.h"
class NetworkModAPI : public ModAPI {
public:
void searchMods(CallerType* caller, SearchArgs&& args) const override;
void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
protected:
virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
virtual auto getModInfoURL(QString& id) const -> QString = 0;
virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0;
};

View File

@ -0,0 +1,124 @@
#include "NetworkResourceAPI.h"
#include "Application.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
NetJob::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const
{
auto search_url_optional = getSearchURL(args);
if (!search_url_optional.has_value()) {
callbacks.on_fail("Failed to create search URL", -1);
return nullptr;
}
auto search_url = search_url_optional.value();
auto response = new QByteArray();
auto netJob = new NetJob(QString("%1::Search").arg(debugName()), APPLICATION->network());
netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response));
QObject::connect(netJob, &NetJob::succeeded, [=]{
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
callbacks.on_fail(parse_error.errorString(), -1);
return;
}
callbacks.on_succeed(doc);
});
QObject::connect(netJob, &NetJob::failed, [=](QString reason){
int network_error_code = -1;
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
callbacks.on_fail(reason, network_error_code);
});
QObject::connect(netJob, &NetJob::aborted, [=]{
callbacks.on_abort();
});
return netJob;
}
NetJob::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
{
auto response = new QByteArray();
auto job = getProject(args.pack.addonId.toString(), response);
QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.pack);
});
return job;
}
NetJob::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
return nullptr;
auto versions_url = versions_url_optional.value();
auto netJob = new NetJob(QString("%1::Versions").arg(args.addonId), APPLICATION->network());
auto response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob, &NetJob::succeeded, [=] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
callbacks.on_succeed(doc, args.addonId);
});
QObject::connect(netJob, &NetJob::finished, [response] {
delete response;
});
return netJob;
}
NetJob::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
return nullptr;
auto project_url = project_url_optional.value();
auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response));
QObject::connect(netJob, &NetJob::finished, [response] {
delete response;
});
return netJob;
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "modplatform/ResourceAPI.h"
class NetworkResourceAPI : public ResourceAPI {
public:
NetJob::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
NetJob::Ptr getProject(QString addonId, QByteArray* response) const override;
NetJob::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
NetJob::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
protected:
[[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
[[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
[[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
};

View File

@ -37,21 +37,24 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format
auto ModrinthAPI::latestVersion(QString hash, auto ModrinthAPI::latestVersion(QString hash,
QString hash_format, QString hash_format,
std::list<Version> mcVersions, std::optional<std::list<Version>> mcVersions,
ModLoaderTypes loaders, std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> NetJob::Ptr QByteArray* response) -> NetJob::Ptr
{ {
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
QJsonObject body_obj; QJsonObject body_obj;
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); if (loaders.has_value())
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
if (mcVersions.has_value()) {
QStringList game_versions; QStringList game_versions;
for (auto& ver : mcVersions) { for (auto& ver : mcVersions.value()) {
game_versions.append(ver.toString()); game_versions.append(ver.toString());
} }
Json::writeStringList(body_obj, "game_versions", game_versions); Json::writeStringList(body_obj, "game_versions", game_versions);
}
QJsonDocument body(body_obj); QJsonDocument body(body_obj);
auto body_raw = body.toJson(); auto body_raw = body.toJson();
@ -66,8 +69,8 @@ auto ModrinthAPI::latestVersion(QString hash,
auto ModrinthAPI::latestVersions(const QStringList& hashes, auto ModrinthAPI::latestVersions(const QStringList& hashes,
QString hash_format, QString hash_format,
std::list<Version> mcVersions, std::optional<std::list<Version>> mcVersions,
ModLoaderTypes loaders, std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> NetJob::Ptr QByteArray* response) -> NetJob::Ptr
{ {
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
@ -77,13 +80,16 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes,
Json::writeStringList(body_obj, "hashes", hashes); Json::writeStringList(body_obj, "hashes", hashes);
Json::writeString(body_obj, "algorithm", hash_format); Json::writeString(body_obj, "algorithm", hash_format);
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); if (loaders.has_value())
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
if (mcVersions.has_value()) {
QStringList game_versions; QStringList game_versions;
for (auto& ver : mcVersions) { for (auto& ver : mcVersions.value()) {
game_versions.append(ver.toString()); game_versions.append(ver.toString());
} }
Json::writeStringList(body_obj, "game_versions", game_versions); Json::writeStringList(body_obj, "game_versions", game_versions);
}
QJsonDocument body(body_obj); QJsonDocument body(body_obj);
auto body_raw = body.toJson(); auto body_raw = body.toJson();
@ -95,7 +101,7 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes,
return netJob; return netJob;
} }
auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* NetJob::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const
{ {
auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
auto searchUrl = getMultipleModInfoURL(addonIds); auto searchUrl = getMultipleModInfoURL(addonIds);

View File

@ -19,13 +19,12 @@
#pragma once #pragma once
#include "BuildConfig.h" #include "BuildConfig.h"
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/helpers/NetworkModAPI.h" #include "modplatform/helpers/NetworkResourceAPI.h"
#include <QDebug> #include <QDebug>
class ModrinthAPI : public NetworkModAPI { class ModrinthAPI : public NetworkResourceAPI {
public: public:
auto currentVersion(QString hash, auto currentVersion(QString hash,
QString hash_format, QString hash_format,
@ -37,17 +36,17 @@ class ModrinthAPI : public NetworkModAPI {
auto latestVersion(QString hash, auto latestVersion(QString hash,
QString hash_format, QString hash_format,
std::list<Version> mcVersions, std::optional<std::list<Version>> mcVersions,
ModLoaderTypes loaders, std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> NetJob::Ptr; QByteArray* response) -> NetJob::Ptr;
auto latestVersions(const QStringList& hashes, auto latestVersions(const QStringList& hashes,
QString hash_format, QString hash_format,
std::list<Version> mcVersions, std::optional<std::list<Version>> mcVersions,
ModLoaderTypes loaders, std::optional<ModLoaderTypes> loaders,
QByteArray* response) -> NetJob::Ptr; QByteArray* response) -> NetJob::Ptr;
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
public: public:
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
@ -55,15 +54,13 @@ class ModrinthAPI : public NetworkModAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{ {
QStringList l; QStringList l;
for (auto loader : {Forge, Fabric, Quilt}) for (auto loader : {Forge, Fabric, Quilt}) {
{ if (types & loader) {
if ((types & loader) || types == Unspecified) l << getModLoaderString(loader);
{
l << ModAPI::getModLoaderString(loader);
} }
} }
if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there
l << ModAPI::getModLoaderString(Fabric); l << getModLoaderString(Fabric);
return l; return l;
} }
@ -78,28 +75,54 @@ class ModrinthAPI : public NetworkModAPI {
} }
private: private:
inline auto getModSearchURL(SearchArgs& args) const -> QString override [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
{ {
if (!validateModLoaders(args.loaders)) { switch (type) {
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; case ModPlatform::ResourceType::MOD:
return ""; return "mod";
default:
qWarning() << "Invalid resource type for Modrinth API!";
break;
} }
return QString(BuildConfig.MODRINTH_PROD_URL + return "";
"/search?" }
"offset=%1&" [[nodiscard]] QString createFacets(SearchArgs const& args) const
"limit=25&" {
"query=%2&" QStringList facets_list;
"index=%3&"
"facets=[[%4],%5[\"project_type:mod\"]]") if (args.loaders.has_value())
.arg(args.offset) facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
.arg(args.search) if (args.versions.has_value())
.arg(args.sorting) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
.arg(getModLoaderFilters(args.loaders)) facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
.arg(getGameVersionsArray(args.versions));
return QString("[%1]").arg(facets_list.join(','));
}
public:
[[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> override
{
if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) {
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
return {};
}
}
QStringList get_arguments;
get_arguments.append(QString("offset=%1").arg(args.offset));
get_arguments.append(QString("limit=25"));
if (args.search.has_value())
get_arguments.append(QString("query=%1").arg(args.search.value()));
if (args.sorting.has_value())
get_arguments.append(QString("index=%1").arg(args.sorting.value()));
get_arguments.append(QString("facets=%1").arg(createFacets(args)));
return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&');
}; };
inline auto getModInfoURL(QString& id) const -> QString override inline auto getInfoURL(QString const& id) const -> std::optional<QString> override
{ {
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
}; };
@ -109,15 +132,16 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
}; };
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> override
{ {
return QString(BuildConfig.MODRINTH_PROD_URL + QStringList get_arguments;
"/project/%1/version?" if (args.mcVersions.has_value())
"game_versions=[%2]&" get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value())));
"loaders=[\"%3\"]") if (args.loaders.has_value())
.arg(args.addonId, get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
getGameVersionsString(args.mcVersions),
getModLoaderStrings(args.loaders).join("\",\"")); return QString("%1/project/%2/version%3%4")
.arg(BuildConfig.MODRINTH_PROD_URL, args.addonId, get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
}; };
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
@ -127,12 +151,12 @@ class ModrinthAPI : public NetworkModAPI {
s += QString("\"versions:%1\",").arg(ver.toString()); s += QString("\"versions:%1\",").arg(ver.toString());
} }
s.remove(s.length() - 1, 1); //remove last comma s.remove(s.length() - 1, 1); //remove last comma
return s.isEmpty() ? QString() : QString("[%1],").arg(s); return s.isEmpty() ? QString() : s;
} }
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool
{ {
return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt)); return loaders & (Forge | Fabric | Quilt);
} }
}; };

View File

@ -4,12 +4,15 @@
#include "Json.h" #include "Json.h"
#include "ModDownloadTask.h" #include "ResourceDownloadTask.h"
#include "modplatform/helpers/HashUtils.h" #include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
static ModrinthAPI api; static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps; static ModPlatform::ProviderCapabilities ProviderCaps;
@ -34,7 +37,7 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes // Create all hashes
QStringList hashes; QStringList hashes;
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10); ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
for (auto* mod : m_mods) { for (auto* mod : m_mods) {
@ -108,13 +111,15 @@ void ModrinthCheckUpdate::executeTask()
// Sometimes a version may have multiple files, one with "forge" and one with "fabric", // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it // so we may want to filter it
QString loader_filter; QString loader_filter;
static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt }; if (m_loaders.has_value()) {
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
for (auto flag : flags) { for (auto flag : flags) {
if (m_loaders.testFlag(flag)) { if (m_loaders.value().testFlag(flag)) {
loader_filter = api.getModLoaderString(flag); loader_filter = api.getModLoaderString(flag);
break; break;
} }
} }
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not: // Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
@ -152,12 +157,12 @@ void ModrinthCheckUpdate::executeTask()
for (auto& author : mod->authors()) for (auto& author : mod->authors())
pack.authors.append({ author }); pack.authors.append({ author });
pack.description = mod->description(); pack.description = mod->description();
pack.provider = ModPlatform::Provider::MODRINTH; pack.provider = ModPlatform::ResourceProvider::MODRINTH;
auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); auto download_task = new ResourceDownloadTask(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog, m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
ModPlatform::Provider::MODRINTH, download_task); ModPlatform::ResourceProvider::MODRINTH, download_task);
} }
} }
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {

View File

@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
Q_OBJECT Q_OBJECT
public: public:
ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder) ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{} {}

View File

@ -33,7 +33,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
if (pack.addonId.toString().isEmpty()) if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id"); pack.addonId = Json::requireString(obj, "id");
pack.provider = ModPlatform::Provider::MODRINTH; pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title"); pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", ""); pack.slug = Json::ensureString(obj, "slug", "");
@ -179,7 +179,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.hash = Json::requireString(hash_list, preferred_hash_type); file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type; file.hash_type = preferred_hash_type;
} else { } else {
auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
for (auto& hash_type : hash_types) { for (auto& hash_type : hash_types) {
if (hash_list.contains(hash_type)) { if (hash_list.contains(hash_type)) {
file.hash = Json::requireString(hash_list, hash_type); file.hash = Json::requireString(hash_list, hash_type);

View File

@ -97,7 +97,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
mod.name = mod_pack.name; mod.name = mod_pack.name;
mod.filename = mod_version.fileName; mod.filename = mod_version.fileName;
if (mod_pack.provider == ModPlatform::Provider::FLAME) { if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) {
mod.mode = "metadata:curseforge"; mod.mode = "metadata:curseforge";
} else { } else {
mod.mode = "url"; mod.mode = "url";
@ -176,11 +176,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
in_stream << QString("\n[update]\n"); in_stream << QString("\n[update]\n");
in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider));
switch (mod.provider) { switch (mod.provider) {
case (ModPlatform::Provider::FLAME): case (ModPlatform::ResourceProvider::FLAME):
in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); in_stream << QString("file-id = %1\n").arg(mod.file_id.toString());
in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); in_stream << QString("project-id = %1\n").arg(mod.project_id.toString());
break; break;
case (ModPlatform::Provider::MODRINTH): case (ModPlatform::ResourceProvider::MODRINTH):
addToStream("mod-id", mod.mod_id().toString()); addToStream("mod-id", mod.mod_id().toString());
addToStream("version", mod.version().toString()); addToStream("version", mod.version().toString());
break; break;
@ -273,7 +273,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
} }
{ // [update] info { // [update] info
using Provider = ModPlatform::Provider; using Provider = ModPlatform::ResourceProvider;
auto update_table = table["update"]; auto update_table = table["update"];
if (!update_table || !update_table.is_table()) { if (!update_table || !update_table.is_table()) {

View File

@ -49,7 +49,7 @@ class V1 {
QString hash {}; QString hash {};
// [update] // [update]
ModPlatform::Provider provider {}; ModPlatform::ResourceProvider provider {};
QVariant file_id {}; QVariant file_id {};
QVariant project_id {}; QVariant project_id {};

View File

@ -52,7 +52,6 @@ class NetAction : public Task {
virtual ~NetAction() = default; virtual ~NetAction() = default;
QUrl url() { return m_url; } QUrl url() { return m_url; }
auto index() -> int { return m_index_within_job; }
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; } void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
@ -75,9 +74,6 @@ class NetAction : public Task {
public: public:
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
/// index within the parent job, FIXME: nuke
int m_index_within_job = 0;
/// the network reply /// the network reply
unique_qobject_ptr<QNetworkReply> m_reply; unique_qobject_ptr<QNetworkReply> m_reply;

View File

@ -38,11 +38,10 @@
auto NetJob::addNetAction(NetAction::Ptr action) -> bool auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{ {
action->m_index_within_job = m_queue.size();
m_queue.append(action);
action->setNetwork(m_network); action->setNetwork(m_network);
addTask(action);
return true; return true;
} }

View File

@ -230,7 +230,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed /// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path) void BlockedModsDialog::buildHashTask(QString path)
{ {
auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1"); auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1");
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;

View File

@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll()
accept(); accept();
} }
auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider
{ {
return ModPlatform::Provider(m_providers.checkedId()); return ModPlatform::ResourceProvider(m_providers.checkedId());
} }
void ChooseProviderDialog::addProviders() void ChooseProviderDialog::addProviders()
@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders()
int btn_index = 0; int btn_index = 0;
QRadioButton* btn; QRadioButton* btn;
for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) { for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) {
btn = new QRadioButton(ProviderCaps.readableName(provider), this); btn = new QRadioButton(ProviderCaps.readableName(provider), this);
m_providers.addButton(btn, btn_index++); m_providers.addButton(btn, btn_index++);
ui->providersLayout->addWidget(btn); ui->providersLayout->addWidget(btn);

View File

@ -8,7 +8,7 @@ class ChooseProviderDialog;
} }
namespace ModPlatform { namespace ModPlatform {
enum class Provider; enum class ResourceProvider;
} }
class Mod; class Mod;
@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog {
bool try_others = false; bool try_others = false;
ModPlatform::Provider chosen; ModPlatform::ResourceProvider chosen;
}; };
public: public:
@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog {
void addProviders(); void addProviders();
void disableInput(); void disableInput();
auto getSelectedProvider() const -> ModPlatform::Provider; auto getSelectedProvider() const -> ModPlatform::ResourceProvider;
private: private:
Ui::ChooseProviderDialog* ui; Ui::ChooseProviderDialog* ui;

View File

@ -19,184 +19,41 @@
#include "ModDownloadDialog.h" #include "ModDownloadDialog.h"
#include <BaseVersion.h>
#include <InstanceList.h>
#include <icons/IconList.h>
#include "Application.h" #include "Application.h"
#include "ReviewMessageBox.h"
#include <QDialogButtonBox> #include "ui/pages/modplatform/flame/FlameResourcePages.h"
#include <QLayout> #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
#include <QPushButton>
#include <QValidator>
#include "ModDownloadTask.h" ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
#include "ui/pages/modplatform/flame/FlameModPage.h" : ResourceDownloadDialog(parent, mods), m_instance(instance)
#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ui/widgets/PageContainer.h"
ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance)
: QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
{ {
setObjectName(QStringLiteral("ModDownloadDialog")); initializeContainer();
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); connectButtons();
resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
setWindowIcon(APPLICATION->getThemedIcon("new"));
// NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not
// move this below.
m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_container = new PageContainer(this);
m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
m_container->layout()->setContentsMargins(0, 0, 0, 0);
m_verticalLayout->addWidget(m_container);
m_container->addButtons(m_buttons);
connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged);
// Bonk Qt over its stupid head and make sure it understands which button is the default one...
// See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
OkButton->setEnabled(false);
OkButton->setDefault(true);
OkButton->setAutoDefault(true);
OkButton->setText(tr("Review and confirm"));
OkButton->setShortcut(tr("Ctrl+Return"));
OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return"));
connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
CancelButton->setDefault(false);
CancelButton->setAutoDefault(false);
connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject);
auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
HelpButton->setDefault(false);
HelpButton->setAutoDefault(false);
connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
QMetaObject::connectSlotsByName(this);
setWindowModality(Qt::WindowModal);
setWindowTitle(dialogTitle());
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
} }
QString ModDownloadDialog::dialogTitle()
{
return tr("Download mods");
}
void ModDownloadDialog::reject()
{
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::reject();
}
void ModDownloadDialog::confirm()
{
auto keys = modTask.keys();
keys.sort(Qt::CaseInsensitive);
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download"));
for (auto& task : keys) {
confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() });
}
if (confirm_dialog->exec()) {
auto deselected = confirm_dialog->deselectedMods();
for (auto name : deselected) {
modTask.remove(name);
}
this->accept();
}
}
void ModDownloadDialog::accept() void ModDownloadDialog::accept()
{ {
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::accept(); QDialog::accept();
} }
void ModDownloadDialog::reject()
{
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::reject();
}
QList<BasePage*> ModDownloadDialog::getPages() QList<BasePage*> ModDownloadDialog::getPages()
{ {
QList<BasePage*> pages; QList<BasePage*> pages;
pages.append(ModrinthModPage::create(this, m_instance)); pages.append(ModrinthModPage::create(this, *m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame) if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameModPage::create(this, m_instance)); pages.append(FlameModPage::create(this, *m_instance));
m_selectedPage = dynamic_cast<ModPage*>(pages[0]); m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
return pages; return pages;
} }
void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
{
removeSelectedMod(name);
modTask.insert(name, task);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
void ModDownloadDialog::removeSelectedMod(QString name)
{
if (modTask.contains(name))
delete modTask.find(name).value();
modTask.remove(name);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
bool ModDownloadDialog::isModSelected(QString name, QString filename) const
{
// FIXME: Is there a way to check for versions without checking the filename
// as a heuristic, other than adding such info to ModDownloadTask itself?
auto iter = modTask.find(name);
return iter != modTask.end() && (iter.value()->getFilename() == filename);
}
bool ModDownloadDialog::isModSelected(QString name) const
{
auto iter = modTask.find(name);
return iter != modTask.end();
}
const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
{
return modTask.values();
}
void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
{
auto* prev_page = dynamic_cast<ModPage*>(previous);
if (!prev_page) {
qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
return;
}
m_selectedPage = dynamic_cast<ModPage*>(selected);
if (!m_selectedPage) {
qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!";
return;
}
// Same effect as having a global search bar
m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
}
bool ModDownloadDialog::selectPage(QString pageId)
{
return m_container->selectPage(pageId);
}
ModPage* ModDownloadDialog::getSelectedPage()
{
return m_selectedPage;
}

View File

@ -19,60 +19,29 @@
#pragma once #pragma once
#include <QDialog>
#include <QVBoxLayout>
#include "ModDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "ui/pages/BasePageProvider.h"
namespace Ui #include "ui/dialogs/ResourceDownloadDialog.h"
{
class ModDownloadDialog;
}
class PageContainer;
class QDialogButtonBox; class QDialogButtonBox;
class ModPage;
class ModrinthModPage;
class ModDownloadDialog final : public QDialog, public BasePageProvider class ModDownloadDialog final : public ResourceDownloadDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance);
~ModDownloadDialog() override = default; ~ModDownloadDialog() override = default;
QString dialogTitle() override; //: String that gets appended to the mod download dialog title ("Download " + resourcesString())
[[nodiscard]] QString resourceString() const override { return tr("mods"); }
QList<BasePage*> getPages() override; QList<BasePage*> getPages() override;
void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
void removeSelectedMod(QString name = QString());
bool isModSelected(QString name, QString filename) const;
bool isModSelected(QString name) const;
const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel>& mods;
bool selectPage(QString pageId);
ModPage* getSelectedPage();
public slots: public slots:
void confirm();
void accept() override; void accept() override;
void reject() override; void reject() override;
private slots:
void selectedPageChanged(BasePage* previous, BasePage* selected);
private: private:
Ui::ModDownloadDialog* ui = nullptr;
PageContainer* m_container = nullptr;
QDialogButtonBox* m_buttons = nullptr;
QVBoxLayout* m_verticalLayout = nullptr;
ModPage* m_selectedPage = nullptr;
QHash<QString, ModDownloadTask*> modTask;
BaseInstance* m_instance; BaseInstance* m_instance;
}; };

View File

@ -21,6 +21,8 @@
#include <QTextBrowser> #include <QTextBrowser>
#include <QTreeWidgetItem> #include <QTreeWidgetItem>
#include <optional>
static ModPlatform::ProviderCapabilities ProviderCaps; static ModPlatform::ProviderCapabilities ProviderCaps;
static std::list<Version> mcVersions(BaseInstance* inst) static std::list<Version> mcVersions(BaseInstance* inst)
@ -28,7 +30,7 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
} }
static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) static std::optional<ResourceAPI::ModLoaderTypes> mcLoaders(BaseInstance* inst)
{ {
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() }; return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
} }
@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool
bool confirm_rest = false; bool confirm_rest = false;
bool try_others_rest = false; bool try_others_rest = false;
bool skip_rest = false; bool skip_rest = false;
ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
auto addToTmp = [&](Mod* m, ModPlatform::Provider p) { auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) {
switch (p) { switch (p) {
case ModPlatform::Provider::MODRINTH: case ModPlatform::ResourceProvider::MODRINTH:
modrinth_tmp.push_back(m); modrinth_tmp.push_back(m);
break; break;
case ModPlatform::Provider::FLAME: case ModPlatform::ResourceProvider::FLAME:
flame_tmp.push_back(m); flame_tmp.push_back(m);
break; break;
} }
@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
} }
if (!modrinth_tmp.empty()) { if (!modrinth_tmp.empty()) {
auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH); onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH);
}); });
if (modrinth_task->getHashingTask()) if (modrinth_task->getHashingTask())
@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
} }
if (!flame_tmp.empty()) { if (!flame_tmp.empty()) {
auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME); onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME);
}); });
if (flame_task->getHashingTask()) if (flame_task->getHashingTask())
@ -306,28 +308,28 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod)
return; return;
switch (mod->metadata()->provider) { switch (mod->metadata()->provider) {
case ModPlatform::Provider::MODRINTH: case ModPlatform::ResourceProvider::MODRINTH:
m_modrinth_to_update.push_back(mod); m_modrinth_to_update.push_back(mod);
break; break;
case ModPlatform::Provider::FLAME: case ModPlatform::ResourceProvider::FLAME:
m_flame_to_update.push_back(mod); m_flame_to_update.push_back(mod);
break; break;
} }
} }
ModPlatform::Provider next(ModPlatform::Provider p) ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
{ {
switch (p) { switch (p) {
case ModPlatform::Provider::MODRINTH: case ModPlatform::ResourceProvider::MODRINTH:
return ModPlatform::Provider::FLAME; return ModPlatform::ResourceProvider::FLAME;
case ModPlatform::Provider::FLAME: case ModPlatform::ResourceProvider::FLAME:
return ModPlatform::Provider::MODRINTH; return ModPlatform::ResourceProvider::MODRINTH;
} }
return ModPlatform::Provider::FLAME; return ModPlatform::ResourceProvider::FLAME;
} }
void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice) void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice)
{ {
if (try_others) { if (try_others) {
auto index_dir = indexDir(); auto index_dir = indexDir();
@ -368,7 +370,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
QString text = info.changelog; QString text = info.changelog;
switch (info.provider) { switch (info.provider) {
case ModPlatform::Provider::MODRINTH: { case ModPlatform::ResourceProvider::MODRINTH: {
text = markdownToHTML(info.changelog.toUtf8()); text = markdownToHTML(info.changelog.toUtf8());
break; break;
} }
@ -386,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
ui->modTreeWidget->addTopLevelItem(item_top); ui->modTreeWidget->addTopLevelItem(item_top);
} }
auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*> auto ModUpdateDialog::getTasks() -> const QList<ResourceDownloadTask*>
{ {
QList<ModDownloadTask*> list; QList<ResourceDownloadTask*> list;
auto* item = ui->modTreeWidget->topLevelItem(0); auto* item = ui->modTreeWidget->topLevelItem(0);

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "BaseInstance.h" #include "BaseInstance.h"
#include "ModDownloadTask.h" #include "ResourceDownloadTask.h"
#include "ReviewMessageBox.h" #include "ReviewMessageBox.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
void appendMod(const CheckUpdateTask::UpdatableMod& info); void appendMod(const CheckUpdateTask::UpdatableMod& info);
const QList<ModDownloadTask*> getTasks(); const QList<ResourceDownloadTask*> getTasks();
auto indexDir() const -> QDir { return m_mod_model->indexDir(); } auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
auto noUpdates() const -> bool { return m_no_updates; }; auto noUpdates() const -> bool { return m_no_updates; };
@ -36,7 +36,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
private slots: private slots:
void onMetadataEnsured(Mod*); void onMetadataEnsured(Mod*);
void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH);
private: private:
QWidget* m_parent; QWidget* m_parent;
@ -54,7 +54,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
QList<std::tuple<Mod*, QString>> m_failed_metadata; QList<std::tuple<Mod*, QString>> m_failed_metadata;
QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update; QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
QHash<QString, ModDownloadTask*> m_tasks; QHash<QString, ResourceDownloadTask*> m_tasks;
BaseInstance* m_instance; BaseInstance* m_instance;
bool m_no_updates = false; bool m_no_updates = false;

View File

@ -0,0 +1,152 @@
#include "ResourceDownloadDialog.h"
#include <QPushButton>
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "ui/dialogs/ReviewMessageBox.h"
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/PageContainer.h"
ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model)
: QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this)
{
setObjectName(QStringLiteral("ResourceDownloadDialog"));
resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
setWindowIcon(APPLICATION->getThemedIcon("new"));
// Bonk Qt over its stupid head and make sure it understands which button is the default one...
// See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
OkButton->setEnabled(false);
OkButton->setDefault(true);
OkButton->setAutoDefault(true);
OkButton->setText(tr("Review and confirm"));
OkButton->setShortcut(tr("Ctrl+Return"));
auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
CancelButton->setDefault(false);
CancelButton->setAutoDefault(false);
auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
HelpButton->setDefault(false);
HelpButton->setAutoDefault(false);
setWindowModality(Qt::WindowModal);
setWindowTitle(dialogTitle());
}
// NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so
// won't work with subclasses if we put it in this ctor.
void ResourceDownloadDialog::initializeContainer()
{
m_container = new PageContainer(this);
m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
m_container->layout()->setContentsMargins(0, 0, 0, 0);
m_vertical_layout.addWidget(m_container);
m_container->addButtons(&m_buttons);
connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged);
}
void ResourceDownloadDialog::connectButtons()
{
auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourceString()));
connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject);
auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
}
void ResourceDownloadDialog::confirm()
{
auto keys = m_selected.keys();
keys.sort(Qt::CaseInsensitive);
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourceString()));
for (auto& task : keys) {
confirm_dialog->appendResource({ task, m_selected.find(task).value()->getFilename() });
}
if (confirm_dialog->exec()) {
auto deselected = confirm_dialog->deselectedResources();
for (auto name : deselected) {
m_selected.remove(name);
}
this->accept();
}
}
bool ResourceDownloadDialog::selectPage(QString pageId)
{
return m_container->selectPage(pageId);
}
ResourcePage* ResourceDownloadDialog::getSelectedPage()
{
return m_selectedPage;
}
void ResourceDownloadDialog::addResource(QString name, ResourceDownloadTask* task)
{
removeResource(name);
m_selected.insert(name, task);
m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
}
void ResourceDownloadDialog::removeResource(QString name)
{
if (m_selected.contains(name))
m_selected.find(name).value()->deleteLater();
m_selected.remove(name);
m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
}
bool ResourceDownloadDialog::isSelected(QString name, QString filename) const
{
auto iter = m_selected.constFind(name);
if (iter == m_selected.constEnd())
return false;
// FIXME: Is there a way to check for versions without checking the filename
// as a heuristic, other than adding such info to ResourceDownloadTask itself?
if (!filename.isEmpty())
return iter.value()->getFilename() == filename;
return true;
}
const QList<ResourceDownloadTask*> ResourceDownloadDialog::getTasks()
{
return m_selected.values();
}
void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
{
auto* prev_page = dynamic_cast<ResourcePage*>(previous);
if (!prev_page) {
qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
return;
}
m_selectedPage = dynamic_cast<ResourcePage*>(selected);
if (!m_selectedPage) {
qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
return;
}
// Same effect as having a global search bar
m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
}

View File

@ -0,0 +1,55 @@
#pragma once
#include <QDialog>
#include <QDialogButtonBox>
#include <QLayout>
#include "ui/pages/BasePageProvider.h"
class ResourceDownloadTask;
class ResourcePage;
class ResourceFolderModel;
class PageContainer;
class QVBoxLayout;
class QDialogButtonBox;
class ResourceDownloadDialog : public QDialog, public BasePageProvider {
Q_OBJECT
public:
ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model);
void initializeContainer();
void connectButtons();
//: String that gets appended to the download dialog title ("Download " + resourcesString())
[[nodiscard]] virtual QString resourceString() const { return tr("resources"); }
QString dialogTitle() override { return tr("Download %1").arg(resourceString()); };
bool selectPage(QString pageId);
ResourcePage* getSelectedPage();
void addResource(QString name, ResourceDownloadTask* task);
void removeResource(QString name);
[[nodiscard]] bool isSelected(QString name, QString filename = "") const;
const QList<ResourceDownloadTask*> getTasks();
[[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
protected slots:
void selectedPageChanged(BasePage* previous, BasePage* selected);
virtual void confirm();
protected:
const std::shared_ptr<ResourceFolderModel> m_base_model;
PageContainer* m_container = nullptr;
ResourcePage* m_selectedPage = nullptr;
QDialogButtonBox m_buttons;
QVBoxLayout m_vertical_layout;
QHash<QString, ResourceDownloadTask*> m_selected;
};

View File

@ -25,7 +25,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
return new ReviewMessageBox(parent, title, icon); return new ReviewMessageBox(parent, title, icon);
} }
void ReviewMessageBox::appendMod(ModInformation&& info) void ReviewMessageBox::appendResource(ResourceInformation&& info)
{ {
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
itemTop->setCheckState(0, Qt::CheckState::Checked); itemTop->setCheckState(0, Qt::CheckState::Checked);
@ -39,7 +39,7 @@ void ReviewMessageBox::appendMod(ModInformation&& info)
ui->modTreeWidget->addTopLevelItem(itemTop); ui->modTreeWidget->addTopLevelItem(itemTop);
} }
auto ReviewMessageBox::deselectedMods() -> QStringList auto ReviewMessageBox::deselectedResources() -> QStringList
{ {
QStringList list; QStringList list;

View File

@ -12,15 +12,15 @@ class ReviewMessageBox : public QDialog {
public: public:
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
using ModInformation = struct { using ResourceInformation = struct {
QString name; QString name;
QString filename; QString filename;
}; };
void appendMod(ModInformation&& info); void appendResource(ResourceInformation&& info);
auto deselectedMods() -> QStringList; auto deselectedResources() -> QStringList;
~ReviewMessageBox(); ~ReviewMessageBox() override;
protected: protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);

View File

@ -59,7 +59,7 @@
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModAPI.h" #include "modplatform/ResourceAPI.h"
#include "Version.h" #include "Version.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
@ -153,12 +153,12 @@ void ModFolderPage::installMods()
return; // this is a null instance or a legacy instance return; // this is a null instance or a legacy instance
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile(); auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (profile->getModLoaders() == ModAPI::Unspecified) { if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return; return;
} }
ModDownloadDialog mdownload(m_model, this, m_instance); ModDownloadDialog mdownload(this, m_model, m_instance);
if (mdownload.exec()) { if (mdownload.exec()) {
ConcurrentTask* tasks = new ConcurrentTask(this); ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) { connect(tasks, &Task::failed, [this, tasks](QString reason) {

View File

@ -73,3 +73,4 @@ public:
return true; return true;
} }
}; };

View File

@ -1,226 +1,81 @@
#include "ModModel.h" #include "ModModel.h"
#include "BuildConfig.h"
#include "Json.h" #include "Json.h"
#include "ModPage.h" #include "ModPage.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox> #include <QMessageBox>
namespace ModPlatform { namespace ModPlatform {
// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted. ListModel::ListModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {}
// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
static QHash<ListModel*, bool> s_running;
ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
ListModel::~ListModel()
{
s_running.find(this).value() = false;
}
auto ListModel::debugName() const -> QString
{
return m_parent->debugName();
}
/******** Make data requests ********/ /******** Make data requests ********/
void ListModel::fetchMore(const QModelIndex& parent) ResourceAPI::SearchArgs ListModel::createSearchArguments()
{ {
if (parent.isValid()) auto profile = static_cast<MinecraftInstance&>(m_associated_page->m_base_instance).getPackProfile();
return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term,
getSorts()[currentSort], profile->getModLoaders(), getMineVersions() };
}
ResourceAPI::SearchCallbacks ListModel::createSearchCallbacks()
{
return { [this](auto& doc) {
if (!s_running_models.constFind(this).value())
return; return;
if (nextSearchOffset == 0) { searchRequestFinished(doc);
qWarning() << "fetchMore with 0 offset is wrong..."; } };
}
ResourceAPI::VersionSearchArgs ListModel::createVersionsArguments(QModelIndex& entry)
{
auto const& pack = m_packs[entry.row()];
auto profile = static_cast<MinecraftInstance&>(m_associated_page->m_base_instance).getPackProfile();
return { pack.addonId.toString(), getMineVersions(), profile->getModLoaders() };
}
ResourceAPI::VersionSearchCallbacks ListModel::createVersionsCallbacks(QModelIndex& entry)
{
auto const& pack = m_packs[entry.row()];
return { [this, pack, entry](auto& doc, auto addonId) {
if (!s_running_models.constFind(this).value())
return; return;
} versionRequestSucceeded(doc, addonId, entry);
performPaginatedSearch(); } };
} }
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant ResourceAPI::ProjectInfoArgs ListModel::createInfoArguments(QModelIndex& entry)
{ {
int pos = index.row(); auto& pack = m_packs[entry.row()];
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return { pack };
return QString("INVALID INDEX %1").arg(pos);
}
ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) {
case 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;
}
case Qt::DecorationRole: {
if (m_logoMap.contains(pack.logoName)) {
return m_logoMap.value(pack.logoName);
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
// un-const-ify this
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
// Custom data
case UserDataTypes::TITLE:
return pack.name;
case UserDataTypes::DESCRIPTION:
return pack.description;
case UserDataTypes::SELECTED:
return m_parent->getDialog()->isModSelected(pack.name);
default:
break;
}
return {};
} }
ResourceAPI::ProjectInfoCallbacks ListModel::createInfoCallbacks(QModelIndex& entry)
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{ {
int pos = index.row(); return { [this, entry](auto& doc, auto& pack) {
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) if (!s_running_models.constFind(this).value())
return false;
modpacks[pos] = value.value<ModPlatform::IndexedPack>();
return true;
}
void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
[this, current, index](QJsonDocument& doc, QString addonId) {
if (!s_running.constFind(this).value())
return; return;
versionRequestSucceeded(doc, addonId, index); infoRequestFinished(doc, pack, entry);
}); } };
}
void ListModel::performPaginatedSearch()
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->searchMods(
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
}
void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
{
m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
if (!s_running.constFind(this).value())
return;
infoRequestFinished(doc, pack, index);
});
}
void ListModel::refresh()
{
if (jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
} else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
} }
void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
{ {
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
return; return;
} }
currentSearchTerm = term; setSearchTerm(term);
currentSort = sort; currentSort = sort;
refresh(); refresh();
} }
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) || url.isEmpty()) {
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 ********/ /******** 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) void ListModel::searchRequestFinished(QJsonDocument& doc)
{ {
jobPtr.reset();
QList<ModPlatform::IndexedPack> newList; QList<ModPlatform::IndexedPack> newList;
auto packs = documentToArray(doc); auto packs = documentToArray(doc);
@ -232,62 +87,27 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
loadIndexedPack(pack, packObj); loadIndexedPack(pack, packObj);
newList.append(pack); newList.append(pack);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); qWarning() << "Error while loading mod from " << m_associated_page->debugName() << ": " << e.cause();
continue; continue;
} }
} }
if (packs.size() < 25) { if (packs.size() < 25) {
searchState = Finished; m_search_state = SearchState::Finished;
} else { } else {
nextSearchOffset += 25; m_next_search_offset += 25;
searchState = CanPossiblyFetchMore; m_search_state = SearchState::CanFetchMore;
} }
// When you have a Qt build with assertions turned on, proceeding here will abort the application // When you have a Qt build with assertions turned on, proceeding here will abort the application
if (newList.size() == 0) if (newList.size() == 0)
return; return;
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
modpacks.append(newList); m_packs.append(newList);
endInsertRows(); endInsertRows();
} }
void ListModel::searchRequestFailed(QString reason)
{
auto failed_action = jobPtr->getFailedActions().at(0);
if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
} else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
QString("%1 %2")
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
}
jobPtr.reset();
searchState = Finished;
}
void ListModel::searchRequestAborted()
{
if (searchState != ResetRequested)
qCritical() << "Search task in ModModel aborted by an unknown reason!";
// Retry fetching
jobPtr.reset();
beginResetModel();
modpacks.clear();
endResetModel();
nextSearchOffset = 0;
performPaginatedSearch();
}
void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{ {
qDebug() << "Loading mod info"; qDebug() << "Loading mod info";
@ -310,12 +130,12 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack
} }
} }
m_parent->updateUi(); m_associated_page->updateUi();
} }
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
{ {
auto& current = m_parent->getCurrent(); auto current = m_associated_page->getCurrentPack();
if (addonId != current.addonId) { if (addonId != current.addonId) {
return; return;
} }
@ -336,15 +156,19 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, cons
qWarning() << "Failed to cache mod versions!"; qWarning() << "Failed to cache mod versions!";
} }
m_associated_page->updateVersionList();
m_parent->updateModVersions();
} }
} // namespace ModPlatform } // namespace ModPlatform
/******** Helpers ********/ /******** Helpers ********/
auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version> #define MOD_PAGE(x) static_cast<ModPage*>(x)
auto ModPlatform::ListModel::getMineVersions() const -> std::optional<std::list<Version>>
{ {
return m_parent->getFilter()->versions; auto versions = MOD_PAGE(m_associated_page)->getFilter()->versions;
if (!versions.empty())
return versions;
return {};
} }

View File

@ -3,90 +3,52 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "net/NetJob.h" #include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/ResourceModel.h"
class ModPage; class ModPage;
class Version; class Version;
namespace ModPlatform { namespace ModPlatform {
using LogoMap = QMap<QString, QIcon>; class ListModel : public ResourceModel {
using LogoCallback = std::function<void (QString)>;
class ListModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
public: public:
ListModel(ModPage* parent); ListModel(ModPage* parent, ResourceAPI* api);
~ListModel() override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 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;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
inline NetJob* activeJob() { return jobPtr.get(); }
/* Ask the API for more information */ /* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, const int sort, const bool filter_changed); void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 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 parent.isValid() ? false : searchState == CanPossiblyFetchMore; };
public slots: public slots:
void searchRequestFinished(QJsonDocument& doc); void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason);
void searchRequestAborted();
void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
protected slots: public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::SearchCallbacks createSearchCallbacks() override;
void logoFailed(QString logo); ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
void logoLoaded(QString logo, QIcon out); ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) override;
void performPaginatedSearch(); ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) override;
protected: protected:
virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
virtual auto getSorts() const -> const char** = 0; virtual auto getSorts() const -> const char** = 0;
void requestLogo(QString file, QString url); inline auto getMineVersions() const -> std::optional<std::list<Version>>;
inline auto getMineVersions() const -> std::list<Version>;
protected: 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 currentSort = 0;
int nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
NetJob::Ptr jobPtr;
}; };
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -35,59 +35,30 @@
*/ */
#include "ModPage.h" #include "ModPage.h"
#include "Application.h" #include "ui_ResourcePage.h"
#include "ui_ModPage.h"
#include <QDesktopServices> #include <QDesktopServices>
#include <QKeyEvent> #include <QKeyEvent>
#include <QRegularExpression> #include <QRegularExpression>
#include <memory> #include <memory>
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include "Markdown.h"
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) #include "ui/pages/modplatform/ModModel.h"
: QWidget(dialog)
, m_instance(instance) ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
, ui(new Ui::ModPage) : ResourcePage(dialog, instance)
, dialog(dialog)
, m_fetch_progress(this, false)
, api(api)
{ {
ui->setupUi(this); connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packView->installEventFilter(this);
connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl);
}
ModPage::~ModPage()
{
delete ui;
} }
void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget) void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
@ -97,59 +68,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget.swap(widget); m_filter_widget.swap(widget);
ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount()); m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance)); m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter(); m_filter = m_filter_widget->getFilter();
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{ connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: underline"); m_ui->searchButton->setStyleSheet("text-decoration: underline");
}); });
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{ connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: none"); m_ui->searchButton->setStyleSheet("text-decoration: none");
}); });
} }
/******** 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;
} else {
if (m_search_timer.isActive())
m_search_timer.stop();
m_search_timer.start(350);
}
} else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
onModSelected();
// To have the 'select mod' button outlined instead of the 'review and confirm' one
ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
keyEvent->accept();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/ /******** Callbacks to events in the UI (set up in the derived classes) ********/
void ModPage::filterMods() void ModPage::filterMods()
@ -163,176 +94,37 @@ void ModPage::triggerSearch()
m_filter = m_filter_widget->getFilter(); m_filter = m_filter_widget->getFilter();
if (changed) { if (changed) {
ui->packView->clearSelection(); m_ui->packView->clearSelection();
ui->packDescription->clear(); m_ui->packDescription->clear();
ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->clear();
updateSelectionButton(); updateSelectionButton();
} }
listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed); static_cast<ModPlatform::ListModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed);
m_fetch_progress.watch(listModel->activeJob()); m_fetch_progress.watch(&m_model->activeJob());
} }
QString ModPage::getSearchTerm() const QMap<QString, QString> ModPage::urlHandlers() const
{ {
return ui->searchEdit->text(); QMap<QString, QString> map;
} map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth");
void ModPage::setSearchTerm(QString term) map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge");
{ map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
ui->searchEdit->setText(term); return map;
}
void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
if (!curr.isValid()) { return; }
current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (!current.versionsLoaded) {
qDebug() << QString("Loading %1 mod versions").arg(debugName());
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
listModel->requestModVersions(current, curr);
} 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();
}
if(!current.extraDataLoaded){
qDebug() << QString("Loading %1 mod info").arg(debugName());
listModel->requestModInfo(current, curr);
}
updateUi();
}
void ModPage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ModPage::onModSelected()
{
if (selectedVersion < 0)
return;
auto& version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed));
}
updateSelectionButton();
/* Force redraw on the mods list when the selection changes */
ui->packView->adjustSize();
}
static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"));
static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"));
static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"));
void ModPage::openUrl(const QUrl& url)
{
// do not allow other url schemes for security reasons
if (!(url.scheme() == "http" || url.scheme() == "https")) {
qWarning() << "Unsupported scheme" << url.scheme();
return;
}
// detect mod URLs and search instead
const QString address = url.host() + url.path();
QRegularExpressionMatch match;
QString page;
match = modrinth.match(address);
if (match.hasMatch())
page = "modrinth";
else if (APPLICATION->capabilities() & Application::SupportsFlame) {
match = curseForge.match(address);
if (!match.hasMatch())
match = curseForgeOld.match(address);
if (match.hasMatch())
page = "curseforge";
}
if (!page.isNull()) {
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
if (slug != current.slug) {
dialog->selectPage(page);
ModPage* newPage = dialog->getSelectedPage();
QLineEdit* searchEdit = newPage->ui->searchEdit;
ModPlatform::ListModel* model = newPage->listModel;
QListView* view = newPage->ui->packView;
auto jump = [url, slug, model, view] {
for (int row = 0; row < model->rowCount({}); row++) {
const QModelIndex index = model->index(row);
const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (pack.slug == slug) {
view->setCurrentIndex(index);
return;
}
}
// The final fallback.
QDesktopServices::openUrl(url);
};
searchEdit->setText(slug);
newPage->triggerSearch();
if (model->activeJob())
connect(model->activeJob(), &Task::finished, jump);
else
jump();
return;
}
}
// open in the user's web browser
QDesktopServices::openUrl(url);
} }
/******** Make changes to the UI ********/ /******** Make changes to the UI ********/
void ModPage::retranslate() void ModPage::updateVersionList()
{ {
ui->retranslateUi(this); m_ui->versionSelectionBox->clear();
} auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
void ModPage::updateModVersions(int prev_count)
{
auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft"); QString mcVersion = packProfile->getComponentVersion("net.minecraft");
for (int i = 0; i < current.versions.size(); i++) { auto current_pack = getCurrentPack();
auto version = current.versions[i]; for (int i = 0; i < current_pack.versions.size(); i++) {
auto version = current_pack.versions[i];
bool valid = false; bool valid = false;
for(auto& mcVer : m_filter->versions){ for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loader, so passing it changes nothing. //NOTE: Flame doesn't care about loader, so passing it changes nothing.
@ -344,87 +136,18 @@ void ModPage::updateModVersions(int prev_count)
// Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out // Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out
if ((valid || m_filter->versions.empty()) && !optedOut(version)) if ((valid || m_filter->versions.empty()) && !optedOut(version))
ui->versionSelectionBox->addItem(version.version, QVariant(i)); m_ui->versionSelectionBox->addItem(version.version, QVariant(i));
} }
if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { if (m_ui->versionSelectionBox->count() == 0) {
ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
} }
updateSelectionButton(); updateSelectionButton();
} }
void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
void ModPage::updateSelectionButton()
{ {
if (!isOpened || selectedVersion < 0) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
ui->modSelectionButton->setEnabled(false); m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel(), is_indexed));
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 ModPage::updateUi()
{
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(", ");
}
if (current.extraDataLoaded) {
if (!current.extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
};
QStringList donates;
for (auto& donate : current.extraData.donate) {
donates.append(donateToStr(donate));
}
text += donates.join(", ");
}
if (!current.extraData.issuesUrl.isEmpty()
|| !current.extraData.sourceUrl.isEmpty()
|| !current.extraData.wikiUrl.isEmpty()
|| !current.extraData.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
if (!current.extraData.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>";
if (!current.extraData.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>";
if (!current.extraData.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>";
if (!current.extraData.discordUrl.isEmpty())
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>";
}
text += "<hr>";
ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : markdownToHTML(current.extraData.body)));
ui->packDescription->flush();
} }

View File

@ -2,104 +2,58 @@
#include <QWidget> #include <QWidget>
#include "Application.h"
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
class ModDownloadDialog; class ModDownloadDialog;
namespace Ui { namespace Ui {
class ModPage; class ResourcePage;
} }
/* This page handles most logic related to browsing and selecting mods to download. */ /* This page handles most logic related to browsing and selecting mods to download. */
class ModPage : public QWidget, public BasePage { class ModPage : public ResourcePage {
Q_OBJECT Q_OBJECT
public: public:
template<typename T> template<typename T>
static T* create(ModDownloadDialog* dialog, BaseInstance* instance) static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
{ {
auto page = new T(dialog, instance); auto page = new T(dialog, instance);
auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page); auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
page->setFilterWidget(filter_widget); page->setFilterWidget(filter_widget);
return page; return page;
} }
~ModPage() override; ~ModPage() override = default;
/* Affects what the user sees */ [[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
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 */ [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
virtual auto metaEntryBase() const -> QString = 0;
virtual auto debugName() const -> QString = 0;
void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
void retranslate() override; virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
void updateUi(); [[nodiscard]] bool supportsFiltering() const override { return true; };
auto shouldDisplay() const -> bool override = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
auto apiProvider() -> ModAPI* { return api.get(); };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; } auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
/** Get the current term in the search bar. */
auto getSearchTerm() const -> QString;
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&); void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
auto getCurrent() -> ModPlatform::IndexedPack& { return current; } public slots:
void updateModVersions(int prev_count = -1); void updateVersionList() override;
void openedImpl() override;
auto eventFilter(QObject* watched, QEvent* event) -> bool override;
BaseInstance* m_instance;
protected: protected:
ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
void updateSelectionButton();
protected slots: protected slots:
virtual void filterMods(); virtual void filterMods();
void triggerSearch(); void triggerSearch() override;
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onModSelected();
virtual void openUrl(const QUrl& url);
protected: protected:
Ui::ModPage* ui = nullptr;
ModDownloadDialog* dialog = nullptr;
unique_qobject_ptr<ModFilterWidget> m_filter_widget; unique_qobject_ptr<ModFilterWidget> m_filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter; std::shared_ptr<ModFilterWidget::Filter> m_filter;
ProgressWidget m_fetch_progress;
ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> api;
int selectedVersion = -1;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
}; };

View File

@ -0,0 +1,258 @@
#include "ResourceModel.h"
#include <QCryptographicHash>
#include <QIcon>
#include <QMessageBox>
#include <QPixmapCache>
#include <QUrl>
#include "Application.h"
#include "BuildConfig.h"
#include "net/Download.h"
#include "net/NetJob.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/ProjectItem.h"
QHash<ResourceModel*, bool> ResourceModel::s_running_models;
ResourceModel::ResourceModel(ResourcePage* parent, ResourceAPI* api) : QAbstractListModel(), m_api(api), m_associated_page(parent)
{
s_running_models.insert(this, true);
}
ResourceModel::~ResourceModel()
{
s_running_models.find(this).value() = false;
}
auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
{
int pos = index.row();
if (pos >= m_packs.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
auto pack = m_packs.at(pos);
switch (role) {
case 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;
}
case Qt::DecorationRole: {
if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack.logoUrl);
icon_or_none.has_value())
return icon_or_none.value();
return APPLICATION->getThemedIcon("screenshot-placeholder");
}
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
// Custom data
case UserDataTypes::TITLE:
return pack.name;
case UserDataTypes::DESCRIPTION:
return pack.description;
case UserDataTypes::SELECTED:
return isPackSelected(pack);
default:
break;
}
return {};
}
bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
int pos = index.row();
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
return false;
m_packs[pos] = value.value<ModPlatform::IndexedPack>();
return true;
}
QString ResourceModel::debugName() const
{
return m_associated_page->debugName() + " (Model)";
}
void ResourceModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
Q_ASSERT(m_next_search_offset != 0);
search();
}
void ResourceModel::search()
{
if (!m_current_job.isRunning())
m_current_job.clear();
auto args{ createSearchArguments() };
auto callbacks{ createSearchCallbacks() };
Q_ASSERT(callbacks.on_succeed);
// Use defaults if no callbacks are set
if (!callbacks.on_fail)
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, network_error_code);
};
if (!callbacks.on_abort)
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
searchRequestAborted();
};
if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
addActiveJob(job);
}
void ResourceModel::loadEntry(QModelIndex& entry)
{
auto const& pack = m_packs[entry.row()];
if (!m_current_job.isRunning())
m_current_job.clear();
if (!pack.versionsLoaded) {
auto args{ createVersionsArguments(entry) };
auto callbacks{ createVersionsCallbacks(entry) };
if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
addActiveJob(job);
}
if (!pack.extraDataLoaded) {
auto args{ createInfoArguments(entry) };
auto callbacks{ createInfoCallbacks(entry) };
if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
addActiveJob(job);
}
}
void ResourceModel::refresh()
{
if (m_current_job.isRunning()) {
m_current_job.abort();
m_search_state = SearchState::ResetRequested;
return;
}
clearData();
m_search_state = SearchState::None;
m_next_search_offset = 0;
search();
}
void ResourceModel::clearData()
{
beginResetModel();
m_packs.clear();
endResetModel();
}
std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
{
QPixmap pixmap;
if (QPixmapCache::find(url.toString(), &pixmap))
return { pixmap };
if (!m_current_icon_job)
m_current_icon_job = new NetJob("IconJob", APPLICATION->network());
if (m_currently_running_icon_actions.contains(url))
return {};
if (m_failed_icon_actions.contains(url))
return {};
auto cache_entry = APPLICATION->metacache()->resolveEntry(
m_associated_page->metaEntryBase(),
QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
auto icon_fetch_action = Net::Download::makeCached(url, cache_entry);
auto full_file_path = cache_entry->getFullPath();
connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
auto icon = QIcon(full_file_path);
QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
m_currently_running_icon_actions.remove(url);
emit dataChanged(index, index, { Qt::DecorationRole });
});
connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
m_currently_running_icon_actions.remove(url);
m_failed_icon_actions.insert(url);
});
m_currently_running_icon_actions.insert(url);
m_current_icon_job->addNetAction(icon_fetch_action);
if (!m_current_icon_job->isRunning())
QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start);
return {};
}
bool ResourceModel::isPackSelected(const ModPlatform::IndexedPack& pack) const
{
return m_associated_page->isPackSelected(pack);
}
void ResourceModel::searchRequestFailed(QString reason, int network_error_code)
{
switch (network_error_code) {
default:
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
break;
case 409:
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
QString("%1 %2")
.arg(m_associated_page->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
break;
}
m_search_state = SearchState::Finished;
}
void ResourceModel::searchRequestAborted()
{
if (m_search_state != SearchState::ResetRequested)
qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!";
// Retry fetching
clearData();
m_next_search_offset = 0;
search();
}

View File

@ -0,0 +1,101 @@
#pragma once
#include <optional>
#include <QAbstractListModel>
#include "QObjectPtr.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/ConcurrentTask.h"
class NetJob;
class ResourcePage;
class ResourceAPI;
namespace ModPlatform {
struct IndexedPack;
}
class ResourceModel : public QAbstractListModel {
Q_OBJECT
public:
ResourceModel(ResourcePage* parent, ResourceAPI* api);
~ResourceModel() override;
[[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
[[nodiscard]] auto debugName() const -> QString;
[[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); }
[[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; };
[[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
inline void addActiveJob(Task::Ptr ptr) { m_current_job.addTask(ptr); if (!m_current_job.isRunning()) m_current_job.start(); }
inline Task const& activeJob() { return m_current_job; }
public slots:
void fetchMore(const QModelIndex& parent) override;
[[nodiscard]] inline bool canFetchMore(const QModelIndex& parent) const override
{
return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore;
}
void setSearchTerm(QString term) { m_search_term = term; }
virtual ResourceAPI::SearchArgs createSearchArguments() = 0;
virtual ResourceAPI::SearchCallbacks createSearchCallbacks() = 0;
virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0;
virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) = 0;
virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0;
virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) = 0;
/** Requests the API for more entries. */
virtual void search();
/** Applies any processing / extra requests needed to fully load the specified entry's information. */
virtual void loadEntry(QModelIndex&);
/** Schedule a refresh, clearing the current state. */
void refresh();
/** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
protected:
/** Resets the model's data. */
void clearData();
[[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&) const;
protected:
/* Basic search parameters */
enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
int m_next_search_offset = 0;
QString m_search_term;
std::unique_ptr<ResourceAPI> m_api;
ConcurrentTask m_current_job;
shared_qobject_ptr<NetJob> m_current_icon_job;
QSet<QUrl> m_currently_running_icon_actions;
QSet<QUrl> m_failed_icon_actions;
ResourcePage* m_associated_page = nullptr;
QList<ModPlatform::IndexedPack> m_packs;
// HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
// This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
static QHash<ResourceModel*, bool> s_running_models;
private:
/* Default search request callbacks */
void searchRequestFailed(QString reason, int network_error_code);
void searchRequestAborted();
};

View File

@ -0,0 +1,347 @@
#include "ResourcePage.h"
#include "ui_ResourcePage.h"
#include <QDesktopServices>
#include <QKeyEvent>
#include "Markdown.h"
#include "ResourceDownloadTask.h"
#include "minecraft/MinecraftInstance.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProjectItem.h"
ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance)
: QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false)
{
m_ui->setupUi(this);
m_ui->searchEdit->installEventFilter(this);
m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch);
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
m_ui->packView->installEventFilter(this);
connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl);
}
ResourcePage::~ResourcePage()
{
delete m_ui;
}
void ResourcePage::retranslate()
{
m_ui->retranslateUi(this);
}
void ResourcePage::openedImpl()
{
if (!supportsFiltering())
m_ui->resourceFilterButton->setVisible(false);
updateSelectionButton();
triggerSearch();
}
auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool
{
if (event->type() == QEvent::KeyPress) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
if (watched == m_ui->searchEdit) {
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
keyEvent->accept();
return true;
} else {
if (m_search_timer.isActive())
m_search_timer.stop();
m_search_timer.start(350);
}
} else if (watched == m_ui->packView) {
if (keyEvent->key() == Qt::Key_Return) {
onResourceSelected();
// To have the 'select mod' button outlined instead of the 'review and confirm' one
m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
keyEvent->accept();
return true;
}
}
}
return QWidget::eventFilter(watched, event);
}
QString ResourcePage::getSearchTerm() const
{
return m_ui->searchEdit->text();
}
void ResourcePage::setSearchTerm(QString term)
{
m_ui->searchEdit->setText(term);
}
ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
{
return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
}
bool ResourcePage::isPackSelected(const ModPlatform::IndexedPack& pack, int version) const
{
if (version < 0 || !pack.versionsLoaded)
return m_parent_dialog->isSelected(pack.name);
return m_parent_dialog->isSelected(pack.name, pack.versions[version].fileName);
}
void ResourcePage::updateUi()
{
auto current_pack = getCurrentPack();
QString text = "";
QString name = current_pack.name;
if (current_pack.websiteUrl.isEmpty())
text = name;
else
text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
if (!current_pack.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_pack.authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
}
if (current_pack.extraDataLoaded) {
if (!current_pack.extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
};
QStringList donates;
for (auto& donate : current_pack.extraData.donate) {
donates.append(donateToStr(donate));
}
text += donates.join(", ");
}
if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() ||
!current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
if (!current_pack.extraData.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>";
if (!current_pack.extraData.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>";
if (!current_pack.extraData.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>";
if (!current_pack.extraData.discordUrl.isEmpty())
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>";
}
text += "<hr>";
m_ui->packDescription->setHtml(
text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body)));
m_ui->packDescription->flush();
}
void ResourcePage::updateSelectionButton()
{
if (!isOpened || m_selected_version_index < 0) {
m_ui->resourceSelectionButton->setEnabled(false);
return;
}
m_ui->resourceSelectionButton->setEnabled(true);
if (!isPackSelected(getCurrentPack(), m_selected_version_index)) {
m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
} else {
m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
}
}
void ResourcePage::updateVersionList()
{
auto current_pack = getCurrentPack();
m_ui->versionSelectionBox->blockSignals(true);
m_ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->blockSignals(false);
for (int i = 0; i < current_pack.versions.size(); i++) {
auto& version = current_pack.versions[i];
if (optedOut(version))
continue;
m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, QVariant(i));
}
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
if (!curr.isValid()) {
return;
}
auto current_pack = getCurrentPack();
bool request_load = false;
if (!current_pack.versionsLoaded) {
m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
m_ui->resourceSelectionButton->setEnabled(false);
request_load = true;
} else {
updateVersionList();
}
if (!current_pack.extraDataLoaded)
request_load = true;
if (request_load)
m_model->loadEntry(curr);
updateUi();
}
void ResourcePage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
m_selected_version_index = -1;
return;
}
m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
{
m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel()));
}
void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion&)
{
m_parent_dialog->removeResource(pack.name);
}
void ResourcePage::onResourceSelected()
{
if (m_selected_version_index < 0)
return;
auto current_pack = getCurrentPack();
auto& version = current_pack.versions[m_selected_version_index];
if (m_parent_dialog->isSelected(current_pack.name, version.fileName))
removeResourceFromDialog(current_pack, version);
else
addResourceToDialog(current_pack, version);
updateSelectionButton();
/* Force redraw on the resource list when the selection changes */
m_ui->packView->adjustSize();
}
void ResourcePage::openUrl(const QUrl& url)
{
// do not allow other url schemes for security reasons
if (!(url.scheme() == "http" || url.scheme() == "https")) {
qWarning() << "Unsupported scheme" << url.scheme();
return;
}
// detect URLs and search instead
const QString address = url.host() + url.path();
QRegularExpressionMatch match;
QString page;
for (auto&& [regex, candidate] : urlHandlers().asKeyValueRange()) {
if (match = QRegularExpression(regex).match(address); match.hasMatch()) {
page = candidate;
break;
}
}
if (!page.isNull()) {
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
if (slug != getCurrentPack().slug) {
m_parent_dialog->selectPage(page);
auto newPage = m_parent_dialog->getSelectedPage();
QLineEdit* searchEdit = newPage->m_ui->searchEdit;
auto model = newPage->m_model;
QListView* view = newPage->m_ui->packView;
auto jump = [url, slug, model, view] {
for (int row = 0; row < model->rowCount({}); row++) {
const QModelIndex index = model->index(row);
const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (pack.slug == slug) {
view->setCurrentIndex(index);
return;
}
}
// The final fallback.
QDesktopServices::openUrl(url);
};
searchEdit->setText(slug);
newPage->triggerSearch();
if (model->activeJob().isRunning())
connect(&model->activeJob(), &Task::finished, jump);
else
jump();
return;
}
}
// open in the user's web browser
QDesktopServices::openUrl(url);
}

View File

@ -0,0 +1,95 @@
#pragma once
#include <QTimer>
#include <QWidget>
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/BasePage.h"
#include "ui/widgets/ProgressWidget.h"
namespace Ui {
class ResourcePage;
}
class BaseInstance;
class ResourceModel;
class ResourceDownloadDialog;
class ResourcePage : public QWidget, public BasePage {
Q_OBJECT
public:
~ResourcePage() override;
/* Affects what the user sees */
[[nodiscard]] auto displayName() const -> QString override = 0;
[[nodiscard]] auto icon() const -> QIcon override = 0;
[[nodiscard]] auto id() const -> QString override = 0;
[[nodiscard]] auto helpPage() const -> QString override = 0;
[[nodiscard]] bool shouldDisplay() const override = 0;
/* Used internally */
[[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
[[nodiscard]] virtual auto debugName() const -> QString = 0;
[[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); }
/* Features this resource's page supports */
[[nodiscard]] virtual bool supportsFiltering() const = 0;
void retranslate() override;
void openedImpl() override;
auto eventFilter(QObject* watched, QEvent* event) -> bool override;
/** Get the current term in the search bar. */
[[nodiscard]] auto getSearchTerm() const -> QString;
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
[[nodiscard]] bool isPackSelected(const ModPlatform::IndexedPack&, int version = -1) const;
[[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
[[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
protected:
ResourcePage(ResourceDownloadDialog* parent, BaseInstance&);
public slots:
virtual void updateUi();
virtual void updateSelectionButton();
virtual void updateVersionList();
virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
protected slots:
virtual void triggerSearch() {}
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onResourceSelected();
/** Associates regex expressions to pages in the order they're given in the map. */
[[nodiscard]] virtual QMap<QString, QString> urlHandlers() const = 0;
virtual void openUrl(const QUrl&);
/** Whether the version is opted out or not. Currently only makes sense in CF. */
virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
public:
BaseInstance& m_base_instance;
protected:
Ui::ResourcePage* m_ui;
ResourceDownloadDialog* m_parent_dialog = nullptr;
ResourceModel* m_model = nullptr;
int m_selected_version_index = -1;
ProgressWidget m_fetch_progress;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
};

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>ModPage</class> <class>ResourcePage</class>
<widget class="QWidget" name="ModPage"> <widget class="QWidget" name="ResourcePage">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
@ -51,7 +51,7 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLineEdit" name="searchEdit"> <widget class="QLineEdit" name="searchEdit">
<property name="placeholderText"> <property name="placeholderText">
<string>Search for mods...</string> <string>Search for resources...</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -74,16 +74,16 @@
<widget class="QComboBox" name="sortByBox"/> <widget class="QComboBox" name="sortByBox"/>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
<widget class="QPushButton" name="modSelectionButton"> <widget class="QPushButton" name="resourceSelectionButton">
<property name="text"> <property name="text">
<string>Select mod for download</string> <string>Select resource for download</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QPushButton" name="modFilterButton"> <widget class="QPushButton" name="resourceFilterButton">
<property name="text"> <property name="text">
<string>Filter options</string> <string>Filter options</string>
</property> </property>

View File

@ -1,4 +1,4 @@
#include "FlameModModel.h" #include "FlameResourceModels.h"
#include "Json.h" #include "Json.h"
#include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/FlameModIndex.h"
@ -20,7 +20,7 @@ void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance);
} }
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include "FlameModPage.h" #include "modplatform/flame/FlameAPI.h"
namespace FlameMod { namespace FlameMod {
@ -8,7 +8,7 @@ class ListModel : public ModPlatform::ListModel {
Q_OBJECT Q_OBJECT
public: public:
ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent, new FlameAPI) {}
~ListModel() override = default; ~ListModel() override = default;
private: private:

View File

@ -34,37 +34,37 @@
* limitations under the License. * limitations under the License.
*/ */
#include "FlameModPage.h" #include "FlameResourcePages.h"
#include "ui_ModPage.h" #include "ui_ResourcePage.h"
#include "FlameModModel.h" #include "FlameResourceModels.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
: ModPage(dialog, instance, new FlameAPI()) : ModPage(dialog, instance)
{ {
listModel = new FlameMod::ListModel(this); m_model = new FlameMod::ListModel(this);
ui->packView->setModel(listModel); m_ui->packView->setModel(m_model);
// index is used to set the sorting with the flame api // index is used to set the sorting with the flame api
ui->sortByBox->addItem(tr("Sort by Featured")); m_ui->sortByBox->addItem(tr("Sort by Featured"));
ui->sortByBox->addItem(tr("Sort by Popularity")); m_ui->sortByBox->addItem(tr("Sort by Popularity"));
ui->sortByBox->addItem(tr("Sort by Last Updated")); m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Name")); m_ui->sortByBox->addItem(tr("Sort by Name"));
ui->sortByBox->addItem(tr("Sort by Author")); m_ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Downloads")); m_ui->sortByBox->addItem(tr("Sort by Downloads"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // 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... // so it's best not to connect them in the parent's contructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected);
ui->packDescription->setMetaEntry(metaEntryBase()); m_ui->packDescription->setMetaEntry(metaEntryBase());
} }
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{ {
Q_UNUSED(loaders); Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();

View File

@ -36,21 +36,22 @@
#pragma once #pragma once
#include "modplatform/ModAPI.h" #include "Application.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/ModPage.h"
class FlameModPage : public ModPage { class FlameModPage : public ModPage {
Q_OBJECT Q_OBJECT
public: public:
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{ {
return ModPage::create<FlameModPage>(dialog, instance); return ModPage::create<FlameModPage>(dialog, instance);
} }
FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~FlameModPage() override = default; ~FlameModPage() override = default;
inline auto displayName() const -> QString override { return "CurseForge"; } inline auto displayName() const -> QString override { return "CurseForge"; }
@ -61,7 +62,7 @@ class FlameModPage : public ModPage {
inline auto debugName() const -> QString override { return "Flame"; } inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
bool optedOut(ModPlatform::IndexedVersion& ver) const override; bool optedOut(ModPlatform::IndexedVersion& ver) const override;
auto shouldDisplay() const -> bool override; auto shouldDisplay() const -> bool override;

View File

@ -16,8 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "ModrinthModModel.h" #include "ModrinthResourceModels.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h" #include "modplatform/modrinth/ModrinthPackIndex.h"
namespace Modrinth { namespace Modrinth {
@ -37,7 +40,7 @@ void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_associated_page->m_base_instance);
} }
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
@ -46,3 +49,5 @@ auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
} }
} // namespace Modrinth } // namespace Modrinth

View File

@ -18,7 +18,11 @@
#pragma once #pragma once
#include "ModrinthModPage.h" #include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
#include "modplatform/modrinth/ModrinthAPI.h"
namespace Modrinth { namespace Modrinth {
@ -26,7 +30,7 @@ class ListModel : public ModPlatform::ListModel {
Q_OBJECT Q_OBJECT
public: public:
ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){}; ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent, new ModrinthAPI){};
~ListModel() override = default; ~ListModel() override = default;
private: private:
@ -42,3 +46,4 @@ class ListModel : public ModPlatform::ListModel {
}; };
} // namespace Modrinth } // namespace Modrinth

View File

@ -33,41 +33,43 @@
* limitations under the License. * limitations under the License.
*/ */
#include "ModrinthModPage.h" #include "ModrinthResourcePages.h"
#include "modplatform/modrinth/ModrinthAPI.h" #include "ui_ResourcePage.h"
#include "ui_ModPage.h"
#include "ModrinthModModel.h" #include "modplatform/modrinth/ModrinthAPI.h"
#include "ModrinthResourceModels.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance) ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance)
: ModPage(dialog, instance, new ModrinthAPI()) : ModPage(dialog, instance)
{ {
listModel = new Modrinth::ListModel(this); m_model = new Modrinth::ListModel(this);
ui->packView->setModel(listModel); m_ui->packView->setModel(m_model);
// index is used to set the sorting with the modrinth api // index is used to set the sorting with the modrinth api
ui->sortByBox->addItem(tr("Sort by Relevance")); m_ui->sortByBox->addItem(tr("Sort by Relevance"));
ui->sortByBox->addItem(tr("Sort by Downloads")); m_ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows")); m_ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by Last Updated")); m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Newest")); m_ui->sortByBox->addItem(tr("Sort by Newest"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // 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 constructor... // so it's best not to connect them in the parent's constructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected);
ui->packDescription->setMetaEntry(metaEntryBase()); m_ui->packDescription->setMetaEntry(metaEntryBase());
} }
auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{ {
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); auto loaderCompatible = !loaders.has_value();
auto loaderCompatible = false; if (!loaderCompatible) {
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value());
for (auto remoteLoader : ver.loaders) for (auto remoteLoader : ver.loaders)
{ {
if (loaderStrings.contains(remoteLoader)) { if (loaderStrings.contains(remoteLoader)) {
@ -75,6 +77,8 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString
break; break;
} }
} }
}
return ver.mcVersion.contains(mineVer) && loaderCompatible; return ver.mcVersion.contains(mineVer) && loaderCompatible;
} }
@ -82,3 +86,4 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString
// other mod providers start loading before being selected, at least with // other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class... // my Qt, so we need to implement this in every derived class...
auto ModrinthModPage::shouldDisplay() const -> bool { return true; } auto ModrinthModPage::shouldDisplay() const -> bool { return true; }

View File

@ -35,32 +35,38 @@
#pragma once #pragma once
#include "modplatform/ModAPI.h" #include "Application.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h" static inline QString displayName() { return "Modrinth"; }
static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); }
static inline QString id() { return "modrinth"; }
static inline QString debugName() { return "Modrinth"; }
static inline QString metaEntryBase() { return "ModrinthPacks"; };
class ModrinthModPage : public ModPage { class ModrinthModPage : public ModPage {
Q_OBJECT Q_OBJECT
public: public:
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{ {
return ModPage::create<ModrinthModPage>(dialog, instance); return ModPage::create<ModrinthModPage>(dialog, instance);
} }
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~ModrinthModPage() override = default; ~ModrinthModPage() override = default;
inline auto displayName() const -> QString override { return "Modrinth"; } [[nodiscard]] bool shouldDisplay() const override;
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; } [[nodiscard]] inline auto displayName() const -> QString override { return ::displayName(); } \
[[nodiscard]] inline auto icon() const -> QIcon override { return ::icon(); } \
[[nodiscard]] inline auto id() const -> QString override { return ::id(); } \
[[nodiscard]] inline auto debugName() const -> QString override { return ::debugName(); } \
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return ::metaEntryBase(); }
inline auto helpPage() const -> QString override { return "Mod-platform"; } inline auto helpPage() const -> QString override { return "Mod-platform"; }
inline auto debugName() const -> QString override { return "Modrinth"; } auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
}; };

View File

@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format)
m_bar->setFormat(format); m_bar->setFormat(format);
} }
void ProgressWidget::watch(Task* task) void ProgressWidget::watch(const Task* task)
{ {
if (!task) if (!task)
return; return;
@ -57,11 +57,11 @@ void ProgressWidget::watch(Task* task)
show(); show();
} }
void ProgressWidget::start(Task* task) void ProgressWidget::start(const Task* task)
{ {
watch(task); watch(task);
if (!m_task->isRunning()) if (!m_task->isRunning())
QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); QMetaObject::invokeMethod(const_cast<Task*>(m_task), "start", Qt::QueuedConnection);
} }
bool ProgressWidget::exec(std::shared_ptr<Task> task) bool ProgressWidget::exec(std::shared_ptr<Task> task)

View File

@ -27,10 +27,10 @@ class ProgressWidget : public QWidget {
public slots: public slots:
/** Watch the progress of a task. */ /** Watch the progress of a task. */
void watch(Task* task); void watch(const Task* task);
/** Watch the progress of a task, and start it if needed */ /** Watch the progress of a task, and start it if needed */
void start(Task* task); void start(const Task* task);
/** Blocking way of waiting for a task to finish. */ /** Blocking way of waiting for a task to finish. */
bool exec(std::shared_ptr<Task> task); bool exec(std::shared_ptr<Task> task);
@ -50,7 +50,7 @@ class ProgressWidget : public QWidget {
private: private:
QLabel* m_label = nullptr; QLabel* m_label = nullptr;
QProgressBar* m_bar = nullptr; QProgressBar* m_bar = nullptr;
Task* m_task = nullptr; const Task* m_task = nullptr;
bool m_hide_if_inactive = false; bool m_hide_if_inactive = false;
}; };

View File

@ -48,7 +48,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "sha512"); QCOMPARE(metadata.hash_format, "sha512");
QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"); QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH);
QCOMPARE(metadata.version(), "ug2qKTPR"); QCOMPARE(metadata.version(), "ug2qKTPR");
QCOMPARE(metadata.mod_id(), "kYq5qkSL"); QCOMPARE(metadata.mod_id(), "kYq5qkSL");
} }
@ -76,7 +76,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "murmur2"); QCOMPARE(metadata.hash_format, "murmur2");
QCOMPARE(metadata.hash, "1781245820"); QCOMPARE(metadata.hash, "1781245820");
QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME);
QCOMPARE(metadata.file_id, 3509043); QCOMPARE(metadata.file_id, 3509043);
QCOMPARE(metadata.project_id, 327154); QCOMPARE(metadata.project_id, 327154);
} }