aboutsummaryrefslogtreecommitdiff
path: root/application/pages
diff options
context:
space:
mode:
Diffstat (limited to 'application/pages')
-rw-r--r--application/pages/instance/VersionPage.cpp2
-rw-r--r--application/pages/modplatform/twitch/TwitchData.h38
-rw-r--r--application/pages/modplatform/twitch/TwitchModel.cpp314
-rw-r--r--application/pages/modplatform/twitch/TwitchModel.h76
-rw-r--r--application/pages/modplatform/twitch/TwitchPage.cpp86
-rw-r--r--application/pages/modplatform/twitch/TwitchPage.h77
-rw-r--r--application/pages/modplatform/twitch/TwitchPage.ui63
7 files changed, 655 insertions, 1 deletions
diff --git a/application/pages/instance/VersionPage.cpp b/application/pages/instance/VersionPage.cpp
index 20298117..60ff8301 100644
--- a/application/pages/instance/VersionPage.cpp
+++ b/application/pages/instance/VersionPage.cpp
@@ -206,7 +206,7 @@ void VersionPage::updateVersionControls()
bool newCraft = controlsEnabled && (minecraftVersion >= Version("1.14"));
bool oldCraft = controlsEnabled && (minecraftVersion <= Version("1.12.2"));
ui->actionInstall_Fabric->setEnabled(newCraft);
- ui->actionInstall_Forge->setEnabled(oldCraft);
+ ui->actionInstall_Forge->setEnabled(true);
ui->actionInstall_LiteLoader->setEnabled(oldCraft);
ui->actionReload->setEnabled(true);
updateButtons();
diff --git a/application/pages/modplatform/twitch/TwitchData.h b/application/pages/modplatform/twitch/TwitchData.h
new file mode 100644
index 00000000..dd000b84
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchData.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+
+namespace Twitch {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct ModpackFile {
+ int addonId;
+ int fileId;
+ QString version;
+ QString mcVersion;
+ QString downloadUrl;
+};
+
+struct Modpack
+{
+ bool broken = true;
+ int addonId = 0;
+
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString mcVersion;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ ModpackFile latestFile;
+};
+}
+
+Q_DECLARE_METATYPE(Twitch::Modpack)
diff --git a/application/pages/modplatform/twitch/TwitchModel.cpp b/application/pages/modplatform/twitch/TwitchModel.cpp
new file mode 100644
index 00000000..d9358941
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchModel.cpp
@@ -0,0 +1,314 @@
+#include "TwitchModel.h"
+#include "MultiMC.h"
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+#include <Env.h>
+
+#include "net/URLConstants.h"
+
+namespace Twitch {
+
+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);
+ }
+
+ Modpack 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("TwitchPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("Twitch 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]
+ {
+ 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("TwitchPacks", 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("Twitch::Search");
+ auto searchUrl = QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "categoryId=0&"
+ "gameId=432&"
+ //"gameVersion=1.12.2&"
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sectionId=4471&"
+ "sort=0"
+ ).arg(nextSearchOffset).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::searchWithTerm(const QString& term)
+{
+ if(currentSearchTerm == term) {
+ return;
+ }
+ currentSearchTerm = term;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void Twitch::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 Twitch at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Modpack> newList;
+ auto objs = doc.array();
+ for(auto projectIter: objs) {
+ Modpack pack;
+ auto project = projectIter.toObject();
+ pack.addonId = project.value("id").toInt(0);
+ if (pack.addonId == 0) {
+ qWarning() << "Pack without an ID, skipping: " << pack.name;
+ continue;
+ }
+ pack.name = project.value("name").toString();
+ pack.websiteUrl = project.value("websiteUrl").toString();
+ pack.description = project.value("summary").toString();
+ bool thumbnailFound = false;
+ auto attachments = project.value("attachments").toArray();
+ for(auto attachmentIter: attachments) {
+ auto attachment = attachmentIter.toObject();
+ bool isDefault = attachment.value("isDefault").toBool(false);
+ if(isDefault) {
+ thumbnailFound = true;
+ pack.logoName = attachment.value("title").toString();
+ pack.logoUrl = attachment.value("thumbnailUrl").toString();
+ break;
+ }
+ }
+ if(!thumbnailFound) {
+ qWarning() << "Pack without an icon, skipping: " << pack.name;
+ continue;
+ }
+ auto authors = project.value("authors").toArray();
+ for(auto authorIter: authors) {
+ auto author = authorIter.toObject();
+ ModpackAuthor packAuthor;
+ packAuthor.name = author.value("name").toString();
+ packAuthor.url = author.value("url").toString();
+ pack.authors.append(packAuthor);
+ }
+ int defaultFileId = project.value("defaultFileId").toInt(0);
+ if(defaultFileId == 0) {
+ qWarning() << "Pack without default file, skipping: " << pack.name;
+ continue;
+ }
+ bool found = false;
+ auto files = project.value("latestFiles").toArray();
+ for(auto fileIter: files) {
+ auto file = fileIter.toObject();
+ int id = file.value("id").toInt(0);
+ // NOTE: for now, ignore everything that's not the default...
+ if(id != defaultFileId) {
+ continue;
+ }
+ pack.latestFile.addonId = pack.addonId;
+ pack.latestFile.fileId = id;
+ // FIXME: what to do when there's more than one, or there's no version?
+ auto versionArray = file.value("gameVersion").toArray();
+ if(versionArray.size() != 1) {
+ continue;
+ }
+ pack.latestFile.mcVersion = versionArray[0].toString();
+ pack.latestFile.version = file.value("displayName").toString();
+ pack.latestFile.downloadUrl = file.value("downloadUrl").toString();
+ found = true;
+ break;
+ }
+ if(!found) {
+ qWarning() << "Pack with no good file, skipping: " << pack.name;
+ continue;
+ }
+ pack.broken = false;
+ newList.append(pack);
+ }
+ if(objs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Twitch::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/twitch/TwitchModel.h b/application/pages/modplatform/twitch/TwitchModel.h
new file mode 100644
index 00000000..ad355c64
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchModel.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <modplatform/legacy_ftb/PackHelpers.h>
+#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 "TwitchData.h"
+
+namespace Twitch {
+
+
+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);
+
+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<Modpack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ int nextSearchOffset = 0;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/application/pages/modplatform/twitch/TwitchPage.cpp b/application/pages/modplatform/twitch/TwitchPage.cpp
new file mode 100644
index 00000000..80d83133
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchPage.cpp
@@ -0,0 +1,86 @@
+#include "TwitchPage.h"
+#include "ui_TwitchPage.h"
+
+#include "MultiMC.h"
+#include "dialogs/NewInstanceDialog.h"
+#include <InstanceImportTask.h>
+#include "TwitchModel.h"
+#include <QKeyEvent>
+
+TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::TwitchPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &TwitchPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ model = new Twitch::ListModel(this);
+ ui->packView->setModel(model);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TwitchPage::onSelectionChanged);
+}
+
+TwitchPage::~TwitchPage()
+{
+ delete ui;
+}
+
+bool TwitchPage::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 TwitchPage::shouldDisplay() const
+{
+ return true;
+}
+
+void TwitchPage::openedImpl()
+{
+ suggestCurrent();
+}
+
+void TwitchPage::triggerSearch()
+{
+ model->searchWithTerm(ui->searchEdit->text());
+}
+
+void TwitchPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+ current = model->data(first, Qt::UserRole).value<Twitch::Modpack>();
+ suggestCurrent();
+}
+
+void TwitchPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+ if(current.broken)
+ {
+ dialog->setSuggestedPack();
+ }
+
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(current.latestFile.downloadUrl));
+ QString editedLogoName;
+ editedLogoName = "twitch_" + current.logoName.section(".", 0, 0);
+ model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
diff --git a/application/pages/modplatform/twitch/TwitchPage.h b/application/pages/modplatform/twitch/TwitchPage.h
new file mode 100644
index 00000000..04e3a1c6
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchPage.h
@@ -0,0 +1,77 @@
+/* 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 <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+#include "TwitchData.h"
+
+namespace Ui
+{
+class TwitchPage;
+}
+
+class NewInstanceDialog;
+
+namespace Twitch {
+ class ListModel;
+}
+
+class TwitchPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit TwitchPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~TwitchPage();
+ virtual QString displayName() const override
+ {
+ return tr("Twitch");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("twitch");
+ }
+ virtual QString id() const override
+ {
+ return "twitch";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Twitch-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+
+private:
+ Ui::TwitchPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Twitch::ListModel* model = nullptr;
+ Twitch::Modpack current;
+};
diff --git a/application/pages/modplatform/twitch/TwitchPage.ui b/application/pages/modplatform/twitch/TwitchPage.ui
new file mode 100644
index 00000000..7a8203b1
--- /dev/null
+++ b/application/pages/modplatform/twitch/TwitchPage.ui
@@ -0,0 +1,63 @@
+<?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>875</width>
+ <height>745</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit"/>
+ </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">
+ <widget class="QTreeView" 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>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ </tabstops>
+ <connections/>
+</ui>