NOISSUE continue reshuffling the codebase

This commit is contained in:
Petr Mrázek
2021-11-22 03:55:16 +01:00
parent 5040231f8d
commit b258eac215
244 changed files with 516 additions and 768 deletions

View File

@ -0,0 +1,132 @@
#include "ImportPage.h"
#include "ui_ImportPage.h"
#include <QFileDialog>
#include <QValidator>
#include "ui/dialogs/NewInstanceDialog.h"
#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());
}
}

View File

@ -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 "ui/pages/BasePage.h"
#include <Application.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 APPLICATION->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;
};

View File

@ -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>

View File

@ -0,0 +1,103 @@
#include "VanillaPage.h"
#include "ui_VanillaPage.h"
#include <QTabBar>
#include "Application.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "Filter.h"
#include "InstanceCreationTask.h"
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 = APPLICATION->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();
}

View File

@ -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 "ui/pages/BasePage.h"
#include <Application.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 APPLICATION->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;
};

View File

@ -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>ui/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>

View File

@ -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;
}
}

View File

@ -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;
};
}

View File

@ -0,0 +1,193 @@
#include "AtlListModel.h"
#include <BuildConfig.h>
#include <Application.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 = APPLICATION->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(APPLICATION->network());
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(APPLICATION->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 = APPLICATION->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(APPLICATION->network());
m_loadingLogos.append(file);
}
}

View File

@ -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;
NetJob::Ptr jobPtr;
QByteArray response;
};
}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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>ui/widgets/ModListView.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,171 @@
#include "AtlPage.h"
#include "ui_AtlPage.h"
#include "modplatform/atlauncher/ATLPackInstallTask.h"
#include "AtlOptionalModDialog.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/dialogs/VersionSelectDialog.h"
#include <BuildConfig.h>
AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
{
ui->setupUi(this);
filterModel = new Atl::FilterModel(this);
listModel = new Atl::ListModel(this);
filterModel->setSourceModel(listModel);
ui->packView->setModel(filterModel);
ui->packView->setSortingEnabled(true);
ui->packView->header()->hide();
ui->packView->setIndentation(0);
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->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 + " " + selectedVersion, 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::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", APPLICATION->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();
}

View File

