Merge pull request #193 from flowln/develop

Allow for downloading multiple mods at once
This commit is contained in:
timoreo22 2022-02-26 07:33:11 +01:00 committed by GitHub
commit ae354688c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 599 additions and 494 deletions

View File

@ -10,6 +10,7 @@ class ModDownloadTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr<ModFolderModel> mods); explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr<ModFolderModel> mods);
const QString& getFilename() const { return filename; }
public slots: public slots:
bool abort() override; bool abort() override;

View File

@ -5,6 +5,7 @@
#include <InstanceList.h> #include <InstanceList.h>
#include "ProgressDialog.h" #include "ProgressDialog.h"
#include "CustomMessageBox.h"
#include <QLayout> #include <QLayout>
#include <QPushButton> #include <QPushButton>
@ -39,9 +40,10 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
// Bonk Qt over its stupid head and make sure it understands which button is the default one... // Bonk Qt over its stupid head and make sure it understands which button is the default one...
// See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
auto OkButton = m_buttons->button(QDialogButtonBox::Ok); auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
OkButton->setEnabled(false);
OkButton->setDefault(true); OkButton->setDefault(true);
OkButton->setAutoDefault(true); OkButton->setAutoDefault(true);
connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
CancelButton->setDefault(false); CancelButton->setDefault(false);
@ -52,6 +54,7 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
HelpButton->setDefault(false); HelpButton->setDefault(false);
HelpButton->setAutoDefault(false); HelpButton->setAutoDefault(false);
connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
QMetaObject::connectSlotsByName(this); QMetaObject::connectSlotsByName(this);
setWindowModality(Qt::WindowModal); setWindowModality(Qt::WindowModal);
setWindowTitle("Download mods"); setWindowTitle("Download mods");
@ -67,6 +70,36 @@ void ModDownloadDialog::reject()
QDialog::reject(); QDialog::reject();
} }
void ModDownloadDialog::confirm()
{
auto keys = modTask.keys();
keys.sort(Qt::CaseInsensitive);
auto info = QString(tr("You're about to download the following mods:"));
info.append("\n\n");
for(auto task : keys){
info.append(task);
info.append("\n --> ");
info.append(tr("File name: "));
info.append(modTask.find(task).value()->getFilename());
info.append('\n');
}
auto confirm_dialog = CustomMessageBox::selectable(
this,
tr("Confirm mods to download"),
info,
QMessageBox::NoIcon,
QMessageBox::Cancel | QMessageBox::Ok,
QMessageBox::Ok
);
auto AcceptButton = confirm_dialog->button(QMessageBox::Ok);
connect(AcceptButton, &QPushButton::clicked, this, &ModDownloadDialog::accept);
confirm_dialog->open();
}
void ModDownloadDialog::accept() void ModDownloadDialog::accept()
{ {
QDialog::accept(); QDialog::accept();
@ -83,16 +116,35 @@ QList<BasePage *> ModDownloadDialog::getPages()
}; };
} }
void ModDownloadDialog::setSuggestedMod(const QString& name, ModDownloadTask* task) void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task)
{ {
modTask.reset(task); removeSelectedMod(name);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(task); modTask.insert(name, task);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
void ModDownloadDialog::removeSelectedMod(const QString &name)
{
if(modTask.contains(name))
delete modTask.find(name).value();
modTask.remove(name);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const
{
// FIXME: Is there a way to check for versions without checking the filename
// as a heuristic, other than adding such info to ModDownloadTask itself?
auto iter = modTask.find(name);
return iter != modTask.end() && (iter.value()->getFilename() == filename);
} }
ModDownloadDialog::~ModDownloadDialog() ModDownloadDialog::~ModDownloadDialog()
{ {
} }
ModDownloadTask *ModDownloadDialog::getTask() { const QList<ModDownloadTask*> ModDownloadDialog::getTasks() {
return modTask.release(); return modTask.values();
} }

View File

@ -29,12 +29,15 @@ public:
QString dialogTitle() override; QString dialogTitle() override;
QList<BasePage *> getPages() override; QList<BasePage *> getPages() override;
void setSuggestedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr);
void removeSelectedMod(const QString & name = QString());
bool isModSelected(const QString & name, const QString & filename) const;
ModDownloadTask * getTask(); const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods; const std::shared_ptr<ModFolderModel> &mods;
public slots: public slots:
void confirm();
void accept() override; void accept() override;
void reject() override; void reject() override;
@ -49,6 +52,6 @@ private:
ModrinthPage *modrinthPage = nullptr; ModrinthPage *modrinthPage = nullptr;
FlameModPage *flameModPage = nullptr; FlameModPage *flameModPage = nullptr;
std::unique_ptr<ModDownloadTask> modTask; QHash<QString, ModDownloadTask*> modTask;
BaseInstance *m_instance; BaseInstance *m_instance;
}; };

