refactor: extract common code in mod pages and model
This creates a hierarchy in which ModPage and ModModel are the parents of every mod provider, providing the basic functionality common to all of them. It also imposes a unique .ui file (they were already equal before, just duplicated basically) on all mod providers.
This commit is contained in:
		| @@ -721,6 +721,11 @@ SET(LAUNCHER_SOURCES | ||||
|     ui/pages/modplatform/VanillaPage.cpp | ||||
|     ui/pages/modplatform/VanillaPage.h | ||||
|  | ||||
|     ui/pages/modplatform/ModPage.cpp | ||||
|     ui/pages/modplatform/ModPage.h | ||||
|     ui/pages/modplatform/ModModel.cpp | ||||
|     ui/pages/modplatform/ModModel.h | ||||
|  | ||||
|     ui/pages/modplatform/atlauncher/AtlFilterModel.cpp | ||||
|     ui/pages/modplatform/atlauncher/AtlFilterModel.h | ||||
|     ui/pages/modplatform/atlauncher/AtlListModel.cpp | ||||
| @@ -879,13 +884,12 @@ qt5_wrap_ui(LAUNCHER_UI | ||||
|     ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui | ||||
|     ui/pages/modplatform/atlauncher/AtlPage.ui | ||||
|     ui/pages/modplatform/VanillaPage.ui | ||||
|     ui/pages/modplatform/ModPage.ui | ||||
|     ui/pages/modplatform/flame/FlamePage.ui | ||||
|     ui/pages/modplatform/flame/FlameModPage.ui | ||||
|     ui/pages/modplatform/legacy_ftb/Page.ui | ||||
|     ui/pages/modplatform/ImportPage.ui | ||||
|     ui/pages/modplatform/ftb/FtbPage.ui | ||||
|     ui/pages/modplatform/technic/TechnicPage.ui | ||||
|     ui/pages/modplatform/modrinth/ModrinthPage.ui | ||||
|     ui/widgets/InstanceCardWidget.ui | ||||
|     ui/widgets/CustomCommands.ui | ||||
|     ui/widgets/MCModInfoFrame.ui | ||||
|   | ||||
							
								
								
									
										166
									
								
								launcher/ui/pages/modplatform/ModModel.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								launcher/ui/pages/modplatform/ModModel.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| #include "ModModel.h" | ||||
| #include "ModPage.h" | ||||
|  | ||||
| #include "ui/dialogs/ModDownloadDialog.h" | ||||
|  | ||||
| #include <QMessageBox> | ||||
|  | ||||
| namespace ModPlatform { | ||||
|  | ||||
| ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} | ||||
|  | ||||
| ListModel::~ListModel() {} | ||||
|  | ||||
| int ListModel::rowCount(const QModelIndex& parent) const | ||||
| { | ||||
|     return modpacks.size(); | ||||
| } | ||||
|  | ||||
| int ListModel::columnCount(const QModelIndex& parent) const | ||||
| { | ||||
|     return 1; | ||||
| } | ||||
|  | ||||
| QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
| { | ||||
|     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); | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         return pack.name; | ||||
|     } else if (role == 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; | ||||
|     } else if (role == Qt::DecorationRole) { | ||||
|         if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } | ||||
|         QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); | ||||
|         ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); | ||||
|         return icon; | ||||
|     } else if (role == Qt::UserRole) { | ||||
|         QVariant v; | ||||
|         v.setValue(pack); | ||||
|         return v; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
| } | ||||
|  | ||||
| void ListModel::logoLoaded(QString logo, QIcon out) | ||||
| { | ||||
|     m_loadingLogos.removeAll(logo); | ||||
|     m_logoMap.insert(logo, out); | ||||
|     for (int i = 0; i < modpacks.size(); i++) { | ||||
|         if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ListModel::logoFailed(QString logo) | ||||
| { | ||||
|     m_failedLogos.append(logo); | ||||
|     m_loadingLogos.removeAll(logo); | ||||
| } | ||||
|  | ||||
| Qt::ItemFlags ListModel::flags(const QModelIndex& index) const | ||||
| { | ||||
|     return QAbstractListModel::flags(index); | ||||
| } | ||||
|  | ||||
| bool ListModel::canFetchMore(const QModelIndex& parent) const | ||||
| { | ||||
|     return searchState == CanPossiblyFetchMore; | ||||
| } | ||||
|  | ||||
| void ListModel::fetchMore(const QModelIndex& parent) | ||||
| { | ||||
|     if (parent.isValid()) return; | ||||
|     if (nextSearchOffset == 0) { | ||||
|         qWarning() << "fetchMore with 0 offset is wrong..."; | ||||
|         return; | ||||
|     } | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
|  | ||||
| void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) | ||||
| { | ||||
|     if (m_logoMap.contains(logo)) { | ||||
|         callback(APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); | ||||
|     } else { | ||||
|         requestLogo(logo, logoUrl); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ListModel::searchWithTerm(const QString& term, const int sort) | ||||
| { | ||||
|     if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } | ||||
|     currentSearchTerm = term; | ||||
|     currentSort = sort; | ||||
|     if (jobPtr) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } else { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|         searchState = None; | ||||
|     } | ||||
|     nextSearchOffset = 0; | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
|  | ||||
| void ListModel::searchRequestFailed(QString reason) | ||||
| { | ||||
|     if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { | ||||
|         // 409 Gone, notify user to update | ||||
|         QMessageBox::critical(nullptr, tr("Error"), | ||||
|                               QString("%1 %2") | ||||
|                                 .arg(m_parent->displayName()) | ||||
|                                 .arg(tr("API version too old!\nPlease update PolyMC!"))); | ||||
|         // self-destruct | ||||
|         ((ModDownloadDialog*)((ModPage*)parent())->parentWidget())->reject(); | ||||
|     } | ||||
|     jobPtr.reset(); | ||||
|  | ||||
|     if (searchState == ResetRequested) { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|  | ||||
|         nextSearchOffset = 0; | ||||
|         performPaginatedSearch(); | ||||
|     } else { | ||||
|         searchState = Finished; | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ListModel::requestLogo(QString logo, QString url) | ||||
| { | ||||
|     if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } | ||||
|  | ||||
|     MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); | ||||
|     auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); | ||||
|     job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); | ||||
|  | ||||
|     auto fullPath = entry->getFullPath(); | ||||
|     QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { | ||||
|         job->deleteLater(); | ||||
|         emit logoLoaded(logo, QIcon(fullPath)); | ||||
|         if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } | ||||
|     }); | ||||
|  | ||||
|     QObject::connect(job, &NetJob::failed, this, [this, logo, job] { | ||||
|         job->deleteLater(); | ||||
|         emit logoFailed(logo); | ||||
|     }); | ||||
|  | ||||
|     job->start(); | ||||
|     m_loadingLogos.append(logo); | ||||
| } | ||||
|  | ||||
| }  // namespace ModPlatform | ||||
							
								
								
									
										60
									
								
								launcher/ui/pages/modplatform/ModModel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								launcher/ui/pages/modplatform/ModModel.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QAbstractListModel> | ||||