@ -0,0 +1,86 @@
/* 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 "Application.h"
#include "ui/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 APPLICATION->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 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;
};

View File

@ -0,0 +1,92 @@
<?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="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>96</width>
<height>48</height>
</size>
</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="0">
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>packView</tabstop>
<tabstop>packDescription</tabstop>
<tabstop>sortByBox</tabstop>
<tabstop>versionSelectionBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,258 @@
#include "FlameModel.h"
#include "Application.h"
#include <Json.h>
#include <MMCStrings.h>
#include <Version.h>
#include <QtMath>
#include <QLabel>
#include <RWStorage.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 = APPLICATION->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 = APPLICATION->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(APPLICATION->network());
m_loadingLogos.append(logo);
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->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(APPLICATION->network());
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;
}
}
}

View File

@ -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;
NetJob::Ptr jobPtr;
QByteArray response;
};
}

View File

@ -0,0 +1,186 @@
#include "FlamePage.h"
#include "ui_FlamePage.h"
#include <QKeyEvent>
#include "Application.h"
#include "Json.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "InstanceImportTask.h"
#include "FlameModel.h"
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(APPLICATION->network());
}
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();
}

View File

@ -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 "ui/pages/BasePage.h"
#include <Application.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 APPLICATION->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;
};

View File

@ -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>

View File

@ -0,0 +1,76 @@
#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;
}
void FilterModel::setSearchTerm(const QString& term)
{
searchTerm = term.trimmed();
invalidate();
}
bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (searchTerm.isEmpty()) {
return true;
}
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>();
return pack.name.contains(searchTerm, Qt::CaseInsensitive);
}
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;
}
}

View File

@ -0,0 +1,35 @@
#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();
void setSearchTerm(const 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 { "" };
};
}

View File

@ -0,0 +1,278 @@
#include "FtbListModel.h"
#include "BuildConfig.h"
#include "Application.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 = APPLICATION->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::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
requestLogo(logo, logoUrl);
}
}
void ListModel::request()
{
beginResetModel();
modpacks.clear();
endResetModel();
auto *netJob = new NetJob("Ftb::Request");
auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all");
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
jobPtr = netJob;
jobPtr->start(APPLICATION->network());
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::requestFinished()
{
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::requestFailed(QString reason)
{
jobPtr.reset();
remainingPacks.clear();
}
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(APPLICATION->network());
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 = APPLICATION->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(APPLICATION->network());
}
}

View File

@ -0,0 +1,61 @@
#pragma once
#include <QAbstractListModel>
#include "modplatform/modpacksch/FTBPackManifest.h"
#include "net/NetJob.h"
#include <QIcon>
namespace Ftb {
struct Logo {
QString fullpath;
NetJob::Ptr 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 request();
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
private slots:
void requestFinished();
void requestFailed(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;
NetJob::Ptr jobPtr;
int currentPack;
QList<int> remainingPacks;
QByteArray response;
};
}

View File

@ -0,0 +1,150 @@
#include "FtbPage.h"
#include "ui_FtbPage.h"
#include <QKeyEvent>
#include "ui/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->searchEdit, &QLineEdit::textChanged, 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()
{
if(!initialised)
{
listModel->request();
initialised = true;
}
suggestCurrent();
}
void FtbPage::suggestCurrent()
{
if(!isOpened)
{
return;
}
if (selectedVersion.isEmpty())
{
dialog->setSuggestedPack();
return;
}
dialog->setSuggestedPack(selected.name + " " + selectedVersion, 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()
{
filterModel->setSearchTerm(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();
}

View File

@ -0,0 +1,83 @@
/* 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 "Application.h"
#include "ui/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 APPLICATION->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;
bool initialised { false };
};

View File

@ -0,0 +1,79 @@
<?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>
<property name="clearButtonEnabled">
<bool>true</bool>
</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="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</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>versionSelectionBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,259 @@
#include "ListModel.h"
#include "Application.h"
#include <MMCStrings.h>
#include <Version.h>
#include <QtMath>
#include <QLabel>
#include <RWStorage.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 = APPLICATION->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 = APPLICATION->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(APPLICATION->network());
m_loadingLogos.append(file);
}
void ListModel::getLogo(const QString &logo, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->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);
}
}

View File

@ -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);
};
}

View File

@ -0,0 +1,371 @@
#include "Page.h"
#include "ui_Page.h"
#include <QInputDialog>
#include "Application.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/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(APPLICATION->network()));
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(APPLICATION->network(), 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();
}
}

View File

@ -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 "ui/pages/BasePage.h"
#include <Application.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 APPLICATION->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;
};
}

View File

@ -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>

View File

@ -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)

View File

@ -0,0 +1,237 @@
/* 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 "Application.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 = APPLICATION->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(APPLICATION->network());
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(APPLICATION->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 = APPLICATION->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(APPLICATION->network());
m_loadingLogos.append(logo);
}

View File

@ -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;
NetJob::Ptr jobPtr;
QByteArray response;
};
}

View File

@ -0,0 +1,201 @@
/* 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 <QKeyEvent>
#include "ui/dialogs/NewInstanceDialog.h"
#include "TechnicModel.h"
#include "modplatform/technic/SingleZipPackInstallTask.h"
#include "modplatform/technic/SolderPackInstallTask.h"
#include "Json.h"
#include "Application.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(APPLICATION->network());
}
// 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(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion));
}
}

View File

@ -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 "ui/pages/BasePage.h"
#include <Application.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 APPLICATION->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;
};

View File

@ -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>ui/widgets/MCModInfoFrame.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>searchButton</tabstop>
<tabstop>packView</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>