View File

@ -365,8 +365,7 @@ void ModFolderPage::on_actionInstall_mods_triggered()
} }
ModDownloadDialog mdownload(m_mods, this, m_inst); ModDownloadDialog mdownload(m_mods, this, m_inst);
if(mdownload.exec()) { if(mdownload.exec()) {
ModDownloadTask *task = mdownload.getTask(); for(auto task : mdownload.getTasks()){
if (task) {
connect(task, &Task::failed, [this, task](QString reason) { connect(task, &Task::failed, [this, task](QString reason) {
task->deleteLater(); task->deleteLater();
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();

View File

@ -4,196 +4,211 @@
#include <QKeyEvent> #include <QKeyEvent>
#include "Application.h" #include "Application.h"
#include "Json.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "InstanceImportTask.h"
#include "FlameModModel.h" #include "FlameModModel.h"
#include "InstanceImportTask.h"
#include "Json.h"
#include "ModDownloadTask.h" #include "ModDownloadTask.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance)
: QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), dialog(dialog) : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage),
{ dialog(dialog) {
ui->setupUi(this); ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &FlameModPage::triggerSearch); connect(ui->searchButton, &QPushButton::clicked, this,
ui->searchEdit->installEventFilter(this); &FlameModPage::triggerSearch);
listModel = new FlameMod::ListModel(this); ui->searchEdit->installEventFilter(this);
ui->packView->setModel(listModel); listModel = new FlameMod::ListModel(this);
ui->packView->setModel(listModel);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
// index is used to set the sorting with the flame api // index is used to set the sorting with the flame api
ui->sortByBox->addItem(tr("Sort by Featured")); ui->sortByBox->addItem(tr("Sort by Featured"));
ui->sortByBox->addItem(tr("Sort by Popularity")); ui->sortByBox->addItem(tr("Sort by Popularity"));
ui->sortByBox->addItem(tr("Sort by last updated")); ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by Name")); ui->sortByBox->addItem(tr("Sort by Name"));
ui->sortByBox->addItem(tr("Sort by Author")); ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Downloads"));
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this,
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); SLOT(triggerSearch()));
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged,
this, &FlameModPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this,
&FlameModPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this,
&FlameModPage::onModSelected);
} }
FlameModPage::~FlameModPage() FlameModPage::~FlameModPage() { delete ui; }
{
delete ui;
}
bool FlameModPage::eventFilter(QObject* watched, QEvent* event) bool FlameModPage::eventFilter(QObject *watched, QEvent *event) {
{ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); if (keyEvent->key() == Qt::Key_Return) {
if (keyEvent->key() == Qt::Key_Return) { triggerSearch();
triggerSearch(); keyEvent->accept();
keyEvent->accept(); return true;
return true;
}
} }
return QWidget::eventFilter(watched, event); }
return QWidget::eventFilter(watched, event);
} }
bool FlameModPage::shouldDisplay() const bool FlameModPage::shouldDisplay() const { return true; }
{
return true; void FlameModPage::openedImpl() {
updateSelectionButton();
triggerSearch();
} }
void FlameModPage::openedImpl() void FlameModPage::triggerSearch() {
{ listModel->searchWithTerm(ui->searchEdit->text(),
suggestCurrent(); ui->sortByBox->currentIndex());
triggerSearch();
} }
void FlameModPage::triggerSearch() void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) {
{ ui->versionSelectionBox->clear();
listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
}
void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!first.isValid()) {
{ return;
ui->versionSelectionBox->clear(); }
if(!first.isValid()) current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>();
{ QString text = "";
if(isOpened) QString name = current.name;
{
dialog->setSuggestedMod(); if (current.websiteUrl.isEmpty())
} text = name;
else
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
if (!current.authors.empty()) {
auto authorToStr = [](FlameMod::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) {
qDebug() << "Loading flame mod versions";
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
auto netJob =
new NetJob(QString("Flame::ModVersions(%1)").arg(current.name),
APPLICATION->network());
auto response = new QByteArray();
int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files")
.arg(addonId),
response));
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 Flame at "
<< parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return; return;
} }
QJsonArray arr = doc.array();
current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>(); try {
QString text = ""; FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(),
QString name = current.name; m_instance);
} catch (const JSONValidationError &e) {
if (current.websiteUrl.isEmpty()) qDebug() << *response;
text = name; qWarning() << "Error while reading Flame mod version: " << e.cause();
else }
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
if (!current.authors.empty()) { QString mcVersion = packProfile->getComponentVersion("net.minecraft");
auto authorToStr = [](FlameMod::ModpackAuthor & author) { QString loaderString =
if(author.url.isEmpty()) { (packProfile->getComponentVersion("net.minecraftforge").isEmpty())
return author.name; ? "fabric"
} : "forge";
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); for (int i = 0; i < current.versions.size(); i++) {
}; auto version = current.versions[i];
QStringList authorStrs; if (!version.mcVersion.contains(mcVersion)) {
for(auto & author: current.authors) { continue;
authorStrs.push_back(authorToStr(author));
} }
text += "<br>" + tr(" by ") + authorStrs.join(", "); ui->versionSelectionBox->addItem(version.version, QVariant(i));
} }
text += "<br><br>"; if (ui->versionSelectionBox->count() == 0) {
ui->versionSelectionBox->addItem(tr("No Valid Version found!"),
QVariant(-1));
}
ui->packDescription->setHtml(text + current.description); ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
updateSelectionButton();
if (!current.versionsLoaded) });
{ QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
qDebug() << "Loading flame mod versions"; netJob->deleteLater();
auto netJob = new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network()); delete response;
auto response = new QByteArray(); });
int addonId = current.addonId; netJob->start();
netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); } else {
for (int i = 0; i < current.versions.size(); i++) {
QObject::connect(netJob, &NetJob::succeeded, this, [this, response] ui->versionSelectionBox->addItem(current.versions[i].version,
{ QVariant(i));
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
QJsonArray arr = doc.array();
try
{
FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance);
}
catch(const JSONValidationError &e)
{
qDebug() << *response;
qWarning() << "Error while reading Flame mod version: " << e.cause();
}
auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge";
for(int i = 0; i < current.versions.size(); i++) {
auto version = current.versions[i];
if(!version.mcVersion.contains(mcVersion)){
continue;
}
ui->versionSelectionBox->addItem(version.version, QVariant(i));
}
if(ui->versionSelectionBox->count() == 0){
ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1));
}
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob]
{
netJob->deleteLater();
delete response;
});
netJob->start();
} }
else if (ui->versionSelectionBox->count() == 0) {
{ ui->versionSelectionBox->addItem(tr("No Valid Version found!"),
for(int i = 0; i < current.versions.size(); i++) { QVariant(-1));
ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
}
if(ui->versionSelectionBox->count() == 0){
ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1));
}
suggestCurrent();
} }
updateSelectionButton();
}
} }
void FlameModPage::suggestCurrent() void FlameModPage::updateSelectionButton() {
{ if (!isOpened || selectedVersion < 0) {
if(!isOpened) ui->modSelectionButton->setEnabled(false);
{ return;
return; }
}
if (selectedVersion == -1) ui->modSelectionButton->setEnabled(true);
{ auto &version = current.versions[selectedVersion];
dialog->setSuggestedMod(); if (!dialog->isModSelected(current.name, version.fileName)) {
return; ui->modSelectionButton->setText(tr("Select mod for download"));
} } else {
ui->modSelectionButton->setText(tr("Deselect mod for download"));
auto version = current.versions[selectedVersion]; }
dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods));
} }
void FlameModPage::onVersionSelectionChanged(QString data) void FlameModPage::onVersionSelectionChanged(QString data) {
{ if (data.isNull() || data.isEmpty()) {
if(data.isNull() || data.isEmpty()) selectedVersion = -1;
{ return;
selectedVersion = -1; }
return; selectedVersion = ui->versionSelectionBox->currentData().toInt();
} updateSelectionButton();
selectedVersion = ui->versionSelectionBox->currentData().toInt(); }
suggestCurrent();
void FlameModPage::onModSelected() {
auto &version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
dialog->addSelectedMod(current.name,
new ModDownloadTask(version.downloadUrl,
version.fileName, dialog->mods));
}
updateSelectionButton();
} }