|  | ||||
| #include "modplatform/ModIndex.h" | ||||
| #include "net/NetJob.h" | ||||
|  | ||||
| class ModPage; | ||||
|  | ||||
| namespace ModPlatform { | ||||
|  | ||||
| typedef QMap<QString, QIcon> LogoMap; | ||||
| typedef std::function<void(QString)> LogoCallback; | ||||
|  | ||||
| class ListModel : public QAbstractListModel { | ||||
|     Q_OBJECT | ||||
|  | ||||
|    public: | ||||
|     ListModel(ModPage* parent); | ||||
|     virtual ~ListModel(); | ||||
|  | ||||
|     int rowCount(const QModelIndex& parent) const override; | ||||
|     int columnCount(const QModelIndex& parent) const override; | ||||
|     QVariant data(const QModelIndex& index, int role) const override; | ||||
|     Qt::ItemFlags flags(const QModelIndex& index) const override; | ||||
|     bool canFetchMore(const QModelIndex& parent) const override; | ||||
|     void fetchMore(const QModelIndex& parent) override; | ||||
|  | ||||
|     void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); | ||||
|     void searchWithTerm(const QString& term, const int sort); | ||||
|  | ||||
|    protected slots: | ||||
|     virtual void performPaginatedSearch() = 0; | ||||
|     virtual void searchRequestFinished() = 0; | ||||
|  | ||||
|     void logoFailed(QString logo); | ||||
|     void logoLoaded(QString logo, QIcon out); | ||||
|  | ||||
|     void searchRequestFailed(QString reason); | ||||
|  | ||||
|    protected: | ||||
|     void requestLogo(QString file, QString url); | ||||
|  | ||||
|    protected: | ||||
|     ModPage* m_parent; | ||||
|  | ||||
|     QList<ModPlatform::IndexedPack> modpacks; | ||||
|     QStringList m_failedLogos; | ||||
|     QStringList m_loadingLogos; | ||||
|     LogoMap m_logoMap; | ||||
|     QMap<QString, LogoCallback> waitingCallbacks; | ||||
|  | ||||
|     QString currentSearchTerm; | ||||
|     int currentSort = 0; | ||||
|     int nextSearchOffset = 0; | ||||
|     enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; | ||||
|     NetJob::Ptr jobPtr; | ||||
|     QByteArray response; | ||||
| }; | ||||
| }  // namespace ModPlatform | ||||
							
								
								
									
										152
									
								
								launcher/ui/pages/modplatform/ModPage.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								launcher/ui/pages/modplatform/ModPage.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| #include "ModPage.h" | ||||
| #include "ui_ModPage.h" | ||||
|  | ||||
| #include <QKeyEvent> | ||||
|  | ||||
| #include "ui/dialogs/ModDownloadDialog.h" | ||||
|  | ||||
| ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance) | ||||
|     : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog) | ||||
| { | ||||
|     ui->setupUi(this); | ||||
|     connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); | ||||
|     ui->searchEdit->installEventFilter(this); | ||||
|  | ||||
|     ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); | ||||
|     ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); | ||||
|  | ||||
| } | ||||
|  | ||||
| ModPage::~ModPage() | ||||
| { | ||||
|     delete ui; | ||||
| } | ||||
|  | ||||
| void ModPage::openedImpl() | ||||
| { | ||||
|     updateSelectionButton(); | ||||
|     triggerSearch(); | ||||
| } | ||||
|  | ||||
| bool ModPage::eventFilter(QObject* watched, QEvent* event) | ||||
| { | ||||
|     if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { | ||||
|         QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); | ||||
|         if (keyEvent->key() == Qt::Key_Return) { | ||||
|             triggerSearch(); | ||||
|             keyEvent->accept(); | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     return QWidget::eventFilter(watched, event); | ||||
| } | ||||
|  | ||||
| void ModPage::updateSelectionButton() | ||||
| { | ||||
|     if (!isOpened || selectedVersion < 0) { | ||||
|         ui->modSelectionButton->setEnabled(false); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     ui->modSelectionButton->setEnabled(true); | ||||
|     auto& version = current.versions[selectedVersion]; | ||||
|     if (!dialog->isModSelected(current.name, version.fileName)) { | ||||
|         ui->modSelectionButton->setText(tr("Select mod for download")); | ||||
|     } else { | ||||
|         ui->modSelectionButton->setText(tr("Deselect mod for download")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ModPage::triggerSearch() | ||||
| { | ||||
|     listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); | ||||
| } | ||||
|  | ||||
| void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) | ||||
| { | ||||
|     ui->versionSelectionBox->clear(); | ||||
|  | ||||
|     if (!first.isValid()) { return; } | ||||
|  | ||||
|     current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); | ||||
|     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) { | ||||
|             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(", "); | ||||
|     } | ||||
|     text += "<br><br>"; | ||||
|  | ||||
|     ui->packDescription->setHtml(text + current.description); | ||||
|  | ||||
|     if (!current.versionsLoaded) { | ||||
|         qDebug() << QString("Loading %1 mod versions").arg(debugName()); | ||||
|  | ||||
|         ui->modSelectionButton->setText(tr("Loading versions...")); | ||||
|         ui->modSelectionButton->setEnabled(false); | ||||
|  | ||||
|         auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName()).arg(current.name), APPLICATION->network()); | ||||
|         auto response = new QByteArray(); | ||||
|         QString addonId = current.addonId.toString(); | ||||
|         //FIXME | ||||
|         if(debugName() == "Modrinth") | ||||
|             netJob->addNetAction( | ||||
|                 Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); | ||||
|         else | ||||
|             netJob->addNetAction( | ||||
|                 Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); | ||||
|  | ||||
|         QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ | ||||
|             onModVersionSucceed(this, response, addonId); | ||||
|         }); | ||||
|  | ||||
|         QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { | ||||
|             netJob->deleteLater(); | ||||
|             delete response; | ||||
|         }); | ||||
|  | ||||
|         netJob->start(); | ||||
|     } else { | ||||
|         for (int i = 0; i < current.versions.size(); i++) { | ||||
|             ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); | ||||
|         } | ||||
|         if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } | ||||
|  | ||||
|         updateSelectionButton(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ModPage::onVersionSelectionChanged(QString data) | ||||
| { | ||||
|     if (data.isNull() || data.isEmpty()) { | ||||
|         selectedVersion = -1; | ||||
|         return; | ||||
|     } | ||||
|     selectedVersion = ui->versionSelectionBox->currentData().toInt(); | ||||
|     updateSelectionButton(); | ||||
| } | ||||
|  | ||||
| void ModPage::onModSelected() | ||||
| { | ||||
|     auto& version = current.versions[selectedVersion]; | ||||
|     if (dialog->isModSelected(current.name, version.fileName)) { | ||||
|         dialog->removeSelectedMod(current.name); | ||||
|     } else { | ||||
|         dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); | ||||
|     } | ||||
|  | ||||
|     updateSelectionButton(); | ||||
| } | ||||
							
								
								
									
										57
									
								
								launcher/ui/pages/modplatform/ModPage.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								launcher/ui/pages/modplatform/ModPage.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <Application.h> | ||||
