aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortimoreo <timo.oreo34@gmail.com>2022-01-14 09:43:42 +0100
committertimoreo <timo.oreo34@gmail.com>2022-01-14 09:56:27 +0100
commit4e9039be2d3bc0357e6bfe577c5f2add8313f8d6 (patch)
tree58ae809b2914e4eda300421ec4c7afbd3bc64067
parentb07853c9ef6063fe0d6d9065acdd598841c62a14 (diff)
downloadPrismLauncher-4e9039be2d3bc0357e6bfe577c5f2add8313f8d6.tar.gz
PrismLauncher-4e9039be2d3bc0357e6bfe577c5f2add8313f8d6.tar.bz2
PrismLauncher-4e9039be2d3bc0357e6bfe577c5f2add8313f8d6.zip
Start of mod downloading
-rw-r--r--launcher/CMakeLists.txt18
-rw-r--r--launcher/ModDownloadTask.cpp25
-rw-r--r--launcher/ModDownloadTask.h36
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp51
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h46
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp110
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h65
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp29
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h1
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.ui9
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp256
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h77
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp180
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.h80
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui90
15 files changed, 1073 insertions, 0 deletions
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index b5c52afa..12274b70 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -37,6 +37,10 @@ set(CORE_SOURCES
InstanceImportTask.h
InstanceImportTask.cpp
+ # Mod downloading task
+ ModDownloadTask.h
+ ModDownloadTask.cpp
+
# Use tracking separate from memory management
Usable.h
@@ -512,6 +516,11 @@ set(FLAME_SOURCES
modplatform/flame/FileResolvingTask.cpp
)
+set(MODRINTH_SOURCES
+ modplatform/modrinth/ModrinthPackIndex.cpp
+ modplatform/modrinth/ModrinthPackIndex.h
+)
+
set(MODPACKSCH_SOURCES
modplatform/modpacksch/FTBPackInstallTask.h
modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -566,6 +575,7 @@ set(LOGIC_SOURCES
${ICONS_SOURCES}
${FTB_SOURCES}
${FLAME_SOURCES}
+ ${MODRINTH_SOURCES}
${MODPACKSCH_SOURCES}
${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES}
@@ -748,6 +758,11 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
+ ui/pages/modplatform/modrinth/ModrinthModel.cpp
+ ui/pages/modplatform/modrinth/ModrinthModel.h
+ ui/pages/modplatform/modrinth/ModrinthPage.cpp
+ ui/pages/modplatform/modrinth/ModrinthPage.h
+
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
ui/dialogs/AboutDialog.h
@@ -785,6 +800,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
+ ui/dialogs/ModDownloadDialog.cpp
+ ui/dialogs/ModDownloadDialog.h
# GUI - widgets
@@ -865,6 +882,7 @@ qt5_wrap_ui(LAUNCHER_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
diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp
new file mode 100644
index 00000000..22955470
--- /dev/null
+++ b/launcher/ModDownloadTask.cpp
@@ -0,0 +1,25 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModDownloadTask.h"
+
+ModDownloadTask::ModDownloadTask(const QUrl sourceUrl) {
+ m_sourceUrl = sourceUrl;
+}
+
+void ModDownloadTask::executeTask() {
+ //TODO actually install the mod
+ emitSucceeded();
+}
diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h
new file mode 100644
index 00000000..067bd91c
--- /dev/null
+++ b/launcher/ModDownloadTask.h
@@ -0,0 +1,36 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include "QObjectPtr.h"
+#include "tasks/Task.h"
+#include <QUrl>
+
+
+class ModDownloadTask : public Task {
+ Q_OBJECT
+public:
+ explicit ModDownloadTask(const QUrl sourceUrl);
+
+protected:
+ //! Entry point for tasks.
+ void executeTask() override;
+
+private:
+ QUrl m_sourceUrl;
+};
+
+
+
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
new file mode 100644
index 00000000..fa421ab2
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -0,0 +1,51 @@
+#include <QObject>
+#include "ModrinthPackIndex.h"
+
+#include "Json.h"
+#include "net/NetJob.h"
+
+void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj)
+{
+ pack.addonId = Json::requireString(obj, "mod_id");
+ pack.name = Json::requireString(obj, "title");
+ pack.websiteUrl = Json::ensureString(obj, "page_url", "");
+ pack.description = Json::ensureString(obj, "description", "");
+
+ pack.logoUrl = Json::requireString(obj, "icon_url");
+ pack.logoName = "logoName";
+
+ Modrinth::ModpackAuthor packAuthor;
+ packAuthor.name = Json::requireString(obj, "author");
+ packAuthor.url = Json::requireString(obj, "author_url");
+ pack.authors.append(packAuthor); //TODO delete this ? only one author ever exists
+}
+
+void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network)
+{
+ QVector<Modrinth::IndexedVersion> unsortedVersions;
+ for(auto versionIter: arr) {
+ auto obj = versionIter.toObject();
+ Modrinth::IndexedVersion file;
+ file.addonId = Json::requireString(obj,"mod_id") ;
+ file.fileId = Json::requireString(obj, "id");
+ file.date = Json::requireString(obj, "date_published");
+ auto versionArray = Json::requireArray(obj, "game_versions");
+ if (versionArray.empty()) {
+ continue;
+ }
+ // pick the latest version supported
+ file.mcVersion = versionArray[0].toString();
+ file.version = Json::requireString(obj, "name");
+ //TODO show all the files ?
+ file.downloadUrl = Json::requireString(Json::requireArray(obj, "files")[0].toObject(),"url");
+ unsortedVersions.append(file);
+ }
+ auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
+ {
+ //dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ pack.versions = unsortedVersions;
+ pack.versionsLoaded = true;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
new file mode 100644
index 00000000..afc31ff2
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVector>
+#include <QNetworkAccessManager>
+#include <QObjectPtr.h>
+#include "net/NetJob.h"
+
+namespace Modrinth {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct IndexedVersion {
+ QString addonId;
+ QString fileId;
+ QString version;
+ QString mcVersion;
+ QString downloadUrl;
+ QString date;
+};
+
+struct IndexedPack
+{
+ QString addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+};
+
+void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr, const shared_qobject_ptr<QNetworkAccessManager>& network);
+void versionJobFinished();
+}
+
+Q_DECLARE_METATYPE(Modrinth::IndexedPack)
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
new file mode 100644
index 00000000..a40980ef
--- /dev/null
+++ b/launcher/ui/dialogs/ModDownloadDialog.cpp
@@ -0,0 +1,110 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModDownloadDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <InstanceList.h>
+
+#include "ProgressDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QValidator>
+#include <QDialogButtonBox>
+
+#include "ui/widgets/PageContainer.h"
+#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
+#include "ModDownloadTask.h"
+
+
+ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget *parent)
+ : QDialog(parent)
+{
+ setObjectName(QStringLiteral("ModDownloadDialog"));
+ resize(400, 347);
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ setWindowIcon(APPLICATION->getThemedIcon("new"));
+ // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below.
+ m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ m_verticalLayout->addWidget(m_container);
+
+ m_container->addButtons(m_buttons);
+
+ // Bonk Qt over its stupid head and make sure it understands which button is the default one...
+ // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::accept);
+
+ auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+ connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject);
+
+ auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+ QMetaObject::connectSlotsByName(this);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle("Download mods");
+}
+
+QString ModDownloadDialog::dialogTitle()
+{
+ return tr("Download mods");
+}
+
+void ModDownloadDialog::reject()
+{
+ QDialog::reject();
+}
+
+void ModDownloadDialog::accept()
+{
+ QDialog::accept();
+}
+
+QList<BasePage *> ModDownloadDialog::getPages()
+{
+ modrinthPage = new ModrinthPage(this);
+ return
+ {
+ modrinthPage
+ };
+}
+
+void ModDownloadDialog::setSuggestedMod(const QString& name, ModDownloadTask* task)
+{
+ modTask.reset(task);
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(task);
+}
+
+ModDownloadDialog::~ModDownloadDialog()
+{
+}
+
+ModDownloadTask *ModDownloadDialog::getTask() {
+ return modTask.release();
+}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h
new file mode 100644
index 00000000..6ce6ff61
--- /dev/null
+++ b/launcher/ui/dialogs/ModDownloadDialog.h
@@ -0,0 +1,65 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QVBoxLayout>
+
+#include "BaseVersion.h"
+#include "ui/pages/BasePageProvider.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "ModDownloadTask.h"
+
+namespace Ui
+{
+class ModDownloadDialog;
+}
+
+class PageContainer;
+class QDialogButtonBox;
+class ModrinthPage;
+
+class ModDownloadDialog : public QDialog, public BasePageProvider
+{
+ Q_OBJECT
+
+public:
+ explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget *parent = nullptr);
+ ~ModDownloadDialog();
+
+ QString dialogTitle() override;
+ QList<BasePage *> getPages() override;
+
+ void setSuggestedMod(const QString & name = QString(), ModDownloadTask * task = nullptr);
+
+ ModDownloadTask * getTask();
+
+public slots:
+ void accept() override;
+ void reject() override;
+
+//private slots:
+
+private:
+ Ui::ModDownloadDialog *ui = nullptr;
+ PageContainer * m_container = nullptr;
+ QDialogButtonBox * m_buttons = nullptr;
+ QVBoxLayout *m_verticalLayout = nullptr;
+
+
+ ModrinthPage *modrinthPage = nullptr;
+ std::unique_ptr<ModDownloadTask> modTask;
+};
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index e63b1434..d2f5dead 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -26,6 +26,7 @@
#include "Application.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/GuiUtil.h"
#include "DesktopServices.h"
@@ -36,6 +37,7 @@
#include "minecraft/PackProfile.h"
#include "Version.h"
+#include "ui/dialogs/ProgressDialog.h"
namespace {
// FIXME: wasteful
@@ -342,6 +344,33 @@ void ModFolderPage::on_actionRemove_triggered()
m_mods->deleteMods(selection.indexes());
}
+void ModFolderPage::on_actionInstall_mods_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ ModDownloadDialog mdownload(m_mods, this);
+ mdownload.exec();
+ ModDownloadTask * task = mdownload.getTask();
+ if(task){
+ connect(task, &Task::failed, [this](QString reason)
+ {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
+ });
+ connect(task, &Task::succeeded, [this, task]()
+ {
+ QStringList warnings = task->warnings();
+ if(warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+ }
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(task);
+ }
+}
+
void ModFolderPage::on_actionView_configs_triggered()
{
DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 8ef7559b..fbda3cd8 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -102,6 +102,7 @@ slots:
void on_actionRemove_triggered();
void on_actionEnable_triggered();
void on_actionDisable_triggered();
+ void on_actionInstall_mods_triggered();
void on_actionView_Folder_triggered();
void on_actionView_configs_triggered();
void ShowContextMenu(const QPoint &pos);
diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ModFolderPage.ui
index 0fb51e84..b5b4c9b2 100644
--- a/launcher/ui/pages/instance/ModFolderPage.ui
+++ b/launcher/ui/pages/instance/ModFolderPage.ui
@@ -88,6 +88,7 @@
<addaction name="actionRemove"/>
<addaction name="actionEnable"/>
<addaction name="actionDisable"/>
+ <addaction name="actionInstall_mods"/>
<addaction name="actionView_configs"/>
<addaction name="actionView_Folder"/>
</widget>
@@ -136,6 +137,14 @@
<string>View &amp;Folder</string>
</property>
</action>
+ <action name="actionInstall_mods">
+ <property name="text">
+ <string>Install mods</string>
+ </property>
+ <property name="toolTip">
+ <string>Install mods from Modrinth or Curseforge</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
new file mode 100644
index 00000000..3bc70e34
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -0,0 +1,256 @@
+#include "ModrinthModel.h"
+#include "Application.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+
+namespace Modrinth {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(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);
+ }
+
+ 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)));
+ NetJob *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]
+ {
+ emit logoLoaded(logo, QIcon(fullPath));
+ if(waitingCallbacks.contains(logo))
+ {
+ waitingCallbacks.value(logo)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ 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[4]{"relevance","downloads","updated","newest"};
+
+void ListModel::performPaginatedSearch()
+{
+ NetJob *netJob = new NetJob("Modrinth::Search", APPLICATION->network());
+ auto searchUrl = QString(
+ "https://api.modrinth.com/api/v1/mod?"
+ "offset=%1&"
+ "limit=25&"
+ "query=%2&"
+ "index=%3"
+ ).arg(nextSearchOffset).arg(currentSearchTerm).arg(sorts[currentSort]);
+ 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, 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 Modrinth::ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+
+ 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;
+ }
+
+ QList<Modrinth::IndexedPack> newList;
+ auto packs = doc.object().value("hits").toArray();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ Modrinth::IndexedPack pack;
+ try
+ {
+ Modrinth::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading mod from Modrinth: " << e.cause();
+ continue;
+ }
+ }
+ if(packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Modrinth::ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
new file mode 100644
index 00000000..7bd06f6a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <RWStorage.h>
+
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QThreadPool>
+#include <QIcon>
+#include <QStyledItemDelegate>
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+#include <functional>
+#include <net/NetJob.h>
+
+#include <modplatform/flame/FlamePackIndex.h>
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+
+namespace Modrinth {
+
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *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<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;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
new file mode 100644
index 00000000..ea1800d2
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -0,0 +1,180 @@
+#include "ModrinthPage.h"
+#include "ui_ModrinthPage.h"
+
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "Json.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+#include "InstanceImportTask.h"
+#include "ModrinthModel.h"
+#include "ModDownloadTask.h"
+
+ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, QWidget *parent)
+ : QWidget(parent), 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);
+
+ 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 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);
+}
+
+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);
+}
+
+bool ModrinthPage::shouldDisplay() const
+{
+ return true;
+}
+
+void ModrinthPage::openedImpl()
+{
+ suggestCurrent();
+ 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())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedMod();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<Modrinth::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 = [](Modrinth::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() << "Loading Modrinth mod versions";
+ NetJob *netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network());
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ QString addonId = current.addonId;
+ addonId.remove(0,6);
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/api/v1/mod/%1/version").arg(addonId), response.get()));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response]
+ {
+ 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());
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading Modrinth mod version: " << e.cause();
+ }
+
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ });
+ netJob->start();
+ }
+ else
+ {
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ }
+}
+
+void ModrinthPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedMod();
+ return;
+ }
+
+ dialog->setSuggestedMod(current.name, new ModDownloadTask(selectedVersion));
+}
+
+void ModrinthPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toString();
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
new file mode 100644
index 00000000..924bc6ce
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#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
+{
+ Q_OBJECT
+
+public:
+ explicit ModrinthPage(ModDownloadDialog* dialog, QWidget *parent = 0);
+ virtual ~ModrinthPage();
+ virtual QString displayName() const override
+ {
+ return tr("Modrinth");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("flame");
+ }
+ virtual QString id() const override
+ {
+ return "modrinth";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Modrinth-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::ModrinthPage *ui = nullptr;
+ ModDownloadDialog* dialog = nullptr;
+ Modrinth::ListModel* listModel = nullptr;
+ Modrinth::IndexedPack current;
+
+ QString selectedVersion;
+};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
new file mode 100644
index 00000000..6d183de5
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ModrinthPage</class>
+ <widget class="QWidget" name="ModrinthPage">
+ <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="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QListView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </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="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" 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="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</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>