aboutsummaryrefslogtreecommitdiff
path: root/application/pages/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'application/pages/modplatform')
-rw-r--r--application/pages/modplatform/ImportPage.cpp7
-rw-r--r--application/pages/modplatform/ImportPage.h2
-rw-r--r--application/pages/modplatform/TechnicPage.cpp26
-rw-r--r--application/pages/modplatform/TechnicPage.ui113
-rw-r--r--application/pages/modplatform/TwitchPage.cpp26
-rw-r--r--application/pages/modplatform/TwitchPage.ui35
-rw-r--r--application/pages/modplatform/VanillaPage.cpp16
-rw-r--r--application/pages/modplatform/VanillaPage.h2
-rw-r--r--application/pages/modplatform/VanillaPage.ui20
-rw-r--r--application/pages/modplatform/atlauncher/AtlFilterModel.cpp81
-rw-r--r--application/pages/modplatform/atlauncher/AtlFilterModel.h34
-rw-r--r--application/pages/modplatform/atlauncher/AtlListModel.cpp194
-rw-r--r--application/pages/modplatform/atlauncher/AtlListModel.h52
-rw-r--r--application/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp209
-rw-r--r--application/pages/modplatform/atlauncher/AtlOptionalModDialog.h66
-rw-r--r--application/pages/modplatform/atlauncher/AtlOptionalModDialog.ui65
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.cpp175
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.h87
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.ui97
-rw-r--r--application/pages/modplatform/flame/FlameModel.cpp259
-rw-r--r--application/pages/modplatform/flame/FlameModel.h76
-rw-r--r--application/pages/modplatform/flame/FlamePage.cpp185
-rw-r--r--application/pages/modplatform/flame/FlamePage.h (renamed from application/pages/modplatform/TwitchPage.h)39
-rw-r--r--application/pages/modplatform/flame/FlamePage.ui90
-rw-r--r--application/pages/modplatform/ftb/FtbFilterModel.cpp64
-rw-r--r--application/pages/modplatform/ftb/FtbFilterModel.h33
-rw-r--r--application/pages/modplatform/ftb/FtbListModel.cpp304
-rw-r--r--application/pages/modplatform/ftb/FtbListModel.h69
-rw-r--r--application/pages/modplatform/ftb/FtbPage.cpp145
-rw-r--r--application/pages/modplatform/ftb/FtbPage.h80
-rw-r--r--application/pages/modplatform/ftb/FtbPage.ui84
-rw-r--r--application/pages/modplatform/legacy_ftb/ListModel.cpp (renamed from application/pages/modplatform/FtbListModel.cpp)70
-rw-r--r--application/pages/modplatform/legacy_ftb/ListModel.h (renamed from application/pages/modplatform/FtbListModel.h)31
-rw-r--r--application/pages/modplatform/legacy_ftb/Page.cpp (renamed from application/pages/modplatform/FTBPage.cpp)185
-rw-r--r--application/pages/modplatform/legacy_ftb/Page.h (renamed from application/pages/modplatform/FTBPage.h)61
-rw-r--r--application/pages/modplatform/legacy_ftb/Page.ui (renamed from application/pages/modplatform/FTBPage.ui)13
-rw-r--r--application/pages/modplatform/technic/TechnicData.h42
-rw-r--r--application/pages/modplatform/technic/TechnicModel.cpp238
-rw-r--r--application/pages/modplatform/technic/TechnicModel.h70
-rw-r--r--application/pages/modplatform/technic/TechnicPage.cpp198
-rw-r--r--application/pages/modplatform/technic/TechnicPage.h (renamed from application/pages/modplatform/TechnicPage.h)19
-rw-r--r--application/pages/modplatform/technic/TechnicPage.ui95
42 files changed, 3377 insertions, 380 deletions
diff --git a/application/pages/modplatform/ImportPage.cpp b/application/pages/modplatform/ImportPage.cpp
index 3cd7c2cf..c2369bdc 100644
--- a/application/pages/modplatform/ImportPage.cpp
+++ b/application/pages/modplatform/ImportPage.cpp
@@ -71,13 +71,20 @@ void ImportPage::updateState()
{
QFileInfo fi(url.fileName());
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
}
}
else
{
+ if(input.endsWith("?client=y")) {
+ input.chop(9);
+ input.append("/file");
+ url = QUrl::fromUserInput(input);
+ }
// hook, line and sinker.
QFileInfo fi(url.fileName());
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
}
}
else
diff --git a/application/pages/modplatform/ImportPage.h b/application/pages/modplatform/ImportPage.h
index 120e7b56..67e3c201 100644
--- a/application/pages/modplatform/ImportPage.h
+++ b/application/pages/modplatform/ImportPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* 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.
diff --git a/application/pages/modplatform/TechnicPage.cpp b/application/pages/modplatform/TechnicPage.cpp
deleted file mode 100644
index 2f95bec8..00000000
--- a/application/pages/modplatform/TechnicPage.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-#include "TechnicPage.h"
-#include "ui_TechnicPage.h"
-
-#include "MultiMC.h"
-#include "dialogs/NewInstanceDialog.h"
-
-TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
- : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
-{
- ui->setupUi(this);
-}
-
-TechnicPage::~TechnicPage()
-{
- delete ui;
-}
-
-bool TechnicPage::shouldDisplay() const
-{
- return true;
-}
-
-void TechnicPage::openedImpl()
-{
- dialog->setSuggestedPack();
-}
diff --git a/application/pages/modplatform/TechnicPage.ui b/application/pages/modplatform/TechnicPage.ui
deleted file mode 100644
index 702427b5..00000000
--- a/application/pages/modplatform/TechnicPage.ui
+++ /dev/null
@@ -1,113 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>TechnicPage</class>
- <widget class="QWidget" name="TechnicPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>546</width>
- <height>405</height>
- </rect>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="label">
- <property name="font">
- <font>
- <pointsize>40</pointsize>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">color:#ffc000</string>
- </property>
- <property name="text">
- <string notr="true">UNDER</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../resources/assets/assets.qrc">:/assets/underconstruction</pixmap>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="font">
- <font>
- <pointsize>40</pointsize>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">color:#7ca32b</string>
- </property>
- <property name="text">
- <string notr="true">CONSTRUCTION</string>
- </property>
- <property name="textFormat">
- <enum>Qt::PlainText</enum>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- <resources>
- <include location="../../resources/assets/assets.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/application/pages/modplatform/TwitchPage.cpp b/application/pages/modplatform/TwitchPage.cpp
deleted file mode 100644
index a984c01c..00000000
--- a/application/pages/modplatform/TwitchPage.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-#include "TwitchPage.h"
-#include "ui_TwitchPage.h"
-
-#include "MultiMC.h"
-#include "dialogs/NewInstanceDialog.h"
-
-TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
- : QWidget(parent), ui(new Ui::TwitchPage), dialog(dialog)
-{
- ui->setupUi(this);
-}
-
-TwitchPage::~TwitchPage()
-{
- delete ui;
-}
-
-bool TwitchPage::shouldDisplay() const
-{
- return false;
-}
-
-void TwitchPage::openedImpl()
-{
- dialog->setSuggestedPack();
-}
diff --git a/application/pages/modplatform/TwitchPage.ui b/application/pages/modplatform/TwitchPage.ui
deleted file mode 100644
index 0930f541..00000000
--- a/application/pages/modplatform/TwitchPage.ui
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>TwitchPage</class>
- <widget class="QWidget" name="TwitchPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>546</width>
- <height>405</height>
- </rect>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="font">
- <font>
- <pointsize>40</pointsize>
- </font>
- </property>
- <property name="pixmap">
- <pixmap resource="../../resources/assets/assets.qrc">:/assets/deadglitch</pixmap>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <resources>
- <include location="../../resources/assets/assets.qrc"/>
- </resources>
- <connections/>
-</ui>
diff --git a/application/pages/modplatform/VanillaPage.cpp b/application/pages/modplatform/VanillaPage.cpp
index 77362fcc..02638315 100644
--- a/application/pages/modplatform/VanillaPage.cpp
+++ b/application/pages/modplatform/VanillaPage.cpp
@@ -23,6 +23,7 @@ VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
}
@@ -58,6 +59,8 @@ void VanillaPage::filterChanged()
out << "(old_snapshot)";
if(ui->releaseFilter->isChecked())
out << "(release)";
+ if(ui->experimentsFilter->isChecked())
+ out << "(experiment)";
auto regexp = out.join('|');
ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
}
@@ -79,10 +82,19 @@ BaseVersionPtr VanillaPage::selectedVersion() const
void VanillaPage::suggestCurrent()
{
- if(m_selectedVersion && isOpened)
+ if (!isOpened)
{
- dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion));
+ return;
}
+
+ if(!m_selectedVersion)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion));
+ dialog->setSuggestedIcon("default");
}
void VanillaPage::setSelectedVersion(BaseVersionPtr version)
diff --git a/application/pages/modplatform/VanillaPage.h b/application/pages/modplatform/VanillaPage.h
index 2b292b01..af6fd392 100644
--- a/application/pages/modplatform/VanillaPage.h
+++ b/application/pages/modplatform/VanillaPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* 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.
diff --git a/application/pages/modplatform/VanillaPage.ui b/application/pages/modplatform/VanillaPage.ui
index ae9cab47..47effc86 100644
--- a/application/pages/modplatform/VanillaPage.ui
+++ b/application/pages/modplatform/VanillaPage.ui
@@ -99,6 +99,16 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="experimentsFilter">
+ <property name="text">
+ <string>Experiments</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -144,6 +154,16 @@
<container>1</container>
</customwidget>
</customwidgets>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>releaseFilter</tabstop>
+ <tabstop>snapshotFilter</tabstop>
+ <tabstop>oldSnapshotFilter</tabstop>
+ <tabstop>betaFilter</tabstop>
+ <tabstop>alphaFilter</tabstop>
+ <tabstop>experimentsFilter</tabstop>
+ <tabstop>refreshBtn</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp
new file mode 100644
index 00000000..b5d8f22b
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp
@@ -0,0 +1,81 @@
+#include "AtlFilterModel.h"
+
+#include <QDebug>
+
+#include <modplatform/atlauncher/ATLPackIndex.h>
+#include <Version.h>
+#include <MMCStrings.h>
+
+namespace Atl {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPopularity;
+ sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+ sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
+
+ searchTerm = "";
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+void FilterModel::setSearchTerm(const QString term)
+{
+ searchTerm = term.trimmed();
+ invalidate();
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ if (searchTerm.isEmpty()) {
+ return true;
+ }
+
+ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ return pack.name.contains(searchTerm, Qt::CaseInsensitive);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ if (currentSorting == ByPopularity) {
+ return leftPack.position > rightPack.position;
+ }
+ else if (currentSorting == ByGameVersion) {
+ Version lv(leftPack.versions.at(0).minecraft);
+ Version rv(rightPack.versions.at(0).minecraft);
+ return lv < rv;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.h b/application/pages/modplatform/atlauncher/AtlFilterModel.h
new file mode 100644
index 00000000..bd72ad91
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlFilterModel.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Atl {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPopularity,
+ ByGameVersion,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+ void setSearchTerm(QString term);
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+ QString searchTerm;
+
+};
+
+}
diff --git a/application/pages/modplatform/atlauncher/AtlListModel.cpp b/application/pages/modplatform/atlauncher/AtlListModel.cpp
new file mode 100644
index 00000000..f3be6198
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -0,0 +1,194 @@
+#include "AtlListModel.h"
+
+#include <BuildConfig.h>
+#include <MultiMC.h>
+#include <Env.h>
+#include <Json.h>
+
+namespace Atl {
+
+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);
+ }
+
+ ATLauncher::IndexedPack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.safeName))
+ {
+ return (m_logoMap.value(pack.safeName));
+ }
+ auto icon = MMC->getThemedIcon("atlauncher-placeholder");
+
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
+ ((ListModel *)this)->requestLogo(pack.safeName, url);
+
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::request()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ auto *netJob = new NetJob("Atl::Request");
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json");
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+}
+
+void ListModel::requestFinished()
+{
+ 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 ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<ATLauncher::IndexedPack> newList;
+
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ATLauncher::IndexedPack pack;
+
+ try {
+ ATLauncher::loadIndexedPack(pack, packObj);
+ }
+ catch (const JSONValidationError &e) {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause();
+ return;
+ }
+
+ // ignore packs without a published version
+ if(pack.versions.length() == 0) continue;
+ // only display public packs (for now)
+ if(pack.type != ATLauncher::PackType::Public) continue;
+ // ignore "system" packs (Vanilla, Vanilla with Forge, etc)
+ if(pack.system) continue;
+
+ newList.append(pack);
+ }
+
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ListModel::requestFailed(QString reason)
+{
+ jobPtr.reset();
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+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].safeName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::requestLogo(QString file, QString url)
+{
+ if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath]
+ {
+ emit logoLoaded(file, QIcon(fullPath));
+ if(waitingCallbacks.contains(file))
+ {
+ waitingCallbacks.value(file)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, file]
+ {
+ emit logoFailed(file);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(file);
+}
+
+}
diff --git a/application/pages/modplatform/atlauncher/AtlListModel.h b/application/pages/modplatform/atlauncher/AtlListModel.h
new file mode 100644
index 00000000..2d30a64e
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlListModel.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "net/NetJob.h"
+#include <QIcon>
+#include <modplatform/atlauncher/ATLPackIndex.h>
+
+namespace Atl {
+
+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;
+
+ void request();
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+
+private slots:
+ void requestFinished();
+ void requestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ATLauncher::IndexedPack> modpacks;
+
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/application/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
new file mode 100644
index 00000000..14bbd18b
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
@@ -0,0 +1,209 @@
+#include "AtlOptionalModDialog.h"
+#include "ui_AtlOptionalModDialog.h"
+
+AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QAbstractListModel(parent), m_mods(mods) {
+
+ // fill mod index
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_index[mod.name] = i;
+ }
+ // set initial state
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_selection[mod.name] = false;
+ setMod(mod, i, mod.selected, false);
+ }
+}
+
+QVector<QString> AtlOptionalModListModel::getResult() {
+ QVector<QString> result;
+
+ for (const auto& mod : m_mods) {
+ if (m_selection[mod.name]) {
+ result.push_back(mod.name);
+ }
+ }
+
+ return result;
+}
+
+int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const {
+ return m_mods.size();
+}
+
+int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const {
+ // Enabled, Name, Description
+ return 3;
+}
+
+QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ if (role == Qt::DisplayRole) {
+ if (index.column() == NameColumn) {
+ return mod.name;
+ }
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::ToolTipRole) {
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::CheckStateRole) {
+ if (index.column() == EnabledColumn) {
+ return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked;
+ }
+ }
+
+ return QVariant();
+}
+
+bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) {
+ if (role == Qt::CheckStateRole) {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ toggleMod(mod, row);
+ return true;
+ }
+
+ return false;
+}
+
+QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
+ switch (section) {
+ case EnabledColumn:
+ return QString();
+ case NameColumn:
+ return QString("Name");
+ case DescriptionColumn:
+ return QString("Description");
+ }
+ }
+
+ return QVariant();
+}
+
+Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const {
+ auto flags = QAbstractListModel::flags(index);
+ if (index.isValid() && index.column() == EnabledColumn) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ return flags;
+}
+
+void AtlOptionalModListModel::selectRecommended() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = mod.recommended;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::clearAll() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = false;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) {
+ setMod(mod, index, !m_selection[mod.name]);
+}
+
+void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) {
+ if (m_selection[mod.name] == enable) return;
+
+ m_selection[mod.name] = enable;
+
+ // disable other mods in the group, if applicable
+ if (enable && !mod.group.isEmpty()) {
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (index == i) continue;
+ auto other = m_mods.at(i);
+
+ if (mod.group == other.group) {
+ setMod(other, i, false, shouldEmit);
+ }
+ }
+ }
+
+ for (const auto& dependencyName : mod.depends) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ // enable/disable dependencies
+ if (enable) {
+ setMod(dependencyMod, dependencyIndex, true, shouldEmit);
+ }
+
+ // if the dependency is 'effectively hidden', then track which mods
+ // depend on it - so we can efficiently disable it when no more dependents
+ // depend on it.
+ auto dependants = m_dependants[dependencyName];
+
+ if (enable) {
+ dependants.append(mod.name);
+ }
+ else {
+ dependants.removeAll(mod.name);
+
+ // if there are no longer any dependents, let's disable the mod
+ if (dependencyMod.effectively_hidden && dependants.isEmpty()) {
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+ }
+
+ // disable mods that depend on this one, if disabling
+ if (!enable) {
+ auto dependants = m_dependants[mod.name];
+ for (const auto& dependencyName : dependants) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+
+ if (shouldEmit) {
+ emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn),
+ AtlOptionalModListModel::index(index, EnabledColumn));
+ }
+}
+
+
+AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QDialog(parent), ui(new Ui::AtlOptionalModDialog) {
+ ui->setupUi(this);
+
+ listModel = new AtlOptionalModListModel(this, mods);
+ ui->treeView->setModel(listModel);
+
+ ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch);
+
+ connect(ui->selectRecommendedButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::selectRecommended);
+ connect(ui->clearAllButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::clearAll);
+ connect(ui->installButton, &QPushButton::pressed,
+ this, &QDialog::close);
+}
+
+AtlOptionalModDialog::~AtlOptionalModDialog() {
+ delete ui;
+}
diff --git a/application/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.h
new file mode 100644
index 00000000..a1df43f6
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <QDialog>
+#include <QAbstractListModel>
+
+#include "modplatform/atlauncher/ATLPackIndex.h"
+
+namespace Ui {
+class AtlOptionalModDialog;
+}
+
+class AtlOptionalModListModel : public QAbstractListModel {
+ Q_OBJECT
+
+public:
+ enum Columns
+ {
+ EnabledColumn = 0,
+ NameColumn,
+ DescriptionColumn,
+ };
+
+ AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+
+ QVector<QString> getResult();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+public slots:
+ void selectRecommended();
+ void clearAll();
+
+private:
+ void toggleMod(ATLauncher::VersionMod mod, int index);
+ void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
+
+private:
+ QVector<ATLauncher::VersionMod> m_mods;
+ QMap<QString, bool> m_selection;
+ QMap<QString, int> m_index;
+ QMap<QString, QVector<QString>> m_dependants;
+};
+
+class AtlOptionalModDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+ ~AtlOptionalModDialog() override;
+
+ QVector<QString> getResult() {
+ return listModel->getResult();
+ }
+
+private:
+ Ui::AtlOptionalModDialog *ui;
+
+ AtlOptionalModListModel *listModel;
+};
diff --git a/application/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
new file mode 100644
index 00000000..5d3193a4
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlOptionalModDialog</class>
+ <widget class="QDialog" name="AtlOptionalModDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>550</width>
+ <height>310</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select Mods To Install</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="3">
+ <widget class="QPushButton" name="installButton">
+ <property name="text">
+ <string>Install</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="selectRecommendedButton">
+ <property name="text">
+ <string>Select Recommended</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="shareCodeButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Use Share Code</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="clearAllButton">
+ <property name="text">
+ <string>Clear All</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="4">
+ <widget class="ModListView" name="treeView"/>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>widgets/ModListView.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/application/pages/modplatform/atlauncher/AtlPage.cpp b/application/pages/modplatform/atlauncher/AtlPage.cpp
new file mode 100644
index 00000000..9fdf111f
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.cpp
@@ -0,0 +1,175 @@
+#include "AtlPage.h"
+#include "ui_AtlPage.h"
+
+#include "dialogs/NewInstanceDialog.h"
+#include "AtlOptionalModDialog.h"
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+#include <BuildConfig.h>
+#include <dialogs/VersionSelectDialog.h>
+
+AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Atl::FilterModel(this);
+ listModel = new Atl::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch);
+ connect(ui->resetButton, &QPushButton::clicked, this, &AtlPage::resetSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged);
+}
+
+AtlPage::~AtlPage()
+{
+ delete ui;
+}
+
+bool AtlPage::shouldDisplay() const
+{
+ return true;
+}
+
+void AtlPage::openedImpl()
+{
+ if(!initialized)
+ {
+ listModel->request();
+ initialized = true;
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion));
+ auto editedLogoName = selected.safeName;
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
+ listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void AtlPage::triggerSearch()
+{
+ filterModel->setSearchTerm(ui->searchEdit->text());
+}
+
+void AtlPage::resetSearch()
+{
+ ui->searchEdit->setText("");
+}
+
+void AtlPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ ui->packDescription->setHtml(selected.description.replace("\n", "<br>"));
+
+ for(const auto& version : selected.versions) {
+ ui->versionSelectionBox->addItem(version.version);
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
+
+QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) {
+ AtlOptionalModDialog optionalModDialog(this, mods);
+ optionalModDialog.exec();
+ return optionalModDialog.getResult();
+}
+
+QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) {
+ VersionSelectDialog vselect(vlist.get(), "Choose Version", MMC->activeWindow(), false);
+ if (minecraftVersion != Q_NULLPTR) {
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+ vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ }
+ else {
+ vselect.setEmptyString(tr("No versions are currently available"));
+ }
+ vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
+
+ // select recommended build
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ if (minecraftVersion != Q_NULLPTR) {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end()) continue;
+ if (iter->equalsVersion != minecraftVersion) continue;
+ }
+
+ // first recommended build we find, we use.
+ if (version->isRecommended()) {
+ vselect.setCurrentVersion(version->descriptor());
+ break;
+ }
+ }
+
+ vselect.exec();
+ return vselect.selectedVersion()->descriptor();
+}
diff --git a/application/pages/modplatform/atlauncher/AtlPage.h b/application/pages/modplatform/atlauncher/AtlPage.h
new file mode 100644
index 00000000..932ec6a6
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.h
@@ -0,0 +1,87 @@
+/* Copyright 2013-2019 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 "AtlFilterModel.h"
+#include "AtlListModel.h"
+
+#include <QWidget>
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class AtlPage;
+}
+
+class NewInstanceDialog;
+
+class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport
+{
+Q_OBJECT
+
+public:
+ explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~AtlPage();
+ virtual QString displayName() const override
+ {
+ return tr("ATLauncher");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("atlauncher");
+ }
+ virtual QString id() const override
+ {
+ return "atl";
+ }
+ virtual QString helpPage() const override
+ {
+ return "ATL-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+private:
+ void suggestCurrent();
+
+ QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
+ QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override;
+
+private slots:
+ void triggerSearch();
+ void resetSearch();
+
+ void onSortingSelectionChanged(QString data);
+
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::AtlPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Atl::ListModel* listModel = nullptr;
+ Atl::FilterModel* filterModel = nullptr;
+
+ ATLauncher::IndexedPack selected;
+ QString selectedVersion;
+
+ bool initialized = false;
+};
diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/application/pages/modplatform/atlauncher/AtlPage.ui
new file mode 100644
index 00000000..f16c24b8
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlPage</class>
+ <widget class="QWidget" name="AtlPage">
+ <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="QTreeView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>96</width>
+ <height>48</height>
+ </size>
+ </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>
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string>
+ </property>
+ <property name="wordWrap">
+ <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="resetButton">
+ <property name="text">
+ <string>Reset</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>resetButton</tabstop>
+ <tabstop>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/application/pages/modplatform/flame/FlameModel.cpp b/application/pages/modplatform/flame/FlameModel.cpp
new file mode 100644
index 00000000..228a88c5
--- /dev/null
+++ b/application/pages/modplatform/flame/FlameModel.cpp
@@ -0,0 +1,259 @@
+#include "FlameModel.h"
+#include "MultiMC.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+#include <Env.h>
+
+namespace Flame {
+
+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 = MMC->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 = ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo));
+ 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(ENV.metacache()->resolveEntry("FlamePacks", 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();
+}
+
+void ListModel::performPaginatedSearch()
+{
+ NetJob *netJob = new NetJob("Flame::Search");
+ auto searchUrl = QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "categoryId=0&"
+ "gameId=432&"
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sectionId=4471&"
+ "sort=%3"
+ ).arg(nextSearchOffset).arg(currentSearchTerm).arg(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 Flame::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 CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Flame::IndexedPack> newList;
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ Flame::IndexedPack pack;
+ try
+ {
+ Flame::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading pack from CurseForge: " << 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 Flame::ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/application/pages/modplatform/flame/FlameModel.h b/application/pages/modplatform/flame/FlameModel.h
new file mode 100644
index 00000000..24383db0
--- /dev/null
+++ b/application/pages/modplatform/flame/FlameModel.h
@@ -0,0 +1,76 @@
+#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>
+
+namespace Flame {
+
+
+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;
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/application/pages/modplatform/flame/FlamePage.cpp b/application/pages/modplatform/flame/FlamePage.cpp
new file mode 100644
index 00000000..ade58431
--- /dev/null
+++ b/application/pages/modplatform/flame/FlamePage.cpp
@@ -0,0 +1,185 @@
+#include "FlamePage.h"
+#include "ui_FlamePage.h"
+
+#include "MultiMC.h"
+#include <Json.h>
+#include "dialogs/NewInstanceDialog.h"
+#include <InstanceImportTask.h>
+#include "FlameModel.h"
+#include <QKeyEvent>
+
+FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ listModel = new Flame::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 curseforge 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 total downloads"));
+
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged);
+}
+
+FlamePage::~FlamePage()
+{
+ delete ui;
+}
+
+bool FlamePage::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 FlamePage::shouldDisplay() const
+{
+ return true;
+}
+
+void FlamePage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void FlamePage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<Flame::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 = [](Flame::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 == false)
+ {
+ qDebug() << "Loading flame modpack versions";
+ NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ int addonId = current.addonId;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").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 CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ QJsonArray arr = doc.array();
+ try
+ {
+ Flame::loadIndexedPackVersions(current, arr);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading flame modpack 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 FlamePage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ QString editedLogoName;
+ editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
+ listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void FlamePage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toString();
+ suggestCurrent();
+}
diff --git a/application/pages/modplatform/TwitchPage.h b/application/pages/modplatform/flame/FlamePage.h
index 36080016..467bb44b 100644
--- a/application/pages/modplatform/TwitchPage.h
+++ b/application/pages/modplatform/flame/FlamePage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* 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.
@@ -20,42 +20,61 @@
#include "pages/BasePage.h"
#include <MultiMC.h>
#include "tasks/Task.h"
+#include <modplatform/flame/FlamePackIndex.h>
namespace Ui
{
-class TwitchPage;
+class FlamePage;
}
class NewInstanceDialog;
-class TwitchPage : public QWidget, public BasePage
+namespace Flame {
+ class ListModel;
+}
+
+class FlamePage : public QWidget, public BasePage
{
Q_OBJECT
public:
- explicit TwitchPage(NewInstanceDialog* dialog, QWidget *parent = 0);
- virtual ~TwitchPage();
+ explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FlamePage();
virtual QString displayName() const override
{
- return tr("Twitch");
+ return tr("CurseForge");
}
virtual QIcon icon() const override
{
- return MMC->getThemedIcon("twitch");
+ return MMC->getThemedIcon("flame");
}
virtual QString id() const override
{
- return "twitch";
+ return "flame";
}
virtual QString helpPage() const override
{
- return "Twitch-platform";
+ return "Flame-platform";
}
virtual bool shouldDisplay() const override;
void openedImpl() override;
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
private:
- Ui::TwitchPage *ui = nullptr;
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FlamePage *ui = nullptr;
NewInstanceDialog* dialog = nullptr;
+ Flame::ListModel* listModel = nullptr;
+ Flame::IndexedPack current;
+
+ QString selectedVersion;
};
diff --git a/application/pages/modplatform/flame/FlamePage.ui b/application/pages/modplatform/flame/FlamePage.ui
new file mode 100644
index 00000000..9723815a
--- /dev/null
+++ b/application/pages/modplatform/flame/FlamePage.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FlamePage</class>
+ <widget class="QWidget" name="FlamePage">
+ <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>
diff --git a/application/pages/modplatform/ftb/FtbFilterModel.cpp b/application/pages/modplatform/ftb/FtbFilterModel.cpp
new file mode 100644
index 00000000..dec3a017
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbFilterModel.cpp
@@ -0,0 +1,64 @@
+#include "FtbFilterModel.h"
+
+#include <QDebug>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include <MMCStrings.h>
+
+namespace Ftb {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPlays;
+ sortings.insert(tr("Sort by plays"), Sorting::ByPlays);
+ sortings.insert(tr("Sort by installs"), Sorting::ByInstalls);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ return true;
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
+ ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ if (currentSorting == ByPlays) {
+ return leftPack.plays < rightPack.plays;
+ }
+ else if (currentSorting == ByInstalls) {
+ return leftPack.installs < rightPack.installs;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/application/pages/modplatform/ftb/FtbFilterModel.h b/application/pages/modplatform/ftb/FtbFilterModel.h
new file mode 100644
index 00000000..4fe2a274
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbFilterModel.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Ftb {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPlays,
+ ByInstalls,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+
+};
+
+}
diff --git a/application/pages/modplatform/ftb/FtbListModel.cpp b/application/pages/modplatform/ftb/FtbListModel.cpp
new file mode 100644
index 00000000..98973f2e
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbListModel.cpp
@@ -0,0 +1,304 @@
+#include "FtbListModel.h"
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include "MultiMC.h"
+#include "Json.h"
+
+#include <QPainter>
+
+namespace Ftb {
+
+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);
+ }
+
+ ModpacksCH::Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.synopsis;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ QIcon placeholder = MMC->getThemedIcon("screenshot-placeholder");
+
+ auto iter = m_logoMap.find(pack.name);
+ if (iter != m_logoMap.end()) {
+ auto & logo = *iter;
+ if(!logo.result.isNull()) {
+ return logo.result;
+ }
+ return placeholder;
+ }
+
+ for(auto art : pack.art) {
+ if(art.type == "square") {
+ ((ListModel *)this)->requestLogo(pack.name, art.url);
+ }
+ }
+ return placeholder;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::performSearch()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ QString searchUrl;
+ if(currentSearchTerm.isEmpty()) {
+ searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all";
+ }
+ else {
+ searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1")
+ .arg(currentSearchTerm);
+ }
+ 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::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::searchWithTerm(const QString &term)
+{
+ if(searchState != Failed && currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ // unless the search has failed, then there is no need to perform an identical search.
+ return;
+ }
+ currentSearchTerm = term;
+
+ if(jobPtr) {
+ jobPtr->abort();
+ jobPtr.reset();
+ }
+
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+
+ performSearch();
+}
+
+void ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto packs = doc.object().value("packs").toArray();
+ for(auto pack : packs) {
+ auto packId = pack.toInt();
+ remainingPacks.append(packId);
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ searchState = Failed;
+}
+
+void ListModel::requestPack()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1")
+ .arg(currentPack);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
+}
+
+void ListModel::packRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Modpack pack;
+ try
+ {
+ ModpacksCH::loadModpack(pack, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from FTB: " << e.cause();
+ return;
+ }
+
+ // Since there is no guarantee that packs have a version, this will just
+ // ignore those "dud" packs.
+ if (pack.versions.empty())
+ {
+ qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions";
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
+ modpacks.append(pack);
+ endInsertRows();
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::packRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+}
+
+void ListModel::logoLoaded(QString logo, bool stale)
+{
+ auto & logoObj = m_logoMap[logo];
+ logoObj.downloadJob.reset();
+ QString smallPath = logoObj.fullpath + ".small";
+
+ QFileInfo smallInfo(smallPath);
+
+ if(stale || !smallInfo.exists()) {
+ QImage image(logoObj.fullpath);
+ if (image.isNull())
+ {
+ logoObj.failed = true;
+ return;
+ }
+ QImage small;
+ if (image.width() > image.height()) {
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ }
+ else {
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ }
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ square.save(logoObj.fullpath + ".small", "PNG");
+ }
+
+ logoObj.result = QIcon(logoObj.fullpath + ".small");
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].name == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_logoMap[logo].failed = true;
+ m_logoMap[logo].downloadJob.reset();
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_logoMap.contains(logo)) {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+
+ bool stale = entry->isStale();
+
+ NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
+ {
+ logoLoaded(logo, stale);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ auto &newLogoEntry = m_logoMap[logo];
+ newLogoEntry.downloadJob = job;
+ newLogoEntry.fullpath = fullPath;
+ job->start();
+}
+
+}
diff --git a/application/pages/modplatform/ftb/FtbListModel.h b/application/pages/modplatform/ftb/FtbListModel.h
new file mode 100644
index 00000000..de94e6ba
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbListModel.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include "net/NetJob.h"
+#include <QIcon>
+
+namespace Ftb {
+
+struct Logo {
+ QString fullpath;
+ NetJobPtr downloadJob;
+ QIcon result;
+ bool failed = false;
+};
+
+typedef QMap<QString, Logo> 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;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void performSearch();
+ void searchRequestFinished();
+ void searchRequestFailed(QString reason);
+
+ void requestPack();
+ void packRequestFinished();
+ void packRequestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, bool stale);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ModpacksCH::Modpack> modpacks;
+ LogoMap m_logoMap;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished,
+ Failed,
+ } searchState = None;
+ NetJobPtr jobPtr;
+ int currentPack;
+ QList<int> remainingPacks;
+ QByteArray response;
+};
+
+}
diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/application/pages/modplatform/ftb/FtbPage.cpp
new file mode 100644
index 00000000..b7f35c5d
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbPage.cpp
@@ -0,0 +1,145 @@
+#include "FtbPage.h"
+#include "ui_FtbPage.h"
+
+#include <QKeyEvent>
+
+#include "dialogs/NewInstanceDialog.h"
+#include "modplatform/modpacksch/FTBPackInstallTask.h"
+
+#include "HoeDown.h"
+
+FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Ftb::FilterModel(this);
+ listModel = new Ftb::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->searchEdit->installEventFilter(this);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
+}
+
+FtbPage::~FtbPage()
+{
+ delete ui;
+}
+
+bool FtbPage::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 FtbPage::shouldDisplay() const
+{
+ return true;
+}
+
+void FtbPage::openedImpl()
+{
+ triggerSearch();
+ suggestCurrent();
+}
+
+void FtbPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion));
+ for(auto art : selected.art) {
+ if(art.type == "square") {
+ QString editedLogoName;
+ editedLogoName = selected.name;
+
+ listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
+ });
+ }
+ }
+}
+
+void FtbPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text());
+}
+
+void FtbPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ HoeDown hoedown;
+ QString output = hoedown.process(selected.description.toUtf8());
+ ui->packDescription->setHtml(output);
+
+ // reverse foreach, so that the newest versions are first
+ for (auto i = selected.versions.size(); i--;) {
+ ui->versionSelectionBox->addItem(selected.versions.at(i).name);
+ }
+
+ suggestCurrent();
+}
+
+void FtbPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
diff --git a/application/pages/modplatform/ftb/FtbPage.h b/application/pages/modplatform/ftb/FtbPage.h
new file mode 100644
index 00000000..c9c93897
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbPage.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 "FtbFilterModel.h"
+#include "FtbListModel.h"
+
+#include <QWidget>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class FtbPage;
+}
+
+class NewInstanceDialog;
+
+class FtbPage : public QWidget, public BasePage
+{
+Q_OBJECT
+
+public:
+ explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FtbPage();
+ virtual QString displayName() const override
+ {
+ return tr("FTB");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("ftb_logo");
+ }
+ virtual QString id() const override
+ {
+ return "ftb";
+ }
+ virtual QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSortingSelectionChanged(QString data);
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FtbPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Ftb::ListModel* listModel = nullptr;
+ Ftb::FilterModel* filterModel = nullptr;
+
+ ModpacksCH::Modpack selected;
+ QString selectedVersion;
+};
diff --git a/application/pages/modplatform/ftb/FtbPage.ui b/application/pages/modplatform/ftb/FtbPage.ui
new file mode 100644
index 00000000..135afc6d
--- /dev/null
+++ b/application/pages/modplatform/ftb/FtbPage.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FtbPage</class>
+ <widget class="QWidget" name="FtbPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>875</width>
+ <height>745</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" 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="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" 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>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/application/pages/modplatform/FtbListModel.cpp b/application/pages/modplatform/legacy_ftb/ListModel.cpp
index f4311afb..32596fb3 100644
--- a/application/pages/modplatform/FtbListModel.cpp
+++ b/application/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -1,4 +1,4 @@
-#include "FtbListModel.h"
+#include "ListModel.h"
#include "MultiMC.h"
#include <MMCStrings.h>
@@ -10,17 +10,21 @@
#include <RWStorage.h>
#include <Env.h>
-FtbFilterModel::FtbFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+#include <BuildConfig.h>
+
+namespace LegacyFTB {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByGameVersion;
sortings.insert(tr("Sort by name"), Sorting::ByName);
sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
}
-bool FtbFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
- FtbModpack leftPack = sourceModel()->data(left, Qt::UserRole).value<FtbModpack>();
- FtbModpack rightPack = sourceModel()->data(right, Qt::UserRole).value<FtbModpack>();
+ Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
+ Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
if(currentSorting == Sorting::ByGameVersion) {
Version lv(leftPack.mcVersion);
@@ -36,66 +40,66 @@ bool FtbFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right)
return true;
}
-bool FtbFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
return true;
}
-const QMap<QString, FtbFilterModel::Sorting> FtbFilterModel::getAvailableSortings()
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
{
return sortings;
}
-QString FtbFilterModel::translateCurrentSorting()
+QString FilterModel::translateCurrentSorting()
{
return sortings.key(currentSorting);
}
-void FtbFilterModel::setSorting(Sorting s)
+void FilterModel::setSorting(Sorting s)
{
currentSorting = s;
invalidate();
}
-FtbFilterModel::Sorting FtbFilterModel::getCurrentSorting()
+FilterModel::Sorting FilterModel::getCurrentSorting()
{
return currentSorting;
}
-FtbListModel::FtbListModel(QObject *parent) : QAbstractListModel(parent)
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
-FtbListModel::~FtbListModel()
+ListModel::~ListModel()
{
}
-QString FtbListModel::translatePackType(FtbPackType type) const
+QString ListModel::translatePackType(PackType type) const
{
switch(type)
{
- case FtbPackType::Public:
+ case PackType::Public:
return tr("Public Modpack");
- case FtbPackType::ThirdParty:
+ case PackType::ThirdParty:
return tr("Third Party Modpack");
- case FtbPackType::Private:
+ case PackType::Private:
return tr("Private Modpack");
}
qWarning() << "Unknown FTB modpack type:" << int(type);
return QString();
}
-int FtbListModel::rowCount(const QModelIndex &parent) const
+int ListModel::rowCount(const QModelIndex &parent) const
{
return modpacks.size();
}
-int FtbListModel::columnCount(const QModelIndex &parent) const
+int ListModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
-QVariant FtbListModel::data(const QModelIndex &index, int role) const
+QVariant ListModel::data(const QModelIndex &index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
@@ -103,7 +107,7 @@ QVariant FtbListModel::data(const QModelIndex &index, int role) const
return QString("INVALID INDEX %1").arg(pos);
}
- FtbModpack pack = modpacks.at(pos);
+ Modpack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
return pack.name + "\n" + translatePackType(pack.type);
@@ -127,7 +131,7 @@ QVariant FtbListModel::data(const QModelIndex &index, int role) const
return (m_logoMap.value(pack.logo));
}
QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
- ((FtbListModel *)this)->requestLogo(pack.logo);
+ ((ListModel *)this)->requestLogo(pack.logo);
return icon;
}
else if(role == Qt::TextColorRole)
@@ -154,33 +158,33 @@ QVariant FtbListModel::data(const QModelIndex &index, int role) const
return QVariant();
}
-void FtbListModel::fill(FtbModpackList modpacks)
+void ListModel::fill(ModpackList modpacks)
{
beginResetModel();
this->modpacks = modpacks;
endResetModel();
}
-void FtbListModel::addPack(FtbModpack modpack)
+void ListModel::addPack(Modpack modpack)
{
beginResetModel();
this->modpacks.append(modpack);
endResetModel();
}
-void FtbListModel::clear()
+void ListModel::clear()
{
beginResetModel();
modpacks.clear();
endResetModel();
}
-FtbModpack FtbListModel::at(int row)
+Modpack ListModel::at(int row)
{
return modpacks.at(row);
}
-void FtbListModel::remove(int row)
+void ListModel::remove(int row)
{
if(row < 0 || row >= modpacks.size())
{
@@ -192,20 +196,20 @@ void FtbListModel::remove(int row)
endRemoveRows();
}
-void FtbListModel::logoLoaded(QString logo, QIcon out)
+void ListModel::logoLoaded(QString logo, QIcon out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out);
emit dataChanged(createIndex(0, 0), createIndex(1, 0));
}
-void FtbListModel::logoFailed(QString logo)
+void ListModel::logoFailed(QString logo)
{
m_failedLogos.append(logo);
m_loadingLogos.removeAll(logo);
}
-void FtbListModel::requestLogo(QString file)
+void ListModel::requestLogo(QString file)
{
if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
{
@@ -214,7 +218,7 @@ void FtbListModel::requestLogo(QString file)
MetaEntryPtr entry = ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file));
- job->addNetAction(Net::Download::makeCached(QUrl(QString("https://ftb.cursecdn.com/FTB2/static/%1").arg(file)), entry));
+ job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
@@ -236,7 +240,7 @@ void FtbListModel::requestLogo(QString file)
m_loadingLogos.append(file);
}
-void FtbListModel::getLogo(const QString &logo, LogoCallback callback)
+void ListModel::getLogo(const QString &logo, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
@@ -248,7 +252,9 @@ void FtbListModel::getLogo(const QString &logo, LogoCallback callback)
}
}
-Qt::ItemFlags FtbListModel::flags(const QModelIndex &index) const
+Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
{
return QAbstractListModel::flags(index);
}
+
+}
diff --git a/application/pages/modplatform/FtbListModel.h b/application/pages/modplatform/legacy_ftb/ListModel.h
index 98301fd7..c55df000 100644
--- a/application/pages/modplatform/FtbListModel.h
+++ b/application/pages/modplatform/legacy_ftb/ListModel.h
@@ -1,6 +1,6 @@
#pragma once
-#include <modplatform/ftb/PackHelpers.h>
+#include <modplatform/legacy_ftb/PackHelpers.h>
#include <RWStorage.h>
#include <QAbstractListModel>
@@ -11,13 +11,16 @@
#include <functional>
-typedef QMap<QString, QIcon> FtbLogoMap;
+namespace LegacyFTB {
+
+typedef QMap<QString, QIcon> FTBLogoMap;
typedef std::function<void(QString)> LogoCallback;
-class FtbFilterModel : public QSortFilterProxyModel
+class FilterModel : public QSortFilterProxyModel
{
+ Q_OBJECT
public:
- FtbFilterModel(QObject* parent = Q_NULLPTR);
+ FilterModel(QObject* parent = Q_NULLPTR);
enum Sorting {
ByName,
ByGameVersion
@@ -37,18 +40,18 @@ private:
};
-class FtbListModel : public QAbstractListModel
+class ListModel : public QAbstractListModel
{
Q_OBJECT
private:
- FtbModpackList modpacks;
+ ModpackList modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
- FtbLogoMap m_logoMap;
+ FTBLogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
void requestLogo(QString file);
- QString translatePackType(FtbPackType type) const;
+ QString translatePackType(PackType type) const;
private slots:
@@ -56,18 +59,20 @@ private slots:
void logoLoaded(QString logo, QIcon out);
public:
- FtbListModel(QObject *parent);
- ~FtbListModel();
+ ListModel(QObject *parent);
+ ~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;
- void fill(FtbModpackList modpacks);
- void addPack(FtbModpack modpack);
+ void fill(ModpackList modpacks);
+ void addPack(Modpack modpack);
void clear();
void remove(int row);
- FtbModpack at(int row);
+ Modpack at(int row);
void getLogo(const QString &logo, LogoCallback callback);
};
+
+}
diff --git a/application/pages/modplatform/FTBPage.cpp b/application/pages/modplatform/legacy_ftb/Page.cpp
index dca86efd..a438f76c 100644
--- a/application/pages/modplatform/FTBPage.cpp
+++ b/application/pages/modplatform/legacy_ftb/Page.cpp
@@ -1,27 +1,29 @@
-#include "FTBPage.h"
-#include "ui_FTBPage.h"
+#include "Page.h"
+#include "ui_Page.h"
#include <QInputDialog>
#include "MultiMC.h"
#include "dialogs/CustomMessageBox.h"
#include "dialogs/NewInstanceDialog.h"
-#include "modplatform/ftb/FtbPackFetchTask.h"
-#include "modplatform/ftb/FtbPackInstallTask.h"
-#include "modplatform/ftb/FtbPrivatePackManager.h"
-#include "FtbListModel.h"
+#include "modplatform/legacy_ftb/PackFetchTask.h"
+#include "modplatform/legacy_ftb/PackInstallTask.h"
+#include "modplatform/legacy_ftb/PrivatePackManager.h"
+#include "ListModel.h"
-FTBPage::FTBPage(NewInstanceDialog* dialog, QWidget *parent)
- : QWidget(parent), dialog(dialog), ui(new Ui::FTBPage)
+namespace LegacyFTB {
+
+Page::Page(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::Page)
{
- ftbFetchTask.reset(new FtbPackFetchTask());
- ftbPrivatePacks.reset(new FtbPrivatePackManager());
+ ftbFetchTask.reset(new PackFetchTask());
+ ftbPrivatePacks.reset(new PrivatePackManager());
ui->setupUi(this);
{
- publicFilterModel = new FtbFilterModel(this);
- publicListModel = new FtbListModel(this);
+ publicFilterModel = new FilterModel(this);
+ publicListModel = new ListModel(this);
publicFilterModel->setSourceModel(publicListModel);
ui->publicPackList->setModel(publicFilterModel);
@@ -39,8 +41,8 @@ FTBPage::FTBPage(NewInstanceDialog* dialog, QWidget *parent)
}
{
- thirdPartyFilterModel = new FtbFilterModel(this);
- thirdPartyModel = new FtbListModel(this);
+ thirdPartyFilterModel = new FilterModel(this);
+ thirdPartyModel = new ListModel(this);
thirdPartyFilterModel->setSourceModel(thirdPartyModel);
ui->thirdPartyPackList->setModel(thirdPartyFilterModel);
@@ -53,8 +55,8 @@ FTBPage::FTBPage(NewInstanceDialog* dialog, QWidget *parent)
}
{
- privateFilterModel = new FtbFilterModel(this);
- privateListModel = new FtbListModel(this);
+ privateFilterModel = new FilterModel(this);
+ privateListModel = new ListModel(this);
privateFilterModel->setSourceModel(privateListModel);
ui->privatePackList->setModel(privateFilterModel);
@@ -69,17 +71,17 @@ FTBPage::FTBPage(NewInstanceDialog* dialog, QWidget *parent)
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
- connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FTBPage::onSortingSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FTBPage::onVersionSelectionItemChanged);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged);
- connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &FTBPage::onPublicPackSelectionChanged);
- connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &FTBPage::onThirdPartyPackSelectionChanged);
- connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &FTBPage::onPrivatePackSelectionChanged);
+ connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged);
+ connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);
+ connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged);
- connect(ui->addPackBtn, &QPushButton::pressed, this, &FTBPage::onAddPackClicked);
- connect(ui->removePackBtn, &QPushButton::pressed, this, &FTBPage::onRemovePackClicked);
+ connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
+ connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
- connect(ui->tabWidget, &QTabWidget::currentChanged, this, &FTBPage::onTabChanged);
+ connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);
// ui->modpackInfo->setOpenExternalLinks(true);
@@ -90,25 +92,25 @@ FTBPage::FTBPage(NewInstanceDialog* dialog, QWidget *parent)
onTabChanged(ui->tabWidget->currentIndex());
}
-FTBPage::~FTBPage()
+Page::~Page()
{
delete ui;
}
-bool FTBPage::shouldDisplay() const
+bool Page::shouldDisplay() const
{
return true;
}
-void FTBPage::openedImpl()
+void Page::openedImpl()
{
if(!initialized)
{
- connect(ftbFetchTask.get(), &FtbPackFetchTask::finished, this, &FTBPage::ftbPackDataDownloadSuccessfully);
- connect(ftbFetchTask.get(), &FtbPackFetchTask::failed, this, &FTBPage::ftbPackDataDownloadFailed);
+ connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed);
- connect(ftbFetchTask.get(), &FtbPackFetchTask::privateFileDownloadFinished, this, &FTBPage::ftbPrivatePackDataDownloadSuccessfully);
- connect(ftbFetchTask.get(), &FtbPackFetchTask::privateFileDownloadFailed, this, &FTBPage::ftbPrivatePackDataDownloadFailed);
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed);
ftbFetchTask->fetch();
ftbPrivatePacks->load();
@@ -118,71 +120,72 @@ void FTBPage::openedImpl()
suggestCurrent();
}
-void FTBPage::suggestCurrent()
+void Page::suggestCurrent()
{
- if(isOpened)
+ if(!isOpened)
{
- if(!selected.broken)
- {
- dialog->setSuggestedPack(selected.name, new FtbPackInstallTask(selected, selectedVersion));
- QString editedLogoName;
- if(selected.logo.toLower().startsWith("ftb"))
- {
- editedLogoName = selected.logo;
- }
- else
- {
- editedLogoName = "ftb_" + selected.logo;
- }
+ return;
+ }
- editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
+ if(selected.broken || selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
- if(selected.type == FtbPackType::Public)
- {
- publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
- {
- dialog->setSuggestedIconFromFile(logo, editedLogoName);
- });
- }
- else if (selected.type == FtbPackType::ThirdParty)
- {
- thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
- {
- dialog->setSuggestedIconFromFile(logo, editedLogoName);
- });
- }
- else if (selected.type == FtbPackType::Private)
- {
- privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
- {
- dialog->setSuggestedIconFromFile(logo, editedLogoName);
- });
- }
- }
- else
+ dialog->setSuggestedPack(selected.name, new PackInstallTask(selected, selectedVersion));
+ QString editedLogoName;
+ if(selected.logo.toLower().startsWith("ftb"))
+ {
+ editedLogoName = selected.logo;
+ }
+ else
+ {
+ editedLogoName = "ftb_" + selected.logo;
+ }
+
+ editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
+
+ if(selected.type == PackType::Public)
+ {
+ publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
{
- dialog->setSuggestedPack();
- }
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::ThirdParty)
+ {
+ thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::Private)
+ {
+ privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
}
}
-void FTBPage::ftbPackDataDownloadSuccessfully(FtbModpackList publicPacks, FtbModpackList thirdPartyPacks)
+void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks)
{
publicListModel->fill(publicPacks);
thirdPartyModel->fill(thirdPartyPacks);
}
-void FTBPage::ftbPackDataDownloadFailed(QString reason)
+void Page::ftbPackDataDownloadFailed(QString reason)
{
//TODO: Display the error
}
-void FTBPage::ftbPrivatePackDataDownloadSuccessfully(FtbModpack pack)
+void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)
{
privateListModel->addPack(pack);
}
-void FTBPage::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
+void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
{
auto reply = QMessageBox::question(
this,
@@ -195,40 +198,40 @@ void FTBPage::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
}
}
-void FTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev)
+void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
- FtbModpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<FtbModpack>();
+ Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
-void FTBPage::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev)
+void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
- FtbModpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<FtbModpack>();
+ Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
-void FTBPage::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev)
+void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
- FtbModpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<FtbModpack>();
+ Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
-void FTBPage::onPackSelectionChanged(FtbModpack* pack)
+void Page::onPackSelectionChanged(Modpack* pack)
{
ui->versionSelectionBox->clear();
if(pack)
@@ -266,7 +269,7 @@ void FTBPage::onPackSelectionChanged(FtbModpack* pack)
suggestCurrent();
}
-void FTBPage::onVersionSelectionItemChanged(QString data)
+void Page::onVersionSelectionItemChanged(QString data)
{
if(data.isNull() || data.isEmpty())
{
@@ -278,15 +281,15 @@ void FTBPage::onVersionSelectionItemChanged(QString data)
suggestCurrent();
}
-void FTBPage::onSortingSelectionChanged(QString data)
+void Page::onSortingSelectionChanged(QString data)
{
- FtbFilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data);
+ FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data);
publicFilterModel->setSorting(toSet);
thirdPartyFilterModel->setSorting(toSet);
privateFilterModel->setSorting(toSet);
}
-void FTBPage::onTabChanged(int tab)
+void Page::onTabChanged(int tab)
{
if(tab == 1)
{
@@ -311,7 +314,7 @@ void FTBPage::onTabChanged(int tab)
QModelIndex idx = currentList->currentIndex();
if(idx.isValid())
{
- auto pack = currentModel->data(idx, Qt::UserRole).value<FtbModpack>();
+ auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&pack);
}
else
@@ -320,7 +323,7 @@ void FTBPage::onTabChanged(int tab)
}
}
-void FTBPage::onAddPackClicked()
+void Page::onAddPackClicked()
{
bool ok;
QString text = QInputDialog::getText(
@@ -338,7 +341,7 @@ void FTBPage::onAddPackClicked()
}
}
-void FTBPage::onRemovePackClicked()
+void Page::onRemovePackClicked()
{
auto index = ui->privatePackList->currentIndex();
if(!index.isValid())
@@ -346,7 +349,7 @@ void FTBPage::onRemovePackClicked()
return;
}
auto row = index.row();
- FtbModpack pack = privateListModel->at(row);
+ Modpack pack = privateListModel->at(row);
auto answer = QMessageBox::question(
this,
tr("Remove pack"),
@@ -362,3 +365,5 @@ void FTBPage::onRemovePackClicked()
privateListModel->remove(row);
onPackSelectionChanged();
}
+
+}
diff --git a/application/pages/modplatform/FTBPage.h b/application/pages/modplatform/legacy_ftb/Page.h
index 6467decc..e840216e 100644
--- a/application/pages/modplatform/FTBPage.h
+++ b/application/pages/modplatform/legacy_ftb/Page.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* 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.
@@ -22,29 +22,32 @@
#include "pages/BasePage.h"
#include <MultiMC.h>
#include "tasks/Task.h"
-#include "modplatform/ftb/PackHelpers.h"
-#include "modplatform/ftb/FtbPackFetchTask.h"
+#include "modplatform/legacy_ftb/PackHelpers.h"
+#include "modplatform/legacy_ftb/PackFetchTask.h"
#include "QObjectPtr.h"
+class NewInstanceDialog;
+
+namespace LegacyFTB {
+
namespace Ui
{
-class FTBPage;
+class Page;
}
-class FtbListModel;
-class FtbFilterModel;
-class NewInstanceDialog;
-class FtbPrivatePackListModel;
-class FtbPrivatePackFilterModel;
-class FtbPrivatePackManager;
+class ListModel;
+class FilterModel;
+class PrivatePackListModel;
+class PrivatePackFilterModel;
+class PrivatePackManager;
-class FTBPage : public QWidget, public BasePage
+class Page : public QWidget, public BasePage
{
Q_OBJECT
public:
- explicit FTBPage(NewInstanceDialog * dialog, QWidget *parent = 0);
- virtual ~FTBPage();
+ explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0);
+ virtual ~Page();
QString displayName() const override
{
return tr("FTB Legacy");
@@ -55,7 +58,7 @@ public:
}
QString id() const override
{
- return "ftb";
+ return "legacy_ftb";
}
QString helpPage() const override
{
@@ -66,13 +69,13 @@ public:
private:
void suggestCurrent();
- void onPackSelectionChanged(FtbModpack *pack = nullptr);
+ void onPackSelectionChanged(Modpack *pack = nullptr);
private slots:
- void ftbPackDataDownloadSuccessfully(FtbModpackList publicPacks, FtbModpackList thirdPartyPacks);
+ void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks);
void ftbPackDataDownloadFailed(QString reason);
- void ftbPrivatePackDataDownloadSuccessfully(FtbModpack pack);
+ void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
void onSortingSelectionChanged(QString data);
@@ -88,27 +91,29 @@ private slots:
void onRemovePackClicked();
private:
- FtbFilterModel* currentModel = nullptr;
+ FilterModel* currentModel = nullptr;
QTreeView* currentList = nullptr;
QTextBrowser* currentModpackInfo = nullptr;
bool initialized = false;
- FtbModpack selected;
+ Modpack selected;
QString selectedVersion;
- FtbListModel* publicListModel = nullptr;
- FtbFilterModel* publicFilterModel = nullptr;
+ ListModel* publicListModel = nullptr;
+ FilterModel* publicFilterModel = nullptr;
- FtbListModel *thirdPartyModel = nullptr;
- FtbFilterModel *thirdPartyFilterModel = nullptr;
+ ListModel *thirdPartyModel = nullptr;
+ FilterModel *thirdPartyFilterModel = nullptr;
- FtbListModel *privateListModel = nullptr;
- FtbFilterModel *privateFilterModel = nullptr;
+ ListModel *privateListModel = nullptr;
+ FilterModel *privateFilterModel = nullptr;
- unique_qobject_ptr<FtbPackFetchTask> ftbFetchTask;
- std::unique_ptr<FtbPrivatePackManager> ftbPrivatePacks;
+ unique_qobject_ptr<PackFetchTask> ftbFetchTask;
+ std::unique_ptr<PrivatePackManager> ftbPrivatePacks;
NewInstanceDialog* dialog = nullptr;
- Ui::FTBPage *ui = nullptr;
+ Ui::Page *ui = nullptr;
};
+
+}
diff --git a/application/pages/modplatform/FTBPage.ui b/application/pages/modplatform/legacy_ftb/Page.ui
index e5ed78cb..15e5d432 100644
--- a/application/pages/modplatform/FTBPage.ui
+++ b/application/pages/modplatform/legacy_ftb/Page.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>FTBPage</class>
- <widget class="QWidget" name="FTBPage">
+ <class>LegacyFTB::Page</class>
+ <widget class="QWidget" name="LegacyFTB::Page">
<property name="geometry">
<rect>
<x>0</x>
@@ -29,6 +29,9 @@
<height>16777215</height>
</size>
</property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="0" column="1">
@@ -52,6 +55,9 @@
<height>16777215</height>
</size>
</property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
</widget>
</item>
</layout>
@@ -69,6 +75,9 @@
<height>16777215</height>
</size>
</property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item row="1" column="0">
diff --git a/application/pages/modplatform/technic/TechnicData.h b/application/pages/modplatform/technic/TechnicData.h
new file mode 100644
index 00000000..50fd75e8
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicData.h
@@ -0,0 +1,42 @@
+/* Copyright 2020-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 <QList>
+#include <QString>
+
+namespace Technic {
+struct Modpack {
+ QString slug;
+
+ QString name;
+ QString logoUrl;
+ QString logoName;
+
+ bool broken = true;
+
+ QString url;
+ bool isSolder = false;
+ QString minecraftVersion;
+
+ bool metadataLoaded = false;
+ QString websiteUrl;
+ QString author;
+ QString description;
+};
+}
+
+Q_DECLARE_METATYPE(Technic::Modpack)
diff --git a/application/pages/modplatform/technic/TechnicModel.cpp b/application/pages/modplatform/technic/TechnicModel.cpp
new file mode 100644
index 00000000..def30783
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicModel.cpp
@@ -0,0 +1,238 @@
+/* Copyright 2020-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 "TechnicModel.h"
+#include "Env.h"
+#include "MultiMC.h"
+#include "Json.h"
+
+#include <QIcon>
+
+Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+Technic::ListModel::~ListModel()
+{
+}
+
+QVariant Technic::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);
+ }
+
+ Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logoName))
+ {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = MMC->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();
+}
+
+int Technic::ListModel::columnCount(const QModelIndex&) const
+{
+ return 1;
+}
+
+int Technic::ListModel::rowCount(const QModelIndex&) const
+{
+ return modpacks.size();
+}
+
+void Technic::ListModel::searchWithTerm(const QString& term)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ return;
+ }
+ currentSearchTerm = term;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ performSearch();
+}
+
+void Technic::ListModel::performSearch()
+{
+ NetJob *netJob = new NetJob("Technic::Search");
+ QString searchUrl = "";
+ if (currentSearchTerm.isEmpty()) {
+ searchUrl = "https://api.technicpack.net/trending?build=multimc";
+ }
+ else
+ {
+ searchUrl = QString(
+ "https://api.technicpack.net/search?build=multimc&q=%1"
+ ).arg(currentSearchTerm);
+ }
+ 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 Technic::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 Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Modpack> newList;
+ try {
+ auto root = Json::requireObject(doc);
+ auto objs = Json::requireArray(root, "modpacks");
+ for (auto technicPack: objs) {
+ Modpack pack;
+ auto technicPackObject = Json::requireObject(technicPack);
+ pack.name = Json::requireString(technicPackObject, "name");
+ pack.slug = Json::requireString(technicPackObject, "slug");
+ if (pack.slug == "vanilla")
+ continue;
+
+ auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
+ if(rawURL == "null") {
+ pack.logoUrl = "null";
+ pack.logoName = "null";
+ }
+ else {
+ pack.logoUrl = rawURL;
+ pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
+ }
+ pack.broken = false;
+ newList.append(pack);
+ }
+ }
+ catch (const JSONValidationError &err)
+ {
+ qCritical() << "Couldn't parse technic search results:" << err.cause() ;
+ return;
+ }
+ searchState = Finished;
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void Technic::ListModel::searchRequestFailed()
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested)
+ {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ performSearch();
+ }
+ else
+ {
+ searchState = Finished;
+ }
+}
+
+
+void Technic::ListModel::logoLoaded(QString logo, QString out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, QIcon(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 Technic::ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void Technic::ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
+ NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
+ {
+ logoLoaded(logo, fullPath);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(logo);
+}
diff --git a/application/pages/modplatform/technic/TechnicModel.h b/application/pages/modplatform/technic/TechnicModel.h
new file mode 100644
index 00000000..82a03842
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicModel.h
@@ -0,0 +1,70 @@
+/* Copyright 2020-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 <QModelIndex>
+
+#include "TechnicData.h"
+#include "net/NetJob.h"
+
+namespace Technic {
+
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ virtual QVariant data(const QModelIndex& index, int role) const;
+ virtual int columnCount(const QModelIndex& parent) const;
+ virtual int rowCount(const QModelIndex& parent) const;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void searchRequestFinished();
+ void searchRequestFailed();
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QString out);
+
+private:
+ void performSearch();
+ void requestLogo(QString logo, QString url);
+
+private:
+ QList<Modpack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ QMap<QString, QIcon> m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/application/pages/modplatform/technic/TechnicPage.cpp b/application/pages/modplatform/technic/TechnicPage.cpp
new file mode 100644
index 00000000..e836f767
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicPage.cpp
@@ -0,0 +1,198 @@
+/* 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 "TechnicPage.h"
+#include "ui_TechnicPage.h"
+
+#include "MultiMC.h"
+#include "dialogs/NewInstanceDialog.h"
+#include "TechnicModel.h"
+#include <QKeyEvent>
+#include "modplatform/technic/SingleZipPackInstallTask.h"
+#include "modplatform/technic/SolderPackInstallTask.h"
+#include "Json.h"
+
+TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ model = new Technic::ListModel(this);
+ ui->packView->setModel(model);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
+}
+
+bool TechnicPage::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);
+}
+
+TechnicPage::~TechnicPage()
+{
+ delete ui;
+}
+
+bool TechnicPage::shouldDisplay() const
+{
+ return true;
+}
+
+void TechnicPage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void TechnicPage::triggerSearch() {
+ model->searchWithTerm(ui->searchEdit->text());
+}
+
+void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ //ui->frame->clear();
+ return;
+ }
+
+ current = model->data(first, Qt::UserRole).value<Technic::Modpack>();
+ suggestCurrent();
+}
+
+void TechnicPage::suggestCurrent()
+{
+ if (!isOpened)
+ {
+ return;
+ }
+ if (current.broken)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0);
+ model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+
+ if (current.metadataLoaded)
+ {
+ metadataLoaded();
+ return;
+ }
+
+ NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ QString slug = current.slug;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
+ {
+ if (current.slug != slug)
+ {
+ return;
+ }
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ QJsonObject obj = doc.object();
+ if(parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ if (!obj.contains("url"))
+ {
+ qWarning() << "Json doesn't contain an url key";
+ return;
+ }
+ QJsonValueRef url = obj["url"];
+ if (url.isString())
+ {
+ current.url = url.toString();
+ }
+ else
+ {
+ if (!obj.contains("solder"))
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ QJsonValueRef solderUrl = obj["solder"];
+ if (solderUrl.isString())
+ {
+ current.url = solderUrl.toString();
+ current.isSolder = true;
+ }
+ else
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ }
+
+ current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__");
+ current.author = Json::ensureString(obj, "user", QString(), "__placeholder__");
+ current.description = Json::ensureString(obj, "description", QString(), "__placeholder__");
+ current.metadataLoaded = true;
+ metadataLoaded();
+ });
+ netJob->start();
+}
+
+// expects current.metadataLoaded to be true
+void TechnicPage::metadataLoaded()
+{
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ // This allows injecting HTML here.
+ text = name;
+ else
+ // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML.
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ if (!current.author.isEmpty()) {
+ // This allows injecting HTML here
+ text += tr(" by ") + current.author;
+ }
+
+ ui->frame->setModText(text);
+ ui->frame->setModDescription(current.description);
+ if (!current.isSolder)
+ {
+ dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
+ }
+ else
+ {
+ while (current.url.endsWith('/')) current.url.chop(1);
+ dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion));
+ }
+}
diff --git a/application/pages/modplatform/TechnicPage.h b/application/pages/modplatform/technic/TechnicPage.h
index 84ea4636..27e1258a 100644
--- a/application/pages/modplatform/TechnicPage.h
+++ b/application/pages/modplatform/technic/TechnicPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2018 MultiMC Contributors
+/* 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.
@@ -20,6 +20,7 @@
#include "pages/BasePage.h"
#include <MultiMC.h>
#include "tasks/Task.h"
+#include "TechnicData.h"
namespace Ui
{
@@ -28,6 +29,10 @@ class TechnicPage;
class NewInstanceDialog;
+namespace Technic {
+ class ListModel;
+}
+
class TechnicPage : public QWidget, public BasePage
{
Q_OBJECT
@@ -55,7 +60,19 @@ public:
void openedImpl() override;
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ void suggestCurrent();
+ void metadataLoaded();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+
private:
Ui::TechnicPage *ui = nullptr;
NewInstanceDialog* dialog = nullptr;
+ Technic::ListModel* model = nullptr;
+ Technic::Modpack current;
};
diff --git a/application/pages/modplatform/technic/TechnicPage.ui b/application/pages/modplatform/technic/TechnicPage.ui
new file mode 100644
index 00000000..2ca45dd2
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicPage.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TechnicPage</class>
+ <widget class="QWidget" name="TechnicPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <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>
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>