diff options
Diffstat (limited to 'launcher/ui/pages/modplatform')
51 files changed, 2849 insertions, 2186 deletions
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index ed58eb32..afd8b292 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 677bc4d6..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,61 +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 <HoeDown.h> +#include "Application.h" +#include "ResourceDownloadTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ui/dialogs/ModDownloadDialog.h" -#include "ui/widgets/ProjectItem.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) @@ -99,59 +70,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget&g |
