diff options
author | Petr Mrázek <peterix@gmail.com> | 2021-07-25 19:11:59 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2021-07-25 19:50:44 +0200 |
commit | 20b9f2b42a3b58b6081af271774fbcc34025dccb (patch) | |
tree | 064fa59facb3357139b47bd4e60bfc8edb35ca11 /launcher/pages/modplatform | |
parent | dd133680858351e3e07690e286882327a4f42ba5 (diff) | |
download | PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.gz PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.bz2 PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.zip |
NOISSUE Flatten gui and logic libraries into MultiMC
Diffstat (limited to 'launcher/pages/modplatform')
39 files changed, 4811 insertions, 0 deletions
diff --git a/launcher/pages/modplatform/ImportPage.cpp b/launcher/pages/modplatform/ImportPage.cpp new file mode 100644 index 00000000..c2369bdc --- /dev/null +++ b/launcher/pages/modplatform/ImportPage.cpp @@ -0,0 +1,130 @@ +#include "ImportPage.h" +#include "ui_ImportPage.h" + +#include "MultiMC.h" +#include "dialogs/NewInstanceDialog.h" +#include <QFileDialog> +#include <QValidator> +#include <InstanceImportTask.h> + +class UrlValidator : public QValidator +{ +public: + using QValidator::QValidator; + + State validate(QString &in, int &pos) const + { + const QUrl url(in); + if (url.isValid() && !url.isRelative() && !url.isEmpty()) + { + return Acceptable; + } + else if (QFile::exists(in)) + { + return Acceptable; + } + else + { + return Intermediate; + } + } +}; + +ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog) +{ + ui->setupUi(this); + ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit)); + connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState); +} + +ImportPage::~ImportPage() +{ + delete ui; +} + +bool ImportPage::shouldDisplay() const +{ + return true; +} + +void ImportPage::openedImpl() +{ + updateState(); +} + +void ImportPage::updateState() +{ + if(!isOpened) + { + return; + } + if(ui->modpackEdit->hasAcceptableInput()) + { + QString input = ui->modpackEdit->text(); + auto url = QUrl::fromUserInput(input); + if(url.isLocalFile()) + { + // FIXME: actually do some validation of what's inside here... this is fake AF + QFileInfo fi(input); + if(fi.exists() && fi.suffix() == "zip") + { + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + if(input.endsWith("?client=y")) { + input.chop(9); + input.append("/file"); + url = QUrl::fromUserInput(input); + } + // hook, line and sinker. + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + dialog->setSuggestedPack(); + } +} + +void ImportPage::setUrl(const QString& url) +{ + ui->modpackEdit->setText(url); + updateState(); +} + +void ImportPage::on_modpackBtn_clicked() +{ + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)")); + if (url.isValid()) + { + if (url.isLocalFile()) + { + ui->modpackEdit->setText(url.toLocalFile()); + } + else + { + ui->modpackEdit->setText(url.toString()); + } + } +} + + +QUrl ImportPage::modpackUrl() const +{ + const QUrl url(ui->modpackEdit->text()); + if (url.isValid() && !url.isRelative() && !url.host().isEmpty()) + { + return url; + } + else + { + return QUrl::fromLocalFile(ui->modpackEdit->text()); + } +} diff --git a/launcher/pages/modplatform/ImportPage.h b/launcher/pages/modplatform/ImportPage.h new file mode 100644 index 00000000..67e3c201 --- /dev/null +++ b/launcher/pages/modplatform/ImportPage.h @@ -0,0 +1,70 @@ +/* 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" + +namespace Ui +{ +class ImportPage; +} + +class NewInstanceDialog; + +class ImportPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ImportPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~ImportPage(); + virtual QString displayName() const override + { + return tr("Import from zip"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("viewfolder"); + } + virtual QString id() const override + { + return "import"; + } + virtual QString helpPage() const override + { + return "Zip-import"; + } + virtual bool shouldDisplay() const override; + + void setUrl(const QString & url); + void openedImpl() override; + +private slots: + void on_modpackBtn_clicked(); + void updateState(); + +private: + QUrl modpackUrl() const; + +private: + Ui::ImportPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; +}; + diff --git a/launcher/pages/modplatform/ImportPage.ui b/launcher/pages/modplatform/ImportPage.ui new file mode 100644 index 00000000..eb63cbe9 --- /dev/null +++ b/launcher/pages/modplatform/ImportPage.ui @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImportPage</class> + <widget class="QWidget" name="ImportPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>405</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1"> + <widget class="QPushButton" name="modpackBtn"> + <property name="text"> + <string>Browse</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="modpackEdit"> + <property name="placeholderText"> + <string notr="true">http://</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="modpackLabel"> + <property name="text"> + <string>Local file or link to a direct download:</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/VanillaPage.cpp b/launcher/pages/modplatform/VanillaPage.cpp new file mode 100644 index 00000000..02638315 --- /dev/null +++ b/launcher/pages/modplatform/VanillaPage.cpp @@ -0,0 +1,104 @@ +#include "VanillaPage.h" +#include "ui_VanillaPage.h" + +#include "MultiMC.h" + +#include <meta/Index.h> +#include <meta/VersionList.h> +#include <dialogs/NewInstanceDialog.h> +#include <Filter.h> +#include <Env.h> +#include <InstanceCreationTask.h> +#include <QTabBar> + +VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion); + filterChanged(); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); +} + +void VanillaPage::openedImpl() +{ + if(!initialized) + { + auto vlist = ENV.metadataIndex()->get("net.minecraft"); + ui->versionList->initialize(vlist.get()); + initialized = true; + } + else + { + suggestCurrent(); + } +} + +void VanillaPage::refresh() +{ + ui->versionList->loadList(); +} + +void VanillaPage::filterChanged() +{ + QStringList out; + if(ui->alphaFilter->isChecked()) + out << "(old_alpha)"; + if(ui->betaFilter->isChecked()) + out << "(old_beta)"; + if(ui->snapshotFilter->isChecked()) + out << "(snapshot)"; + if(ui->oldSnapshotFilter->isChecked()) + out << "(old_snapshot)"; + if(ui->releaseFilter->isChecked()) + out << "(release)"; + if(ui->experimentsFilter->isChecked()) + out << "(experiment)"; + auto regexp = out.join('|'); + ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); +} + +VanillaPage::~VanillaPage() +{ + delete ui; +} + +bool VanillaPage::shouldDisplay() const +{ + return true; +} + +BaseVersionPtr VanillaPage::selectedVersion() const +{ + return m_selectedVersion; +} + +void VanillaPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + + if(!m_selectedVersion) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + dialog->setSuggestedIcon("default"); +} + +void VanillaPage::setSelectedVersion(BaseVersionPtr version) +{ + m_selectedVersion = version; + suggestCurrent(); +} diff --git a/launcher/pages/modplatform/VanillaPage.h b/launcher/pages/modplatform/VanillaPage.h new file mode 100644 index 00000000..af6fd392 --- /dev/null +++ b/launcher/pages/modplatform/VanillaPage.h @@ -0,0 +1,75 @@ +/* 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" + +namespace Ui +{ +class VanillaPage; +} + +class NewInstanceDialog; + +class VanillaPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0); + virtual ~VanillaPage(); + virtual QString displayName() const override + { + return tr("Vanilla"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("minecraft"); + } + virtual QString id() const override + { + return "vanilla"; + } + virtual QString helpPage() const override + { + return "Vanilla-platform"; + } + virtual bool shouldDisplay() const override; + void openedImpl() override; + + BaseVersionPtr selectedVersion() const; + +public slots: + void setSelectedVersion(BaseVersionPtr version); + +private slots: + void filterChanged(); + +private: + void refresh(); + void suggestCurrent(); + +private: + bool initialized = false; + NewInstanceDialog *dialog = nullptr; + Ui::VanillaPage *ui = nullptr; + bool m_versionSetByUser = false; + BaseVersionPtr m_selectedVersion; +}; diff --git a/launcher/pages/modplatform/VanillaPage.ui b/launcher/pages/modplatform/VanillaPage.ui new file mode 100644 index 00000000..47effc86 --- /dev/null +++ b/launcher/pages/modplatform/VanillaPage.ui @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VanillaPage</class> + <widget class="QWidget" name="VanillaPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>815</width> + <height>607</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true"/> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Filter</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="releaseFilter"> + <property name="text"> + <string>Releases</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="snapshotFilter"> + <property name="text"> + <string>Snapshots</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="oldSnapshotFilter"> + <property name="text"> + <string>Old Snapshots</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="betaFilter"> + <property name="text"> + <string>Betas</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="alphaFilter"> + <property name="text"> + <string>Alphas</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="experimentsFilter"> + <property name="text"> + <string>Experiments</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refreshBtn"> + <property name="text"> + <string>Refresh</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="VersionSelectWidget" name="versionList" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>VersionSelectWidget</class> + <extends>QWidget</extends> + <header>widgets/VersionSelectWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>releaseFilter</tabstop> + <tabstop>snapshotFilter</tabstop> + <tabstop>oldSnapshotFilter</tabstop> + <tabstop>betaFilter</tabstop> + <tabstop>alphaFilter</tabstop> + <tabstop>experimentsFilter</tabstop> + <tabstop>refreshBtn</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp new file mode 100644 index 00000000..b5d8f22b --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -0,0 +1,81 @@ +#include "AtlFilterModel.h" + +#include <QDebug> + +#include <modplatform/atlauncher/ATLPackIndex.h> +#include <Version.h> +#include <MMCStrings.h> + +namespace Atl { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByPopularity; + sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity); + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); + + searchTerm = ""; +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value<ATLauncher::IndexedPack>(); + ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value<ATLauncher::IndexedPack>(); + + if (currentSorting == ByPopularity) { + return leftPack.position > rightPack.position; + } + else if (currentSorting == ByGameVersion) { + Version lv(leftPack.versions.at(0).minecraft); + Version rv(rightPack.versions.at(0).minecraft); + return lv < rv; + } + else if (currentSorting == ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} diff --git a/launcher/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/pages/modplatform/atlauncher/AtlFilterModel.h new file mode 100644 index 00000000..bd72ad91 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlFilterModel.h @@ -0,0 +1,34 @@ +#pragma once + +#include <QtCore/QSortFilterProxyModel> + +namespace Atl { + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPopularity, + ByGameVersion, + ByName, + }; + const QMap<QString, Sorting> getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(QString term); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap<QString, Sorting> sortings; + Sorting currentSorting; + QString searchTerm; + +}; + +} diff --git a/launcher/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp new file mode 100644 index 00000000..f3be6198 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp @@ -0,0 +1,194 @@ +#include "AtlListModel.h" + +#include <BuildConfig.h> +#include <MultiMC.h> +#include <Env.h> +#include <Json.h> + +namespace Atl { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ATLauncher::IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.safeName)) + { + return (m_logoMap.value(pack.safeName)); + } + auto icon = MMC->getThemedIcon("atlauncher-placeholder"); + + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel *)this)->requestLogo(pack.safeName, url); + + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::request() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); + + auto *netJob = new NetJob("Atl::Request"); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::requestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<ATLauncher::IndexedPack> newList; + + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ATLauncher::IndexedPack pack; + + try { + ATLauncher::loadIndexedPack(pack, packObj); + } + catch (const JSONValidationError &e) { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); + return; + } + + // ignore packs without a published version + if(pack.versions.length() == 0) continue; + // only display public packs (for now) + if(pack.type != ATLauncher::PackType::Public) continue; + // ignore "system" packs (Vanilla, Vanilla with Forge, etc) + if(pack.system) continue; + + newList.append(pack); + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::requestFailed(QString reason) +{ + jobPtr.reset(); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::requestLogo(QString file, QString url) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(); + + m_loadingLogos.append(file); +} + +} diff --git a/launcher/pages/modplatform/atlauncher/AtlListModel.h b/launcher/pages/modplatform/atlauncher/AtlListModel.h new file mode 100644 index 00000000..2d30a64e --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlListModel.h @@ -0,0 +1,52 @@ +#pragma once + +#include <QAbstractListModel> + +#include "net/NetJob.h" +#include <QIcon> +#include <modplatform/atlauncher/ATLPackIndex.h> + +namespace Atl { + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void request(); + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + +private slots: + void requestFinished(); + void requestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +private: + void requestLogo(QString file, QString url); + +private: + QList<ATLauncher::IndexedPack> modpacks; + + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + NetJobPtr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp new file mode 100644 index 00000000..14bbd18b --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -0,0 +1,209 @@ +#include "AtlOptionalModDialog.h" +#include "ui_AtlOptionalModDialog.h" + +AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods) + : QAbstractListModel(parent), m_mods(mods) { + + // fill mod index + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_index[mod.name] = i; + } + // set initial state + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_selection[mod.name] = false; + setMod(mod, i, mod.selected, false); + } +} + +QVector<QString> AtlOptionalModListModel::getResult() { + QVector<QString> result; + + for (const auto& mod : m_mods) { + if (m_selection[mod.name]) { + result.push_back(mod.name); + } + } + + return result; +} + +int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { + return m_mods.size(); +} + +int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { + // Enabled, Name, Description + return 3; +} + +QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { + auto row = index.row(); + auto mod = m_mods.at(row); + + if (role == Qt::DisplayRole) { + if (index.column() == NameColumn) { + return mod.name; + } + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::ToolTipRole) { + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::CheckStateRole) { + if (index.column() == EnabledColumn) { + return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; + } + } + + return QVariant(); +} + +bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (role == Qt::CheckStateRole) { + auto row = index.row(); + auto mod = m_mods.at(row); + + toggleMod(mod, row); + return true; + } + + return false; +} + +QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case EnabledColumn: + return QString(); + case NameColumn: + return QString("Name"); + case DescriptionColumn: + return QString("Description"); + } + } + + return QVariant(); +} + +Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { + auto flags = QAbstractListModel::flags(index); + if (index.isValid() && index.column() == EnabledColumn) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +void AtlOptionalModListModel::selectRecommended() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = mod.recommended; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::clearAll() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { + setMod(mod, index, !m_selection[mod.name]); +} + +void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { + if (m_selection[mod.name] == enable) return; + + m_selection[mod.name] = enable; + + // disable other mods in the group, if applicable + if (enable && !mod.group.isEmpty()) { + for (int i = 0; i < m_mods.size(); i++) { + if (index == i) continue; + auto other = m_mods.at(i); + + if (mod.group == other.group) { + setMod(other, i, false, shouldEmit); + } + } + } + + for (const auto& dependencyName : mod.depends) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + // enable/disable dependencies + if (enable) { + setMod(dependencyMod, dependencyIndex, true, shouldEmit); + } + + // if the dependency is 'effectively hidden', then track which mods + // depend on it - so we can efficiently disable it when no more dependents + // depend on it. + auto dependants = m_dependants[dependencyName]; + + if (enable) { + dependants.append(mod.name); + } + else { + dependants.removeAll(mod.name); + + // if there are no longer any dependents, let's disable the mod + if (dependencyMod.effectively_hidden && dependants.isEmpty()) { + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + } + + // disable mods that depend on this one, if disabling + if (!enable) { + auto dependants = m_dependants[mod.name]; + for (const auto& dependencyName : dependants) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + + if (shouldEmit) { + emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn), + AtlOptionalModListModel::index(index, EnabledColumn)); + } +} + + +AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods) + : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { + ui->setupUi(this); + + listModel = new AtlOptionalModListModel(this, mods); + ui->treeView->setModel(listModel); + + ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + + connect(ui->selectRecommendedButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::selectRecommended); + connect(ui->clearAllButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::clearAll); + connect(ui->installButton, &QPushButton::pressed, + this, &QDialog::close); +} + +AtlOptionalModDialog::~AtlOptionalModDialog() { + delete ui; +} diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h new file mode 100644 index 00000000..a1df43f6 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -0,0 +1,66 @@ +#pragma once + +#include <QDialog> +#include <QAbstractListModel> + +#include "modplatform/atlauncher/ATLPackIndex.h" + +namespace Ui { +class AtlOptionalModDialog; +} + +class AtlOptionalModListModel : public QAbstractListModel { + Q_OBJECT + +public: + enum Columns + { + EnabledColumn = 0, + NameColumn, + DescriptionColumn, + }; + + AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + + QVector<QString> getResult(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + +public slots: + void selectRecommended(); + void clearAll(); + +private: + void toggleMod(ATLauncher::VersionMod mod, int index); + void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + +private: + QVector<ATLauncher::VersionMod> m_mods; + QMap<QString, bool> m_selection; + QMap<QString, int> m_index; + QMap<QString, QVector<QString>> m_dependants; +}; + +class AtlOptionalModDialog : public QDialog { + Q_OBJECT + +public: + AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + ~AtlOptionalModDialog() override; + + QVector<QString> getResult() { + return listModel->getResult(); + } + +private: + Ui::AtlOptionalModDialog *ui; + + AtlOptionalModListModel *listModel; +}; diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui new file mode 100644 index 00000000..5d3193a4 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AtlOptionalModDialog</class> + <widget class="QDialog" name="AtlOptionalModDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>310</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select Mods To Install</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="3"> + <widget class="QPushButton" name="installButton"> + <property name="text"> + <string>Install</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="selectRecommendedButton"> + <property name="text"> + <string>Select Recommended</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="shareCodeButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use Share Code</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="clearAllButton"> + <property name="text"> + <string>Clear All</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="4"> + <widget class="ModListView" name="treeView"/> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ModListView</class> + <extends>QTreeView</extends> + <header>widgets/ModListView.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/pages/modplatform/atlauncher/AtlPage.cpp new file mode 100644 index 00000000..9fdf111f --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlPage.cpp @@ -0,0 +1,175 @@ +#include "AtlPage.h" +#include "ui_AtlPage.h" + +#include "dialogs/NewInstanceDialog.h" +#include "AtlOptionalModDialog.h" +#include <modplatform/atlauncher/ATLPackInstallTask.h> +#include <BuildConfig.h> +#include <dialogs/VersionSelectDialog.h> + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Atl::FilterModel(this); + listModel = new Atl::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch); + connect(ui->resetButton, &QPushButton::clicked, this, &AtlPage::resetSearch); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); +} + +AtlPage::~AtlPage() +{ + delete ui; +} + +bool AtlPage::shouldDisplay() const +{ + return true; +} + +void AtlPage::openedImpl() +{ + if(!initialized) + { + listModel->request(); + initialized = true; + } + + suggestCurrent(); +} + +void AtlPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); + auto editedLogoName = selected.safeName; + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); + listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void AtlPage::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void AtlPage::resetSearch() +{ + ui->searchEdit->setText(""); +} + +void AtlPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>(); + + ui->packDescription->setHtml(selected.description.replace("\n", "<br>")); + + for(const auto& version : selected.versions) { + ui->versionSelectionBox->addItem(version.version); + } + + suggestCurrent(); +} + +void AtlPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) { + AtlOptionalModDialog optionalModDialog(this, mods); + optionalModDialog.exec(); + return optionalModDialog.getResult(); +} + +QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) { + VersionSelectDialog vselect(vlist.get(), "Choose Version", MMC->activeWindow(), false); + if (minecraftVersion != Q_NULLPTR) { + vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + } + else { + vselect.setEmptyString(tr("No versions are currently available")); + } + vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!")); + + // select recommended build + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->requires(); + + // filter by minecraft version, if the loader depends on a certain version. + if (minecraftVersion != Q_NULLPTR) { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != minecraftVersion) continue; + } + + // first recommended build we find, we use. + if (version->isRecommended()) { + vselect.setCurrentVersion(version->descriptor()); + break; + } + } + + vselect.exec(); + return vselect.selectedVersion()->descriptor(); +} diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.h b/launcher/pages/modplatform/atlauncher/AtlPage.h new file mode 100644 index 00000000..932ec6a6 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlPage.h @@ -0,0 +1,87 @@ +/* Copyright 2013-2019 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "AtlFilterModel.h" +#include "AtlListModel.h" + +#include <QWidget> +#include <modplatform/atlauncher/ATLPackInstallTask.h> + +#include "MultiMC.h" +#include "pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class AtlPage; +} + +class NewInstanceDialog; + +class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport +{ +Q_OBJECT + +public: + explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~AtlPage(); + virtual QString displayName() const override + { + return tr("ATLauncher"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("atlauncher"); + } + virtual QString id() const override + { + return "atl"; + } + virtual QString helpPage() const override + { + return "ATL-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + +private: + void suggestCurrent(); + + QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; + QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override; + +private slots: + void triggerSearch(); + void resetSearch(); + + void onSortingSelectionChanged(QString data); + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::AtlPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Atl::ListModel* listModel = nullptr; + Atl::FilterModel* filterModel = nullptr; + + ATLauncher::IndexedPack selected; + QString selectedVersion; + + bool initialized = false; +}; diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.ui b/launcher/pages/modplatform/atlauncher/AtlPage.ui new file mode 100644 index 00000000..f16c24b8 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlPage.ui @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AtlPage</class> + <widget class="QWidget" name="AtlPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QTreeView" name="packView"> + <property name="iconSize"> + <size> + <width>96</width> + <height>48</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="resetButton"> + <property name="text"> + <string>Reset</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>resetButton</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/flame/FlameModel.cpp b/launcher/pages/modplatform/flame/FlameModel.cpp new file mode 100644 index 00000000..228a88c5 --- /dev/null +++ b/launcher/pages/modplatform/flame/FlameModel.cpp @@ -0,0 +1,259 @@ +#include "FlameModel.h" +#include "MultiMC.h" +#include <Json.h> + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> +#include <QLabel> + +#include <RWStorage.h> +#include <Env.h> + +namespace Flame { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = MMC->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + emit logoLoaded(logo, QIcon(fullPath)); + if(waitingCallbacks.contains(logo)) + { + waitingCallbacks.value(logo)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + emit logoFailed(logo); + }); + + job->start(); + + m_loadingLogos.append(logo); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +bool ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if(nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +void ListModel::performPaginatedSearch() +{ + NetJob *netJob = new NetJob("Flame::Search"); + auto searchUrl = QString( + "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "categoryId=0&" + "gameId=432&" + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sectionId=4471&" + "sort=%3" + ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void ListModel::searchWithTerm(const QString& term, int sort) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + currentSearchTerm = term; + currentSort = sort; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void Flame::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<Flame::IndexedPack> newList; + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + Flame::IndexedPack pack; + try + { + Flame::loadIndexedPack(pack, packObj); + newList.append(pack); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); + continue; + } + } + if(packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Flame::ListModel::searchRequestFailed(QString reason) +{ + jobPtr.reset(); + + if(searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +} + diff --git a/launcher/pages/modplatform/flame/FlameModel.h b/launcher/pages/modplatform/flame/FlameModel.h new file mode 100644 index 00000000..24383db0 --- /dev/null +++ b/launcher/pages/modplatform/flame/FlameModel.h @@ -0,0 +1,76 @@ +#pragma once + +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> +#include <QList> +#include <QString> +#include <QStringList> +#include <QMetaType> + +#include <functional> +#include <net/NetJob.h> + +#include <modplatform/flame/FlamePackIndex.h> + +namespace Flame { + + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool canFetchMore(const QModelIndex & parent) const override; + void fetchMore(const QModelIndex & parent) override; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term, const int sort); + +private slots: + void performPaginatedSearch(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void searchRequestFinished(); + void searchRequestFailed(QString reason); + +private: + void requestLogo(QString file, QString url); + +private: + QList<IndexedPack> modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + NetJobPtr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/pages/modplatform/flame/FlamePage.cpp b/launcher/pages/modplatform/flame/FlamePage.cpp new file mode 100644 index 00000000..ade58431 --- /dev/null +++ b/launcher/pages/modplatform/flame/FlamePage.cpp @@ -0,0 +1,185 @@ +#include "FlamePage.h" +#include "ui_FlamePage.h" + +#include "MultiMC.h" +#include <Json.h> +#include "dialogs/NewInstanceDialog.h" +#include <InstanceImportTask.h> +#include "FlameModel.h" +#include <QKeyEvent> + +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); + ui->searchEdit->installEventFilter(this); + listModel = new Flame::ListModel(this); + ui->packView->setModel(listModel); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + // index is used to set the sorting with the curseforge api + ui->sortByBox->addItem(tr("Sort by featured")); + ui->sortByBox->addItem(tr("Sort by popularity")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by name")); + ui->sortByBox->addItem(tr("Sort by author")); + ui->sortByBox->addItem(tr("Sort by total downloads")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); +} + +FlamePage::~FlamePage() +{ + delete ui; +} + +bool FlamePage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FlamePage::shouldDisplay() const +{ + return true; +} + +void FlamePage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void FlamePage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + text += "<br><br>"; + + ui->packDescription->setHtml(text + current.description); + + if (current.versionsLoaded == false) + { + qDebug() << "Loading flame modpack versions"; + NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name)); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + int addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] + { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + Flame::loadIndexedPackVersions(current, arr); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + } + + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + }); + netJob->start(); + } + else + { + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + } +} + +void FlamePage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + QString editedLogoName; + editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); + listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void FlamePage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); +} diff --git a/launcher/pages/modplatform/flame/FlamePage.h b/launcher/pages/modplatform/flame/FlamePage.h new file mode 100644 index 00000000..467bb44b --- /dev/null +++ b/launcher/pages/modplatform/flame/FlamePage.h @@ -0,0 +1,80 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "pages/BasePage.h" +#include <MultiMC.h> +#include "tasks/Task.h" +#include <modplatform/flame/FlamePackIndex.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); + void onVersionSelectionChanged(QString data); + +private: + Ui::FlamePage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Flame::ListModel* listModel = nullptr; + Flame::IndexedPack current; + + QString selectedVersion; +}; diff --git a/launcher/pages/modplatform/flame/FlamePage.ui b/launcher/pages/modplatform/flame/FlamePage.ui new file mode 100644 index 00000000..9723815a --- /dev/null +++ b/launcher/pages/modplatform/flame/FlamePage.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FlamePage</class> + <widget class="QWidget" name="FlamePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QListView" name="packView"> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp new file mode 100644 index 00000000..dec3a017 --- /dev/null +++ b/launcher/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/launcher/pages/modplatform/ftb/FtbFilterModel.h b/launcher/pages/modplatform/ftb/FtbFilterModel.h new file mode 100644 index 00000000..4fe2a274 --- /dev/null +++ b/launcher/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/launcher/pages/modplatform/ftb/FtbListModel.cpp b/launcher/pages/modplatform/ftb/FtbListModel.cpp new file mode 100644 index 00000000..98973f2e --- /dev/null +++ b/launcher/pages/modplatform/ftb/FtbListModel.cpp @@ -0,0 +1,304 @@ +#include "FtbListModel.h" + +#include "BuildConfig.h" +#include "Env.h" +#include "MultiMC.h" +#include "Json.h" + +#include <QPainter> + +namespace Ftb { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ModpacksCH::Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.synopsis; + } + else if(role == Qt::DecorationRole) + { + QIcon placeholder = MMC->getThemedIcon("screenshot-placeholder"); + + auto iter = m_logoMap.find(pack.name); + if (iter != m_logoMap.end()) { + auto & logo = *iter; + if(!logo.result.isNull()) { + return logo.result; + } + return placeholder; + } + + for(auto art : pack.art) { + if(art.type == "square") { + ((ListModel *)this)->requestLogo(pack.name, art.url); + } + } + return placeholder; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::performSearch() +{ + auto *netJob = new NetJob("Ftb::Search"); + QString searchUrl; + if(currentSearchTerm.isEmpty()) { + searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all"; + } + else { + searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1") + .arg(currentSearchTerm); + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::searchWithTerm(const QString &term) +{ + if(searchState != Failed && currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + // unless the search has failed, then there is no need to perform an identical search. + return; + } + currentSearchTerm = term; + + if(jobPtr) { + jobPtr->abort(); + jobPtr.reset(); + } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + + performSearch(); +} + +void ListModel::searchRequestFinished() +{ + jobPtr.reset(); + remainingPacks.clear(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto packs = doc.object().value("packs").toArray(); + for(auto pack : packs) { + auto packId = pack.toInt(); + remainingPacks.append(packId); + } + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::searchRequestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.clear(); + + searchState = Failed; +} + +void ListModel::requestPack() +{ + auto *netJob = new NetJob("Ftb::Search"); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1") + .arg(currentPack); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); +} + +void ListModel::packRequestFinished() +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ModpacksCH::Modpack pack; + try + { + ModpacksCH::loadModpack(pack, obj); + } + catch (const JSONValidationError &e) + { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + return; + } + + // Since there is no guarantee that packs have a version, this will just + // ignore those "dud" packs. + if (pack.versions.empty()) + { + qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + } + else + { + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size()); + modpacks.append(pack); + endInsertRows(); + } + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::packRequestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); +} + +void ListModel::logoLoaded(QString logo, bool stale) +{ + auto & logoObj = m_logoMap[logo]; + logoObj.downloadJob.reset(); + QString smallPath = logoObj.fullpath + ".small"; + + QFileInfo smallInfo(smallPath); + + if(stale || !smallInfo.exists()) { + QImage image(logoObj.fullpath); + if (image.isNull()) + { + logoObj.failed = true; + return; + } + QImage small; + if (image.width() > image.height()) { + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + } + else { + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + } + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + square.save(logoObj.fullpath + ".small", "PNG"); + } + + logoObj.result = QIcon(logoObj.fullpath + ".small"); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].name == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_logoMap[logo].failed = true; + m_logoMap[logo].downloadJob.reset(); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if(m_logoMap.contains(logo)) { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + + bool stale = entry->isStale(); + + NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale] + { + logoLoaded(logo, stale); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + auto &newLogoEntry = m_logoMap[logo]; + newLogoEntry.downloadJob = job; + newLogoEntry.fullpath = fullPath; + job->start(); +} + +} diff --git a/launcher/pages/modplatform/ftb/FtbListModel.h b/launcher/pages/modplatform/ftb/FtbListModel.h new file mode 100644 index 00000000..de94e6ba --- /dev/null +++ b/launcher/pages/modplatform/ftb/FtbListModel.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QAbstractListModel> + +#include "modplatform/modpacksch/FTBPackManifest.h" +#include "net/NetJob.h" +#include <QIcon> + +namespace Ftb { + +struct Logo { + QString fullpath; + NetJobPtr downloadJob; + QIcon result; + bool failed = false; +}; + +typedef QMap<QString, Logo> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term); + +private slots: + void performSearch(); + void searchRequestFinished(); + void searchRequestFailed(QString reason); + + void requestPack(); + void packRequestFinished(); + void packRequestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, bool stale); + +private: + void requestLogo(QString file, QString url); + +private: + QList<ModpacksCH::Modpack> modpacks; + LogoMap m_logoMap; + + QString currentSearchTerm; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished, + Failed, + } searchState = None; + NetJobPtr jobPtr; + int currentPack; + QList<int> remainingPacks; + QByteArray response; +}; + +} diff --git a/launcher/pages/modplatform/ftb/FtbPage.cpp b/launcher/pages/modplatform/ftb/FtbPage.cpp new file mode 100644 index 00000000..b7f35c5d --- /dev/null +++ b/launcher/pages/modplatform/ftb/FtbPage.cpp @@ -0,0 +1,145 @@ +#include "FtbPage.h" +#include "ui_FtbPage.h" + +#include <QKeyEvent> + +#include "dialogs/NewInstanceDialog.h" +#include "modplatform/modpacksch/FTBPackInstallTask.h" + +#include "HoeDown.h" + +FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Ftb::FilterModel(this); + listModel = new Ftb::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + ui->searchEdit->installEventFilter(this); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged); +} + +FtbPage::~FtbPage() +{ + delete ui; +} + +bool FtbPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FtbPage::shouldDisplay() const +{ + return true; +} + +void FtbPage::openedImpl() +{ + triggerSearch(); + suggestCurrent(); +} + +void FtbPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + for(auto art : selected.art) { + if(art.type == "square") { + QString editedLogoName; + editedLogoName = selected.name; + + listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); + }); + } + } +} + +void FtbPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text()); +} + +void FtbPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>(); + + HoeDown hoedown; + QString output = hoedown.process(selected.description.toUtf8()); + ui->packDescription->setHtml(output); + + // reverse foreach, so that the newest versions are first + for (auto i = selected.versions.size(); i--;) { + ui->versionSelectionBox->addItem(selected.versions.at(i).name); + } + + suggestCurrent(); +} + +void FtbPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} diff --git a/launcher/pages/modplatform/ftb/FtbPage.h b/launcher/pages/modplatform/ftb/FtbPage.h new file mode 100644 index 00000000..c9c93897 --- /dev/null +++ b/launcher/pages/modplatform/ftb/FtbPage.h @@ -0,0 +1,80 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "FtbFilterModel.h" +#include "FtbListModel.h" + +#include <QWidget> + +#include "MultiMC.h" +#include "pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class FtbPage; +} + +class NewInstanceDialog; + +class FtbPage : public QWidget, public BasePage +{ +Q_OBJECT + +public: + explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FtbPage(); + virtual QString displayName() const override + { + return tr("FTB"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("ftb_logo"); + } + virtual QString id() const override + { + return "ftb"; + } + virtual QString helpPage() const override + { + return "FTB-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + void onSortingSelectionChanged(QString data); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::FtbPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Ftb::ListModel* listModel = nullptr; + Ftb::FilterModel* filterModel = nullptr; + + ModpacksCH::Modpack selected; + QString selectedVersion; +}; diff --git a/launcher/pages/modplatform/ftb/FtbPage.ui b/launcher/pages/modplatform/ftb/FtbPage.ui new file mode 100644 index 00000000..135afc6d --- /dev/null +++ b/launcher/pages/modplatform/ftb/FtbPage.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FtbPage</class> + <widget class="QWidget" name="FtbPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>875</width> + <height>745</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QTreeView" name="packView"> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/pages/modplatform/legacy_ftb/ListModel.cpp new file mode 100644 index 00000000..32596fb3 --- /dev/null +++ b/launcher/pages/modplatform/legacy_ftb/ListModel.cpp @@ -0,0 +1,260 @@ +#include "ListModel.h" +#include "MultiMC.h" + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> +#include <QLabel> + +#include <RWStorage.h> +#include <Env.h> + +#include <BuildConfig.h> + +namespace LegacyFTB { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByGameVersion; + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>(); + Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>(); + + if(currentSorting == Sorting::ByGameVersion) { + Version lv(leftPack.mcVersion); + Version rv(rightPack.mcVersion); + return lv < rv; + + } else if(currentSorting == Sorting::ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + //UHM, some inavlid value set?! + qWarning() << "Invalid sorting set!"; + return true; +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return true; +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ + currentSorting = s; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +QString ListModel::translatePackType(PackType type) const +{ + switch(type) + { + case PackType::Public: + return tr("Public Modpack"); + case PackType::ThirdParty: + return tr("Third Party Modpack"); + case PackType::Private: + return tr("Private Modpack"); + } + qWarning() << "Unknown FTB modpack type:" << int(type); + return QString(); +} + +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 + "\n" + translatePackType(pack.type); + } + 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.logo)) + { + return (m_logoMap.value(pack.logo)); + } + QIcon icon = MMC->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logo); + return icon; + } + else if(role == Qt::TextColorRole) + { + if(pack.broken) + { + //FIXME: Hardcoded color + return QColor(255, 0, 50); + } + else if(pack.bugged) + { + //FIXME: Hardcoded color + //bugged pack, currently only indicates bugged xml + return QColor(244, 229, 66); + } + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::fill(ModpackList modpacks) +{ + beginResetModel(); + this->modpacks = modpacks; + endResetModel(); +} + +void ListModel::addPack(Modpack modpack) +{ + beginResetModel(); + this->modpacks.append(modpack); + endResetModel(); +} + +void ListModel::clear() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); +} + +Modpack ListModel::at(int row) +{ + return modpacks.at(row); +} + +void ListModel::remove(int row) +{ + if(row < 0 || row >= modpacks.size()) + { + qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; + return; + } + beginRemoveRows(QModelIndex(), row, row); + modpacks.removeAt(row); + endRemoveRows(); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + emit dataChanged(createIndex(0, 0), createIndex(1, 0)); +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::requestLogo(QString file) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file)); + job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, 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); +} + +void ListModel::getLogo(const QString &logo, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +} diff --git a/launcher/pages/modplatform/legacy_ftb/ListModel.h b/launcher/pages/modplatform/legacy_ftb/ListModel.h new file mode 100644 index 00000000..c55df000 --- /dev/null +++ b/launcher/pages/modplatform/legacy_ftb/ListModel.h @@ -0,0 +1,78 @@ +#pragma once + +#include <modplatform/legacy_ftb/PackHelpers.h> +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> + +#include <functional> + +namespace LegacyFTB { + +typedef QMap<QString, QIcon> FTBLogoMap; +typedef std::function<void(QString)> LogoCallback; + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByName, + ByGameVersion + }; + 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; + +}; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT +private: + ModpackList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + FTBLogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + void requestLogo(QString file); + QString translatePackType(PackType type) const; + + +private slots: + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +public: + ListModel(QObject *parent); + ~ListModel(); + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void fill(ModpackList modpacks); + void addPack(Modpack modpack); + void clear(); + void remove(int row); + + Modpack at(int row); + void getLogo(const QString &logo, LogoCallback callback); +}; + +} diff --git a/launcher/pages/modplatform/legacy_ftb/Page.cpp b/launcher/pages/modplatform/legacy_ftb/Page.cpp new file mode 100644 index 00000000..a438f76c --- /dev/null +++ b/launcher/pages/modplatform/legacy_ftb/Page.cpp @@ -0,0 +1,369 @@ +#include "Page.h" +#include "ui_Page.h" + +#include <QInputDialog> + +#include "MultiMC.h" +#include "dialogs/CustomMessageBox.h" +#include "dialogs/NewInstanceDialog.h" +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "modplatform/legacy_ftb/PackInstallTask.h" +#include "modplatform/legacy_ftb/PrivatePackManager.h" +#include "ListModel.h" + +namespace LegacyFTB { + +Page::Page(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::Page) +{ + ftbFetchTask.reset(new PackFetchTask()); + ftbPrivatePacks.reset(new PrivatePackManager()); + + ui->setupUi(this); + + { + publicFilterModel = new FilterModel(this); + publicListModel = new ListModel(this); + publicFilterModel->setSourceModel(publicListModel); + + ui->publicPackList->setModel(publicFilterModel); + ui->publicPackList->setSortingEnabled(true); + ui->publicPackList->header()->hide(); + ui->publicPackList->setIndentation(0); + ui->publicPackList->setIconSize(QSize(42, 42)); + + for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i)); + } + + ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting()); + } + + { + thirdPartyFilterModel = new FilterModel(this); + thirdPartyModel = new ListModel(this); + thirdPartyFilterModel->setSourceModel(thirdPartyModel); + + ui->thirdPartyPackList->setModel(thirdPartyFilterModel); + ui->thirdPartyPackList->setSortingEnabled(true); + ui->thirdPartyPackList->header()->hide(); + ui->thirdPartyPackList->setIndentation(0); + ui->thirdPartyPackList->setIconSize(QSize(42, 42)); + + thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + { + privateFilterModel = new FilterModel(this); + privateListModel = new ListModel(this); + privateFilterModel->setSourceModel(privateListModel); + + ui->privatePackList->setModel(privateFilterModel); + ui->privatePackList->setSortingEnabled(true); + ui->privatePackList->header()->hide(); + ui->privatePackList->setIndentation(0); + ui->privatePackList->setIconSize(QSize(42, 42)); + + privateFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); + + connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); + connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); + connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); + + connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked); + connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked); + + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); + + // ui->modpackInfo->setOpenExternalLinks(true); + + ui->publicPackList->selectionModel()->reset(); + ui->thirdPartyPackList->selectionModel()->reset(); + ui->privatePackList->selectionModel()->reset(); + + onTabChanged(ui->tabWidget->currentIndex()); +} + +Page::~Page() +{ + delete ui; +} + +bool Page::shouldDisplay() const +{ + return true; +} + +void Page::openedImpl() +{ + if(!initialized) + { + connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); + + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); + + ftbFetchTask->fetch(); + ftbPrivatePacks->load(); + ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList()); + initialized = true; + } + suggestCurrent(); +} + +void Page::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if(selected.broken || selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new PackInstallTask(selected, selectedVersion)); + QString editedLogoName; + if(selected.logo.toLower().startsWith("ftb")) + { + editedLogoName = selected.logo; + } + else + { + editedLogoName = "ftb_" + selected.logo; + } + + editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); + + if(selected.type == PackType::Public) + { + publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::ThirdParty) + { + thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::Private) + { + privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } +} + +void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks) +{ + publicListModel->fill(publicPacks); + thirdPartyModel->fill(thirdPartyPacks); +} + +void Page::ftbPackDataDownloadFailed(QString reason) +{ + //TODO: Display the error +} + +void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) +{ + privateListModel->addPack(pack); +} + +void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode) +{ + auto reply = QMessageBox::question( + this, + tr("FTB private packs"), + tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode) + ); + if(reply == QMessageBox::Yes) + { + ftbPrivatePacks->remove(packCode); + } +} + +void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPackSelectionChanged(Modpack* pack) +{ + ui->versionSelectionBox->clear(); + if(pack) + { + currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" + + "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>") + + "</li></ul>"); + bool currentAdded = false; + + for(int i = 0; i < pack->oldVersions.size(); i++) + { + if(pack->currentVersion == pack->oldVersions.at(i)) + { + currentAdded = true; + } + ui->versionSelectionBox->addItem(pack->oldVersions.at(i)); + } + + if(!currentAdded) + { + ui->versionSelectionBox->addItem(pack->currentVersion); + } + selected = *pack; + } + else + { + currentModpackInfo->setHtml(""); + ui->versionSelectionBox->clear(); + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + suggestCurrent(); +} + +void Page::onVersionSelectionItemChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +void Page::onSortingSelectionChanged(QString data) +{ + FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data); + publicFilterModel->setSorting(toSet); + thirdPartyFilterModel->setSorting(toSet); + privateFilterModel->setSorting(toSet); +} + +void Page::onTabChanged(int tab) +{ + if(tab == 1) + { + currentModel = thirdPartyFilterModel; + currentList = ui->thirdPartyPackList; + currentModpackInfo = ui->thirdPartyPackDescription; + } + else if(tab == 2) + { + currentModel = privateFilterModel; + currentList = ui->privatePackList; + currentModpackInfo = ui->privatePackDescription; + } + else + { + currentModel = publicFilterModel; + currentList = ui->publicPackList; + currentModpackInfo = ui->publicPackDescription; + } + + currentList->selectionModel()->reset(); + QModelIndex idx = currentList->currentIndex(); + if(idx.isValid()) + { + auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&pack); + } + else + { + onPackSelectionChanged(); + } +} + +void Page::onAddPackClicked() +{ + bool ok; + QString text = QInputDialog::getText( + this, + tr("Add FTB pack"), + tr("Enter pack code:"), + QLineEdit::Normal, + QString(), + &ok + ); + if(ok && !text.isEmpty()) + { + ftbPrivatePacks->add(text); + ftbFetchTask->fetchPrivate({text}); + } +} + +void Page::onRemovePackClicked() +{ + auto index = ui->privatePackList->currentIndex(); + if(!index.isValid()) + { + return; + } + auto row = index.row(); + Modpack pack = privateListModel->at(row); + auto answer = QMessageBox::question( + this, + tr("Remove pack"), + tr("Are you sure you want to remove pack %1?").arg(pack.name), + QMessageBox::Yes | QMessageBox::No + ); + if(answer != QMessageBox::Yes) + { + return; + } + + ftbPrivatePacks->remove(pack.packCode); + privateListModel->remove(row); + onPackSelectionChanged(); +} + +} diff --git a/launcher/pages/modplatform/legacy_ftb/Page.h b/launcher/pages/modplatform/legacy_ftb/Page.h new file mode 100644 index 00000000..e840216e --- /dev/null +++ b/launcher/pages/modplatform/legacy_ftb/Page.h @@ -0,0 +1,119 @@ +/* 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 <QTreeView> +#include <QTextBrowser> + +#include "pages/BasePage.h" +#include <MultiMC.h> +#include "tasks/Task.h" +#include "modplatform/legacy_ftb/PackHelpers.h" +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "QObjectPtr.h" + +class NewInstanceDialog; + +namespace LegacyFTB { + +namespace Ui +{ +class Page; +} + +class ListModel; +class FilterModel; +class PrivatePackListModel; +class PrivatePackFilterModel; +class PrivatePackManager; + +class Page : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0); + virtual ~Page(); + QString displayName() const override + { + return tr("FTB Legacy"); + } + QIcon icon() const override + { + return MMC->getThemedIcon("ftb_logo"); + } + QString id() const override + { + return "legacy_ftb"; + } + QString helpPage() const override + { + return "FTB-platform"; + } + bool shouldDisplay() const override; + void openedImpl() override; + +private: + void suggestCurrent(); + void onPackSelectionChanged(Modpack *pack = nullptr); + +private slots: + void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); + void ftbPackDataDownloadFailed(QString reason); + + void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); + void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); + + void onSortingSelectionChanged(QString data); + void onVersionSelectionItemChanged(QString data); + + void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second); + void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second); + + void onTabChanged(int tab); + + void onAddPackClicked(); + void onRemovePackClicked(); + +private: + FilterModel* currentModel = nullptr; + QTreeView* currentList = nullptr; + QTextBrowser* currentModpackInfo = nullptr; + + bool initialized = false; + Modpack selected; + QString selectedVersion; + + ListModel* publicListModel = nullptr; + FilterModel* publicFilterModel = nullptr; + + ListModel *thirdPartyModel = nullptr; + FilterModel *thirdPartyFilterModel = nullptr; + + ListModel *privateListModel = nullptr; + FilterModel *privateFilterModel = nullptr; + + unique_qobject_ptr<PackFetchTask> ftbFetchTask; + std::unique_ptr<PrivatePackManager> ftbPrivatePacks; + + NewInstanceDialog* dialog = nullptr; + + Ui::Page *ui = nullptr; +}; + +} diff --git a/launcher/pages/modplatform/legacy_ftb/Page.ui b/launcher/pages/modplatform/legacy_ftb/Page.ui new file mode 100644 index 00000000..15e5d432 --- /dev/null +++ b/launcher/pages/modplatform/legacy_ftb/Page.ui @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LegacyFTB::Page</class> + <widget class="QWidget" name="LegacyFTB::Page"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>709</width> + <height>602</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Public</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QTreeView" name="publicPackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QTextBrowser" name="publicPackDescription"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>3rd Party</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="1"> + <widget class="QTextBrowser" name="thirdPartyPackDescription"/> + </item> + <item row="0" column="0"> + <widget class="QTreeView" name="thirdPartyPackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_3"> + <attribute name="title"> + <string>Private</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QTreeView" name="privatePackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="addPackBtn"> + <property name="text"> + <string>Add pack</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="removePackBtn"> + <property name="text"> + <string>Remove selected pack</string> + </property> + </widget> + </item> + <item row="0" column="1" rowspan="3"> + <widget class="QTextBrowser" name="privatePackDescription"/> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_4"> + <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="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"> + <property name="minimumSize"> + <size> + <width>265</width> + <height>0</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/pages/modplatform/technic/TechnicData.h b/launcher/pages/modplatform/technic/TechnicData.h new file mode 100644 index 00000000..50fd75e8 --- /dev/null +++ b/launcher/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/launcher/pages/modplatform/technic/TechnicModel.cpp b/launcher/pages/modplatform/technic/TechnicModel.cpp new file mode 100644 index 00000000..def30783 --- /dev/null +++ b/launcher/pages/modplatform/technic/TechnicModel.cpp @@ -0,0 +1,238 @@ +/* Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TechnicModel.h" +#include "Env.h" +#include "MultiMC.h" +#include "Json.h" + +#include <QIcon> + +Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +Technic::ListModel::~ListModel() +{ +} + +QVariant Technic::ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = MMC->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + return QVariant(); +} + +int Technic::ListModel::columnCount(const QModelIndex&) const +{ + return 1; +} + +int Technic::ListModel::rowCount(const QModelIndex&) const +{ + return modpacks.size(); +} + +void Technic::ListModel::searchWithTerm(const QString& term) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + return; + } + currentSearchTerm = term; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + performSearch(); +} + +void Technic::ListModel::performSearch() +{ + NetJob *netJob = new NetJob("Technic::Search"); + QString searchUrl = ""; + if (currentSearchTerm.isEmpty()) { + searchUrl = "https://api.technicpack.net/trending?build=multimc"; + } + else + { + searchUrl = QString( + "https://api.technicpack.net/search?build=multimc&q=%1" + ).arg(currentSearchTerm); + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void Technic::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<Modpack> newList; + try { + auto root = Json::requireObject(doc); + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + } + catch (const JSONValidationError &err) + { + qCritical() << "Couldn't parse technic search results:" << err.cause() ; + return; + } + searchState = Finished; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void Technic::ListModel::searchRequestFailed() +{ + jobPtr.reset(); + + if(searchState == ResetRequested) + { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + performSearch(); + } + else + { + searchState = Finished; + } +} + + +void Technic::ListModel::logoLoaded(QString logo, QString out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, QIcon(out)); + for(int i = 0; i < modpacks.size(); i++) + { + if(modpacks[i].logoName == logo) + { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void Technic::ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void Technic::ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); + NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + logoLoaded(logo, fullPath); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + job->start(); + + m_loadingLogos.append(logo); +} diff --git a/launcher/pages/modplatform/technic/TechnicModel.h b/launcher/pages/modplatform/technic/TechnicModel.h new file mode 100644 index 00000000..82a03842 --- /dev/null +++ b/launcher/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/launcher/pages/modplatform/technic/TechnicPage.cpp b/launcher/pages/modplatform/technic/TechnicPage.cpp new file mode 100644 index 00000000..e836f767 --- /dev/null +++ b/launcher/pages/modplatform/technic/TechnicPage.cpp @@ -0,0 +1,198 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TechnicPage.h" +#include "ui_TechnicPage.h" + +#include "MultiMC.h" +#include "dialogs/NewInstanceDialog.h" +#include "TechnicModel.h" +#include <QKeyEvent> +#include "modplatform/technic/SingleZipPackInstallTask.h" +#include "modplatform/technic/SolderPackInstallTask.h" +#include "Json.h" + +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Technic::ListModel(this); + ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); +} + +bool TechnicPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +TechnicPage::~TechnicPage() +{ + delete ui; +} + +bool TechnicPage::shouldDisplay() const +{ + return true; +} + +void TechnicPage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void TechnicPage::triggerSearch() { + model->searchWithTerm(ui->searchEdit->text()); +} + +void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + //ui->frame->clear(); + return; + } + + current = model->data(first, Qt::UserRole).value<Technic::Modpack>(); + suggestCurrent(); +} + +void TechnicPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + if (current.broken) + { + dialog->setSuggestedPack(); + return; + } + + QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + + if (current.metadataLoaded) + { + metadataLoaded(); + return; + } + + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + QString slug = current.slug; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + { + if (current.slug != slug) + { + return; + } + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonObject obj = doc.object(); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + if (!obj.contains("url")) + { + qWarning() << "Json doesn't contain an url key"; + return; + } + QJsonValueRef url = obj["url"]; + if (url.isString()) + { + current.url = url.toString(); + } + else + { + if (!obj.contains("solder")) + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + QJsonValueRef solderUrl = obj["solder"]; + if (solderUrl.isString()) + { + current.url = solderUrl.toString(); + current.isSolder = true; + } + else + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + } + + current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); + current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); + current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.metadataLoaded = true; + metadataLoaded(); + }); + netJob->start(); +} + +// expects current.metadataLoaded to be true +void TechnicPage::metadataLoaded() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + // This allows injecting HTML here. + text = name; + else + // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.author.isEmpty()) { + // This allows injecting HTML here + text += tr(" by ") + current.author; + } + + ui->frame->setModText(text); + ui->frame->setModDescription(current.description); + if (!current.isSolder) + { + dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + } + else + { + while (current.url.endsWith('/')) current.url.chop(1); + dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion)); + } +} diff --git a/launcher/pages/modplatform/technic/TechnicPage.h b/launcher/pages/modplatform/technic/TechnicPage.h new file mode 100644 index 00000000..27e1258a --- /dev/null +++ b/launcher/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/launcher/pages/modplatform/technic/TechnicPage.ui b/launcher/pages/modplatform/technic/TechnicPage.ui new file mode 100644 index 00000000..2ca45dd2 --- /dev/null +++ b/launcher/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TechnicPage</class> + <widget class="QWidget" name="TechnicPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>405</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QListView" name="packView"> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="MCModInfoFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>MCModInfoFrame</class> + <extends>QFrame</extends> + <header>widgets/MCModInfoFrame.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> |