diff options
Diffstat (limited to 'application')
-rw-r--r-- | application/CMakeLists.txt | 5 | ||||
-rw-r--r-- | application/dialogs/NewInstanceDialog.cpp | 6 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicData.h | 40 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicModel.cpp | 223 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicModel.h | 70 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicPage.cpp | 204 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicPage.h | 78 | ||||
-rw-r--r-- | application/pages/modplatform/technic/TechnicPage.ui | 62 | ||||
-rw-r--r-- | application/pages/modplatform/twitch/TwitchModel.cpp | 2 | ||||
-rw-r--r-- | application/resources/assets/underconstruction.png | bin | 0 -> 14490 bytes |
10 files changed, 688 insertions, 2 deletions
diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 802789a2..38bd586b 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -137,6 +137,10 @@ SET(MULTIMC_SOURCES pages/modplatform/twitch/TwitchModel.h pages/modplatform/twitch/TwitchPage.cpp pages/modplatform/twitch/TwitchPage.h + pages/modplatform/technic/TechnicModel.cpp + pages/modplatform/technic/TechnicModel.h + pages/modplatform/technic/TechnicPage.cpp + pages/modplatform/technic/TechnicPage.h pages/modplatform/ImportPage.cpp pages/modplatform/ImportPage.h @@ -257,6 +261,7 @@ SET(MULTIMC_UIS pages/modplatform/ftb/FtbPage.ui pages/modplatform/legacy_ftb/Page.ui pages/modplatform/twitch/TwitchPage.ui + pages/modplatform/technic/TechnicPage.ui pages/modplatform/ImportPage.ui # Dialogs diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index d8abdbd4..c2887b01 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2019 MultiMC Contributors +/* Copyright 2013-2020 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,8 @@ #include <pages/modplatform/legacy_ftb/Page.h> #include <pages/modplatform/twitch/TwitchPage.h> #include <pages/modplatform/ImportPage.h> +#include <pages/modplatform/technic/TechnicPage.h> + NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent) @@ -122,12 +124,14 @@ QList<BasePage *> NewInstanceDialog::getPages() { importPage = new ImportPage(this); twitchPage = new TwitchPage(this); + auto technicPage = new TechnicPage(this); return { new VanillaPage(this), importPage, new FtbPage(this), new LegacyFTB::Page(this), + technicPage, twitchPage }; } diff --git a/application/pages/modplatform/technic/TechnicData.h b/application/pages/modplatform/technic/TechnicData.h new file mode 100644 index 00000000..5c746619 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicData.h @@ -0,0 +1,40 @@ +/* Copyright 2020 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; +}; +} + +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..b3d36bac --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.cpp @@ -0,0 +1,223 @@ +/* Copyright 2020 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 <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; + auto objs = doc["modpacks"].toArray(); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = technicPack.toObject(); + pack.name = technicPackObject["name"].toString(); + pack.slug = technicPackObject["slug"].toString(); + if (pack.slug == "vanilla") + continue; + if (technicPackObject["iconUrl"].isString()) + { + pack.logoUrl = technicPackObject["iconUrl"].toString(); + pack.logoName = pack.logoUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else + { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + pack.broken = false; + newList.append(pack); + } + 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..bd0aec69 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.h @@ -0,0 +1,70 @@ +/* Copyright 2020 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..75efd3ed --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.cpp @@ -0,0 +1,204 @@ +/* Copyright 2013-2020 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.metadataLoaded = true; + metadataLoaded(); + }); + netJob->start(); + } +} + +// expects current.metadataLoaded to be true +void TechnicPage::metadataLoaded() +{ + /*QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](Technic::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += tr(" by ") + authorStrs.join(", "); + } + + 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..1a10af71 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2020 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..be56fa82 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,62 @@ +<?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> + </layout> + </widget> + <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/resources/assets/underconstruction.png b/application/resources/assets/underconstruction.png Binary files differnew file mode 100644 index 00000000..6ae06476 --- /dev/null +++ b/application/resources/assets/underconstruction.png |