View File

@ -50,12 +50,13 @@ public:
BaseInstance *m_instance; BaseInstance *m_instance;
private: private:
void suggestCurrent(); void updateSelectionButton();
private slots: private slots:
void triggerSearch(); void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second); void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data); void onVersionSelectionChanged(QString data);
void onModSelected();
private: private:
Ui::FlameModPage *ui = nullptr; Ui::FlameModPage *ui = nullptr;

View File

@ -1,90 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>FlameModPage</class> <class>FlameModPage</class>
<widget class="QWidget" name="FlameModPage"> <widget class="QWidget" name="FlameModPage">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>837</width> <width>837</width>
<height>685</height> <height>685</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0,0,0" columnminimumwidth="0,0,0">
<item row="1" column="0"> <item row="1" column="2">
<widget class="QListView" name="packView"> <widget class="QComboBox" name="versionSelectionBox"/>
<property name="iconSize"> </item>
<size> <item row="1" column="0">
<width>48</width> <widget class="QComboBox" name="sortByBox"/>
<height>48</height> </item>
</size> <item row="1" column="1">
</property> <widget class="QLabel" name="label">
<property name="horizontalScrollBarPolicy"> <property name="text">
<enum>Qt::ScrollBarAlwaysOff</enum> <string>Version selected:</string>
</property> </property>
<property name="alternatingRowColors"> <property name="alignment">
<bool>true</bool> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="2">
<widget class="QTextBrowser" name="packDescription"> <widget class="QPushButton" name="modSelectionButton">
<property name="openExternalLinks"> <property name="text">
<bool>true</bool> <string>Select mod for download</string>
</property> </property>
<property name="openLinks"> </widget>
<bool>true</bool> </item>
</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> </layout>
</widget> </item>
<tabstops> <item row="0" column="0">
<tabstop>searchEdit</tabstop> <widget class="QLineEdit" name="searchEdit">
<tabstop>searchButton</tabstop> <property name="placeholderText">
<tabstop>packView</tabstop> <string>Search and filter ...</string>
<tabstop>packDescription</tabstop> </property>
<tabstop>sortByBox</tabstop> </widget>
<tabstop>versionSelectionBox</tabstop> </item>
</tabstops> <item row="1" column="0" colspan="2">
<resources/> <layout class="QGridLayout" name="gridLayout_3">
<connections/> <item row="1" column="0">
<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 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="0" column="1">
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</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> </ui>

