aboutsummaryrefslogtreecommitdiff
path: root/application/pages/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'application/pages/modplatform')
-rw-r--r--application/pages/modplatform/ImportPage.h2
-rw-r--r--application/pages/modplatform/VanillaPage.h2
-rw-r--r--application/pages/modplatform/atlauncher/AtlFilterModel.cpp67
-rw-r--r--application/pages/modplatform/atlauncher/AtlFilterModel.h32
-rw-r--r--application/pages/modplatform/atlauncher/AtlModel.cpp185
-rw-r--r--application/pages/modplatform/atlauncher/AtlModel.h52
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.cpp100
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.h78
-rw-r--r--application/pages/modplatform/atlauncher/AtlPage.ui55
-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.cpp (renamed from application/pages/modplatform/ftb/FtbModel.cpp)19
-rw-r--r--application/pages/modplatform/ftb/FtbListModel.h (renamed from application/pages/modplatform/ftb/FtbModel.h)0
-rw-r--r--application/pages/modplatform/ftb/FtbPage.cpp34
-rw-r--r--application/pages/modplatform/ftb/FtbPage.h9
-rw-r--r--application/pages/modplatform/ftb/FtbPage.ui41
-rw-r--r--application/pages/modplatform/legacy_ftb/Page.h2
-rw-r--r--application/pages/modplatform/technic/TechnicData.h42
-rw-r--r--application/pages/modplatform/technic/TechnicModel.cpp231
-rw-r--r--application/pages/modplatform/technic/TechnicModel.h70
-rw-r--r--application/pages/modplatform/technic/TechnicPage.cpp199
-rw-r--r--application/pages/modplatform/technic/TechnicPage.h78
-rw-r--r--application/pages/modplatform/technic/TechnicPage.ui91
-rw-r--r--application/pages/modplatform/twitch/TwitchModel.cpp2
-rw-r--r--application/pages/modplatform/twitch/TwitchPage.h2
25 files changed, 1451 insertions, 39 deletions
diff --git a/application/pages/modplatform/ImportPage.h b/application/pages/modplatform/ImportPage.h
index 3afb0045..67e3c201 100644
--- a/application/pages/modplatform/ImportPage.h
+++ b/application/pages/modplatform/ImportPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2019 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.h b/application/pages/modplatform/VanillaPage.h
index cc77733c..af6fd392 100644
--- a/application/pages/modplatform/VanillaPage.h
+++ b/application/pages/modplatform/VanillaPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2019 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/atlauncher/AtlFilterModel.cpp b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp
new file mode 100644
index 00000000..8ea1546a
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp
@@ -0,0 +1,67 @@
+#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);
+}
+
+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
+{
+ 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..2aef81fb
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlFilterModel.h
@@ -0,0 +1,32 @@
+#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();
+
+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/atlauncher/AtlModel.cpp b/application/pages/modplatform/atlauncher/AtlModel.cpp
new file mode 100644
index 00000000..46e35ec6
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlModel.cpp
@@ -0,0 +1,185 @@
+#include "AtlModel.h"
+
+#include <BuildConfig.h>
+#include <MultiMC.h>
+#include <Env.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.description;
+ }
+ 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;
+ ATLauncher::loadIndexedPack(pack, packObj);
+
+ // 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/AtlModel.h b/application/pages/modplatform/atlauncher/AtlModel.h
new file mode 100644
index 00000000..2d30a64e
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlModel.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/AtlPage.cpp b/application/pages/modplatform/atlauncher/AtlPage.cpp
new file mode 100644
index 00000000..cfc61e8d
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.cpp
@@ -0,0 +1,100 @@
+#include "AtlPage.h"
+#include "ui_AtlPage.h"
+
+#include "dialogs/NewInstanceDialog.h"
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+#include <BuildConfig.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);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ 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()
+{
+ listModel->request();
+}
+
+void AtlPage::suggestCurrent()
+{
+ if(isOpened) {
+ dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(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::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>();
+
+ 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();
+}
diff --git a/application/pages/modplatform/atlauncher/AtlPage.h b/application/pages/modplatform/atlauncher/AtlPage.h
new file mode 100644
index 00000000..fceb0abf
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.h
@@ -0,0 +1,78 @@
+/* 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 "AtlModel.h"
+
+#include <QWidget>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class AtlPage;
+}
+
+class NewInstanceDialog;
+
+class AtlPage : public QWidget, public BasePage
+{
+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();
+
+private slots:
+ 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;
+};
diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/application/pages/modplatform/atlauncher/AtlPage.ui
new file mode 100644
index 00000000..fa88597e
--- /dev/null
+++ b/application/pages/modplatform/atlauncher/AtlPage.ui
@@ -0,0 +1,55 @@
+<?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>875</width>
+ <height>745</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" 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" colspan="2">
+ <widget class="QTreeView" name="packView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>96</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>packView</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/FtbModel.cpp b/application/pages/modplatform/ftb/FtbListModel.cpp
index ecdcb00b..63236827 100644
--- a/application/pages/modplatform/ftb/FtbModel.cpp
+++ b/application/pages/modplatform/ftb/FtbListModel.cpp
@@ -1,4 +1,4 @@
-#include "FtbModel.h"
+#include "FtbListModel.h"
#include "BuildConfig.h"
#include "Env.h"
@@ -79,7 +79,7 @@ void ListModel::performSearch()
auto *netJob = new NetJob("Ftb::Search");
QString searchUrl;
if(currentSearchTerm.isEmpty()) {
- searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/popular/plays/100";
+ searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all";
}
else {
searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1")
@@ -206,9 +206,18 @@ void ListModel::packRequestFinished()
return;
}
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
- modpacks.append(pack);
- endInsertRows();
+ // 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);
diff --git a/application/pages/modplatform/ftb/FtbModel.h b/application/pages/modplatform/ftb/FtbListModel.h
index 9c057d73..9c057d73 100644
--- a/application/pages/modplatform/ftb/FtbModel.h
+++ b/application/pages/modplatform/ftb/FtbListModel.h
diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/application/pages/modplatform/ftb/FtbPage.cpp
index c82508b3..60294de0 100644
--- a/application/pages/modplatform/ftb/FtbPage.cpp
+++ b/application/pages/modplatform/ftb/FtbPage.cpp
@@ -10,12 +10,26 @@ FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
{
ui->setupUi(this);
- connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch);
+
+ 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);
- model = new Ftb::ListModel(this);
- ui->packView->setModel(model);
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
+ 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);
}
@@ -59,7 +73,7 @@ void FtbPage::suggestCurrent()
QString editedLogoName;
editedLogoName = selected.name;
- model->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
+ listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
});
@@ -70,7 +84,13 @@ void FtbPage::suggestCurrent()
void FtbPage::triggerSearch()
{
- model->searchWithTerm(ui->searchEdit->text());
+ 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)
@@ -86,7 +106,7 @@ void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
return;
}
- selected = model->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
+ selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
// reverse foreach, so that the newest versions are first
for (auto i = selected.versions.size(); i--;) {
diff --git a/application/pages/modplatform/ftb/FtbPage.h b/application/pages/modplatform/ftb/FtbPage.h
index 80f152c6..c9c93897 100644
--- a/application/pages/modplatform/ftb/FtbPage.h
+++ b/application/pages/modplatform/ftb/FtbPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2019 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.
@@ -15,7 +15,8 @@
#pragma once
-#include "FtbModel.h"
+#include "FtbFilterModel.h"
+#include "FtbListModel.h"
#include <QWidget>
@@ -64,13 +65,15 @@ private:
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* model = 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
index 772b0276..3a2203db 100644
--- a/application/pages/modplatform/ftb/FtbPage.ui
+++ b/application/pages/modplatform/ftb/FtbPage.ui
@@ -11,22 +11,6 @@
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
- <item row="1" column="0" colspan="2">
- <widget class="QListView" name="packView">
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="iconSize">
- <size>
- <width>48</width>
- <height>48</height>
- </size>
- </property>
- <property name="uniformItemSizes">
- <bool>true</bool>
- </property>
- </widget>
- </item>
<item row="0" column="1">
<widget class="QPushButton" name="searchButton">
<property name="text">
@@ -38,19 +22,38 @@
<widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="2">
- <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0" rowminimumheight="0" columnminimumwidth="0,0">
- <item row="0" column="1">
+ <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="0">
+ <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="1" column="0" colspan="2">
+ <widget class="QTreeView" name="packView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
<tabstops>
diff --git a/application/pages/modplatform/legacy_ftb/Page.h b/application/pages/modplatform/legacy_ftb/Page.h
index ed6d1657..e840216e 100644
--- a/application/pages/modplatform/legacy_ftb/Page.h
+++ b/application/pages/modplatform/legacy_ftb/Page.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2019 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/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..bf256ab6
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicModel.cpp
@@ -0,0 +1,231 @@
+/* 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) {
+ 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");
+ auto 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..d246edf2
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicPage.cpp
@@ -0,0 +1,199 @@
+/* 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()
+{
+ dialog->setSuggestedPack();
+}
+
+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;
+ 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();
+ }
+ else
+ {
+ 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/technic/TechnicPage.h b/application/pages/modplatform/technic/TechnicPage.h
new file mode 100644
index 00000000..27e1258a
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicPage.h
@@ -0,0 +1,78 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+#include "TechnicData.h"
+
+namespace Ui
+{
+class TechnicPage;
+}
+
+class NewInstanceDialog;
+
+namespace Technic {
+ class ListModel;
+}
+
+class TechnicPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~TechnicPage();
+ virtual QString displayName() const override
+ {
+ return tr("Technic");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("technic");
+ }
+ virtual QString id() const override
+ {
+ return "technic";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Technic-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ 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..36ce2ecf
--- /dev/null
+++ b/application/pages/modplatform/technic/TechnicPage.ui
@@ -0,0 +1,91 @@
+<?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"/>
+ </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>
diff --git a/application/pages/modplatform/twitch/TwitchModel.cpp b/application/pages/modplatform/twitch/TwitchModel.cpp
index 9e3c3ad2..5c6c7858 100644
--- a/application/pages/modplatform/twitch/TwitchModel.cpp
+++ b/application/pages/modplatform/twitch/TwitchModel.cpp
@@ -104,7 +104,7 @@ void ListModel::requestLogo(QString logo, QString url)
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath]
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
{
emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo))
diff --git a/application/pages/modplatform/twitch/TwitchPage.h b/application/pages/modplatform/twitch/TwitchPage.h
index 04e3a1c6..093900ff 100644
--- a/application/pages/modplatform/twitch/TwitchPage.h
+++ b/application/pages/modplatform/twitch/TwitchPage.h
@@ -1,4 +1,4 @@
-/* Copyright 2013-2019 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.