aboutsummaryrefslogtreecommitdiff
path: root/launcher/ui
diff options
context:
space:
mode:
authorSefa Eyeoglu <contact@scrumplex.net>2023-01-23 23:58:52 +0100
committerGitHub <noreply@github.com>2023-01-23 23:58:52 +0100
commit16477a8f6c1fc646208b41b76598ce8e7a60369e (patch)
tree782055d4c36affd2b9bd84905805c94f04134590 /launcher/ui
parent04e4900415aa102f3ad2f40375e3b59bfc7ee9ba (diff)
parentea5020e188d7cb6d4c8dcf7f953161759ed17899 (diff)
downloadPrismLauncher-16477a8f6c1fc646208b41b76598ce8e7a60369e.tar.gz
PrismLauncher-16477a8f6c1fc646208b41b76598ce8e7a60369e.tar.bz2
PrismLauncher-16477a8f6c1fc646208b41b76598ce8e7a60369e.zip
Merge pull request #675 from flowln/generalize_mod_model
Diffstat (limited to 'launcher/ui')
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp6
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h6
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp202
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h78
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp44
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h8
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp232
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h111
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp28
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h11
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.ui14
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.h4
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h1
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp348
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h94
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp361
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h97
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp444
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h147
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp411
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h111
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.ui (renamed from launcher/ui/pages/modplatform/ModPage.ui)18
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp31
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h26
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp37
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h30
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.cpp)44
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.h (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.h)43
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp)60
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h)41
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp11
-rw-r--r--launcher/ui/widgets/ProgressWidget.h6
38 files changed, 1899 insertions, 1271 deletions
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index 8b49bd1a..5977fd10 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -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;
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp
index 89935d9a..83748e1e 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.cpp
+++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -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);
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h
index 4a3b9f29..be9735b5 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.h
+++ b/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -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;
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
deleted file mode 100644
index 24d23ba9..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "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 "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)
-{
- 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());
-
- 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();
-}
-
-QList<BasePage*> ModDownloadDialog::getPages()
-{
- QList<BasePage*> pages;
-
- pages.append(ModrinthModPage::create(this, m_instance));
- if (APPLICATION->capabilities() & Application::SupportsFlame)
- 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;
-}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h
deleted file mode 100644
index fcf6f4fc..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QDialog>
-#include <QVBoxLayout>
-
-#include "ModDownloadTask.h"
-#include "minecraft/mod/ModFolderModel.h"
-#include "ui/pages/BasePageProvider.h"
-
-namespace Ui
-{
-class ModDownloadDialog;
-}
-
-class PageContainer;
-class QDialogButtonBox;
-class ModPage;
-class ModrinthModPage;
-
-class ModDownloadDialog final : public QDialog, public BasePageProvider
-{
- Q_OBJECT
-
- public:
- explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
- ~ModDownloadDialog() override = default;
-
- QString dialogTitle() override;
- QList<BasePage*> getPages() override;
-
- void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
- void removeSelectedMod(QString name = QString());
- bool isModSelected(QString name, QString filename) const;
- bool isModSelected(QString name) const;
-
- const QList<ModDownloadTask*> getTasks();
- const std::shared_ptr<ModFolderModel>& mods;
-
- bool selectPage(QString pageId);
- ModPage* getSelectedPage();
-
- public slots:
- 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;
-};
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index 2704243e..4ef42d6c 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -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);
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
index bd486f0d..3e3dd90d 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.h
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -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;
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
new file mode 100644
index 00000000..b9367c16
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ResourceDownloadDialog.h"
+
+#include <QPushButton>
+
+#include "Application.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
+#include "ui/dialogs/ReviewMessageBox.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
+
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
+
+#include "ui/widgets/PageContainer.h"
+
+namespace ResourceDownload {
+
+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);
+}
+
+void ResourceDownloadDialog::accept()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::accept();
+}
+
+void ResourceDownloadDialog::reject()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::reject();
+}
+
+// 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(resourcesString()));
+ 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(resourcesString()));
+ confirm_dialog->retranslateUi(resourcesString());
+
+ for (auto& task : keys) {
+ auto selected = m_selected.constFind(task).value();
+ confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() });
+ }
+
+ 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(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
+{
+ removeResource(pack, ver);
+
+ ver.is_currently_selected = true;
+ m_selected.insert(pack.name, new ResourceDownloadTask(pack, ver, getBaseModel(), is_indexed));
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
+{
+ Q_ASSERT(pack.versionsLoaded);
+ auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; });
+ Q_ASSERT(it != pack.versions.end());
+ return *it;
+}
+
+void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver)
+{
+ if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) {
+ auto selected_task = *selected_task_it;
+ auto old_version_id = selected_task->getVersionID();
+
+ // If the new and old version IDs don't match, search for the old one and deselect it.
+ if (ver.fileId != old_version_id)
+ getVersionWithID(pack, old_version_id).is_currently_selected = false;
+ }
+
+ // Deselect the new version too, since all versions of that pack got removed.
+ ver.is_currently_selected = false;
+
+ m_selected.remove(pack.name);
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+const QList<ResourceDownloadDialog::DownloadTaskPtr> 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());
+}
+
+
+
+ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
+ : ResourceDownloadDialog(parent, mods), m_instance(instance)
+{
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
+}
+
+QList<BasePage*> ModDownloadDialog::getPages()
+{
+ QList<BasePage*> pages;
+
+ pages.append(ModrinthModPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameModPage::create(this, *m_instance));
+
+ m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
+
+ return pages;
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
new file mode 100644
index 00000000..19843532
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHash>
+#include <QLayout>
+
+#include "QObjectPtr.h"
+#include "modplatform/ModIndex.h"
+#include "ui/pages/BasePageProvider.h"
+
+class BaseInstance;
+class ModFolderModel;
+class PageContainer;
+class QVBoxLayout;
+class QDialogButtonBox;
+class ResourceDownloadTask;
+class ResourceFolderModel;
+
+namespace ResourceDownload {
+
+class ResourcePage;
+
+class ResourceDownloadDialog : public QDialog, public BasePageProvider {
+ Q_OBJECT
+
+ public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
+ 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 resourcesString() const { return tr("resources"); }
+
+ QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); };
+
+ bool selectPage(QString pageId);
+ ResourcePage* getSelectedPage();
+
+ void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
+ void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+
+ const QList<DownloadTaskPtr> getTasks();
+ [[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
+
+ public slots:
+ void accept() override;
+ void reject() override;
+
+ protected slots:
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ virtual void confirm();
+
+ protected:
+ [[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+
+ 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, DownloadTaskPtr> m_selected;
+};
+
+
+
+class ModDownloadDialog final : public ResourceDownloadDialog {
+ Q_OBJECT
+
+ public:
+ explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
+
+ //: String that gets appended to the mod download dialog title ("Download " + resourcesString())
+ [[nodiscard]] QString resourcesString() const override { return tr("mods"); }
+ [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index 7c25c91c..7b2df278 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -1,6 +1,8 @@
#include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h"
+#include "Application.h"
+
#include <QPushButton>
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
@@ -11,6 +13,10 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back"));
+ ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->modTreeWidget->header()->setStretchLastSection(false);
+ ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}
@@ -25,7 +31,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);
@@ -36,10 +42,20 @@ void ReviewMessageBox::appendMod(ModInformation&& info)
itemTop->insertChildren(0, { filenameItem });
+ if (!info.custom_file_path.isEmpty()) {
+ auto customPathItem = new QTreeWidgetItem(itemTop);
+ customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
+
+ itemTop->insertChildren(1, { customPathItem });
+
+ itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow")));
+ itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
+ }
+
ui->modTreeWidget->addTopLevelItem(itemTop);
}
-auto ReviewMessageBox::deselectedMods() -> QStringList
+auto ReviewMessageBox::deselectedResources() -> QStringList
{
QStringList list;
@@ -55,3 +71,11 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
return list;
}
+
+void ReviewMessageBox::retranslateUi(QString resources_name)
+{
+ setWindowTitle(tr("Confirm %1 selection").arg(resources_name));
+
+ ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
+ ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
+}
diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h
index 9cfa679a..5ec2bc23 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.h
+++ b/launcher/ui/dialogs/ReviewMessageBox.h
@@ -12,15 +12,18 @@ class ReviewMessageBox : public QDialog {
public:
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
- using ModInformation = struct {
+ using ResourceInformation = struct res_info {
QString name;
QString filename;
+ QString custom_file_path {};
};
- void appendMod(ModInformation&& info);
- auto deselectedMods() -> QStringList;
+ void appendResource(ResourceInformation&& info);
+ auto deselectedResources() -> QStringList;
- ~ReviewMessageBox();
+ void retranslateUi(QString resources_name);
+
+ ~ReviewMessageBox() override;
protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui
index ab3bcc2f..bf53ae80 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.ui
+++ b/launcher/ui/dialogs/ReviewMessageBox.ui
@@ -10,9 +10,6 @@
<height>350</height>
</rect>
</property>
- <property name="windowTitle">
- <string>Confirm mod selection</string>
- </property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
@@ -39,22 +36,21 @@
<string/>
</property>
</column>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="explainLabel">
- <property name="text">
- <string>You're about to download the following mods:</string>
- </property>
</widget>
</item>
<item row="5" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="onlyCheckedLabel">
- <property name="text">
- <string>Only mods with a check will be downloaded!</string>
- </property>
</widget>
</item>
<item>
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 8d56d894..dc983d9a 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h
index d29a5e88..1ac6fc03 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.h
+++ b/launcher/ui/pages/instance/ManagedPackPage.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
@@ -12,6 +12,8 @@
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
+#include "net/NetJob.h"
+
#include "ui/pages/BasePage.h"
#include <QWidget>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 627e71e5..d9069915 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -49,8 +49,8 @@
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/dialogs/ModUpdateDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
#include "DesktopServices.h"
@@ -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);
+ ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance);
if (mdownload.exec()) {
ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index 9633e3b4..db8af0c5 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -73,3 +73,4 @@ public:
return true;
}
};
+
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index ed58eb32..3ffe6cb0 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -1,350 +1,70 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#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;
-}
+namespace ResourceDownload {
-auto ListModel::debugName() const -> QString
-{
- return m_parent->debugName();
-}
+ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
-void ListModel::fetchMore(const QModelIndex& parent)
-{
- if (parent.isValid())
- return;
- if (nextSearchOffset == 0) {
- qWarning() << "fetchMore with 0 offset is wrong...";
- return;
- }
- performPaginatedSearch();
-}
-
-auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
-{
- int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- ModPlatform::IndexedPack pack = modpacks.at(pos);
- 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()
+ResourceAPI::SearchArgs ModModel::createSearchArguments()
{
- auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
-
- m_parent->apiProvider()->searchMods(
- this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
-}
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
-void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
-{
- m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
- if (!s_running.constFind(this).value())
- return;
- infoRequestFinished(doc, pack, index);
- });
-}
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
-void ListModel::refresh()
-{
- if (jobPtr) {
- jobPtr->abort();
- searchState = ResetRequested;
- return;
- } else {
- beginResetModel();
- modpacks.clear();
- endResetModel();
- searchState = None;
- }
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
+ std::optional<std::list<Version>> versions{};
-void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
-{
- if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
- return;
+ { // Version filter
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
}
- currentSearchTerm = term;
- currentSort = sort;
-
- refresh();
-}
+ auto sort = getCurrentSortingMethodByIndex();
-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);
- }
+ return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions };
}
-void ListModel::requestLogo(QString logo, QString url)
+ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
{
- if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) {
- return;
- }
+ auto& pack = m_packs[entry.row()];
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
- 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));
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
- 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);
- }
- });
+ std::optional<std::list<Version>> versions{};
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
- 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 });
- }
- }
+ return { pack, versions, profile->getModLoaders() };
}
-void ListModel::logoFailed(QString logo)
+ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
{
- m_failedLogos.append(logo);
- m_loadingLogos.removeAll(logo);
+ auto& pack = m_packs[entry.row()];
+ return { pack };
}
-void ListModel::searchRequestFinished(QJsonDocument& doc)
+void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed)
{
- jobPtr.reset();
-
- QList<ModPlatform::IndexedPack> newList;
- auto packs = documentToArray(doc);
-
- for (auto packRaw : packs) {
- auto packObj = packRaw.toObject();
-
- ModPlatform::IndexedPack pack;
- try {
- loadIndexedPack(pack, packObj);
- newList.append(pack);
- } catch (const JSONValidationError& e) {
- qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
- continue;
- }
- }
-
- if (packs.size() < 25) {
- searchState = Finished;
- } else {
- nextSearchOffset += 25;
- searchState = CanPossiblyFetchMore;
- }
-
- // When you have a Qt build with assertions turned on, proceeding here will abort the application
- if (newList.size() == 0)
+ if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) {
return;
-
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
- modpacks.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();
-}
+ setSearchTerm(term);
+ m_current_sort_index = sort;
-void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
-{
- qDebug() << "Loading mod info";
-
- try {
- auto obj = Json::requireObject(doc);
- loadExtraPackInfo(pack, obj);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
- }
-
- // Check if the index is still valid for this mod or not
- if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(pack);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod info!";
- }
- }
-
- m_parent->updateUi();
-}
-
-void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
-{
- auto& current = m_parent->getCurrent();
- if (addonId != current.addonId) {
- return;
- }
-
- auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
-
- try {
- loadIndexedPackVersions(current, arr);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
- }
-
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(current);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod versions!";
- }
-
-
- m_parent->updateModVersions();
+ refresh();
}
-} // namespace ModPlatform
-
-/******** Helpers ********/
-
-auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version>
-{
- return m_parent->getFilter()->versions;
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index 36840649..5d4a7785 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -1,92 +1,52 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QAbstractListModel>
+#include "BaseInstance.h"
+
#include "modplatform/ModIndex.h"
-#include "net/NetJob.h"
+#include "modplatform/ResourceAPI.h"
+
+#include "ui/pages/modplatform/ResourceModel.h"
+#include "ui/widgets/ModFilterWidget.h"
-class ModPage;
class Version;
-namespace ModPlatform {
+namespace ResourceDownload {
-using LogoMap = QMap<QString, QIcon>;
-using LogoCallback = std::function<void (QString)>;
+class ModPage;
-class ListModel : public QAbstractListModel {
+class ModModel : 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(); }
+ ModModel(const BaseInstance&, 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);
+ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
- 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 loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 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; };
+ void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
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:
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, QIcon out);
-
- void performPaginatedSearch();
+ ResourceAPI::SearchArgs createSearchArguments() override;
+ ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
+ ResourceAPI::ProjectInfoArgs createInfoArguments(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>;
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
protected:
- ModPage* m_parent;
-
- QList<ModPlatform::IndexedPack> modpacks;
-
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
+ const BaseInstance& m_base_instance;
- QString currentSearchTerm;
- int currentSort = 0;
- int nextSearchOffset = 0;
- enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
-
- NetJob::Ptr jobPtr;
+ std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
-} // namespace ModPlatform
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 0f30689e..04be43ad 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,59 +37,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)
-{
- 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);
+#include "ui/dialogs/ResourceDownloadDialog.h"
- 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);
-}
+namespace ResourceDownload {
-ModPage::~ModPage()
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ResourcePage(dialog, instance)
{
- 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 +70,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 +96,37 @@ void ModPage::triggerSearch()
m_filter = m_filter_widget->getFilter();
if (changed) {
- ui->packView->clearSelection();
- ui->packDescription->clear();
- ui->versionSelectionBox->clear();
- updateSelectionButton();
- }
-
- listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
- m_fetch_progress.watch(listModel->activeJob());
-}
-
-QString ModPage::getSearchTerm() 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)); }
-
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_ui->versionSelectionBox->clear();
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_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
+ m_fetch_progress.watch(m_model->activeSearchJob().get());
}
-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)
+QMap<QString, QString> ModPage::urlHandlers() const
{
- // 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()
-{
- ui->retranslateUi(this);
-}
-
-void ModPage::updateModVersions(int prev_count)
+void ModPage::updateVersionList()
{
- 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 +138,20 @@ 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"));
- }
+ bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
+ m_parent_dialog->addResource(pack, version, is_indexed);
}
-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();
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index c9ccbaf2..c3b58cd6 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -1,105 +1,76 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QWidget>
-#include "Application.h"
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
-#include "ui/pages/BasePage.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h"
-#include "ui/widgets/ProgressWidget.h"
-
-class ModDownloadDialog;
namespace Ui {
-class ModPage;
+class ResourcePage;
}
+namespace ResourceDownload {
+
+class ModDownloadDialog;
+
/* 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 model = static_cast<ModModel*>(page->getModel());
- 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);
+ model->setFilter(page->getFilter());
+
+ connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
+ connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
- ~ModPage() override;
-
- /* Affects what the user sees */
- auto displayName() const -> QString override = 0;
- auto icon() const -> QIcon override = 0;
- auto id() const -> QString override = 0;
- auto helpPage() const -> QString override = 0;
+ ~ModPage() override = default;
- /* Used internally */
- virtual auto metaEntryBase() const -> QString = 0;
- virtual auto debugName() const -> QString = 0;
+ //: The plural version of 'mod'
+ [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); }
+ //: The singular version of 'mods'
+ [[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
+ [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void retranslate() override;
+ void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
- void updateUi();
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
- 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;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp
new file mode 100644
index 00000000..8af70104
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -0,0 +1,444 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "ResourceModel.h"
+
+#include <QCryptographicHash>
+#include <QIcon>
+#include <QMessageBox>
+#include <QPixmapCache>
+#include <QUrl>
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+
+#include "net/Download.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+#include "ui/widgets/ProjectItem.h"
+
+namespace ResourceDownload {
+
+QHash<ResourceModel*, bool> ResourceModel::s_running_models;
+
+ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api)
+{
+ 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 pack.isAnyVersionSelected();
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QHash<int, QByteArray> ResourceModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+
+ roles[Qt::ToolTipRole] = "toolTip";
+ roles[Qt::DecorationRole] = "decoration";
+ roles[Qt::SizeHintRole] = "sizeHint";
+ roles[Qt::UserRole] = "pack";
+ roles[UserDataTypes::TITLE] = "title";
+ roles[UserDataTypes::DESCRIPTION] = "description";
+ roles[UserDataTypes::SELECTED] = "selected";
+
+ return roles;
+}
+
+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>();
+ emit dataChanged(index, index);
+
+ return true;
+}
+
+QString ResourceModel::debugName() const
+{
+ return "ResourceDownload (Model)";
+}
+
+void ResourceModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid() || m_search_state == SearchState::Finished)
+ return;
+
+ search();
+}
+
+void ResourceModel::search()
+{
+ if (hasActiveSearchJob())
+ return;
+
+ auto args{ createSearchArguments() };
+
+ auto callbacks{ createSearchCallbacks() };
+
+ // Use defaults if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this](auto& doc) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestSucceeded(doc);
+ };
+ 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)
+ runSearchJob(job);
+}
+
+void ResourceModel::loadEntry(QModelIndex& entry)
+{
+ auto const& pack = m_packs[entry.row()];
+
+ if (!hasActiveInfoJob())
+ m_current_info_job.clear();
+
+ if (!pack.versionsLoaded) {
+ auto args{ createVersionsArguments(entry) };
+ auto callbacks{ createVersionsCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ versionRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+
+ if (!pack.extraDataLoaded) {
+ auto args{ createInfoArguments(entry) };
+ auto callbacks{ createInfoCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ infoRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+}
+
+void ResourceModel::refresh()
+{
+ bool reset_requested = false;
+
+ if (hasActiveInfoJob()) {
+ m_current_info_job.abort();
+ reset_requested = true;
+ }
+
+ if (hasActiveSearchJob()) {
+ m_current_search_job->abort();
+ reset_requested = true;
+ }
+
+ if (reset_requested) {
+ 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();
+}
+
+void ResourceModel::runSearchJob(Task::Ptr ptr)
+{
+ m_current_search_job = ptr;
+ m_current_search_job->start();
+}
+void ResourceModel::runInfoJob(Task::Ptr ptr)
+{
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.clear();
+
+ m_current_info_job.addTask(ptr);
+
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.run();
+}
+
+std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const
+{
+ std::optional<ResourceAPI::SortingMethod> sort{};
+
+ { // Find sorting method by ID
+ auto sorting_methods = getSortingMethods();
+ auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(),
+ [this](auto const& e) { return m_current_sort_index == e.index; });
+ if (method != sorting_methods.constEnd())
+ sort = *method;
+ }
+
+ return sort;
+}
+
+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(
+ 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 {};
+}
+
+// No 'forgor to implement' shall pass here :blobfox_knife:
+#define NEED_FOR_CALLBACK_ASSERT(name) \
+ Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
+
+QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const
+{
+ NEED_FOR_CALLBACK_ASSERT("documentToArray");
+ return {};
+}
+void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
+}
+void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
+}
+void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
+}
+
+/* Default callbacks */
+
+void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
+{
+ QList<ModPlatform::IndexedPack> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack pack;
+ try {
+ loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ } catch (const JSONValidationError& e) {
+ qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ if (packs.size() < 25) {
+ m_search_state = SearchState::Finished;
+ } else {
+ 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(), m_packs.size(), m_packs.size() + newList.size() - 1);
+ m_packs.append(newList);
+ endInsertRows();
+}
+
+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"),
+ QString("%1").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();
+}
+
+void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ loadIndexedPackVersions(current_pack, arr);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource versions!";
+ return;
+ }
+
+ emit versionListUpdated();
+}
+
+void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto obj = Json::requireObject(doc);
+ loadExtraPackInfo(current_pack, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource info!";
+ return;
+ }
+
+ emit projectInfoUpdated();
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h
new file mode 100644
index 00000000..610b631c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.h
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include <optional>
+
+#include <QAbstractListModel>
+
+#include "QObjectPtr.h"
+
+#include "modplatform/ResourceAPI.h"
+
+#include "tasks/ConcurrentTask.h"
+
+class NetJob;
+class ResourceAPI;
+
+namespace ModPlatform {
+struct IndexedPack;
+}
+
+namespace ResourceDownload {
+
+class ResourceModel : public QAbstractListModel {
+ Q_OBJECT
+
+ Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
+
+ public:
+ ResourceModel(ResourceAPI* api);
+ ~ResourceModel() override;
+
+ [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override;
+ [[nodiscard]] auto roleNames() const -> QHash<int, QByteArray> override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role) override;
+
+ [[nodiscard]] virtual auto debugName() const -> QString;
+ [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
+
+ [[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); }
+
+ [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); }
+ [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); }
+ [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; }
+
+ [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); }
+
+ public slots:
+ void fetchMore(const QModelIndex& parent) override;
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+ 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() { return {}; }
+
+ virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; }
+
+ virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; }
+
+ /** 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();
+
+ void runSearchJob(Task::Ptr);
+ void runInfoJob(Task::Ptr);
+
+ [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>;
+
+ /** Converts a JSON document to a common array format.
+ *
+ * This is needed so that different providers, with different JSON structures, can be parsed
+ * uniformally. You NEED to re-implement this if you intend on using the default callbacks.
+ */
+ [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray;
+
+ /** Functions to load data into a pack.
+ *
+ * Those are needed for the same reason as ddocumentToArray, and NEED to be re-implemented in the same way.
+ */
+
+ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
+
+ 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;
+ unsigned int m_current_sort_index = 0;
+
+ std::unique_ptr<ResourceAPI> m_api;
+
+ // Job for searching for new entries
+ shared_qobject_ptr<Task> m_current_search_job;
+ // Job for fetching versions and extra info on existing entries
+ ConcurrentTask m_current_info_job;
+
+ shared_qobject_ptr<NetJob> m_current_icon_job;
+ QSet<QUrl> m_currently_running_icon_actions;
+ QSet<QUrl> m_failed_icon_actions;
+
+ 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 searchRequestSucceeded(QJsonDocument&);
+ void searchRequestFailed(QString reason, int network_error_code);
+ void searchRequestAborted();
+
+ void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ signals:
+ void versionListUpdated();
+ void projectInfoUpdated();
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
new file mode 100644
index 00000000..bbd465bc
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -0,0 +1,411 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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"
+
+namespace ResourceDownload {
+
+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);
+
+ //: String in the search bar of the mod downloading dialog
+ m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString()));
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+
+ 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);
+}
+
+void ResourcePage::addSortings()
+{
+ Q_ASSERT(m_model);
+
+ auto sorts = m_model->getSortingMethods();
+ std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; });
+
+ for (auto&& sorting : sorts)
+ m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
+}
+
+bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
+{
+ QVariant v;
+ v.setValue(pack);
+ return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
+}
+
+ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
+{
+ return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
+}
+
+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 (!getCurrentPack().isVersionSelected(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, version);
+}
+
+void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+{
+ m_parent_dialog->removeResource(pack, version);
+}
+
+void ResourcePage::onResourceSelected()
+{
+ if (m_selected_version_index < 0)
+ return;
+
+ auto current_pack = getCurrentPack();
+ if (!current_pack.versionsLoaded)
+ return;
+
+ auto& version = current_pack.versions[m_selected_version_index];
+ if (version.is_currently_selected)
+ removeResourceFromDialog(current_pack, version);
+ else
+ addResourceToDialog(current_pack, version);
+
+ // Save the modified pack (and prevent warning in release build)
+ [[maybe_unused]] bool set = setCurrentPack(current_pack);
+ Q_ASSERT(set);
+
+ 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;
+
+ auto handlers = urlHandlers();
+ for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) {
+ auto&& [regex, candidate] = *it;
+ 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->hasActiveSearchJob())
+ connect(model->activeSearchJob().get(), &Task::finished, jump);
+ else
+ jump();
+
+ return;
+ }
+ }
+
+ // open in the user's web browser
+ QDesktopServices::openUrl(url);
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h
new file mode 100644
index 00000000..1896d53e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.h
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#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;
+
+namespace ResourceDownload {
+
+class ResourceDownloadDialog;
+class ResourceModel;
+
+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;
+
+ //: The plural version of 'resource'
+ [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); }
+ //: The singular version of 'resources'
+ [[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 setCurrentPack(ModPlatform::IndexedPack);
+ [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
+ [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
+ [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
+
+ protected:
+ ResourcePage(ResourceDownloadDialog* parent, BaseInstance&);
+
+ void addSortings();
+
+ 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();
+
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+
+ /** Associates regex expressions to pages in the order they're given in the map. */
+ 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;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui
index 94365aa5..73a9d3b1 100644
--- a/launcher/ui/pages/modplatform/ModPage.ui
+++ b/launcher/ui/pages/modplatform/ResourcePage.ui
@@ -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>
@@ -49,11 +49,7 @@
</widget>
</item>
<item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search for mods...</string>
- </property>
- </widget>
+ <widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0">
@@ -74,16 +70,12 @@
<widget class="QComboBox" name="sortByBox"/>
</item>
<item row="1" column="2">
- <widget class="QPushButton" name="modSelectionButton">
- <property name="text">
- <string>Select mod for download</string>
- </property>
- </widget>
+ <widget class="QPushButton" name="resourceSelectionButton"/>
</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>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
deleted file mode 100644
index bc2c686c..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-#include "FlameModModel.h"
-#include "Json.h"
-#include "modplatform/flame/FlameModIndex.h"
-
-namespace FlameMod {
-
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
-
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadIndexedPack(m, obj);
-}
-
-// We already deal with the URLs when initializing the pack, due to the API response's structure
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadBody(m, obj);
-}
-
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
-{
- FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
-}
-
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
-{
- return Json::ensureArray(obj.object(), "data");
-}
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
deleted file mode 100644
index 6a6aef2e..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma once
-
-#include "FlameModPage.h"
-
-namespace FlameMod {
-
-class ListModel : public ModPlatform::ListModel {
- Q_OBJECT
-
- public:
- ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
- ~ListModel() override = default;
-
- private:
- void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
-
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[6];
- inline auto getSorts() const -> const char** override { return sorts; };
-};
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
new file mode 100644
index 00000000..de1f2122
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "FlameResourceModels.h"
+
+#include "Json.h"
+
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+
+namespace ResourceDownload {
+
+FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {}
+
+void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadIndexedPack(m, obj);
+}
+
+// We already deal with the URLs when initializing the pack, due to the API response's structure
+void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadBody(m, obj);
+}
+
+void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+{
+ FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
+}
+
+auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+{
+ return Json::ensureArray(obj.object(), "data");
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
new file mode 100644
index 00000000..625a2a7d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+
+namespace ResourceDownload {
+
+class FlameModModel : public ModModel {
+ Q_OBJECT
+
+ public:
+ FlameModModel(const BaseInstance&);
+ ~FlameModModel() override = default;
+
+ private:
+ [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
+
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
+
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
index bad78c97..485431a7 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -34,37 +36,33 @@
* limitations under the License.
*/
-#include "FlameModPage.h"
-#include "ui_ModPage.h"
+#include "FlameResourcePages.h"
+#include "ui_ResourcePage.h"
-#include "FlameModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "FlameResourceModels.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
-FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new FlameAPI())
+namespace ResourceDownload {
+
+FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new FlameMod::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new FlameModModel(instance);
+ 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"));
+ addSortings();
// 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();
@@ -95,3 +93,5 @@ void FlameModPage::openUrl(const QUrl& url)
ModPage::openUrl(url);
}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
index 58479ab9..b21a53ad 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -36,35 +38,48 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/flame/FlameAPI.h"
+namespace ResourceDownload {
+
+namespace Flame {
+static inline QString displayName() { return "CurseForge"; }
+static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); }
+static inline QString id() { return "curseforge"; }
+static inline QString debugName() { return "Flame"; }
+static inline QString metaEntryBase() { return "FlameMods"; }
+}
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"; }
- inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
- inline auto id() const -> QString override { return "curseforge"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Flame"; }
- inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
- bool optedOut(ModPlatform::IndexedVersion& ver) const override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const override;
+ bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index e6704eef..80850b4c 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -40,7 +40,6 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index af92e63e..73d55133 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -16,33 +18,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-#include "ModrinthModModel.h"
+#include "ModrinthResourceModels.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-namespace Modrinth {
+namespace ResourceDownload {
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
+ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadIndexedPack(m, obj);
+ ::Modrinth::loadIndexedPack(m, obj);
}
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadExtraPackData(m, obj);
+ ::Modrinth::loadExtraPackData(m, obj);
}
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
+ ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index 386897fd..56cab146 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -18,27 +20,29 @@
#pragma once
-#include "ModrinthModPage.h"
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
-namespace Modrinth {
+namespace ResourceDownload {
-class ListModel : public ModPlatform::ListModel {
+class ModrinthModPage;
+
+class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
- ~ListModel() override = default;
+ ModrinthModModel(const BaseInstance&);
+ ~ModrinthModModel() override = default;
private:
+ [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
+
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[5];
- inline auto getSorts() const -> const char** override { return sorts; };
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
index c531ea90..b82f800e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -33,48 +35,50 @@
* limitations under the License.
*/
-#include "ModrinthModPage.h"
+#include "ModrinthResourcePages.h"
+#include "ui_ResourcePage.h"
+
#include "modplatform/modrinth/ModrinthAPI.h"
-#include "ui_ModPage.h"
-#include "ModrinthModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
+
+#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
+
+namespace ResourceDownload {
-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 ModrinthModModel(instance);
+ 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"));
+ addSortings();
// 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,5 @@ 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; }
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
index 40d82e6f..be38eff1 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,32 +37,45 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/modrinth/ModrinthAPI.h"
+namespace ResourceDownload {
+
+namespace Modrinth {
+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"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Modrinth"; }
- inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index b60d9a7a..f736af08 100644
--- a/launcher/ui/widgets/ProgressWidget.cpp
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -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;
@@ -54,14 +54,17 @@ void ProgressWidget::watch(Task* task)
connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
- show();
+ if (m_task->isRunning())
+ show();
+ else
+ connect(m_task, &Task::started, this, &ProgressWidget::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)
diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h
index 4d9097b8..b0458f33 100644
--- a/launcher/ui/widgets/ProgressWidget.h
+++ b/launcher/ui/widgets/ProgressWidget.h
@@ -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;
};