View File

@ -4,180 +4,199 @@
#include <QKeyEvent> #include <QKeyEvent>
#include "Application.h" #include "Application.h"
#include "Json.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include "ModrinthModel.h" #include "Json.h"
#include "ModDownloadTask.h" #include "ModDownloadTask.h"
#include "ModrinthModel.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance)
: QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), dialog(dialog) : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage),
{ dialog(dialog) {
ui->setupUi(this); ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); connect(ui->searchButton, &QPushButton::clicked, this,
ui->searchEdit->installEventFilter(this); &ModrinthPage::triggerSearch);
listModel = new Modrinth::ListModel(this); ui->searchEdit->installEventFilter(this);
ui->packView->setModel(listModel); listModel = new Modrinth::ListModel(this);
ui->packView->setModel(listModel);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
// index is used to set the sorting with the modrinth api // index is used to set the sorting with the modrinth api
ui->sortByBox->addItem(tr("Sort by Relevence")); ui->sortByBox->addItem(tr("Sort by Relevence"));
ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows")); ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by last updated")); ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by newest")); ui->sortByBox->addItem(tr("Sort by newest"));
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this,
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); SLOT(triggerSearch()));
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged,
this, &ModrinthPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this,
&ModrinthPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this,
&ModrinthPage::onModSelected);
} }
ModrinthPage::~ModrinthPage() ModrinthPage::~ModrinthPage() { delete ui; }
{
delete ui;
}
bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) {
{ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); if (keyEvent->key() == Qt::Key_Return) {
if (keyEvent->key() == Qt::Key_Return) { triggerSearch();
triggerSearch(); keyEvent->accept();
keyEvent->accept(); return true;
return true;
}
} }
return QWidget::eventFilter(watched, event); }
return QWidget::eventFilter(watched, event);
} }
bool ModrinthPage::shouldDisplay() const bool ModrinthPage::shouldDisplay() const { return true; }
{
return true; void ModrinthPage::openedImpl() {
updateSelectionButton();
triggerSearch();
} }
void ModrinthPage::openedImpl() void ModrinthPage::triggerSearch() {
{ listModel->searchWithTerm(ui->searchEdit->text(),
suggestCurrent(); ui->sortByBox->currentIndex());
triggerSearch();
} }
void ModrinthPage::triggerSearch() void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) {
{ ui->versionSelectionBox->clear();
listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
}
void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!first.isValid()) {
{ return;
ui->versionSelectionBox->clear(); }
if(!first.isValid()) current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>();
{ QString text = "";
if(isOpened) QString name = current.name;
{
dialog->setSuggestedMod(); if (current.websiteUrl.isEmpty())
} text = name;
else
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
text += "<br>" + tr(" by ") + "<a href=\"" + current.author.url + "\">" +
current.author.name + "</a><br><br>";
ui->packDescription->setHtml(text + current.description);
if (!current.versionsLoaded) {
qDebug() << "Loading Modrinth mod versions";
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
auto netJob =
new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name),
APPLICATION->network());
auto response = new QByteArray();
QString addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId),
response));
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 Modrinth at "
<< parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return; return;
} }
QJsonArray arr = doc.array();
current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>(); try {
QString text = ""; Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(),
QString name = current.name; m_instance);
} catch (const JSONValidationError &e) {
if (current.websiteUrl.isEmpty()) qDebug() << *response;
text = name; qWarning() << "Error while reading Modrinth mod version: " << e.cause();
else }
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
text += "<br>"+ tr(" by ") + "<a href=\""+current.author.url+"\">"+current.author.name+"</a><br><br>"; QString mcVersion = packProfile->getComponentVersion("net.minecraft");
ui->packDescription->setHtml(text + current.description); QString loaderString =
(packProfile->getComponentVersion("net.minecraftforge").isEmpty())
if (!current.versionsLoaded) ? "fabric"
{ : "forge";
qDebug() << "Loading Modrinth mod versions"; for (int i = 0; i < current.versions.size(); i++) {
auto netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network()); auto version = current.versions[i];
auto response = new QByteArray(); if (!version.mcVersion.contains(mcVersion) ||
QString addonId = current.addonId; !version.loaders.contains(loaderString)) {
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); continue;
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 Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
QJsonArray arr = doc.array();
try
{
Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance);
}
catch(const JSONValidationError &e)
{
qDebug() << *response;
qWarning() << "Error while reading Modrinth mod version: " << e.cause();
}
auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge";
for(int i = 0; i < current.versions.size(); i++) {
auto version = current.versions[i];
if(!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)){
continue;
}
ui->versionSelectionBox->addItem(version.version, QVariant(i));
}
if(ui->versionSelectionBox->count() == 0){
ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1));
}
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob]{
netJob->deleteLater();
delete response;
});
netJob->start();
}
else
{
for(int i = 0; i < current.versions.size(); i++) {
ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
} }
if(ui->versionSelectionBox->count() == 0){ ui->versionSelectionBox->addItem(version.version, QVariant(i));
ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); }
} if (ui->versionSelectionBox->count() == 0) {
suggestCurrent(); ui->versionSelectionBox->addItem(tr("No Valid Version found !"),
} QVariant(-1));
} }
void ModrinthPage::suggestCurrent() ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
{ updateSelectionButton();
if(!isOpened) });
{
return; QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
netJob->deleteLater();
delete response;
});
netJob->start();
} else {
for (int i = 0; i < current.versions.size(); i++) {
ui->versionSelectionBox->addItem(current.versions[i].version,
QVariant(i));
}
if (ui->versionSelectionBox->count() == 0) {
ui->versionSelectionBox->addItem(tr("No Valid Version found !"),
QVariant(-1));
} }
if (selectedVersion == -1) updateSelectionButton();
{ }
dialog->setSuggestedMod();
return;
}
auto version = current.versions[selectedVersion];
dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods));
} }
void ModrinthPage::onVersionSelectionChanged(QString data) void ModrinthPage::updateSelectionButton() {
{ if (!isOpened || selectedVersion < 0) {
if(data.isNull() || data.isEmpty()) ui->modSelectionButton->setEnabled(false);
{ return;
selectedVersion = -1; }
return;
} ui->modSelectionButton->setEnabled(true);
selectedVersion = ui->versionSelectionBox->currentData().toInt(); auto &version = current.versions[selectedVersion];
suggestCurrent(); if (!dialog->isModSelected(current.name, version.fileName)) {
ui->modSelectionButton->setText(tr("Select mod for download"));
} else {
ui->modSelectionButton->setText(tr("Deselect mod for download"));
}
}
void ModrinthPage::onVersionSelectionChanged(QString data) {
if (data.isNull() || data.isEmpty()) {
selectedVersion = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ModrinthPage::onModSelected() {
auto &version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
dialog->addSelectedMod(current.name,
new ModDownloadTask(version.downloadUrl,
version.fileName, dialog->mods));
}
updateSelectionButton();
} }

View File

@ -50,12 +50,13 @@ public:
BaseInstance *m_instance; BaseInstance *m_instance;
private: private:
void suggestCurrent(); void updateSelectionButton();
private slots: private slots:
void triggerSearch(); void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second); void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data); void onVersionSelectionChanged(QString data);
void onModSelected();
private: private:
Ui::ModrinthPage *ui = nullptr; Ui::ModrinthPage *ui = nullptr;

