From a0cb1a0d427087c84690224b37c2c9d0fba4f6cb Mon Sep 17 00:00:00 2001 From: kb1000 Date: Wed, 24 Mar 2021 00:59:43 +0100 Subject: NOISSUE rename Twitch to flame internally for consistency and to CurseForge for user displayed strings --- application/pages/modplatform/flame/FlameData.h | 38 +++ application/pages/modplatform/flame/FlameModel.cpp | 312 +++++++++++++++++++++ application/pages/modplatform/flame/FlameModel.h | 75 +++++ application/pages/modplatform/flame/FlamePage.cpp | 111 ++++++++ application/pages/modplatform/flame/FlamePage.h | 77 +++++ application/pages/modplatform/flame/FlamePage.ui | 91 ++++++ application/pages/modplatform/twitch/TwitchData.h | 38 --- .../pages/modplatform/twitch/TwitchModel.cpp | 312 --------------------- application/pages/modplatform/twitch/TwitchModel.h | 76 ----- .../pages/modplatform/twitch/TwitchPage.cpp | 111 -------- application/pages/modplatform/twitch/TwitchPage.h | 77 ----- application/pages/modplatform/twitch/TwitchPage.ui | 73 ----- 12 files changed, 704 insertions(+), 687 deletions(-) create mode 100644 application/pages/modplatform/flame/FlameData.h create mode 100644 application/pages/modplatform/flame/FlameModel.cpp create mode 100644 application/pages/modplatform/flame/FlameModel.h create mode 100644 application/pages/modplatform/flame/FlamePage.cpp create mode 100644 application/pages/modplatform/flame/FlamePage.h create mode 100644 application/pages/modplatform/flame/FlamePage.ui delete mode 100644 application/pages/modplatform/twitch/TwitchData.h delete mode 100644 application/pages/modplatform/twitch/TwitchModel.cpp delete mode 100644 application/pages/modplatform/twitch/TwitchModel.h delete mode 100644 application/pages/modplatform/twitch/TwitchPage.cpp delete mode 100644 application/pages/modplatform/twitch/TwitchPage.h delete mode 100644 application/pages/modplatform/twitch/TwitchPage.ui (limited to 'application/pages/modplatform') diff --git a/application/pages/modplatform/flame/FlameData.h b/application/pages/modplatform/flame/FlameData.h new file mode 100644 index 00000000..9245ba8a --- /dev/null +++ b/application/pages/modplatform/flame/FlameData.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace Flame { + +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 authors; + QString mcVersion; + QString logoName; + QString logoUrl; + QString websiteUrl; + + ModpackFile latestFile; +}; +} + +Q_DECLARE_METATYPE(Flame::Modpack) diff --git a/application/pages/modplatform/flame/FlameModel.cpp b/application/pages/modplatform/flame/FlameModel.cpp new file mode 100644 index 00000000..cd3109e0 --- /dev/null +++ b/application/pages/modplatform/flame/FlameModel.cpp @@ -0,0 +1,312 @@ +#include "FlameModel.h" +#include "MultiMC.h" + +#include +#include + +#include +#include + +#include +#include + +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); + } + + 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("
")).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&" + //"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 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 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 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..b4dded76 --- /dev/null +++ b/application/pages/modplatform/flame/FlameModel.h @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "FlameData.h" + +namespace Flame { + + +typedef QMap LogoMap; +typedef std::function 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 modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + 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..3889f15a --- /dev/null +++ b/application/pages/modplatform/flame/FlamePage.cpp @@ -0,0 +1,111 @@ +#include "FlamePage.h" +#include "ui_FlamePage.h" + +#include "MultiMC.h" +#include "dialogs/NewInstanceDialog.h" +#include +#include "FlameModel.h" +#include + +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); + model = new Flame::ListModel(this); + ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); +} + +FlamePage::~FlamePage() +{ + delete ui; +} + +bool FlamePage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(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(); +} + +void FlamePage::triggerSearch() +{ + model->searchWithTerm(ui->searchEdit->text()); +} + +void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + ui->frame->clear(); + return; + } + + current = model->data(first, Qt::UserRole).value(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += tr(" by ") + authorStrs.join(", "); + } + + ui->frame->setModText(text); + ui->frame->setModDescription(current.description); + suggestCurrent(); +} + +void FlamePage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + if(current.broken) + { + dialog->setSuggestedPack(); + } + + dialog->setSuggestedPack(current.name, new InstanceImportTask(current.latestFile.downloadUrl)); + QString editedLogoName; + editedLogoName = "curseforge_" + 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/flame/FlamePage.h b/application/pages/modplatform/flame/FlamePage.h new file mode 100644 index 00000000..e50186f5 --- /dev/null +++ b/application/pages/modplatform/flame/FlamePage.h @@ -0,0 +1,77 @@ +/* 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 + +#include "pages/BasePage.h" +#include +#include "tasks/Task.h" +#include "FlameData.h" + +namespace Ui +{ +class FlamePage; +} + +class NewInstanceDialog; + +namespace Flame { + class ListModel; +} + +class FlamePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FlamePage(); + virtual QString displayName() const override + { + return tr("CurseForge"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("flame"); + } + virtual QString id() const override + { + return "flame"; + } + virtual QString helpPage() const override + { + return "Flame-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::FlamePage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Flame::ListModel* model = nullptr; + Flame::Modpack current; +}; diff --git a/application/pages/modplatform/flame/FlamePage.ui b/application/pages/modplatform/flame/FlamePage.ui new file mode 100644 index 00000000..21e23f1f --- /dev/null +++ b/application/pages/modplatform/flame/FlamePage.ui @@ -0,0 +1,91 @@ + + + FlamePage + + + + 0 + 0 + 875 + 745 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Search + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + MCModInfoFrame + QFrame +
widgets/MCModInfoFrame.h
+ 1 +
+
+ + searchEdit + searchButton + packView + + + +
diff --git a/application/pages/modplatform/twitch/TwitchData.h b/application/pages/modplatform/twitch/TwitchData.h deleted file mode 100644 index dd000b84..00000000 --- a/application/pages/modplatform/twitch/TwitchData.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include - -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 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 deleted file mode 100644 index 5c6c7858..00000000 --- a/application/pages/modplatform/twitch/TwitchModel.cpp +++ /dev/null @@ -1,312 +0,0 @@ -#include "TwitchModel.h" -#include "MultiMC.h" - -#include -#include - -#include -#include - -#include -#include - -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("
")).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::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("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 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 deleted file mode 100644 index ad355c64..00000000 --- a/application/pages/modplatform/twitch/TwitchModel.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "TwitchData.h" - -namespace Twitch { - - -typedef QMap LogoMap; -typedef std::function 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 modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap 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 deleted file mode 100644 index 1e9f9dbb..00000000 --- a/application/pages/modplatform/twitch/TwitchPage.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "TwitchPage.h" -#include "ui_TwitchPage.h" - -#include "MultiMC.h" -#include "dialogs/NewInstanceDialog.h" -#include -#include "TwitchModel.h" -#include - -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(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(); - } - ui->frame->clear(); - return; - } - - current = model->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](Twitch::ModpackAuthor & author) { - if(author.url.isEmpty()) { - return author.name; - } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for(auto & author: current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += tr(" by ") + authorStrs.join(", "); - } - - ui->frame->setModText(text); - ui->frame->setModDescription(current.description); - 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 deleted file mode 100644 index 093900ff..00000000 --- a/application/pages/modplatform/twitch/TwitchPage.h +++ /dev/null @@ -1,77 +0,0 @@ -/* 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 - -#include "pages/BasePage.h" -#include -#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 deleted file mode 100644 index c78d8ce0..00000000 --- a/application/pages/modplatform/twitch/TwitchPage.ui +++ /dev/null @@ -1,73 +0,0 @@ - - - TwitchPage - - - - 0 - 0 - 875 - 745 - - - - - - - - - - Search - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - - MCModInfoFrame - QFrame -
widgets/MCModInfoFrame.h
- 1 -
-
- - searchEdit - searchButton - packView - - - -
-- cgit