| #include <QWidget> | ||||
|  | ||||
| #include "modplatform/ModIndex.h" | ||||
| #include "tasks/Task.h" | ||||
| #include "ui/pages/BasePage.h" | ||||
| #include "ui/pages/modplatform/ModModel.h" | ||||
|  | ||||
| class ModDownloadDialog; | ||||
|  | ||||
| namespace Ui { | ||||
| class ModPage; | ||||
| } | ||||
|  | ||||
| class ModPage : public QWidget, public BasePage { | ||||
|     Q_OBJECT | ||||
|  | ||||
|    public: | ||||
|     explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance); | ||||
|     virtual ~ModPage(); | ||||
|  | ||||
|     inline virtual QString displayName() const override = 0; | ||||
|     inline virtual QIcon icon() const override = 0; | ||||
|     inline virtual QString id() const override = 0; | ||||
|     inline virtual QString helpPage() const override = 0; | ||||
|  | ||||
|     inline virtual QString debugName() const = 0; | ||||
|     inline virtual QString metaEntryBase() const = 0; | ||||
|  | ||||
|     virtual bool shouldDisplay() const override = 0; | ||||
|  | ||||
|     void openedImpl() override; | ||||
|     bool eventFilter(QObject* watched, QEvent* event) override; | ||||
|  | ||||
|     BaseInstance* m_instance; | ||||
|  | ||||
|    protected: | ||||
|     virtual void onModVersionSucceed(ModPage*, QByteArray*, QString) = 0; | ||||
|  | ||||
|     void updateSelectionButton(); | ||||
|  | ||||
|    protected slots: | ||||
|     void triggerSearch(); | ||||
|     void onSelectionChanged(QModelIndex first, QModelIndex second); | ||||
|     void onVersionSelectionChanged(QString data); | ||||
|     void onModSelected(); | ||||
|  | ||||
|    protected: | ||||
|     Ui::ModPage* ui = nullptr; | ||||
|     ModDownloadDialog* dialog = nullptr; | ||||
|     ModPlatform::ListModel* listModel = nullptr; | ||||
|     ModPlatform::IndexedPack current; | ||||
|  | ||||
|     int selectedVersion = -1; | ||||
| }; | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ModrinthPage</class> | ||||
|  <widget class="QWidget" name="ModrinthPage"> | ||||
|  <class>ModPage</class> | ||||
|  <widget class="QWidget" name="ModPage"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
| @@ -1,169 +1,25 @@ | ||||
| #include "FlameModModel.h" | ||||
| #include "Application.h" | ||||
| #include "FlameModPage.h" | ||||
| #include "minecraft/MinecraftInstance.h" | ||||
| #include "minecraft/PackProfile.h" | ||||
| #include "FlameModPage.h" | ||||
|  | ||||
| #include <Json.h> | ||||
|  | ||||
| #include <MMCStrings.h> | ||||
| #include <Version.h> | ||||
|  | ||||
| #include <QtMath> | ||||
|  | ||||
|  | ||||
| namespace FlameMod { | ||||
|  | ||||
| ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) | ||||
| { | ||||
| } | ||||
| ListModel::ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} | ||||
|  | ||||
| ListModel::~ListModel() | ||||
| { | ||||
| } | ||||
| ListModel::~ListModel() {} | ||||
|  | ||||
| int ListModel::rowCount(const QModelIndex &parent) const | ||||
| { | ||||
|     return modpacks.size(); | ||||
| } | ||||
|  | ||||
| int ListModel::columnCount(const QModelIndex &parent) const | ||||
| { | ||||
|     return 1; | ||||
| } | ||||
|  | ||||
| QVariant ListModel::data(const QModelIndex &index, int role) const | ||||
| { | ||||
|     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); | ||||
|     if(role == Qt::DisplayRole) | ||||
|     { | ||||
|         return pack.name; | ||||
|     } | ||||
|     else if (role == 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; | ||||
|     } | ||||
|     else if(role == Qt::DecorationRole) | ||||
|     { | ||||
|         if(m_logoMap.contains(pack.logoName)) | ||||
|         { | ||||
|             return (m_logoMap.value(pack.logoName)); | ||||
|         } | ||||
|         QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); | ||||
|         ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); | ||||
|         return icon; | ||||
|     } | ||||
|     else if(role == Qt::UserRole) | ||||
|     { | ||||
|         QVariant v; | ||||
|         v.setValue(pack); | ||||
|         return v; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
| } | ||||
|  | ||||
| void ListModel::logoLoaded(QString logo, QIcon out) | ||||
| { | ||||
|     m_loadingLogos.removeAll(logo); | ||||
|     m_logoMap.insert(logo, out); | ||||
|     for(int i = 0; i < modpacks.size(); i++) { | ||||
|         if(modpacks[i].logoName == logo) { | ||||
|             emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ListModel::logoFailed(QString logo) | ||||
| { | ||||
|     m_failedLogos.append(logo); | ||||
|     m_loadingLogos.removeAll(logo); | ||||
| } | ||||
|  | ||||
| void ListModel::requestLogo(QString logo, QString url) | ||||
| { | ||||
|     if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) | ||||
|     { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0))); | ||||
|     auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); | ||||
|     job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); | ||||
|  | ||||
|     auto fullPath = entry->getFullPath(); | ||||
|     QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] | ||||
|     { | ||||
|         job->deleteLater(); | ||||
|         emit logoLoaded(logo, QIcon(fullPath)); | ||||
|         if(waitingCallbacks.contains(logo)) | ||||
|         { | ||||
|             waitingCallbacks.value(logo)(fullPath); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     QObject::connect(job, &NetJob::failed, this, [this, logo, job] | ||||
|     { | ||||
|         job->deleteLater(); | ||||
|         emit logoFailed(logo); | ||||
|     }); | ||||
|  | ||||
|     job->start(); | ||||
|     m_loadingLogos.append(logo); | ||||
| } | ||||
|  | ||||
| void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) | ||||
| { | ||||
|     if(m_logoMap.contains(logo)) | ||||
|     { | ||||
|         callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         requestLogo(logo, logoUrl); | ||||
|     } | ||||
| } | ||||
|  | ||||
| Qt::ItemFlags ListModel::flags(const QModelIndex &index) const | ||||
| { | ||||
|     return QAbstractListModel::flags(index); | ||||
| } | ||||
|  | ||||
| bool ListModel::canFetchMore(const QModelIndex& parent) const | ||||
| { | ||||
|     return searchState == CanPossiblyFetchMore; | ||||
| } | ||||
|  | ||||
| void ListModel::fetchMore(const QModelIndex& parent) | ||||
| { | ||||
|     if (parent.isValid()) | ||||
|         return; | ||||
|     if(nextSearchOffset == 0) { | ||||
|         qWarning() << "fetchMore with 0 offset is wrong..."; | ||||
|         return; | ||||
|     } | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
| const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"}; | ||||
| const char* sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; | ||||
|  | ||||
| void ListModel::performPaginatedSearch() | ||||
| { | ||||
|  | ||||
|     QString mcVersion =  ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); | ||||
|     bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); | ||||
|     QString mcVersion = ((MinecraftInstance*)((FlameModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); | ||||
|     bool hasFabric = !((MinecraftInstance*)((FlameModPage*)parent())->m_instance) | ||||
|                           ->getPackProfile() | ||||
|                           ->getComponentVersion("net.fabricmc.fabric-loader") | ||||
|                           .isEmpty(); | ||||
|     auto netJob = new NetJob("Flame::Search", APPLICATION->network()); | ||||
|     auto searchUrl = QString( | ||||
|         "https://addons-ecs.forgesvc.net/api/v2/addon/search?" | ||||
| @@ -176,44 +32,22 @@ void ListModel::performPaginatedSearch() | ||||
|         "searchFilter=%2&" | ||||
|         "sort=%3&" | ||||
|         "modLoaderType=%4&" | ||||
|         "gameVersion=%5" | ||||
|     ) | ||||
|         .arg(nextSearchOffset) | ||||
|         .arg(currentSearchTerm) | ||||
|         .arg(sorts[currentSort]) | ||||
|         .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType | ||||
|         .arg(mcVersion); | ||||
|         "gameVersion=%5") | ||||
|             .arg(nextSearchOffset) | ||||
|             .arg(currentSearchTerm) | ||||
|             .arg(sorts[currentSort]) | ||||
|             .arg(hasFabric ? 4 : 1)  // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType | ||||
|             .arg(mcVersion); | ||||
|  | ||||
|     netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); | ||||
|     jobPtr = netJob; | ||||
|     jobPtr->start(); | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); | ||||
|  | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, &FlameMod::ListModel::searchRequestFinished); | ||||
|     QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); | ||||
| } | ||||
|  | ||||
| void ListModel::searchWithTerm(const QString &term, const int sort) | ||||
| { | ||||
|     if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { | ||||
|         return; | ||||
|     } | ||||
|     currentSearchTerm = term; | ||||
|     currentSort = sort; | ||||
|     if(jobPtr) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } | ||||
|     else { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|         searchState = None; | ||||
|     } | ||||
|     nextSearchOffset = 0; | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
|  | ||||
| void ListModel::searchRequestFinished() | ||||
| void FlameMod::ListModel::searchRequestFinished() | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|  | ||||
| @@ -253,21 +87,4 @@ void ListModel::searchRequestFinished() | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void ListModel::searchRequestFailed(QString reason) | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|  | ||||
|     if(searchState == ResetRequested) { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|  | ||||
|         nextSearchOffset = 0; | ||||
|         performPaginatedSearch(); | ||||
|     } else { | ||||
|         searchState = Finished; | ||||
|     } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| }  // namespace FlameMod | ||||
|   | ||||
| @@ -2,78 +2,36 @@ | ||||
|  | ||||
| #include <RWStorage.h> | ||||
|  | ||||
| #include <QAbstractListModel> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QThreadPool> | ||||
| #include <QIcon> | ||||
| #include <QStyledItemDelegate> | ||||
| #include <QList> | ||||
| #include <QMetaType> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QString> | ||||
| #include <QStringList> | ||||
| #include <QMetaType> | ||||
| #include <QStyledItemDelegate> | ||||
| #include <QThreadPool> | ||||
|  | ||||
| #include <functional> | ||||
| #include <net/NetJob.h> | ||||
| #include <functional> | ||||
|  | ||||
| #include <modplatform/flame/FlamePackIndex.h> | ||||
| #include "modplatform/flame/FlameModIndex.h" | ||||
| #include "BaseInstance.h" | ||||
| #include "FlameModPage.h" | ||||
| #include "modplatform/flame/FlameModIndex.h" | ||||
|  | ||||
| namespace FlameMod { | ||||
|  | ||||
|  | ||||
| typedef QMap<QString, QIcon> LogoMap; | ||||
| typedef std::function<void(QString)> LogoCallback; | ||||
|  | ||||
| class ListModel : public QAbstractListModel | ||||
| { | ||||
| class ListModel : public ModPlatform::ListModel { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     ListModel(FlameModPage *parent); | ||||
|    public: | ||||
|     ListModel(FlameModPage* parent); | ||||
|     virtual ~ListModel(); | ||||
|  | ||||
|     int rowCount(const QModelIndex &parent) const override; | ||||
|     int columnCount(const QModelIndex &parent) const override; | ||||
|     QVariant data(const QModelIndex &index, int role) const override; | ||||
|     Qt::ItemFlags flags(const QModelIndex &index) const override; | ||||
|     bool canFetchMore(const QModelIndex & parent) const override; | ||||
|     void fetchMore(const QModelIndex & parent) override; | ||||
|  | ||||
|     void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); | ||||
|     void searchWithTerm(const QString &term, const int sort); | ||||
|  | ||||
| private slots: | ||||
|     void performPaginatedSearch(); | ||||
|  | ||||
|     void logoFailed(QString logo); | ||||
|     void logoLoaded(QString logo, QIcon out); | ||||
|  | ||||
|     void searchRequestFinished(); | ||||
|     void searchRequestFailed(QString reason); | ||||
|  | ||||
| private: | ||||
|     void requestLogo(QString file, QString url); | ||||
|  | ||||
| private: | ||||
|     QList<ModPlatform::IndexedPack> modpacks; | ||||
|     QStringList m_failedLogos; | ||||
|     QStringList m_loadingLogos; | ||||
|     LogoMap m_logoMap; | ||||
|     QMap<QString, LogoCallback> waitingCallbacks; | ||||
|  | ||||
|     QString currentSearchTerm; | ||||
|     int currentSort = 0; | ||||
|     int nextSearchOffset = 0; | ||||
|     enum SearchState { | ||||
|         None, | ||||
|         CanPossiblyFetchMore, | ||||
|         ResetRequested, | ||||
|         Finished | ||||
|     } searchState = None; | ||||
|     NetJob::Ptr jobPtr; | ||||
|     QByteArray response; | ||||
|    private slots: | ||||
|     void performPaginatedSearch() override; | ||||
|     void searchRequestFinished() override; | ||||
| }; | ||||
|  | ||||
| } | ||||
| }  // namespace Modrinth | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| #include "FlameModPage.h" | ||||
| #include "ui_FlameModPage.h" | ||||
| #include "ui_ModPage.h" | ||||
|  | ||||
| #include <QKeyEvent> | ||||
|  | ||||
| @@ -12,206 +12,59 @@ | ||||
| #include "minecraft/PackProfile.h" | ||||
| #include "ui/dialogs/ModDownloadDialog.h" | ||||
|  | ||||
| FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) | ||||
|     : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), | ||||
|       dialog(dialog) { | ||||
|   ui->setupUi(this); | ||||
|   connect(ui->searchButton, &QPushButton::clicked, this, | ||||
|           &FlameModPage::triggerSearch); | ||||
|   ui->searchEdit->installEventFilter(this); | ||||
|   listModel = new FlameMod::ListModel(this); | ||||
|   ui->packView->setModel(listModel); | ||||
| FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)  | ||||
|     : ModPage(dialog, instance) | ||||
| { | ||||
|     listModel = new FlameMod::ListModel(this); | ||||
|     ui->packView->setModel(listModel); | ||||
|  | ||||
|   ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( | ||||
|       Qt::ScrollBarAsNeeded); | ||||
|   ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); | ||||
|     // 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")); | ||||
|  | ||||
|   // 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")); | ||||
|  | ||||
|   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); | ||||
| } | ||||
|  | ||||
| FlameModPage::~FlameModPage() { delete ui; } | ||||
|  | ||||
| bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { | ||||
|   if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { | ||||
|     QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); | ||||
|     if (keyEvent->key() == Qt::Key_Return) { | ||||
|       triggerSearch(); | ||||
|       keyEvent->accept(); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|   return QWidget::eventFilter(watched, event); | ||||
|     // 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); | ||||
| } | ||||
|  | ||||
| bool FlameModPage::shouldDisplay() const { return true; } | ||||
|  | ||||
| void FlameModPage::openedImpl() { | ||||
|   updateSelectionButton(); | ||||
|   triggerSearch(); | ||||
| } | ||||
|  | ||||
| void FlameModPage::triggerSearch() { | ||||
|   listModel->searchWithTerm(ui->searchEdit->text(), | ||||
|                             ui->sortByBox->currentIndex()); | ||||
| } | ||||
|  | ||||
| void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { | ||||
|   ui->versionSelectionBox->clear(); | ||||
|  | ||||
|   if (!first.isValid()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); | ||||
|   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) { | ||||
|       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)); | ||||
| void FlameModPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) | ||||
| { | ||||
|     if (addonId != current.addonId) { | ||||
|         return;  // wrong request | ||||
|     } | ||||
|     text += "<br>" + tr(" by ") + authorStrs.join(", "); | ||||
|   } | ||||
|   text += "<br><br>"; | ||||
|  | ||||
|   ui->packDescription->setHtml(text + current.description); | ||||
|  | ||||
|   if (!current.versionsLoaded) { | ||||
|     qDebug() << "Loading flame mod versions"; | ||||
|  | ||||
|     ui->modSelectionButton->setText(tr("Loading versions...")); | ||||
|     ui->modSelectionButton->setEnabled(false); | ||||
|  | ||||
|     auto netJob = | ||||
|         new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), | ||||
|                    APPLICATION->network()); | ||||
|     auto response = new QByteArray(); | ||||
|     int addonId = current.addonId.toInt(); | ||||
|     netJob->addNetAction(Net::Download::makeByteArray( | ||||
|         QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files") | ||||
|             .arg(addonId), | ||||
|         response)); | ||||
|  | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { | ||||
|         if(addonId != current.addonId){ | ||||
|             return; //wrong request | ||||
|         } | ||||
|       QJsonParseError parse_error; | ||||
|       QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); | ||||
|       if (parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Flame at " | ||||
|                    << parse_error.offset | ||||
|                    << " reason: " << parse_error.errorString(); | ||||
|     QJsonParseError parse_error; | ||||
|     QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); | ||||
|     if (parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); | ||||
|         qWarning() << *response; | ||||
|         return; | ||||
|       } | ||||
|       QJsonArray arr = doc.array(); | ||||
|       try { | ||||
|         FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), | ||||
|                                           m_instance); | ||||
|       } catch (const JSONValidationError &e) { | ||||
|     } | ||||
|     QJsonArray arr = doc.array(); | ||||
|     try { | ||||
|         FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); | ||||
|     } catch (const JSONValidationError& e) { | ||||
|         qDebug() << *response; | ||||
|         qWarning() << "Error while reading Flame mod version: " << e.cause(); | ||||
|       } | ||||
|       auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); | ||||
|       QString mcVersion = packProfile->getComponentVersion("net.minecraft"); | ||||
|       QString loaderString = | ||||
|           (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) | ||||
|               ? "fabric" | ||||
|               : "forge"; | ||||
|       for (int i = 0; i < current.versions.size(); i++) { | ||||
|         auto version = current.versions[i]; | ||||
|         if (!version.mcVersion.contains(mcVersion)) { | ||||
|           continue; | ||||
|         } | ||||
|         ui->versionSelectionBox->addItem(version.version, QVariant(i)); | ||||
|       } | ||||
|       if (ui->versionSelectionBox->count() == 0) { | ||||
|         ui->versionSelectionBox->addItem(tr("No Valid Version found!"), | ||||
|                                          QVariant(-1)); | ||||
|       } | ||||
|  | ||||
|       ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); | ||||
|       updateSelectionButton(); | ||||
|     }); | ||||
|     QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { | ||||
|       netJob->deleteLater(); | ||||
|       delete response; | ||||
|     }); | ||||
|     netJob->start(); | ||||
|   } else { | ||||
|     } | ||||
|     auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); | ||||
|     QString mcVersion = packProfile->getComponentVersion("net.minecraft"); | ||||
|     QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; | ||||
|     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)); | ||||
|         auto version = current.versions[i]; | ||||
|         if (!version.mcVersion.contains(mcVersion)) { continue; } | ||||
|         ui->versionSelectionBox->addItem(version.version, QVariant(i)); | ||||
|     } | ||||
|     if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } | ||||
|  | ||||
|     ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); | ||||
|     updateSelectionButton(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void FlameModPage::updateSelectionButton() { | ||||
|   if (!isOpened || selectedVersion < 0) { | ||||
|     ui->modSelectionButton->setEnabled(false); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ui->modSelectionButton->setEnabled(true); | ||||
|   auto &version = current.versions[selectedVersion]; | ||||
|   if (!dialog->isModSelected(current.name, version.fileName)) { | ||||
|     ui->modSelectionButton->setText(tr("Select mod for download")); | ||||
|   } else { | ||||
|     ui->modSelectionButton->setText(tr("Deselect mod for download")); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void FlameModPage::onVersionSelectionChanged(QString data) { | ||||
|   if (data.isNull() || data.isEmpty()) { | ||||
|     selectedVersion = -1; | ||||
|     return; | ||||
|   } | ||||
|   selectedVersion = ui->versionSelectionBox->currentData().toInt(); | ||||
|   updateSelectionButton(); | ||||
| } | ||||
|  | ||||
| void FlameModPage::onModSelected() { | ||||
|   auto &version = current.versions[selectedVersion]; | ||||
|   if (dialog->isModSelected(current.name, version.fileName)) { | ||||
|     dialog->removeSelectedMod(current.name); | ||||
|   } else { | ||||
|     dialog->addSelectedMod(current.name, | ||||
|                            new ModDownloadTask(version.downloadUrl, | ||||
|                                                version.fileName, dialog->mods)); | ||||
|   } | ||||
|  | ||||
|   updateSelectionButton(); | ||||
| } | ||||
|   | ||||
| @@ -1,68 +1,24 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QWidget> | ||||
| #include "ui/pages/modplatform/ModPage.h" | ||||
|  | ||||
| #include "ui/pages/BasePage.h" | ||||
| #include <Application.h> | ||||
| #include "tasks/Task.h" | ||||
| #include "modplatform/flame/FlameModIndex.h" | ||||
|  | ||||
| namespace Ui | ||||
| { | ||||
| class FlameModPage; | ||||
| } | ||||
|  | ||||
| class ModDownloadDialog; | ||||
|  | ||||
| namespace FlameMod { | ||||
|     class ListModel; | ||||
| } | ||||
|  | ||||
| class FlameModPage : public QWidget, public BasePage | ||||
| { | ||||
| class FlameModPage : public ModPage { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); | ||||
|     virtual ~FlameModPage(); | ||||
|     virtual QString displayName() const override | ||||
|     { | ||||
|         return tr("CurseForge"); | ||||
|     } | ||||
|     virtual QIcon icon() const override | ||||
|     { | ||||
|         return APPLICATION->getThemedIcon("flame"); | ||||
|     } | ||||
|     virtual QString id() const override | ||||
|     { | ||||
|         return "curseforge"; | ||||
|     } | ||||
|     virtual QString helpPage() const override | ||||
|     { | ||||
|         return "Flame-platform"; | ||||
|     } | ||||
|     virtual bool shouldDisplay() const override; | ||||
|    public: | ||||
|     explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); | ||||
|     virtual ~FlameModPage() = default; | ||||
|  | ||||
|     void openedImpl() override; | ||||
|     inline QString displayName() const override { return tr("CurseForge"); } | ||||
|     inline QIcon icon() const override { return APPLICATION->getThemedIcon("flame"); } | ||||
|     inline QString id() const override { return "curseforge"; } | ||||
|     inline QString helpPage() const override { return "Flame-platform"; } | ||||
|  | ||||
|     bool eventFilter(QObject * watched, QEvent * event) override; | ||||
|     inline QString debugName() const override { return tr("Flame"); } | ||||
|     inline QString metaEntryBase() const override { return "FlameMods"; }; | ||||
|  | ||||
|     BaseInstance *m_instance; | ||||
|     bool shouldDisplay() const override; | ||||
|  | ||||
| private: | ||||
|     void updateSelectionButton(); | ||||
|  | ||||
| private slots: | ||||
|     void triggerSearch(); | ||||
|     void onSelectionChanged(QModelIndex first, QModelIndex second); | ||||
|     void onVersionSelectionChanged(QString data); | ||||
|     void onModSelected(); | ||||
|  | ||||
| private: | ||||
|     Ui::FlameModPage *ui = nullptr; | ||||
|     ModDownloadDialog* dialog = nullptr; | ||||
|     FlameMod::ListModel* listModel = nullptr; | ||||
|     ModPlatform::IndexedPack current; | ||||
|  | ||||
|     int selectedVersion = -1; | ||||
|    private: | ||||
|     void onModVersionSucceed(ModPage*, QByteArray*, QString) override; | ||||
| }; | ||||
|   | ||||
| @@ -1,97 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>FlameModPage</class> | ||||
|  <widget class="QWidget" name="FlameModPage"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>837</width> | ||||
|     <height>685</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QGridLayout" name="gridLayout"> | ||||
|    <item row="2" column="0" colspan="2"> | ||||
|     <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0,0,0" columnminimumwidth="0,0,0"> | ||||
|      <item row="1" column="2"> | ||||
|       <widget class="QComboBox" name="versionSelectionBox"/> | ||||
|      </item> | ||||
|      <item row="1" column="0"> | ||||
|       <widget class="QComboBox" name="sortByBox"/> | ||||
|      </item> | ||||
|      <item row="1" column="1"> | ||||
|       <widget class="QLabel" name="label"> | ||||
|        <property name="text"> | ||||
|         <string>Version selected:</string> | ||||
|        </property> | ||||
|        <property name="alignment"> | ||||
|         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item row="2" column="2"> | ||||
|       <widget class="QPushButton" name="modSelectionButton"> | ||||
|        <property name="text"> | ||||
|         <string>Select mod for download</string> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="0" column="0"> | ||||
|     <widget class="QLineEdit" name="searchEdit"> | ||||
|      <property name="placeholderText"> | ||||
|       <string>Search and filter ...</string> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="1" column="0" colspan="2"> | ||||
|     <layout class="QGridLayout" name="gridLayout_3"> | ||||
|      <item row="1" column="0"> | ||||
|       <widget class="QListView" name="packView"> | ||||
|        <property name="horizontalScrollBarPolicy"> | ||||
|         <enum>Qt::ScrollBarAlwaysOff</enum> | ||||
|        </property> | ||||
|        <property name="alternatingRowColors"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="iconSize"> | ||||
|         <size> | ||||
|          <width>48</width> | ||||
|          <height>48</height> | ||||
|         </size> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item row="1" column="1"> | ||||
|       <widget class="QTextBrowser" name="packDescription"> | ||||
|        <property name="openExternalLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="openLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="0" column="1"> | ||||
|     <widget class="QPushButton" name="searchButton"> | ||||
|      <property name="text"> | ||||
|       <string>Search</string> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <tabstops> | ||||
|   <tabstop>searchEdit</tabstop> | ||||
|   <tabstop>searchButton</tabstop> | ||||
|   <tabstop>packView</tabstop> | ||||
|   <tabstop>packDescription</tabstop> | ||||
|   <tabstop>sortByBox</tabstop> | ||||
|   <tabstop>versionSelectionBox</tabstop> | ||||
|  </tabstops> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
| @@ -1,171 +1,25 @@ | ||||
| #include "ModrinthModel.h" | ||||
| #include "Application.h" | ||||
| #include "ModrinthPage.h" | ||||
| #include "minecraft/MinecraftInstance.h" | ||||
| #include "minecraft/PackProfile.h" | ||||
| #include "ModrinthPage.h" | ||||
| #include "ui/dialogs/ModDownloadDialog.h" | ||||
|  | ||||
| #include <Json.h> | ||||
|  | ||||
| #include <MMCStrings.h> | ||||
| #include <Version.h> | ||||
|  | ||||
| #include <QtMath> | ||||
| #include <QMessageBox> | ||||
|  | ||||
|  | ||||
| namespace Modrinth { | ||||
|  | ||||
| ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) | ||||
| { | ||||
| } | ||||
| ListModel::ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent) {} | ||||
|  | ||||
| ListModel::~ListModel() | ||||
| { | ||||
| } | ||||
| ListModel::~ListModel() {} | ||||
|  | ||||
| int ListModel::rowCount(const QModelIndex &parent) const | ||||
| { | ||||
|     return modpacks.size(); | ||||
| } | ||||
|  | ||||
| int ListModel::columnCount(const QModelIndex &parent) const | ||||
| { | ||||
|     return 1; | ||||
| } | ||||
|  | ||||
| QVariant ListModel::data(const QModelIndex &index, int role) const | ||||
| { | ||||
|     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); | ||||
|     if(role == Qt::DisplayRole) | ||||
|     { | ||||
|         return pack.name; | ||||
|     } | ||||
|     else if (role == 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; | ||||
|     } | ||||
|     else if(role == Qt::DecorationRole) | ||||
|     { | ||||
|         if(m_logoMap.contains(pack.logoName)) | ||||
|         { | ||||
|             return (m_logoMap.value(pack.logoName)); | ||||
|         } | ||||
|         QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); | ||||
|         ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); | ||||
|         return icon; | ||||
|     } | ||||
|     else if(role == Qt::UserRole) | ||||
|     { | ||||
|         QVariant v; | ||||
|         v.setValue(pack); | ||||
|         return v; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
| } | ||||
|  | ||||
| void ListModel::logoLoaded(QString logo, QIcon out) | ||||
| { | ||||
|     m_loadingLogos.removeAll(logo); | ||||
|     m_logoMap.insert(logo, out); | ||||
|     for(int i = 0; i < modpacks.size(); i++) { | ||||
|         if(modpacks[i].logoName == logo) { | ||||
|             emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ListModel::logoFailed(QString logo) | ||||
| { | ||||
|     m_failedLogos.append(logo); | ||||
|     m_loadingLogos.removeAll(logo); | ||||
| } | ||||
|  | ||||
| void ListModel::requestLogo(QString logo, QString url) | ||||
| { | ||||
|     if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) | ||||
|     { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); | ||||
|     auto job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); | ||||
|     job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); | ||||
|  | ||||
|     auto fullPath = entry->getFullPath(); | ||||
|     QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] | ||||
|     { | ||||
|         job->deleteLater(); | ||||
|         emit logoLoaded(logo, QIcon(fullPath)); | ||||
|         if(waitingCallbacks.contains(logo)) | ||||
|         { | ||||
|             waitingCallbacks.value(logo)(fullPath); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     QObject::connect(job, &NetJob::failed, this, [this, logo, job] | ||||
|     { | ||||
|         job->deleteLater(); | ||||
|         emit logoFailed(logo); | ||||
|     }); | ||||
|  | ||||
|     job->start(); | ||||
|     m_loadingLogos.append(logo); | ||||
| } | ||||
|  | ||||
| void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) | ||||
| { | ||||
|     if(m_logoMap.contains(logo)) | ||||
|     { | ||||
|         callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         requestLogo(logo, logoUrl); | ||||
|     } | ||||
| } | ||||
|  | ||||
| Qt::ItemFlags ListModel::flags(const QModelIndex &index) const | ||||
| { | ||||
|     return QAbstractListModel::flags(index); | ||||
| } | ||||
|  | ||||
| bool ListModel::canFetchMore(const QModelIndex& parent) const | ||||
| { | ||||
|     return searchState == CanPossiblyFetchMore; | ||||
| } | ||||
|  | ||||
| void ListModel::fetchMore(const QModelIndex& parent) | ||||
| { | ||||
|     if (parent.isValid()) | ||||
|         return; | ||||
|     if(nextSearchOffset == 0) { | ||||
|         qWarning() << "fetchMore with 0 offset is wrong..."; | ||||
|         return; | ||||
|     } | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
| const char* sorts[5]{"relevance","downloads","follows","updated","newest"}; | ||||
| const char* sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; | ||||
|  | ||||
| void ListModel::performPaginatedSearch() | ||||
| { | ||||
|  | ||||
|     QString mcVersion =  ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); | ||||
|     bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); | ||||
|     QString mcVersion = ((MinecraftInstance*)((ModrinthPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); | ||||
|     bool hasFabric = !((MinecraftInstance*)((ModrinthPage*)parent())->m_instance) | ||||
|                           ->getPackProfile() | ||||
|                           ->getComponentVersion("net.fabricmc.fabric-loader") | ||||
|                           .isEmpty(); | ||||
|     auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); | ||||
|     auto searchUrl = QString( | ||||
|         "https://api.modrinth.com/v2/search?" | ||||
| @@ -173,41 +27,19 @@ void ListModel::performPaginatedSearch() | ||||
|         "limit=25&" | ||||
|         "query=%2&" | ||||
|         "index=%3&" | ||||
|         "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]" | ||||
|     ) | ||||
|         .arg(nextSearchOffset) | ||||
|         .arg(currentSearchTerm) | ||||
|         .arg(sorts[currentSort]) | ||||
|         .arg(hasFabric ? "fabric" : "forge") | ||||
|         .arg(mcVersion); | ||||
|         "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") | ||||
|             .arg(nextSearchOffset) | ||||
|             .arg(currentSearchTerm) | ||||
|             .arg(sorts[currentSort]) | ||||
|             .arg(hasFabric ? "fabric" : "forge") | ||||
|             .arg(mcVersion); | ||||
|  | ||||
|     netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); | ||||
|     jobPtr = netJob; | ||||
|     jobPtr->start(); | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); | ||||
|     QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); | ||||
| } | ||||
|  | ||||
| void ListModel::searchWithTerm(const QString &term, const int sort) | ||||
| { | ||||
|     if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { | ||||
|         return; | ||||
|     } | ||||
|     currentSearchTerm = term; | ||||
|     currentSort = sort; | ||||
|     if(jobPtr) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } | ||||
|     else { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|         searchState = None; | ||||
|     } | ||||
|     nextSearchOffset = 0; | ||||
|     performPaginatedSearch(); | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, &Modrinth::ListModel::searchRequestFinished); | ||||
|     QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); | ||||
| } | ||||
|  | ||||
| void Modrinth::ListModel::searchRequestFinished() | ||||
| @@ -216,30 +48,28 @@ void Modrinth::ListModel::searchRequestFinished() | ||||
|  | ||||
|     QJsonParseError parse_error; | ||||
|     QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); | ||||
|     if(parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); | ||||
|     if (parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset | ||||
|                    << " reason: " << parse_error.errorString(); | ||||
|         qWarning() << response; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QList<ModPlatform::IndexedPack> newList; | ||||
|     auto packs = doc.object().value("hits").toArray(); | ||||
|     for(auto packRaw : packs) { | ||||
|     for (auto packRaw : packs) { | ||||
|         auto packObj = packRaw.toObject(); | ||||
|  | ||||
|         ModPlatform::IndexedPack pack; | ||||
|         try | ||||
|         { | ||||
|         try { | ||||
|             Modrinth::loadIndexedPack(pack, packObj); | ||||
|             newList.append(pack); | ||||
|         } | ||||
|         catch(const JSONValidationError &e) | ||||
|         { | ||||
|         } catch (const JSONValidationError& e) { | ||||
|             qWarning() << "Error while loading mod from Modrinth: " << e.cause(); | ||||
|             continue; | ||||
|         } | ||||
|     } | ||||
|     if(packs.size() < 25) { | ||||
|     if (packs.size() < 25) { | ||||
|         searchState = Finished; | ||||
|     } else { | ||||
|         nextSearchOffset += 25; | ||||
| @@ -250,27 +80,4 @@ void Modrinth::ListModel::searchRequestFinished() | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void Modrinth::ListModel::searchRequestFailed(QString reason) | ||||
| { | ||||
|     if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){ | ||||
|         //409 Gone, notify user to update | ||||
|         QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!")); | ||||
|         //self-destruct | ||||
|         ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject(); | ||||
|     } | ||||
|     jobPtr.reset(); | ||||
|  | ||||
|     if(searchState == ResetRequested) { | ||||
|         beginResetModel(); | ||||
|         modpacks.clear(); | ||||
|         endResetModel(); | ||||
|  | ||||
|         nextSearchOffset = 0; | ||||
|         performPaginatedSearch(); | ||||
|     } else { | ||||
|         searchState = Finished; | ||||
|     } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| }  // namespace Modrinth | ||||
|   | ||||
| @@ -2,78 +2,36 @@ | ||||
|  | ||||
| #include <RWStorage.h> | ||||
|  | ||||
| #include <QAbstractListModel> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QThreadPool> | ||||
| #include <QIcon> | ||||
| #include <QStyledItemDelegate> | ||||
| #include <QList> | ||||
| #include <QMetaType> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QString> | ||||
| #include <QStringList> | ||||
| #include <QMetaType> | ||||
| #include <QStyledItemDelegate> | ||||
| #include <QThreadPool> | ||||
|  | ||||
| #include <functional> | ||||
| #include <net/NetJob.h> | ||||
| #include <functional> | ||||
|  | ||||
| #include <modplatform/flame/FlamePackIndex.h> | ||||
| #include "modplatform/modrinth/ModrinthPackIndex.h" | ||||
| #include "BaseInstance.h" | ||||
| #include "ModrinthPage.h" | ||||
| #include "modplatform/modrinth/ModrinthPackIndex.h" | ||||
|  | ||||
| namespace Modrinth { | ||||
|  | ||||
|  | ||||
| typedef QMap<QString, QIcon> LogoMap; | ||||
| typedef std::function<void(QString)> LogoCallback; | ||||
|  | ||||
| class ListModel : public QAbstractListModel | ||||
| { | ||||
| class ListModel : public ModPlatform::ListModel { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     ListModel(ModrinthPage *parent); | ||||
|    public: | ||||
|     ListModel(ModrinthPage* parent); | ||||
|     virtual ~ListModel(); | ||||
|  | ||||
|     int rowCount(const QModelIndex &parent) const override; | ||||
|     int columnCount(const QModelIndex &parent) const override; | ||||
|     QVariant data(const QModelIndex &index, int role) const override; | ||||
|     Qt::ItemFlags flags(const QModelIndex &index) const override; | ||||
|     bool canFetchMore(const QModelIndex & parent) const override; | ||||
|     void fetchMore(const QModelIndex & parent) override; | ||||
|  | ||||
|     void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); | ||||
|     void searchWithTerm(const QString &term, const int sort); | ||||
|  | ||||
| private slots: | ||||
|     void performPaginatedSearch(); | ||||
|  | ||||
|     void logoFailed(QString logo); | ||||
|     void logoLoaded(QString logo, QIcon out); | ||||
|  | ||||
|     void searchRequestFinished(); | ||||
|     void searchRequestFailed(QString reason); | ||||
|  | ||||
| private: | ||||
|     void requestLogo(QString file, QString url); | ||||
|  | ||||
| private: | ||||
|     QList<ModPlatform::IndexedPack> modpacks; | ||||
|     QStringList m_failedLogos; | ||||
|     QStringList m_loadingLogos; | ||||
|     LogoMap m_logoMap; | ||||
|     QMap<QString, LogoCallback> waitingCallbacks; | ||||
|  | ||||
|     QString currentSearchTerm; | ||||
|     int currentSort = 0; | ||||
|     int nextSearchOffset = 0; | ||||
|     enum SearchState { | ||||
|         None, | ||||
|         CanPossiblyFetchMore, | ||||
|         ResetRequested, | ||||
|         Finished | ||||
|     } searchState = None; | ||||
|     NetJob::Ptr jobPtr; | ||||
|     QByteArray response; | ||||
|    private slots: | ||||
|     void performPaginatedSearch() override; | ||||
|     void searchRequestFinished() override; | ||||
| }; | ||||
|  | ||||
| } | ||||
| }  // namespace Modrinth | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| #include "ModrinthPage.h" | ||||
| #include "ui_ModrinthPage.h" | ||||
| #include "ui_ModPage.h" | ||||
|  | ||||
| #include <QKeyEvent> | ||||
|  | ||||
| @@ -12,194 +12,57 @@ | ||||
| #include "minecraft/PackProfile.h" | ||||
| #include "ui/dialogs/ModDownloadDialog.h" | ||||
|  | ||||
| ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) | ||||
|     : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), | ||||
|       dialog(dialog) { | ||||
|   ui->setupUi(this); | ||||
|   connect(ui->searchButton, &QPushButton::clicked, this, | ||||
|           &ModrinthPage::triggerSearch); | ||||
|   ui->searchEdit->installEventFilter(this); | ||||
|   listModel = new Modrinth::ListModel(this); | ||||
|   ui->packView->setModel(listModel); | ||||
| ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) | ||||
|     : ModPage(dialog, instance) | ||||
| { | ||||
|     listModel = new Modrinth::ListModel(this); | ||||
|     ui->packView->setModel(listModel); | ||||
|  | ||||
|   ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( | ||||
|       Qt::ScrollBarAsNeeded); | ||||
|   ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); | ||||
|     // index is used to set the sorting with the modrinth api | ||||
|     ui->sortByBox->addItem(tr("Sort by Relevence")); | ||||
|     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")); | ||||
|  | ||||
|   // index is used to set the sorting with the modrinth api | ||||
|   ui->sortByBox->addItem(tr("Sort by Relevence")); | ||||
|   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")); | ||||
|  | ||||
|   connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, | ||||
|           SLOT(triggerSearch())); | ||||
|   connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, | ||||
|           this, &ModrinthPage::onSelectionChanged); | ||||
|   connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, | ||||
|           &ModrinthPage::onVersionSelectionChanged); | ||||
|   connect(ui->modSelectionButton, &QPushButton::clicked, this, | ||||
|           &ModrinthPage::onModSelected); | ||||
| } | ||||
|  | ||||
| ModrinthPage::~ModrinthPage() { delete ui; } | ||||
|  | ||||
| bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { | ||||
|   if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { | ||||
|     QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); | ||||
|     if (keyEvent->key() == Qt::Key_Return) { | ||||
|       triggerSearch(); | ||||
|       keyEvent->accept(); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|   return QWidget::eventFilter(watched, event); | ||||
|     // 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, &ModrinthPage::onSelectionChanged); | ||||
|     connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); | ||||
|     connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); | ||||
| } | ||||
|  | ||||
| bool ModrinthPage::shouldDisplay() const { return true; } | ||||
|  | ||||
| void ModrinthPage::openedImpl() { | ||||
|   updateSelectionButton(); | ||||
|   triggerSearch(); | ||||
| } | ||||
|  | ||||
| void ModrinthPage::triggerSearch() { | ||||
|   listModel->searchWithTerm(ui->searchEdit->text(), | ||||
|                             ui->sortByBox->currentIndex()); | ||||
| } | ||||
|  | ||||
| void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { | ||||
|   ui->versionSelectionBox->clear(); | ||||
|  | ||||
|   if (!first.isValid()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); | ||||
|   QString text = ""; | ||||
|   QString name = current.name; | ||||
|  | ||||
|   if (current.websiteUrl.isEmpty()) | ||||
|     text = name; | ||||
|   else | ||||
|     text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; | ||||
|   text += "<br>" + tr(" by ") + "<a href=\"" + current.authors[0].url + "\">" + | ||||
|           current.authors[0].name + "</a><br><br>"; | ||||
|   ui->packDescription->setHtml(text + current.description); | ||||
|  | ||||
|   if (!current.versionsLoaded) { | ||||
|     qDebug() << "Loading Modrinth mod versions"; | ||||
|  | ||||
|     ui->modSelectionButton->setText(tr("Loading versions...")); | ||||
|     ui->modSelectionButton->setEnabled(false); | ||||
|  | ||||
|     auto netJob = | ||||
|         new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), | ||||
|                    APPLICATION->network()); | ||||
|     auto response = new QByteArray(); | ||||
|     QString addonId = current.addonId.toString(); | ||||
|     netJob->addNetAction(Net::Download::makeByteArray( | ||||
|         QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), | ||||
|         response)); | ||||
|  | ||||
|     QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { | ||||
|         if(addonId != current.addonId){ | ||||
|             return; | ||||
|         } | ||||
|       QJsonParseError parse_error; | ||||
|       QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); | ||||
|       if (parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Modrinth at " | ||||
|                    << parse_error.offset | ||||
| void ModrinthPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) | ||||
| { | ||||
|     if (addonId != current.addonId) { return; } | ||||
|     QJsonParseError parse_error; | ||||
|     QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); | ||||
|     if (parse_error.error != QJsonParseError::NoError) { | ||||
|         qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset | ||||
|                    << " reason: " << parse_error.errorString(); | ||||
|         qWarning() << *response; | ||||
|         return; | ||||
|       } | ||||
|       QJsonArray arr = doc.array(); | ||||
|       try { | ||||
|         Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), | ||||
|                                           m_instance); | ||||
|       } catch (const JSONValidationError &e) { | ||||
|     } | ||||
|     QJsonArray arr = doc.array(); | ||||
|     try { | ||||
|         Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); | ||||
|     } catch (const JSONValidationError& e) { | ||||
|         qDebug() << *response; | ||||
|         qWarning() << "Error while reading Modrinth mod version: " << e.cause(); | ||||
|       } | ||||
|       auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); | ||||
|       QString mcVersion = packProfile->getComponentVersion("net.minecraft"); | ||||
|       QString loaderString = | ||||
|           (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) | ||||
|               ? "fabric" | ||||
|               : "forge"; | ||||
|       for (int i = 0; i < current.versions.size(); i++) { | ||||
|         auto version = current.versions[i]; | ||||
|         if (!version.mcVersion.contains(mcVersion) || | ||||
|             !version.loaders.contains(loaderString)) { | ||||
|           continue; | ||||
|         } | ||||
|         ui->versionSelectionBox->addItem(version.version, QVariant(i)); | ||||
|       } | ||||
|       if (ui->versionSelectionBox->count() == 0) { | ||||
|         ui->versionSelectionBox->addItem(tr("No Valid Version found !"), | ||||
|                                          QVariant(-1)); | ||||
|       } | ||||
|  | ||||
|       ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); | ||||
|       updateSelectionButton(); | ||||
|     }); | ||||
|  | ||||
|     QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { | ||||
|       netJob->deleteLater(); | ||||
|       delete response; | ||||
|     }); | ||||
|  | ||||
|     netJob->start(); | ||||
|   } else { | ||||
|     } | ||||
|     auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); | ||||
|     QString mcVersion = packProfile->getComponentVersion("net.minecraft"); | ||||
|     QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; | ||||
|     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)); | ||||
|         auto version = current.versions[i]; | ||||
|         if (!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)) { continue; } | ||||
|         ui->versionSelectionBox->addItem(version.version, QVariant(i)); | ||||
|     } | ||||
|     if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } | ||||
|  | ||||
|     ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); | ||||
|     updateSelectionButton(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ModrinthPage::updateSelectionButton() { | ||||
|   if (!isOpened || selectedVersion < 0) { | ||||
|     ui->modSelectionButton->setEnabled(false); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ui->modSelectionButton->setEnabled(true); | ||||
|   auto &version = current.versions[selectedVersion]; | ||||
|   if (!dialog->isModSelected(current.name, version.fileName)) { | ||||
|     ui->modSelectionButton->setText(tr("Select mod for download")); | ||||
|   } else { | ||||
|     ui->modSelectionButton->setText(tr("Deselect mod for download")); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ModrinthPage::onVersionSelectionChanged(QString data) { | ||||
|   if (data.isNull() || data.isEmpty()) { | ||||
|     selectedVersion = -1; | ||||
|     return; | ||||
|   } | ||||
|   selectedVersion = ui->versionSelectionBox->currentData().toInt(); | ||||
|   updateSelectionButton(); | ||||
| } | ||||
|  | ||||
| void ModrinthPage::onModSelected() { | ||||
|   auto &version = current.versions[selectedVersion]; | ||||
|   if (dialog->isModSelected(current.name, version.fileName)) { | ||||
|     dialog->removeSelectedMod(current.name); | ||||
|   } else { | ||||
|     dialog->addSelectedMod(current.name, | ||||
|                            new ModDownloadTask(version.downloadUrl, | ||||
|                                                version.fileName, dialog->mods)); | ||||
|   } | ||||
|  | ||||
|   updateSelectionButton(); | ||||
| } | ||||
|   | ||||
| @@ -1,68 +1,24 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QWidget> | ||||
| #include "ui/pages/modplatform/ModPage.h" | ||||
|  | ||||
| #include "ui/pages/BasePage.h" | ||||
| #include <Application.h> | ||||
| #include "tasks/Task.h" | ||||
| #include "modplatform/modrinth/ModrinthPackIndex.h" | ||||
|  | ||||
| namespace Ui | ||||
| { | ||||
| class ModrinthPage; | ||||
| } | ||||
|  | ||||
| class ModDownloadDialog; | ||||
|  | ||||
| namespace Modrinth { | ||||
|     class ListModel; | ||||
| } | ||||
|  | ||||
| class ModrinthPage : public QWidget, public BasePage | ||||
| { | ||||
| class ModrinthPage : public ModPage { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); | ||||
|     virtual ~ModrinthPage(); | ||||
|     virtual QString displayName() const override | ||||
|     { | ||||
|         return tr("Modrinth"); | ||||
|     } | ||||
|     virtual QIcon icon() const override | ||||
|     { | ||||
|         return APPLICATION->getThemedIcon("modrinth"); | ||||
|     } | ||||
|     virtual QString id() const override | ||||
|     { | ||||
|         return "modrinth"; | ||||
|     } | ||||
|     virtual QString helpPage() const override | ||||
|     { | ||||
|         return "Modrinth-platform"; | ||||
|     } | ||||
|     virtual bool shouldDisplay() const override; | ||||
|    public: | ||||
|     explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); | ||||
|     virtual ~ModrinthPage() = default; | ||||
|  | ||||
|     void openedImpl() override; | ||||
|     inline QString displayName() const override { return tr("Modrinth"); } | ||||
|     inline QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } | ||||
|     inline QString id() const override { return "modrinth"; } | ||||
|     inline QString helpPage() const override { return "Modrinth-platform"; } | ||||
|  | ||||
|     bool eventFilter(QObject * watched, QEvent * event) override; | ||||
|     inline QString debugName() const override { return tr("Modrinth"); } | ||||
|     inline QString metaEntryBase() const override { return "ModrinthPacks"; }; | ||||
|  | ||||
|     BaseInstance *m_instance; | ||||
|     bool shouldDisplay() const override; | ||||
|  | ||||
| private: | ||||
|     void updateSelectionButton(); | ||||
|  | ||||
| private slots: | ||||
|     void triggerSearch(); | ||||
|     void onSelectionChanged(QModelIndex first, QModelIndex second); | ||||
|     void onVersionSelectionChanged(QString data); | ||||
|     void onModSelected(); | ||||
|  | ||||
| private: | ||||
|     Ui::ModrinthPage *ui = nullptr; | ||||
|     ModDownloadDialog* dialog = nullptr; | ||||
|     Modrinth::ListModel* listModel = nullptr; | ||||
|     ModPlatform::IndexedPack current; | ||||
|  | ||||
|     int selectedVersion = -1; | ||||
|    private: | ||||
|     void onModVersionSucceed(ModPage*, QByteArray*, QString) override; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user