View File

@ -1,90 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ModrinthPage</class> <class>ModrinthPage</class>
<widget class="QWidget" name="ModrinthPage"> <widget class="QWidget" name="ModrinthPage">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>837</width> <width>837</width>
<height>685</height> <height>685</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="2"> <item row="1" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0"> <item row="1" column="2">
<widget class="QListView" name="packView"> <widget class="QTextBrowser" name="packDescription">
<property name="iconSize"> <property name="openExternalLinks">
<size> <bool>true</bool>
<width>48</width> </property>
<height>48</height> <property name="openLinks">
</size> <bool>true</bool>
</property> </property>
<property name="horizontalScrollBarPolicy"> </widget>
<enum>Qt::ScrollBarAlwaysOff</enum> </item>
</property> <item row="1" column="0">
<property name="alternatingRowColors"> <widget class="QListView" name="packView">
<bool>true</bool> <property name="horizontalScrollBarPolicy">
</property> <enum>Qt::ScrollBarAlwaysOff</enum>
</widget> </property>
</item> <property name="alternatingRowColors">
<item row="1" column="1"> <bool>true</bool>
<widget class="QTextBrowser" name="packDescription"> </property>
<property name="openExternalLinks"> <property name="iconSize">
<bool>true</bool> <size>
</property> <width>48</width>
<property name="openLinks"> <height>48</height>
<bool>true</bool> </size>
</property> </property>
</widget> </widget>
</item> </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> </layout>
</widget> </item>
<tabstops> <item row="0" column="1">
<tabstop>searchEdit</tabstop> <widget class="QPushButton" name="searchButton">
<tabstop>searchButton</tabstop> <property name="text">
<tabstop>packView</tabstop> <string>Search</string>
<tabstop>packDescription</tabstop> </property>
<tabstop>sortByBox</tabstop> </widget>
<tabstop>versionSelectionBox</tabstop> </item>
</tabstops> <item row="0" column="0">
<resources/> <widget class="QLineEdit" name="searchEdit">
<connections/> <property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,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>
<item row="1" column="2">
<widget class="QPushButton" name="modSelectionButton">
<property name="text">
<string>Select mod for download</string>
</property>
</widget>
</item>
</layout>
</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> </ui>