refactor: generalize mod models and APIs to resources

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

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

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

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

Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
flow
2022-11-25 09:23:46 -03:00
parent b937d33436
commit 6a18079953
68 changed files with 1965 additions and 1520 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
return new ReviewMessageBox(parent, title, icon);
}
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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