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:
parent
b937d33436
commit
6a18079953
@ -38,9 +38,9 @@ set(CORE_SOURCES
|
||||
InstanceImportTask.h
|
||||
InstanceImportTask.cpp
|
||||
|
||||
# Mod downloading task
|
||||
ModDownloadTask.h
|
||||
ModDownloadTask.cpp
|
||||
# Resource downloading task
|
||||
ResourceDownloadTask.h
|
||||
ResourceDownloadTask.cpp
|
||||
|
||||
# Use tracking separate from memory management
|
||||
Usable.h
|
||||
@ -473,7 +473,7 @@ set(API_SOURCES
|
||||
modplatform/ModIndex.h
|
||||
modplatform/ModIndex.cpp
|
||||
|
||||
modplatform/ModAPI.h
|
||||
modplatform/ResourceAPI.h
|
||||
|
||||
modplatform/EnsureMetadataTask.h
|
||||
modplatform/EnsureMetadataTask.cpp
|
||||
@ -484,8 +484,8 @@ set(API_SOURCES
|
||||
modplatform/flame/FlameAPI.cpp
|
||||
modplatform/modrinth/ModrinthAPI.h
|
||||
modplatform/modrinth/ModrinthAPI.cpp
|
||||
modplatform/helpers/NetworkModAPI.h
|
||||
modplatform/helpers/NetworkModAPI.cpp
|
||||
modplatform/helpers/NetworkResourceAPI.h
|
||||
modplatform/helpers/NetworkResourceAPI.cpp
|
||||
modplatform/helpers/HashUtils.h
|
||||
modplatform/helpers/HashUtils.cpp
|
||||
modplatform/helpers/OverrideUtils.h
|
||||
@ -771,6 +771,11 @@ SET(LAUNCHER_SOURCES
|
||||
ui/pages/modplatform/VanillaPage.cpp
|
||||
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.h
|
||||
ui/pages/modplatform/ModModel.cpp
|
||||
@ -803,10 +808,10 @@ SET(LAUNCHER_SOURCES
|
||||
ui/pages/modplatform/flame/FlameModel.h
|
||||
ui/pages/modplatform/flame/FlamePage.cpp
|
||||
ui/pages/modplatform/flame/FlamePage.h
|
||||
ui/pages/modplatform/flame/FlameModModel.cpp
|
||||
ui/pages/modplatform/flame/FlameModModel.h
|
||||
ui/pages/modplatform/flame/FlameModPage.cpp
|
||||
ui/pages/modplatform/flame/FlameModPage.h
|
||||
ui/pages/modplatform/flame/FlameResourceModels.cpp
|
||||
ui/pages/modplatform/flame/FlameResourceModels.h
|
||||
ui/pages/modplatform/flame/FlameResourcePages.cpp
|
||||
ui/pages/modplatform/flame/FlameResourcePages.h
|
||||
|
||||
ui/pages/modplatform/modrinth/ModrinthPage.cpp
|
||||
ui/pages/modplatform/modrinth/ModrinthPage.h
|
||||
@ -821,10 +826,10 @@ SET(LAUNCHER_SOURCES
|
||||
ui/pages/modplatform/ImportPage.cpp
|
||||
ui/pages/modplatform/ImportPage.h
|
||||
|
||||
ui/pages/modplatform/modrinth/ModrinthModModel.cpp
|
||||
ui/pages/modplatform/modrinth/ModrinthModModel.h
|
||||
ui/pages/modplatform/modrinth/ModrinthModPage.cpp
|
||||
ui/pages/modplatform/modrinth/ModrinthModPage.h
|
||||
ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
|
||||
ui/pages/modplatform/modrinth/ModrinthResourceModels.h
|
||||
ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
|
||||
ui/pages/modplatform/modrinth/ModrinthResourcePages.h
|
||||
|
||||
# GUI - dialogs
|
||||
ui/dialogs/AboutDialog.cpp
|
||||
@ -869,6 +874,8 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/VersionSelectDialog.h
|
||||
ui/dialogs/SkinUploadDialog.cpp
|
||||
ui/dialogs/SkinUploadDialog.h
|
||||
ui/dialogs/ResourceDownloadDialog.cpp
|
||||
ui/dialogs/ResourceDownloadDialog.h
|
||||
ui/dialogs/ModDownloadDialog.cpp
|
||||
ui/dialogs/ModDownloadDialog.h
|
||||
ui/dialogs/ScrollMessageBox.cpp
|
||||
@ -965,7 +972,7 @@ qt_wrap_ui(LAUNCHER_UI
|
||||
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
|
||||
ui/pages/modplatform/atlauncher/AtlPage.ui
|
||||
ui/pages/modplatform/VanillaPage.ui
|
||||
ui/pages/modplatform/ModPage.ui
|
||||
ui/pages/modplatform/ResourcePage.ui
|
||||
ui/pages/modplatform/flame/FlamePage.ui
|
||||
ui/pages/modplatform/legacy_ftb/Page.ui
|
||||
ui/pages/modplatform/ImportPage.ui
|
||||
|
@ -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};
|
||||
}
|
80
launcher/ResourceDownloadTask.cpp
Normal file
80
launcher/ResourceDownloadTask.cpp
Normal 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 };
|
||||
}
|
@ -25,18 +25,18 @@
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||
|
||||
class ModFolderModel;
|
||||
class ResourceFolderModel;
|
||||
|
||||
class ModDownloadTask : public SequentialTask {
|
||||
class ResourceDownloadTask : public SequentialTask {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
|
||||
const QString& getFilename() const { return m_mod_version.fileName; }
|
||||
explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
|
||||
const QString& getFilename() const { return m_pack_version.fileName; }
|
||||
|
||||
private:
|
||||
ModPlatform::IndexedPack m_mod;
|
||||
ModPlatform::IndexedVersion m_mod_version;
|
||||
const std::shared_ptr<ModFolderModel> mods;
|
||||
ModPlatform::IndexedPack m_pack;
|
||||
ModPlatform::IndexedVersion m_pack_version;
|
||||
const std::shared_ptr<ResourceFolderModel> m_pack_model;
|
||||
|
||||
NetJob::Ptr m_filesNetJob;
|
||||
LocalModUpdateTask::Ptr m_update_task;
|
||||
@ -50,7 +50,7 @@ private:
|
||||
std::tuple<QString, QString> to_delete {"", ""};
|
||||
|
||||
private slots:
|
||||
void hasOldMod(QString name, QString filename);
|
||||
void hasOldResource(QString name, QString filename);
|
||||
};
|
||||
|
||||
|
@ -55,12 +55,13 @@
|
||||
#include "PackProfile_p.h"
|
||||
#include "ComponentUpdateTask.h"
|
||||
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "Application.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{
|
||||
{"net.minecraftforge", ModAPI::Forge},
|
||||
{"net.fabricmc.fabric-loader", ModAPI::Fabric},
|
||||
{"org.quiltmc.quilt-loader", ModAPI::Quilt}
|
||||
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
|
||||
{"net.minecraftforge", ResourceAPI::Forge},
|
||||
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
|
||||
{"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
|
||||
};
|
||||
|
||||
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();
|
||||
Component* c = getComponent(i.key());
|
||||
if (c != nullptr && c->isEnabled()) {
|
||||
if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) {
|
||||
result |= i.value();
|
||||
has_any_loader = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_any_loader)
|
||||
return {};
|
||||
return result;
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
||||
#include "BaseVersion.h"
|
||||
#include "MojangDownloadInfo.h"
|
||||
#include "net/Mode.h"
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
class MinecraftInstance;
|
||||
struct PackProfileData;
|
||||
@ -145,7 +145,7 @@ public:
|
||||
// todo(merged): is this the best approach
|
||||
void appendComponent(ComponentPtr component);
|
||||
|
||||
ModAPI::ModLoaderTypes getModLoaders();
|
||||
std::optional<ResourceAPI::ModLoaderTypes> getModLoaders();
|
||||
|
||||
private:
|
||||
void scheduleSave();
|
||||
|
@ -1,18 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class ModDownloadTask;
|
||||
class ResourceDownloadTask;
|
||||
class ModFolderModel;
|
||||
|
||||
class CheckUpdateTask : public Task {
|
||||
Q_OBJECT
|
||||
|
||||
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) {};
|
||||
|
||||
struct UpdatableMod {
|
||||
@ -21,11 +21,11 @@ class CheckUpdateTask : public Task {
|
||||
QString old_version;
|
||||
QString new_version;
|
||||
QString changelog;
|
||||
ModPlatform::Provider provider;
|
||||
ModDownloadTask* download;
|
||||
ModPlatform::ResourceProvider provider;
|
||||
ResourceDownloadTask* download;
|
||||
|
||||
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)
|
||||
{}
|
||||
};
|
||||
@ -44,7 +44,7 @@ class CheckUpdateTask : public Task {
|
||||
protected:
|
||||
QList<Mod*>& m_mods;
|
||||
std::list<Version>& m_game_versions;
|
||||
ModAPI::ModLoaderTypes m_loaders;
|
||||
std::optional<ResourceAPI::ModLoaderTypes> m_loaders;
|
||||
std::shared_ptr<ModFolderModel> m_mods_folder;
|
||||
|
||||
std::vector<UpdatableMod> m_updatable;
|
||||
|
@ -20,7 +20,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
static ModrinthAPI modrinth_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)
|
||||
{
|
||||
auto hash_task = createNewHash(mod);
|
||||
@ -31,7 +31,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider
|
||||
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)
|
||||
{
|
||||
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
|
||||
@ -110,10 +110,10 @@ void EnsureMetadataTask::executeTask()
|
||||
NetJob::Ptr version_task;
|
||||
|
||||
switch (m_provider) {
|
||||
case (ModPlatform::Provider::MODRINTH):
|
||||
case (ModPlatform::ResourceProvider::MODRINTH):
|
||||
version_task = modrinthVersionsTask();
|
||||
break;
|
||||
case (ModPlatform::Provider::FLAME):
|
||||
case (ModPlatform::ResourceProvider::FLAME):
|
||||
version_task = flameVersionsTask();
|
||||
break;
|
||||
}
|
||||
@ -130,10 +130,10 @@ void EnsureMetadataTask::executeTask()
|
||||
NetJob::Ptr project_task;
|
||||
|
||||
switch (m_provider) {
|
||||
case (ModPlatform::Provider::MODRINTH):
|
||||
case (ModPlatform::ResourceProvider::MODRINTH):
|
||||
project_task = modrinthProjectsTask();
|
||||
break;
|
||||
case (ModPlatform::Provider::FLAME):
|
||||
case (ModPlatform::ResourceProvider::FLAME):
|
||||
project_task = flameProjectsTask();
|
||||
break;
|
||||
}
|
||||
@ -212,7 +212,7 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
|
||||
|
||||
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 ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
|
||||
|
@ -14,8 +14,8 @@ class EnsureMetadataTask : public Task {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
|
||||
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
|
||||
EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
|
||||
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
|
||||
|
||||
~EnsureMetadataTask() = default;
|
||||
|
||||
@ -57,7 +57,7 @@ class EnsureMetadataTask : public Task {
|
||||
private:
|
||||
QHash<QString, Mod*> m_mods;
|
||||
QDir m_index_dir;
|
||||
ModPlatform::Provider m_provider;
|
||||
ModPlatform::ResourceProvider m_provider;
|
||||
|
||||
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
||||
ConcurrentTask* m_hashing_task;
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
@ -24,47 +24,47 @@
|
||||
|
||||
namespace ModPlatform {
|
||||
|
||||
auto ProviderCapabilities::name(Provider p) -> const char*
|
||||
auto ProviderCapabilities::name(ResourceProvider p) -> const char*
|
||||
{
|
||||
switch (p) {
|
||||
case Provider::MODRINTH:
|
||||
case ResourceProvider::MODRINTH:
|
||||
return "modrinth";
|
||||
case Provider::FLAME:
|
||||
case ResourceProvider::FLAME:
|
||||
return "curseforge";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
auto ProviderCapabilities::readableName(Provider p) -> QString
|
||||
auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
|
||||
{
|
||||
switch (p) {
|
||||
case Provider::MODRINTH:
|
||||
case ResourceProvider::MODRINTH:
|
||||
return "Modrinth";
|
||||
case Provider::FLAME:
|
||||
case ResourceProvider::FLAME:
|
||||
return "CurseForge";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
auto ProviderCapabilities::hashType(Provider p) -> QStringList
|
||||
auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
|
||||
{
|
||||
switch (p) {
|
||||
case Provider::MODRINTH:
|
||||
case ResourceProvider::MODRINTH:
|
||||
return { "sha512", "sha1" };
|
||||
case Provider::FLAME:
|
||||
case ResourceProvider::FLAME:
|
||||
// Try newer formats first, fall back to old format
|
||||
return { "sha1", "md5", "murmur2" };
|
||||
}
|
||||
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;
|
||||
switch (p) {
|
||||
case Provider::MODRINTH: {
|
||||
case ResourceProvider::MODRINTH: {
|
||||
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
|
||||
break;
|
||||
}
|
||||
case Provider::FLAME:
|
||||
case ResourceProvider::FLAME:
|
||||
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
|
||||
break;
|
||||
}
|
||||
|
@ -28,17 +28,16 @@ class QIODevice;
|
||||
|
||||
namespace ModPlatform {
|
||||
|
||||
enum class Provider {
|
||||
MODRINTH,
|
||||
FLAME
|
||||
};
|
||||
enum class ResourceProvider { MODRINTH, FLAME };
|
||||
|
||||
enum class ResourceType { MOD, RESOURCE_PACK };
|
||||
|
||||
class ProviderCapabilities {
|
||||
public:
|
||||
auto name(Provider) -> const char*;
|
||||
auto readableName(Provider) -> QString;
|
||||
auto hashType(Provider) -> QStringList;
|
||||
auto hash(Provider, QIODevice*, QString type = "") -> QString;
|
||||
auto name(ResourceProvider) -> const char*;
|
||||
auto readableName(ResourceProvider) -> QString;
|
||||
auto hashType(ResourceProvider) -> QStringList;
|
||||
auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
|
||||
};
|
||||
|
||||
struct ModpackAuthor {
|
||||
@ -81,7 +80,7 @@ struct ExtraPackData {
|
||||
|
||||
struct IndexedPack {
|
||||
QVariant addonId;
|
||||
Provider provider;
|
||||
ResourceProvider provider;
|
||||
QString name;
|
||||
QString slug;
|
||||
QString description;
|
||||
@ -101,4 +100,4 @@ struct IndexedPack {
|
||||
} // namespace ModPlatform
|
||||
|
||||
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
|
||||
Q_DECLARE_METATYPE(ModPlatform::Provider)
|
||||
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
|
||||
|
149
launcher/modplatform/ResourceAPI.h
Normal file
149
launcher/modplatform/ResourceAPI.h
Normal 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;
|
||||
}
|
||||
};
|
@ -106,13 +106,19 @@ auto FlameAPI::getModDescription(int modId) -> QString
|
||||
|
||||
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;
|
||||
|
||||
auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
|
||||
auto response = new QByteArray();
|
||||
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] {
|
||||
QJsonParseError parse_error{};
|
||||
@ -161,7 +167,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
|
||||
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());
|
||||
|
||||
@ -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));
|
||||
|
||||
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; });
|
||||
|
||||
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());
|
||||
|
||||
@ -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));
|
||||
|
||||
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; });
|
||||
|
||||
return netJob;
|
||||
|
@ -1,21 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/helpers/NetworkModAPI.h"
|
||||
#include "modplatform/helpers/NetworkResourceAPI.h"
|
||||
|
||||
class FlameAPI : public NetworkModAPI {
|
||||
class FlameAPI : public NetworkResourceAPI {
|
||||
public:
|
||||
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
|
||||
auto getModFileChangelog(int modId, int fileId) -> QString;
|
||||
auto getModDescription(int modId) -> QString;
|
||||
|
||||
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
|
||||
|
||||
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
|
||||
auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*;
|
||||
NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
|
||||
NetJob::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
|
||||
NetJob::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
|
||||
|
||||
private:
|
||||
inline auto getSortFieldInt(QString sortString) const -> int
|
||||
static int getSortFieldInt(QString const& sortString)
|
||||
{
|
||||
return sortString == "Featured" ? 1
|
||||
: sortString == "Popularity" ? 2
|
||||
@ -28,48 +28,16 @@ class FlameAPI : public NetworkModAPI {
|
||||
: 1;
|
||||
}
|
||||
|
||||
private:
|
||||
inline auto getModSearchURL(SearchArgs& args) const -> QString override
|
||||
static int getClassId(ModPlatform::ResourceType type)
|
||||
{
|
||||
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(
|
||||
"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
|
||||
static int getMappedModLoader(ModLoaderTypes loaders)
|
||||
{
|
||||
// https://docs.curseforge.com/?http#tocS_ModLoaderType
|
||||
if (loaders & Forge)
|
||||
@ -81,4 +49,43 @@ class FlameAPI : public NetworkModAPI {
|
||||
return 4; // Quilt would probably be 5
|
||||
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('&');
|
||||
};
|
||||
};
|
||||
|
@ -7,7 +7,10 @@
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/ResourceFolderModel.h"
|
||||
|
||||
static FlameAPI api;
|
||||
|
||||
@ -160,7 +163,7 @@ void FlameCheckUpdate::executeTask()
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
pack.description = mod->description();
|
||||
pack.provider = ModPlatform::Provider::FLAME;
|
||||
pack.provider = ModPlatform::ResourceProvider::FLAME;
|
||||
|
||||
auto old_version = mod->version();
|
||||
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
|
||||
@ -168,10 +171,10 @@ void FlameCheckUpdate::executeTask()
|
||||
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,
|
||||
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
|
||||
ModPlatform::Provider::FLAME, download_task);
|
||||
ModPlatform::ResourceProvider::FLAME, download_task);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask {
|
||||
Q_OBJECT
|
||||
|
||||
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)
|
||||
{}
|
||||
|
||||
|
@ -183,7 +183,7 @@ bool FlameCreationTask::updateInstance()
|
||||
|
||||
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
|
||||
QJsonParseError 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));
|
||||
}
|
||||
});
|
||||
connect(job, &NetJob::finished, &loop, &QEventLoop::quit);
|
||||
connect(job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
m_process_update_file_info_job = job;
|
||||
job->start();
|
||||
|
@ -86,7 +86,7 @@ class FlameCreationTask final : public InstanceCreationTask {
|
||||
Flame::Manifest m_pack;
|
||||
|
||||
// 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;
|
||||
|
||||
QString m_managed_id, m_managed_version_id;
|
||||
|
@ -11,7 +11,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
|
||||
{
|
||||
pack.addonId = Json::requireInteger(obj, "id");
|
||||
pack.provider = ModPlatform::Provider::FLAME;
|
||||
pack.provider = ModPlatform::ResourceProvider::FLAME;
|
||||
pack.name = Json::requireString(obj, "name");
|
||||
pack.slug = Json::requireString(obj, "slug");
|
||||
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");
|
||||
for (auto h : hash_list) {
|
||||
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"));
|
||||
if (hash_types.contains(hash_algo)) {
|
||||
file.hash = Json::requireString(hash_entry, "value");
|
||||
|
@ -12,12 +12,12 @@ namespace Hashing {
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
|
||||
Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
|
||||
{
|
||||
switch (provider) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
case ModPlatform::ResourceProvider::MODRINTH:
|
||||
return createModrinthHasher(file_path);
|
||||
case ModPlatform::Provider::FLAME:
|
||||
case ModPlatform::ResourceProvider::FLAME:
|
||||
return createFlameHasher(file_path);
|
||||
default:
|
||||
qCritical() << "[Hashing]"
|
||||
@ -36,12 +36,12 @@ Hasher::Ptr createFlameHasher(QString 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);
|
||||
}
|
||||
|
||||
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);
|
||||
hasher->useHashType(type);
|
||||
@ -62,8 +62,8 @@ void ModrinthHasher::executeTask()
|
||||
return;
|
||||
}
|
||||
|
||||
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||
m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
|
||||
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
|
||||
m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
|
||||
|
||||
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) {
|
||||
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
|
||||
hash_type = ProviderCaps.hashType(provider).first();
|
||||
|
@ -42,21 +42,21 @@ class ModrinthHasher : public Hasher {
|
||||
|
||||
class BlockedModHasher : public Hasher {
|
||||
public:
|
||||
BlockedModHasher(QString file_path, ModPlatform::Provider provider);
|
||||
BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
|
||||
|
||||
void executeTask() override;
|
||||
|
||||
QStringList getHashTypes();
|
||||
bool useHashType(QString type);
|
||||
private:
|
||||
ModPlatform::Provider provider;
|
||||
ModPlatform::ResourceProvider provider;
|
||||
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 createModrinthHasher(QString file_path);
|
||||
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider);
|
||||
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type);
|
||||
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
|
||||
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
|
||||
|
||||
} // namespace Hashing
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
};
|
124
launcher/modplatform/helpers/NetworkResourceAPI.cpp
Normal file
124
launcher/modplatform/helpers/NetworkResourceAPI.cpp
Normal 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;
|
||||
}
|
18
launcher/modplatform/helpers/NetworkResourceAPI.h
Normal file
18
launcher/modplatform/helpers/NetworkResourceAPI.h
Normal 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;
|
||||
};
|
@ -37,21 +37,24 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format
|
||||
|
||||
auto ModrinthAPI::latestVersion(QString hash,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
std::optional<std::list<Version>> mcVersions,
|
||||
std::optional<ModLoaderTypes> loaders,
|
||||
QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
|
||||
|
||||
QJsonObject body_obj;
|
||||
|
||||
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
|
||||
if (loaders.has_value())
|
||||
Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
|
||||
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions) {
|
||||
game_versions.append(ver.toString());
|
||||
if (mcVersions.has_value()) {
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions.value()) {
|
||||
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);
|
||||
auto body_raw = body.toJson();
|
||||
@ -66,8 +69,8 @@ auto ModrinthAPI::latestVersion(QString hash,
|
||||
|
||||
auto ModrinthAPI::latestVersions(const QStringList& hashes,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
std::optional<std::list<Version>> mcVersions,
|
||||
std::optional<ModLoaderTypes> loaders,
|
||||
QByteArray* response) -> NetJob::Ptr
|
||||
{
|
||||
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::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()));
|
||||
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions) {
|
||||
game_versions.append(ver.toString());
|
||||
if (mcVersions.has_value()) {
|
||||
QStringList game_versions;
|
||||
for (auto& ver : mcVersions.value()) {
|
||||
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);
|
||||
auto body_raw = body.toJson();
|
||||
@ -95,7 +101,7 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes,
|
||||
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 searchUrl = getMultipleModInfoURL(addonIds);
|
||||
|
@ -19,13 +19,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "BuildConfig.h"
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/helpers/NetworkModAPI.h"
|
||||
#include "modplatform/helpers/NetworkResourceAPI.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
class ModrinthAPI : public NetworkModAPI {
|
||||
class ModrinthAPI : public NetworkResourceAPI {
|
||||
public:
|
||||
auto currentVersion(QString hash,
|
||||
QString hash_format,
|
||||
@ -37,17 +36,17 @@ class ModrinthAPI : public NetworkModAPI {
|
||||
|
||||
auto latestVersion(QString hash,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
std::optional<std::list<Version>> mcVersions,
|
||||
std::optional<ModLoaderTypes> loaders,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto latestVersions(const QStringList& hashes,
|
||||
QString hash_format,
|
||||
std::list<Version> mcVersions,
|
||||
ModLoaderTypes loaders,
|
||||
std::optional<std::list<Version>> mcVersions,
|
||||
std::optional<ModLoaderTypes> loaders,
|
||||
QByteArray* response) -> NetJob::Ptr;
|
||||
|
||||
auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
|
||||
NetJob::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
|
||||
|
||||
public:
|
||||
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
|
||||
{
|
||||
QStringList l;
|
||||
for (auto loader : {Forge, Fabric, Quilt})
|
||||
{
|
||||
if ((types & loader) || types == Unspecified)
|
||||
{
|
||||
l << ModAPI::getModLoaderString(loader);
|
||||
for (auto loader : {Forge, Fabric, Quilt}) {
|
||||
if (types & loader) {
|
||||
l << getModLoaderString(loader);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -78,28 +75,54 @@ class ModrinthAPI : public NetworkModAPI {
|
||||
}
|
||||
|
||||
private:
|
||||
inline auto getModSearchURL(SearchArgs& args) const -> QString override
|
||||
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
|
||||
{
|
||||
if (!validateModLoaders(args.loaders)) {
|
||||
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
|
||||
return "";
|
||||
switch (type) {
|
||||
case ModPlatform::ResourceType::MOD:
|
||||
return "mod";
|
||||
default:
|
||||
qWarning() << "Invalid resource type for Modrinth API!";
|
||||
break;
|
||||
}
|
||||
|
||||
return QString(BuildConfig.MODRINTH_PROD_URL +
|
||||
"/search?"
|
||||
"offset=%1&"
|
||||
"limit=25&"
|
||||
"query=%2&"
|
||||
"index=%3&"
|
||||
"facets=[[%4],%5[\"project_type:mod\"]]")
|
||||
.arg(args.offset)
|
||||
.arg(args.search)
|
||||
.arg(args.sorting)
|
||||
.arg(getModLoaderFilters(args.loaders))
|
||||
.arg(getGameVersionsArray(args.versions));
|
||||
return "";
|
||||
}
|
||||
[[nodiscard]] QString createFacets(SearchArgs const& args) const
|
||||
{
|
||||
QStringList facets_list;
|
||||
|
||||
if (args.loaders.has_value())
|
||||
facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
|
||||
if (args.versions.has_value())
|
||||
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
|
||||
facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
|
||||
|
||||
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;
|
||||
};
|
||||
@ -109,15 +132,16 @@ class ModrinthAPI : public NetworkModAPI {
|
||||
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 +
|
||||
"/project/%1/version?"
|
||||
"game_versions=[%2]&"
|
||||
"loaders=[\"%3\"]")
|
||||
.arg(args.addonId,
|
||||
getGameVersionsString(args.mcVersions),
|
||||
getModLoaderStrings(args.loaders).join("\",\""));
|
||||
QStringList get_arguments;
|
||||
if (args.mcVersions.has_value())
|
||||
get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value())));
|
||||
if (args.loaders.has_value())
|
||||
get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).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
|
||||
@ -127,12 +151,12 @@ class ModrinthAPI : public NetworkModAPI {
|
||||
s += QString("\"versions:%1\",").arg(ver.toString());
|
||||
}
|
||||
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
|
||||
{
|
||||
return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt));
|
||||
return loaders & (Forge | Fabric | Quilt);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -4,12 +4,15 @@
|
||||
|
||||
#include "Json.h"
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
#include "modplatform/helpers/HashUtils.h"
|
||||
|
||||
#include "tasks/ConcurrentTask.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/ResourceFolderModel.h"
|
||||
|
||||
static ModrinthAPI api;
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
@ -34,7 +37,7 @@ void ModrinthCheckUpdate::executeTask()
|
||||
|
||||
// Create all 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);
|
||||
for (auto* mod : m_mods) {
|
||||
@ -108,11 +111,13 @@ void ModrinthCheckUpdate::executeTask()
|
||||
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
|
||||
// so we may want to filter it
|
||||
QString loader_filter;
|
||||
static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
|
||||
for (auto flag : flags) {
|
||||
if (m_loaders.testFlag(flag)) {
|
||||
loader_filter = api.getModLoaderString(flag);
|
||||
break;
|
||||
if (m_loaders.has_value()) {
|
||||
static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
|
||||
for (auto flag : flags) {
|
||||
if (m_loaders.value().testFlag(flag)) {
|
||||
loader_filter = api.getModLoaderString(flag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,12 +157,12 @@ void ModrinthCheckUpdate::executeTask()
|
||||
for (auto& author : mod->authors())
|
||||
pack.authors.append({ author });
|
||||
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,
|
||||
ModPlatform::Provider::MODRINTH, download_task);
|
||||
ModPlatform::ResourceProvider::MODRINTH, download_task);
|
||||
}
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
|
@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
|
||||
Q_OBJECT
|
||||
|
||||
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)
|
||||
{}
|
||||
|
||||
|
@ -33,7 +33,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
|
||||
if (pack.addonId.toString().isEmpty())
|
||||
pack.addonId = Json::requireString(obj, "id");
|
||||
|
||||
pack.provider = ModPlatform::Provider::MODRINTH;
|
||||
pack.provider = ModPlatform::ResourceProvider::MODRINTH;
|
||||
pack.name = Json::requireString(obj, "title");
|
||||
|
||||
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_type = preferred_hash_type;
|
||||
} else {
|
||||
auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
|
||||
auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
|
||||
for (auto& hash_type : hash_types) {
|
||||
if (hash_list.contains(hash_type)) {
|
||||
file.hash = Json::requireString(hash_list, hash_type);
|
||||
|
@ -97,7 +97,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
|
||||
mod.name = mod_pack.name;
|
||||
mod.filename = mod_version.fileName;
|
||||
|
||||
if (mod_pack.provider == ModPlatform::Provider::FLAME) {
|
||||
if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) {
|
||||
mod.mode = "metadata:curseforge";
|
||||
} else {
|
||||
mod.mode = "url";
|
||||
@ -176,11 +176,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
|
||||
in_stream << QString("\n[update]\n");
|
||||
in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(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("project-id = %1\n").arg(mod.project_id.toString());
|
||||
break;
|
||||
case (ModPlatform::Provider::MODRINTH):
|
||||
case (ModPlatform::ResourceProvider::MODRINTH):
|
||||
addToStream("mod-id", mod.mod_id().toString());
|
||||
addToStream("version", mod.version().toString());
|
||||
break;
|
||||
@ -273,7 +273,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
|
||||
}
|
||||
|
||||
{ // [update] info
|
||||
using Provider = ModPlatform::Provider;
|
||||
using Provider = ModPlatform::ResourceProvider;
|
||||
|
||||
auto update_table = table["update"];
|
||||
if (!update_table || !update_table.is_table()) {
|
||||
|
@ -49,7 +49,7 @@ class V1 {
|
||||
QString hash {};
|
||||
|
||||
// [update]
|
||||
ModPlatform::Provider provider {};
|
||||
ModPlatform::ResourceProvider provider {};
|
||||
QVariant file_id {};
|
||||
QVariant project_id {};
|
||||
|
||||
|
@ -52,7 +52,6 @@ class NetAction : public Task {
|
||||
virtual ~NetAction() = default;
|
||||
|
||||
QUrl url() { return m_url; }
|
||||
auto index() -> int { return m_index_within_job; }
|
||||
|
||||
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
|
||||
|
||||
@ -75,9 +74,6 @@ class NetAction : public Task {
|
||||
public:
|
||||
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
||||
|
||||
/// index within the parent job, FIXME: nuke
|
||||
int m_index_within_job = 0;
|
||||
|
||||
/// the network reply
|
||||
unique_qobject_ptr<QNetworkReply> m_reply;
|
||||
|
||||
|
@ -38,11 +38,10 @@
|
||||
|
||||
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
|
||||
{
|
||||
action->m_index_within_job = m_queue.size();
|
||||
m_queue.append(action);
|
||||
|
||||
action->setNetwork(m_network);
|
||||
|
||||
addTask(action);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -230,7 +230,7 @@ void BlockedModsDialog::addHashTask(QString path)
|
||||
/// @param path the path to the local file being hashed
|
||||
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;
|
||||
|
||||
|
@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll()
|
||||
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()
|
||||
@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders()
|
||||
int btn_index = 0;
|
||||
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);
|
||||
m_providers.addButton(btn, btn_index++);
|
||||
ui->providersLayout->addWidget(btn);
|
||||
|
@ -8,7 +8,7 @@ class ChooseProviderDialog;
|
||||
}
|
||||
|
||||
namespace ModPlatform {
|
||||
enum class Provider;
|
||||
enum class ResourceProvider;
|
||||
}
|
||||
|
||||
class Mod;
|
||||
@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog {
|
||||
|
||||
bool try_others = false;
|
||||
|
||||
ModPlatform::Provider chosen;
|
||||
ModPlatform::ResourceProvider chosen;
|
||||
};
|
||||
|
||||
public:
|
||||
@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog {
|
||||
void addProviders();
|
||||
void disableInput();
|
||||
|
||||
auto getSelectedProvider() const -> ModPlatform::Provider;
|
||||
auto getSelectedProvider() const -> ModPlatform::ResourceProvider;
|
||||
|
||||
private:
|
||||
Ui::ChooseProviderDialog* ui;
|
||||
|
@ -19,184 +19,41 @@
|
||||
|
||||
#include "ModDownloadDialog.h"
|
||||
|
||||
#include <BaseVersion.h>
|
||||
#include <InstanceList.h>
|
||||
#include <icons/IconList.h>
|
||||
|
||||
#include "Application.h"
|
||||
#include "ReviewMessageBox.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLayout>
|
||||
#include <QPushButton>
|
||||
#include <QValidator>
|
||||
#include "ui/pages/modplatform/flame/FlameResourcePages.h"
|
||||
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
#include "ui/pages/modplatform/flame/FlameModPage.h"
|
||||
#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)
|
||||
ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
|
||||
: ResourceDownloadDialog(parent, mods), m_instance(instance)
|
||||
{
|
||||
setObjectName(QStringLiteral("ModDownloadDialog"));
|
||||
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
|
||||
|
||||
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());
|
||||
initializeContainer();
|
||||
connectButtons();
|
||||
|
||||
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()
|
||||
{
|
||||
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void ModDownloadDialog::reject()
|
||||
{
|
||||
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
|
||||
QDialog::reject();
|
||||
}
|
||||
|
||||
QList<BasePage*> ModDownloadDialog::getPages()
|
||||
{
|
||||
QList<BasePage*> pages;
|
||||
|
||||
pages.append(ModrinthModPage::create(this, m_instance));
|
||||
pages.append(ModrinthModPage::create(this, *m_instance));
|
||||
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]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -19,60 +19,29 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ModDownloadTask.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "ui/pages/BasePageProvider.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class ModDownloadDialog;
|
||||
}
|
||||
#include "ui/dialogs/ResourceDownloadDialog.h"
|
||||
|
||||
class PageContainer;
|
||||
class QDialogButtonBox;
|
||||
class ModPage;
|
||||
class ModrinthModPage;
|
||||
|
||||
class ModDownloadDialog final : public QDialog, public BasePageProvider
|
||||
class ModDownloadDialog final : public ResourceDownloadDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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:
|
||||
void confirm();
|
||||
void accept() override;
|
||||
void reject() override;
|
||||
|
||||
private slots:
|
||||
void selectedPageChanged(BasePage* previous, BasePage* selected);
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -21,6 +21,8 @@
|
||||
#include <QTextBrowser>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
#include <optional>
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
|
||||
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() };
|
||||
}
|
||||
|
||||
static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
|
||||
static std::optional<ResourceAPI::ModLoaderTypes> mcLoaders(BaseInstance* inst)
|
||||
{
|
||||
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
|
||||
}
|
||||
@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
bool confirm_rest = false;
|
||||
bool try_others_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) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
case ModPlatform::ResourceProvider::MODRINTH:
|
||||
modrinth_tmp.push_back(m);
|
||||
break;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
case ModPlatform::ResourceProvider::FLAME:
|
||||
flame_tmp.push_back(m);
|
||||
break;
|
||||
}
|
||||
@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
}
|
||||
|
||||
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::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())
|
||||
@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
}
|
||||
|
||||
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::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())
|
||||
@ -306,28 +308,28 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod)
|
||||
return;
|
||||
|
||||
switch (mod->metadata()->provider) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
case ModPlatform::ResourceProvider::MODRINTH:
|
||||
m_modrinth_to_update.push_back(mod);
|
||||
break;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
case ModPlatform::ResourceProvider::FLAME:
|
||||
m_flame_to_update.push_back(mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ModPlatform::Provider next(ModPlatform::Provider p)
|
||||
ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
|
||||
{
|
||||
switch (p) {
|
||||
case ModPlatform::Provider::MODRINTH:
|
||||
return ModPlatform::Provider::FLAME;
|
||||
case ModPlatform::Provider::FLAME:
|
||||
return ModPlatform::Provider::MODRINTH;
|
||||
case ModPlatform::ResourceProvider::MODRINTH:
|
||||
return ModPlatform::ResourceProvider::FLAME;
|
||||
case ModPlatform::ResourceProvider::FLAME:
|
||||
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) {
|
||||
auto index_dir = indexDir();
|
||||
@ -368,7 +370,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
|
||||
|
||||
QString text = info.changelog;
|
||||
switch (info.provider) {
|
||||
case ModPlatform::Provider::MODRINTH: {
|
||||
case ModPlatform::ResourceProvider::MODRINTH: {
|
||||
text = markdownToHTML(info.changelog.toUtf8());
|
||||
break;
|
||||
}
|
||||
@ -386,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
|
||||
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);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "BaseInstance.h"
|
||||
#include "ModDownloadTask.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
#include "ReviewMessageBox.h"
|
||||
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
||||
|
||||
void appendMod(const CheckUpdateTask::UpdatableMod& info);
|
||||
|
||||
const QList<ModDownloadTask*> getTasks();
|
||||
const QList<ResourceDownloadTask*> getTasks();
|
||||
auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
|
||||
|
||||
auto noUpdates() const -> bool { return m_no_updates; };
|
||||
@ -36,7 +36,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
||||
|
||||
private slots:
|
||||
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:
|
||||
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, QUrl>> m_failed_check_update;
|
||||
|
||||
QHash<QString, ModDownloadTask*> m_tasks;
|
||||
QHash<QString, ResourceDownloadTask*> m_tasks;
|
||||
BaseInstance* m_instance;
|
||||
|
||||
bool m_no_updates = false;
|
||||
|
152
launcher/ui/dialogs/ResourceDownloadDialog.cpp
Normal file
152
launcher/ui/dialogs/ResourceDownloadDialog.cpp
Normal 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());
|
||||
}
|
55
launcher/ui/dialogs/ResourceDownloadDialog.h
Normal file
55
launcher/ui/dialogs/ResourceDownloadDialog.h
Normal 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;
|
||||
};
|
@ -25,7 +25,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
|
||||
return new ReviewMessageBox(parent, title, icon);
|
||||
}
|
||||
|
||||
void ReviewMessageBox::appendMod(ModInformation&& info)
|
||||
void ReviewMessageBox::appendResource(ResourceInformation&& info)
|
||||
{
|
||||
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
|
||||
itemTop->setCheckState(0, Qt::CheckState::Checked);
|
||||
@ -39,7 +39,7 @@ void ReviewMessageBox::appendMod(ModInformation&& info)
|
||||
ui->modTreeWidget->addTopLevelItem(itemTop);
|
||||
}
|
||||
|
||||
auto ReviewMessageBox::deselectedMods() -> QStringList
|
||||
auto ReviewMessageBox::deselectedResources() -> QStringList
|
||||
{
|
||||
QStringList list;
|
||||
|
||||
|
@ -12,15 +12,15 @@ class ReviewMessageBox : public QDialog {
|
||||
public:
|
||||
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
|
||||
|
||||
using ModInformation = struct {
|
||||
using ResourceInformation = struct {
|
||||
QString name;
|
||||
QString filename;
|
||||
};
|
||||
|
||||
void appendMod(ModInformation&& info);
|
||||
auto deselectedMods() -> QStringList;
|
||||
void appendResource(ResourceInformation&& info);
|
||||
auto deselectedResources() -> QStringList;
|
||||
|
||||
~ReviewMessageBox();
|
||||
~ReviewMessageBox() override;
|
||||
|
||||
protected:
|
||||
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
|
||||
|
@ -59,7 +59,7 @@
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
#include "Version.h"
|
||||
#include "tasks/ConcurrentTask.h"
|
||||
@ -153,12 +153,12 @@ void ModFolderPage::installMods()
|
||||
return; // this is a null instance or a legacy instance
|
||||
|
||||
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!"));
|
||||
return;
|
||||
}
|
||||
|
||||
ModDownloadDialog mdownload(m_model, this, m_instance);
|
||||
ModDownloadDialog mdownload(this, m_model, m_instance);
|
||||
if (mdownload.exec()) {
|
||||
ConcurrentTask* tasks = new ConcurrentTask(this);
|
||||
connect(tasks, &Task::failed, [this, tasks](QString reason) {
|
||||
|
@ -73,3 +73,4 @@ public:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,226 +1,81 @@
|
||||
#include "ModModel.h"
|
||||
|
||||
#include "BuildConfig.h"
|
||||
#include "Json.h"
|
||||
#include "ModPage.h"
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "minecraft/PackProfile.h"
|
||||
#include "ui/dialogs/ModDownloadDialog.h"
|
||||
|
||||
#include "ui/widgets/ProjectItem.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace ModPlatform {
|
||||
|
||||
// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
|
||||
// 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();
|
||||
}
|
||||
ListModel::ListModel(ModPage* parent, ResourceAPI* api) : ResourceModel(parent, api) {}
|
||||
|
||||
/******** Make data requests ********/
|
||||
|
||||
void ListModel::fetchMore(const QModelIndex& parent)
|
||||
ResourceAPI::SearchArgs ListModel::createSearchArguments()
|
||||
{
|
||||
if (parent.isValid())
|
||||
return;
|
||||
if (nextSearchOffset == 0) {
|
||||
qWarning() << "fetchMore with 0 offset is wrong...";
|
||||
return;
|
||||
}
|
||||
performPaginatedSearch();
|
||||
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() };
|
||||
}
|
||||
|
||||
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
|
||||
ResourceAPI::SearchCallbacks ListModel::createSearchCallbacks()
|
||||
{
|
||||
int pos = index.row();
|
||||
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
|
||||
return QString("INVALID INDEX %1").arg(pos);
|
||||
}
|
||||
|
||||
ModPlatform::IndexedPack pack = modpacks.at(pos);
|
||||
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 {};
|
||||
}
|
||||
|
||||
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
int pos = index.row();
|
||||
if (pos >= modpacks.size() || pos < 0 || !index.isValid())
|
||||
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;
|
||||
versionRequestSucceeded(doc, addonId, index);
|
||||
});
|
||||
}
|
||||
|
||||
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 { [this](auto& doc) {
|
||||
if (!s_running_models.constFind(this).value())
|
||||
return;
|
||||
infoRequestFinished(doc, pack, index);
|
||||
});
|
||||
searchRequestFinished(doc);
|
||||
} };
|
||||
}
|
||||
|
||||
void ListModel::refresh()
|
||||
ResourceAPI::VersionSearchArgs ListModel::createVersionsArguments(QModelIndex& entry)
|
||||
{
|
||||
if (jobPtr) {
|
||||
jobPtr->abort();
|
||||
searchState = ResetRequested;
|
||||
return;
|
||||
} else {
|
||||
beginResetModel();
|
||||
modpacks.clear();
|
||||
endResetModel();
|
||||
searchState = None;
|
||||
}
|
||||
nextSearchOffset = 0;
|
||||
performPaginatedSearch();
|
||||
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;
|
||||
versionRequestSucceeded(doc, addonId, entry);
|
||||
} };
|
||||
}
|
||||
|
||||
ResourceAPI::ProjectInfoArgs ListModel::createInfoArguments(QModelIndex& entry)
|
||||
{
|
||||
auto& pack = m_packs[entry.row()];
|
||||
return { pack };
|
||||
}
|
||||
ResourceAPI::ProjectInfoCallbacks ListModel::createInfoCallbacks(QModelIndex& entry)
|
||||
{
|
||||
return { [this, entry](auto& doc, auto& pack) {
|
||||
if (!s_running_models.constFind(this).value())
|
||||
return;
|
||||
infoRequestFinished(doc, pack, entry);
|
||||
} };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
currentSearchTerm = term;
|
||||
setSearchTerm(term);
|
||||
currentSort = sort;
|
||||
|
||||
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 ********/
|
||||
|
||||
void ListModel::logoLoaded(QString logo, QIcon out)
|
||||
{
|
||||
m_loadingLogos.removeAll(logo);
|
||||
m_logoMap.insert(logo, out);
|
||||
for (int i = 0; i < modpacks.size(); i++) {
|
||||
if (modpacks[i].logoName == logo) {
|
||||
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListModel::logoFailed(QString logo)
|
||||
{
|
||||
m_failedLogos.append(logo);
|
||||
m_loadingLogos.removeAll(logo);
|
||||
}
|
||||
|
||||
void ListModel::searchRequestFinished(QJsonDocument& doc)
|
||||
{
|
||||
jobPtr.reset();
|
||||
|
||||
QList<ModPlatform::IndexedPack> newList;
|
||||
auto packs = documentToArray(doc);
|
||||
|
||||
@ -232,62 +87,27 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
|
||||
loadIndexedPack(pack, packObj);
|
||||
newList.append(pack);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (packs.size() < 25) {
|
||||
searchState = Finished;
|
||||
m_search_state = SearchState::Finished;
|
||||
} else {
|
||||
nextSearchOffset += 25;
|
||||
searchState = CanPossiblyFetchMore;
|
||||
m_next_search_offset += 25;
|
||||
m_search_state = SearchState::CanFetchMore;
|
||||
}
|
||||
|
||||
// When you have a Qt build with assertions turned on, proceeding here will abort the application
|
||||
if (newList.size() == 0)
|
||||
return;
|
||||
|
||||
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
|
||||
modpacks.append(newList);
|
||||
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
|
||||
m_packs.append(newList);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
auto& current = m_parent->getCurrent();
|
||||
auto current = m_associated_page->getCurrentPack();
|
||||
if (addonId != current.addonId) {
|
||||
return;
|
||||
}
|
||||
@ -336,15 +156,19 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, cons
|
||||
qWarning() << "Failed to cache mod versions!";
|
||||
}
|
||||
|
||||
|
||||
m_parent->updateModVersions();
|
||||
m_associated_page->updateVersionList();
|
||||
}
|
||||
|
||||
} // namespace ModPlatform
|
||||
|
||||
/******** 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 {};
|
||||
}
|
||||
|
@ -3,90 +3,52 @@
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
#include "ui/pages/modplatform/ResourceModel.h"
|
||||
|
||||
class ModPage;
|
||||
class Version;
|
||||
|
||||
namespace ModPlatform {
|
||||
|
||||
using LogoMap = QMap<QString, QIcon>;
|
||||
using LogoCallback = std::function<void (QString)>;
|
||||
|
||||
class ListModel : public QAbstractListModel {
|
||||
class ListModel : public ResourceModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ListModel(ModPage* parent);
|
||||
~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(); }
|
||||
ListModel(ModPage* parent, ResourceAPI* api);
|
||||
|
||||
/* 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 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 loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
|
||||
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0;
|
||||
|
||||
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
|
||||
|
||||
inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; };
|
||||
|
||||
public slots:
|
||||
void searchRequestFinished(QJsonDocument& doc);
|
||||
void searchRequestFailed(QString reason);
|
||||
void searchRequestAborted();
|
||||
|
||||
void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, 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);
|
||||
void logoLoaded(QString logo, QIcon out);
|
||||
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
|
||||
ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) override;
|
||||
|
||||
void performPaginatedSearch();
|
||||
ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
|
||||
ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) override;
|
||||
|
||||
protected:
|
||||
virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
|
||||
virtual auto getSorts() const -> const char** = 0;
|
||||
|
||||
void requestLogo(QString file, QString url);
|
||||
|
||||
inline auto getMineVersions() const -> std::list<Version>;
|
||||
inline auto getMineVersions() const -> std::optional<std::list<Version>>;
|
||||
|
||||
protected:
|
||||
ModPage* m_parent;
|
||||
|
||||
QList<ModPlatform::IndexedPack> modpacks;
|
||||
|
||||
LogoMap m_logoMap;
|
||||
QMap<QString, LogoCallback> waitingCallbacks;
|
||||
QStringList m_failedLogos;
|
||||
QStringList m_loadingLogos;
|
||||
|
||||
QString currentSearchTerm;
|
||||
int currentSort = 0;
|
||||
int nextSearchOffset = 0;
|
||||
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
|
||||
|
||||
NetJob::Ptr jobPtr;
|
||||
};
|
||||
} // namespace ModPlatform
|
||||
|
@ -35,59 +35,30 @@
|
||||
*/
|
||||
|
||||
#include "ModPage.h"
|
||||
#include "Application.h"
|
||||
#include "ui_ModPage.h"
|
||||
#include "ui_ResourcePage.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QKeyEvent>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Application.h"
|
||||
#include "ResourceDownloadTask.h"
|
||||
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "minecraft/PackProfile.h"
|
||||
|
||||
#include "ui/dialogs/ModDownloadDialog.h"
|
||||
#include "ui/widgets/ProjectItem.h"
|
||||
#include "Markdown.h"
|
||||
|
||||
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
|
||||
: QWidget(dialog)
|
||||
, m_instance(instance)
|
||||
, ui(new Ui::ModPage)
|
||||
, dialog(dialog)
|
||||
, m_fetch_progress(this, false)
|
||||
, api(api)
|
||||
#include "ui/pages/modplatform/ModModel.h"
|
||||
|
||||
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
: ResourcePage(dialog, instance)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
|
||||
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;
|
||||
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
|
||||
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
|
||||
connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
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, [&]{
|
||||
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) ********/
|
||||
|
||||
void ModPage::filterMods()
|
||||
@ -163,176 +94,37 @@ void ModPage::triggerSearch()
|
||||
m_filter = m_filter_widget->getFilter();
|
||||
|
||||
if (changed) {
|
||||
ui->packView->clearSelection();
|
||||
ui->packDescription->clear();
|
||||
ui->versionSelectionBox->clear();
|
||||
m_ui->packView->clearSelection();
|
||||
m_ui->packDescription->clear();
|
||||
m_ui->versionSelectionBox->clear();
|
||||
updateSelectionButton();
|
||||
}
|
||||
|
||||
listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
|
||||
m_fetch_progress.watch(listModel->activeJob());
|
||||
static_cast<ModPlatform::ListModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentIndex(), changed);
|
||||
m_fetch_progress.watch(&m_model->activeJob());
|
||||
}
|
||||
|
||||
QString ModPage::getSearchTerm() const
|
||||
QMap<QString, QString> ModPage::urlHandlers() const
|
||||
{
|
||||
return ui->searchEdit->text();
|
||||
}
|
||||
void ModPage::setSearchTerm(QString term)
|
||||
{
|
||||
ui->searchEdit->setText(term);
|
||||
}
|
||||
|
||||
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);
|
||||
QMap<QString, QString> map;
|
||||
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth");
|
||||
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge");
|
||||
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
|
||||
return map;
|
||||
}
|
||||
|
||||
/******** Make changes to the UI ********/
|
||||
|
||||
void ModPage::retranslate()
|
||||
void ModPage::updateVersionList()
|
||||
{
|
||||
ui->retranslateUi(this);
|
||||
}
|
||||
|
||||
void ModPage::updateModVersions(int prev_count)
|
||||
{
|
||||
auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
|
||||
m_ui->versionSelectionBox->clear();
|
||||
auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
|
||||
|
||||
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
|
||||
|
||||
for (int i = 0; i < current.versions.size(); i++) {
|
||||
auto version = current.versions[i];
|
||||
auto current_pack = getCurrentPack();
|
||||
for (int i = 0; i < current_pack.versions.size(); i++) {
|
||||
auto version = current_pack.versions[i];
|
||||
bool valid = false;
|
||||
for(auto& mcVer : m_filter->versions){
|
||||
//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
|
||||
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) {
|
||||
ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
|
||||
ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
|
||||
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 ModPage::updateSelectionButton()
|
||||
void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
|
||||
{
|
||||
if (!isOpened || selectedVersion < 0) {
|
||||
ui->modSelectionButton->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ui->modSelectionButton->setEnabled(true);
|
||||
auto& version = current.versions[selectedVersion];
|
||||
if (!dialog->isModSelected(current.name, version.fileName)) {
|
||||
ui->modSelectionButton->setText(tr("Select mod for download"));
|
||||
} else {
|
||||
ui->modSelectionButton->setText(tr("Deselect mod for download"));
|
||||
}
|
||||
}
|
||||
|
||||
void 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();
|
||||
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
|
||||
m_parent_dialog->addResource(pack.name, new ResourceDownloadTask(pack, version, m_parent_dialog->getBaseModel(), is_indexed));
|
||||
}
|
||||
|
@ -2,104 +2,58 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "Application.h"
|
||||
#include "modplatform/ModAPI.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/ProgressWidget.h"
|
||||
|
||||
class ModDownloadDialog;
|
||||
|
||||
namespace Ui {
|
||||
class ModPage;
|
||||
class ResourcePage;
|
||||
}
|
||||
|
||||
/* 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
|
||||
|
||||
public:
|
||||
template<typename T>
|
||||
static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
static T* create(ModDownloadDialog* dialog, BaseInstance& 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);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
~ModPage() override;
|
||||
~ModPage() override = default;
|
||||
|
||||
/* Affects what the user sees */
|
||||
auto displayName() const -> QString override = 0;
|
||||
auto icon() const -> QIcon override = 0;
|
||||
auto id() const -> QString override = 0;
|
||||
auto helpPage() const -> QString override = 0;
|
||||
[[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
|
||||
|
||||
/* Used internally */
|
||||
virtual auto metaEntryBase() const -> QString = 0;
|
||||
virtual auto debugName() const -> QString = 0;
|
||||
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
|
||||
|
||||
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();
|
||||
|
||||
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(); };
|
||||
[[nodiscard]] bool supportsFiltering() const override { return true; };
|
||||
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>&);
|
||||
|
||||
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
|
||||
void updateModVersions(int prev_count = -1);
|
||||
|
||||
void openedImpl() override;
|
||||
auto eventFilter(QObject* watched, QEvent* event) -> bool override;
|
||||
|
||||
BaseInstance* m_instance;
|
||||
public slots:
|
||||
void updateVersionList() override;
|
||||
|
||||
protected:
|
||||
ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
|
||||
void updateSelectionButton();
|
||||
ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
|
||||
|
||||
protected slots:
|
||||
virtual void filterMods();
|
||||
void triggerSearch();
|
||||
void onSelectionChanged(QModelIndex first, QModelIndex second);
|
||||
void onVersionSelectionChanged(QString data);
|
||||
void onModSelected();
|
||||
virtual void openUrl(const QUrl& url);
|
||||
void triggerSearch() override;
|
||||
|
||||
protected:
|
||||
Ui::ModPage* ui = nullptr;
|
||||
ModDownloadDialog* dialog = nullptr;
|
||||
|
||||
unique_qobject_ptr<ModFilterWidget> m_filter_widget;
|
||||
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;
|
||||
};
|
||||
|
258
launcher/ui/pages/modplatform/ResourceModel.cpp
Normal file
258
launcher/ui/pages/modplatform/ResourceModel.cpp
Normal 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();
|
||||
}
|
101
launcher/ui/pages/modplatform/ResourceModel.h
Normal file
101
launcher/ui/pages/modplatform/ResourceModel.h
Normal 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();
|
||||
};
|
347
launcher/ui/pages/modplatform/ResourcePage.cpp
Normal file
347
launcher/ui/pages/modplatform/ResourcePage.cpp
Normal 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);
|
||||
}
|
95
launcher/ui/pages/modplatform/ResourcePage.h
Normal file
95
launcher/ui/pages/modplatform/ResourcePage.h
Normal 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;
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ModPage</class>
|
||||
<widget class="QWidget" name="ModPage">
|
||||
<class>ResourcePage</class>
|
||||
<widget class="QWidget" name="ResourcePage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
@ -51,7 +51,7 @@
|
||||
<item row="0" column="0">
|
||||
<widget class="QLineEdit" name="searchEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Search for mods...</string>
|
||||
<string>Search for resources...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -74,16 +74,16 @@
|
||||
<widget class="QComboBox" name="sortByBox"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="modSelectionButton">
|
||||
<widget class="QPushButton" name="resourceSelectionButton">
|
||||
<property name="text">
|
||||
<string>Select mod for download</string>
|
||||
<string>Select resource for download</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="modFilterButton">
|
||||
<widget class="QPushButton" name="resourceFilterButton">
|
||||
<property name="text">
|
||||
<string>Filter options</string>
|
||||
</property>
|
@ -1,4 +1,4 @@
|
||||
#include "FlameModModel.h"
|
||||
#include "FlameResourceModels.h"
|
||||
#include "Json.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)
|
||||
{
|
||||
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
|
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "FlameModPage.h"
|
||||
#include "modplatform/flame/FlameAPI.h"
|
||||
|
||||
namespace FlameMod {
|
||||
|
||||
@ -8,7 +8,7 @@ class ListModel : public ModPlatform::ListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
|
||||
ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent, new FlameAPI) {}
|
||||
~ListModel() override = default;
|
||||
|
||||
private:
|
@ -34,37 +34,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "FlameModPage.h"
|
||||
#include "ui_ModPage.h"
|
||||
#include "FlameResourcePages.h"
|
||||
#include "ui_ResourcePage.h"
|
||||
|
||||
#include "FlameModModel.h"
|
||||
#include "FlameResourceModels.h"
|
||||
#include "ui/dialogs/ModDownloadDialog.h"
|
||||
|
||||
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
: ModPage(dialog, instance, new FlameAPI())
|
||||
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
: ModPage(dialog, instance)
|
||||
{
|
||||
listModel = new FlameMod::ListModel(this);
|
||||
ui->packView->setModel(listModel);
|
||||
m_model = new FlameMod::ListModel(this);
|
||||
m_ui->packView->setModel(m_model);
|
||||
|
||||
// index is used to set the sorting with the flame api
|
||||
ui->sortByBox->addItem(tr("Sort by Featured"));
|
||||
ui->sortByBox->addItem(tr("Sort by Popularity"));
|
||||
ui->sortByBox->addItem(tr("Sort by Last Updated"));
|
||||
ui->sortByBox->addItem(tr("Sort by Name"));
|
||||
ui->sortByBox->addItem(tr("Sort by Author"));
|
||||
ui->sortByBox->addItem(tr("Sort by Downloads"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Featured"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Popularity"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Name"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Author"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Downloads"));
|
||||
|
||||
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
|
||||
// so it's best not to connect them in the parent's contructor...
|
||||
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
|
||||
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
|
||||
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
|
||||
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
|
||||
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
|
||||
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);
|
||||
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
|
@ -36,21 +36,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "ui/pages/modplatform/ModPage.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include "modplatform/flame/FlameAPI.h"
|
||||
#include "modplatform/ResourceAPI.h"
|
||||
|
||||
#include "ui/pages/modplatform/ModPage.h"
|
||||
|
||||
class FlameModPage : public ModPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
{
|
||||
return ModPage::create<FlameModPage>(dialog, instance);
|
||||
}
|
||||
|
||||
FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
||||
FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance);
|
||||
~FlameModPage() override = default;
|
||||
|
||||
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 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;
|
||||
|
||||
auto shouldDisplay() const -> bool override;
|
@ -16,8 +16,11 @@
|
||||
* 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"
|
||||
|
||||
namespace Modrinth {
|
||||
@ -37,7 +40,7 @@ void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||
|
||||
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
|
||||
@ -46,3 +49,5 @@ auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
|
||||
}
|
||||
|
||||
} // namespace Modrinth
|
||||
|
||||
|
@ -18,7 +18,11 @@
|
||||
|
||||
#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 {
|
||||
|
||||
@ -26,7 +30,7 @@ class ListModel : public ModPlatform::ListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
|
||||
ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent, new ModrinthAPI){};
|
||||
~ListModel() override = default;
|
||||
|
||||
private:
|
||||
@ -42,3 +46,4 @@ class ListModel : public ModPlatform::ListModel {
|
||||
};
|
||||
|
||||
} // namespace Modrinth
|
||||
|
@ -33,48 +33,52 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ModrinthModPage.h"
|
||||
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||
#include "ui_ModPage.h"
|
||||
#include "ModrinthResourcePages.h"
|
||||
#include "ui_ResourcePage.h"
|
||||
|
||||
#include "ModrinthModModel.h"
|
||||
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||
|
||||
#include "ModrinthResourceModels.h"
|
||||
#include "ui/dialogs/ModDownloadDialog.h"
|
||||
|
||||
ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
: ModPage(dialog, instance, new ModrinthAPI())
|
||||
ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
: ModPage(dialog, instance)
|
||||
{
|
||||
listModel = new Modrinth::ListModel(this);
|
||||
ui->packView->setModel(listModel);
|
||||
m_model = new Modrinth::ListModel(this);
|
||||
m_ui->packView->setModel(m_model);
|
||||
|
||||
// index is used to set the sorting with the modrinth api
|
||||
ui->sortByBox->addItem(tr("Sort by Relevance"));
|
||||
ui->sortByBox->addItem(tr("Sort by Downloads"));
|
||||
ui->sortByBox->addItem(tr("Sort by Follows"));
|
||||
ui->sortByBox->addItem(tr("Sort by Last Updated"));
|
||||
ui->sortByBox->addItem(tr("Sort by Newest"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Relevance"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Downloads"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Follows"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Last Updated"));
|
||||
m_ui->sortByBox->addItem(tr("Sort by Newest"));
|
||||
|
||||
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
|
||||
// so it's best not to connect them in the parent's constructor...
|
||||
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
|
||||
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
|
||||
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected);
|
||||
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
|
||||
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
|
||||
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;
|
||||
for (auto remoteLoader : ver.loaders)
|
||||
{
|
||||
if (loaderStrings.contains(remoteLoader)) {
|
||||
loaderCompatible = true;
|
||||
break;
|
||||
if (!loaderCompatible) {
|
||||
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value());
|
||||
for (auto remoteLoader : ver.loaders)
|
||||
{
|
||||
if (loaderStrings.contains(remoteLoader)) {
|
||||
loaderCompatible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// my Qt, so we need to implement this in every derived class...
|
||||
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }
|
||||
|
@ -35,32 +35,38 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "modplatform/ModAPI.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include "modplatform/ResourceAPI.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 {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
|
||||
{
|
||||
return ModPage::create<ModrinthModPage>(dialog, instance);
|
||||
}
|
||||
|
||||
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
||||
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance);
|
||||
~ModrinthModPage() override = default;
|
||||
|
||||
inline auto displayName() const -> QString override { return "Modrinth"; }
|
||||
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
|
||||
inline auto id() const -> QString override { return "modrinth"; }
|
||||
[[nodiscard]] bool shouldDisplay() const override;
|
||||
|
||||
[[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 debugName() const -> QString override { return "Modrinth"; }
|
||||
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;
|
||||
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
|
||||
};
|
@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format)
|
||||
m_bar->setFormat(format);
|
||||
}
|
||||
|
||||
void ProgressWidget::watch(Task* task)
|
||||
void ProgressWidget::watch(const Task* task)
|
||||
{
|
||||
if (!task)
|
||||
return;
|
||||
@ -57,11 +57,11 @@ void ProgressWidget::watch(Task* task)
|
||||
show();
|
||||
}
|
||||
|
||||
void ProgressWidget::start(Task* task)
|
||||
void ProgressWidget::start(const Task* task)
|
||||
{
|
||||
watch(task);
|
||||
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)
|
||||
|
@ -27,10 +27,10 @@ class ProgressWidget : public QWidget {
|
||||
|
||||
public slots:
|
||||
/** 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 */
|
||||
void start(Task* task);
|
||||
void start(const Task* task);
|
||||
|
||||
/** Blocking way of waiting for a task to finish. */
|
||||
bool exec(std::shared_ptr<Task> task);
|
||||
@ -50,7 +50,7 @@ class ProgressWidget : public QWidget {
|
||||
private:
|
||||
QLabel* m_label = nullptr;
|
||||
QProgressBar* m_bar = nullptr;
|
||||
Task* m_task = nullptr;
|
||||
const Task* m_task = nullptr;
|
||||
|
||||
bool m_hide_if_inactive = false;
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ class PackwizTest : public QObject {
|
||||
QCOMPARE(metadata.hash_format, "sha512");
|
||||
QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
|
||||
|
||||
QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH);
|
||||
QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH);
|
||||
QCOMPARE(metadata.version(), "ug2qKTPR");
|
||||
QCOMPARE(metadata.mod_id(), "kYq5qkSL");
|
||||
}
|
||||
@ -76,7 +76,7 @@ class PackwizTest : public QObject {
|
||||
QCOMPARE(metadata.hash_format, "murmur2");
|
||||
QCOMPARE(metadata.hash, "1781245820");
|
||||
|
||||
QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME);
|
||||
QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME);
|
||||
QCOMPARE(metadata.file_id, 3509043);
|
||||
QCOMPARE(metadata.project_id, 327154);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user