Merge pull request #939 from flowln/mod_downloader_improve

Some more UI / UX improvements to the mod downloader!
This commit is contained in:
flow 2022-09-07 08:27:11 -03:00 committed by GitHub
commit 1b0ca47682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 535 additions and 147 deletions

View File

@ -685,6 +685,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", "");
m_settings->registerSetting("ModDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this?
{
m_settings->registerSetting("PastebinURL", "");

View File

@ -896,6 +896,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/PageContainer.cpp
ui/widgets/PageContainer.h
ui/widgets/PageContainer_p.h
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp

View File

@ -73,7 +73,7 @@ class ModAPI {
};
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0;
virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0;
virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
@ -85,7 +85,7 @@ class ModAPI {
ModLoaderTypes loaders;
};
virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0;
virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0;
static auto getModLoaderString(ModLoaderType type) -> const QString {
switch (type) {

View File

@ -75,6 +75,8 @@ struct ExtraPackData {
QString sourceUrl;
QString wikiUrl;
QString discordUrl;
QString body;
};
struct IndexedPack {

View File

@ -67,6 +67,43 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
return changelog;
}
auto FlameAPI::getModDescription(int modId) -> QString
{
QEventLoop lock;
QString description;
auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network());
auto* response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(
QString("https://api.curseforge.com/v1/mods/%1/description")
.arg(QString::number(modId)), response));
QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame::ModDescription at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
netJob->failed(parse_error.errorString());
return;
}
description = Json::ensureString(doc.object(), "data");
});
QObject::connect(netJob, &NetJob::finished, [response, &lock] {
delete response;
lock.quit();
});
netJob->start();
lock.exec();
return description;
}
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{
QEventLoop loop;

View File

@ -7,6 +7,7 @@ class FlameAPI : public NetworkModAPI {
public:
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;

View File

@ -4,10 +4,9 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FlameAPI.h"
#include "net/NetJob.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static FlameAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
@ -31,10 +30,11 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.authors.append(packAuthor);
}
loadExtraPackData(pack, obj);
pack.extraDataLoaded = false;
loadURLs(pack, obj);
}
void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
auto links_obj = Json::ensureObject(obj, "links");
@ -50,7 +50,16 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
if(pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1);
pack.extraDataLoaded = true;
if (!pack.extraData.body.isEmpty())
pack.extraDataLoaded = true;
}
void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.extraData.body = api.getModDescription(pack.addonId.toInt());
if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty())
pack.extraDataLoaded = true;
}
static QString enumToString(int hash_algorithm)

View File

@ -12,7 +12,8 @@
namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,

View File

@ -31,48 +31,48 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
netJob->start();
}
void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack)
void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback)
{
auto response = new QByteArray();
auto job = getProject(pack.addonId.toString(), response);
QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] {
QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
caller->infoRequestFinished(doc, pack);
callback(doc, pack);
});
job->start();
}
void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const
{
auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network());
auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network());
auto response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] {
QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
caller->versionRequestSucceeded(doc, args.addonId);
callback(doc, args.addonId);
});
QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] {
QObject::connect(netJob, &NetJob::finished, [response, netJob] {
netJob->deleteLater();
delete response;
});

View File

@ -5,8 +5,8 @@
class NetworkModAPI : public ModAPI {
public:
void searchMods(CallerType* caller, SearchArgs&& args) const override;
void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override;
void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;

View File

@ -87,6 +87,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraData.donate.append(donate);
}
pack.extraData.body = Json::ensureString(obj, "body");
pack.extraDataLoaded = true;
}

View File

@ -19,36 +19,33 @@
#include "ModDownloadDialog.h"
#include <BaseVersion.h>
#include <icons/IconList.h>
#include <InstanceList.h>
#include <icons/IconList.h>
#include "Application.h"
#include "ProgressDialog.h"
#include "ReviewMessageBox.h"
#include <QDialogButtonBox>
#include <QLayout>
#include <QPushButton>
#include <QValidator>
#include <QDialogButtonBox>
#include "ui/widgets/PageContainer.h"
#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ModDownloadTask.h"
#include "ui/pages/modplatform/flame/FlameModPage.h"
#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ui/widgets/PageContainer.h"
ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent,
BaseInstance *instance)
: QDialog(parent), mods(mods), m_instance(instance)
ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance)
: QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
{
setObjectName(QStringLiteral("ModDownloadDialog"));
resize(std::max(0.5*parent->width(), 400.0), std::max(0.75*parent->height(), 400.0));
m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
setWindowIcon(APPLICATION->getThemedIcon("new"));
// NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below.
// NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not
// move this below.
m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_container = new PageContainer(this);
@ -58,12 +55,17 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
m_container->addButtons(m_buttons);
connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged);
// 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
auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
OkButton->setEnabled(false);
OkButton->setDefault(true);
OkButton->setAutoDefault(true);
OkButton->setText(tr("Review and confirm"));
OkButton->setShortcut(tr("Ctrl+Return"));
OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return"));
connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
@ -78,7 +80,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
QMetaObject::connectSlotsByName(this);
setWindowModality(Qt::WindowModal);
setWindowTitle("Download mods");
setWindowTitle(dialogTitle());
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
}
QString ModDownloadDialog::dialogTitle()
@ -88,6 +92,7 @@ QString ModDownloadDialog::dialogTitle()
void ModDownloadDialog::reject()
{
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::reject();
}
@ -114,12 +119,13 @@ void ModDownloadDialog::confirm()
void ModDownloadDialog::accept()
{
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::accept();
}
QList<BasePage *> ModDownloadDialog::getPages()
QList<BasePage*> ModDownloadDialog::getPages()
{
QList<BasePage *> pages;
QList<BasePage*> pages;
pages.append(new ModrinthModPage(this, m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame)
@ -128,7 +134,7 @@ QList<BasePage *> ModDownloadDialog::getPages()
return pages;
}
void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task)
void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
{
removeSelectedMod(name);
modTask.insert(name, task);
@ -136,16 +142,16 @@ void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* tas
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
}
void ModDownloadDialog::removeSelectedMod(const QString &name)
void ModDownloadDialog::removeSelectedMod(QString name)
{
if(modTask.contains(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
bool ModDownloadDialog::isModSelected(QString name, 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?
@ -153,16 +159,31 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena
return iter != modTask.end() && (iter.value()->getFilename() == filename);
}
bool ModDownloadDialog::isModSelected(const QString &name) const
bool ModDownloadDialog::isModSelected(QString name) const
{
auto iter = modTask.find(name);
return iter != modTask.end();
}
ModDownloadDialog::~ModDownloadDialog()
const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
{
}
const QList<ModDownloadTask*> ModDownloadDialog::getTasks() {
return modTask.values();
}
void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
{
auto* prev_page = dynamic_cast<ModPage*>(previous);
if (!prev_page) {
qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
return;
}
auto* selected_page = dynamic_cast<ModPage*>(selected);
if (!selected_page) {
qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!";
return;
}
// Same effect as having a global search bar
selected_page->setSearchTerm(prev_page->getSearchTerm());
}

View File

@ -21,11 +21,9 @@
#include <QDialog>
#include <QVBoxLayout>
#include "BaseVersion.h"
#include "ui/pages/BasePageProvider.h"
#include "minecraft/mod/ModFolderModel.h"
#include "ModDownloadTask.h"
#include "ui/pages/modplatform/flame/FlameModPage.h"
#include "minecraft/mod/ModFolderModel.h"
#include "ui/pages/BasePageProvider.h"
namespace Ui
{
@ -36,21 +34,21 @@ class PageContainer;
class QDialogButtonBox;
class ModrinthModPage;
class ModDownloadDialog : public QDialog, public BasePageProvider
class ModDownloadDialog final : public QDialog, public BasePageProvider
{
Q_OBJECT
public:
explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, BaseInstance *instance);
~ModDownloadDialog();
explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
~ModDownloadDialog() override = default;
QString dialogTitle() override;
QList<BasePage *> getPages() override;
QList<BasePage*> getPages() override;
void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr);
void removeSelectedMod(const QString & name = QString());
bool isModSelected(const QString & name, const QString & filename) const;
bool isModSelected(const QString & name) const;
void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
void removeSelectedMod(QString name = QString());
bool isModSelected(QString name, QString filename) const;
bool isModSelected(QString name) const;
const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods;
@ -60,6 +58,9 @@ public slots:
void accept() override;
void reject() override;
private slots:
void selectedPageChanged(BasePage* previous, BasePage* selected);
private:
Ui::ModDownloadDialog *ui = nullptr;
PageContainer * m_container = nullptr;

View File

@ -1,11 +1,16 @@
#include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h"
#include <QPushButton>
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
: QDialog(parent), ui(new Ui::ReviewMessageBox)
{
ui->setupUi(this);
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back"));
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}

View File

@ -2,15 +2,27 @@
#include "BuildConfig.h"
#include "Json.h"
#include "ModPage.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
namespace ModPlatform {
ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {}
// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
static QHash<ListModel*, bool> s_running;
ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
ListModel::~ListModel()
{
s_running.find(this).value() = false;
}
auto ListModel::debugName() const -> QString
{
@ -39,9 +51,6 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) {
case Qt::DisplayRole: {
return pack.name;
}
case Qt::ToolTipRole: {
if (pack.description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
@ -64,20 +73,20 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
case Qt::FontRole: {
QFont font;
if (m_parent->getDialog()->isModSelected(pack.name)) {
font.setBold(true);
font.setUnderline(true);
}
return font;
}
// Custom data
case UserDataTypes::TITLE:
return pack.name;
case UserDataTypes::DESCRIPTION:
return pack.description;
case UserDataTypes::SELECTED:
return m_parent->getDialog()->isModSelected(pack.name);
default:
break;
}
@ -85,11 +94,27 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
return {};
}
void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid())
return false;
modpacks[pos] = value.value<ModPlatform::IndexedPack>();
return true;
}
void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() });
m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
[this, current, index](QJsonDocument& doc, QString addonId) {
if (!s_running.constFind(this).value())
return;
versionRequestSucceeded(doc, addonId, index);
});
}
void ListModel::performPaginatedSearch()
@ -100,9 +125,13 @@ void ListModel::performPaginatedSearch()
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
}
void ListModel::requestModInfo(ModPlatform::IndexedPack& current)
void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
{
m_parent->apiProvider()->getModInfo(this, current);
m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
if (!s_running.constFind(this).value())
return;
infoRequestFinished(doc, pack, index);
});
}
void ListModel::refresh()
@ -256,7 +285,7 @@ void ListModel::searchRequestFailed(QString reason)
}
}
void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack)
void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
qDebug() << "Loading mod info";
@ -268,10 +297,20 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack
qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
}
// Check if the index is still valid for this mod or not
if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
// Cache info :^)
QVariant new_pack;
new_pack.setValue(pack);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache mod info!";
}
}
m_parent->updateUi();
}
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
{
auto& current = m_parent->getCurrent();
if (addonId != current.addonId) {
@ -287,6 +326,14 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
}
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache mod versions!";
}
m_parent->updateModVersions();
}

View File

@ -2,7 +2,6 @@
#include <QAbstractListModel>
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
#include "net/NetJob.h"
@ -19,7 +18,7 @@ class ListModel : public QAbstractListModel {
public:
ListModel(ModPage* parent);
~ListModel() override = default;
~ListModel() override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
@ -29,15 +28,17 @@ class ListModel : public QAbstractListModel {
/* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
inline NetJob* activeJob() { return jobPtr.get(); }
/* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
void requestModInfo(ModPlatform::IndexedPack& current);
void requestModVersions(const ModPlatform::IndexedPack& current);
void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {};
@ -51,9 +52,9 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason);
void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack);
void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
void versionRequestSucceeded(QJsonDocument doc, QString addonId);
void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
protected slots:

View File

@ -40,9 +40,12 @@
#include <QKeyEvent>
#include <memory>
#include <HoeDown.h>
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
: QWidget(dialog)
@ -50,17 +53,30 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
, ui(new Ui::ModPage)
, dialog(dialog)
, filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this)
, m_fetch_progress(this, false)
, api(api)
{
ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount());
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
ui->gridLayout_3->addWidget(&filter_widget, 1, 0, 1, ui->gridLayout_3->columnCount());
filter_widget.setInstance(static_cast<MinecraftInstance*>(m_instance));
m_filter = filter_widget.getFilter();
@ -71,6 +87,9 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: none");
});
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packView->installEventFilter(this);
}
ModPage::~ModPage()
@ -93,6 +112,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
keyEvent->accept();
return true;
} else {
if (m_search_timer.isActive())
m_search_timer.stop();
m_search_timer.start(350);
}
} else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
onModSelected();
// To have the 'select mod' button outlined instead of the 'review and confirm' one
ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
keyEvent->accept();
return true;
}
@ -120,16 +156,26 @@ void ModPage::triggerSearch()
updateSelectionButton();
}
listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), changed);
listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
m_fetch_progress.watch(listModel->activeJob());
}
void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
QString ModPage::getSearchTerm() const
{
return ui->searchEdit->text();
}
void ModPage::setSearchTerm(QString term)
{
ui->searchEdit->setText(term);
}
void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
if (!first.isValid()) { return; }
if (!curr.isValid()) { return; }
current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>();
current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (!current.versionsLoaded) {
qDebug() << QString("Loading %1 mod versions").arg(debugName());
@ -137,7 +183,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
listModel->requestModVersions(current);
listModel->requestModVersions(current, curr);
} else {
for (int i = 0; i < current.versions.size(); i++) {
ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
@ -149,7 +195,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
if(!current.extraDataLoaded){
qDebug() << QString("Loading %1 mod info").arg(debugName());
listModel->requestModInfo(current);
listModel->requestModInfo(current, curr);
}
updateUi();
@ -167,6 +214,9 @@ void ModPage::onVersionSelectionChanged(QString data)
void ModPage::onModSelected()
{
if (selectedVersion < 0)
return;
auto& version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
@ -176,6 +226,9 @@ void ModPage::onModSelected()
}
updateSelectionButton();
/* Force redraw on the mods list when the selection changes */
ui->packView->adjustSize();
}
@ -285,5 +338,6 @@ void ModPage::updateUi()
text += "<hr>";
ui->packDescription->setHtml(text + current.description);
HoeDown h;
ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8())));
}

View File

@ -8,6 +8,7 @@
#include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
class ModDownloadDialog;
@ -45,6 +46,11 @@ class ModPage : public QWidget, public BasePage {
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
/** Get the current term in the search bar. */
auto getSearchTerm() const -> QString;
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1);
@ -70,10 +76,15 @@ class ModPage : public QWidget, public BasePage {
ModFilterWidget filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
ProgressWidget m_fetch_progress;
ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> api;
int selectedVersion = -1;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
};

View File

@ -12,6 +12,12 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);

View File

@ -13,6 +13,7 @@ class ListModel : public ModPlatform::ListModel {
private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;

View File

@ -1,27 +1,33 @@
#include "Common.h"
// Origin: Qt
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
qreal &widthUsed)
// More specifically, this is a trimmed down version on the algorithm in:
// https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846
QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height)
{
QStringList lines;
QList<std::pair<qreal, QString>> lines;
height = 0;
widthUsed = 0;
textLayout.beginLayout();
QString str = textLayout.text();
while (true)
{
while (true) {
QTextLine line = textLayout.createLine();
if (!line.isValid())
break;
if (line.textLength() == 0)
break;
line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, height));
height += line.height();
lines.append(str.mid(line.textStart(), line.textLength()));
widthUsed = qMax(widthUsed, line.naturalTextWidth());
lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength())));
}
textLayout.endLayout();
return lines;
}

View File

@ -1,6 +1,9 @@
#pragma once
#include <QStringList>
#include <QTextLayout>
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
qreal &widthUsed);
/** Cuts out the text in textLayout into smaller pieces, according to the lineWidth.
* Returns a list of pairs, each containing the width of that line and that line's string, respectively.
* The total height of those lines is set in the last argument, 'height'.
*/
QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height);

View File

@ -244,7 +244,14 @@ void PageContainer::help()
void PageContainer::currentChanged(const QModelIndex &current)
{
showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1);
int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1;
auto* selected = m_model->pages().at(selected_index);
auto* previous = m_currentPage;
emit selectedPageChanged(previous, selected);
showPage(selected_index);
}
bool PageContainer::prepareToClose()

View File

@ -95,6 +95,10 @@ private:
public slots:
void help();
signals:
/** Emitted when the currently selected page is changed */
void selectedPageChanged(BasePage* previous, BasePage* selected);
private slots:
void currentChanged(const QModelIndex &current);
void showPage(int row);

View File

@ -1,66 +1,104 @@
// Licensed under the Apache-2.0 license. See README.md for details.
#include "ProgressWidget.h"
#include <QProgressBar>
#include <QLabel>
#include <QVBoxLayout>
#include <QEventLoop>
#include <QLabel>
#include <QProgressBar>
#include <QVBoxLayout>
#include "tasks/Task.h"
ProgressWidget::ProgressWidget(QWidget *parent)
: QWidget(parent)
ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent)
{
m_label = new QLabel(this);
m_label->setWordWrap(true);
auto* layout = new QVBoxLayout(this);
if (show_label) {
m_label = new QLabel(this);
m_label->setWordWrap(true);
layout->addWidget(m_label);
}
m_bar = new QProgressBar(this);
m_bar->setMinimum(0);
m_bar->setMaximum(100);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_label);
layout->addWidget(m_bar);
layout->addStretch();
setLayout(layout);
}
void ProgressWidget::start(std::shared_ptr<Task> task)
void ProgressWidget::reset()
{
if (m_task)
{
disconnect(m_task.get(), 0, this, 0);
}
m_task = task;
connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus);
connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed);
if (!m_task->isRunning())
{
QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
}
m_bar->reset();
}
void ProgressWidget::progressFormat(QString format)
{
if (format.isEmpty())
m_bar->setTextVisible(false);
else
m_bar->setFormat(format);
}
void ProgressWidget::watch(Task* task)
{
if (!task)
return;
if (m_task)
disconnect(m_task, nullptr, this, nullptr);
m_task = task;
connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish);
connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus);
connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
show();
}
void ProgressWidget::start(Task* task)
{
watch(task);
if (!m_task->isRunning())
QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
}
bool ProgressWidget::exec(std::shared_ptr<Task> task)
{
QEventLoop loop;
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
start(task);
start(task.get());
if (task->isRunning())
{
loop.exec();
}
return task->wasSuccessful();
}
void ProgressWidget::show()
{
setHidden(false);
}
void ProgressWidget::hide()
{
setHidden(true);
}
void ProgressWidget::handleTaskFinish()
{
if (!m_task->wasSuccessful())
{
if (!m_task->wasSuccessful() && m_label)
m_label->setText(m_task->failReason());
}
if (m_hide_if_inactive)
hide();
}
void ProgressWidget::handleTaskStatus(const QString &status)
void ProgressWidget::handleTaskStatus(const QString& status)
{
m_label->setText(status);
if (m_label)
m_label->setText(status);
}
void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
{

View File

@ -9,24 +9,48 @@ class Task;
class QProgressBar;
class QLabel;
class ProgressWidget : public QWidget
{
class ProgressWidget : public QWidget {
Q_OBJECT
public:
explicit ProgressWidget(QWidget *parent = nullptr);
public:
explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true);
public slots:
void start(std::shared_ptr<Task> task);
/** Whether to hide the widget automatically if it's watching no running task. */
void hideIfInactive(bool hide) { m_hide_if_inactive = hide; }
/** Reset the displayed progress to 0 */
void reset();
/** The text that shows up in the middle of the progress bar.
* By default it's '%p%', with '%p' being the total progress in percentage.
*/
void progressFormat(QString);
public slots:
/** Watch the progress of a task. */
void watch(Task* task);
/** Watch the progress of a task, and start it if needed */
void start(Task* task);
/** Blocking way of waiting for a task to finish. */
bool exec(std::shared_ptr<Task> task);
private slots:
/** Un-hide the widget if needed. */
void show();
/** Make the widget invisible. */
void hide();
private slots:
void handleTaskFinish();
void handleTaskStatus(const QString &status);
void handleTaskStatus(const QString& status);
void handleTaskProgress(qint64 current, qint64 total);
void taskDestroyed();
private:
QLabel *m_label;
QProgressBar *m_bar;
std::shared_ptr<Task> m_task;
private:
QLabel* m_label = nullptr;
QProgressBar* m_bar = nullptr;
Task* m_task = nullptr;
bool m_hide_if_inactive = false;
};

View File

@ -0,0 +1,78 @@
#include "ProjectItem.h"
#include "Common.h"
#include <QIcon>
#include <QPainter>
ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
painter->save();
QStyleOptionViewItem opt(option);
initStyleOption(&opt, index);
auto& rect = opt.rect;
auto icon_width = rect.height(), icon_height = rect.height();
auto remaining_width = rect.width() - icon_width;
if (opt.state & QStyle::State_Selected) {
painter->fillRect(rect, opt.palette.highlight());
painter->setPen(opt.palette.highlightedText().color());
} else if (opt.state & QStyle::State_MouseOver) {
painter->fillRect(rect, opt.palette.window());
}
{ // Icon painting
// Square-sized, occupying the left portion
opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height);
}
{ // Title painting
auto title = index.data(UserDataTypes::TITLE).toString();
painter->save();
auto font = opt.font;
if (index.data(UserDataTypes::SELECTED).toBool()) {
// Set nice font
font.setBold(true);
font.setUnderline(true);
}
font.setPointSize(font.pointSize() + 2);
painter->setFont(font);
// On the top, aligned to the left after the icon
painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title);
painter->restore();
}
{ // Description painting
auto description = index.data(UserDataTypes::DESCRIPTION).toString();
QTextLayout text_layout(description, opt.font);
qreal height = 0;
auto cut_text = viewItemTextLayout(text_layout, remaining_width, height);
// Get first line unconditionally
description = cut_text.first().second;
// Get second line, elided if needed
if (cut_text.size() > 1) {
if (cut_text.size() > 2)
description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first);
else
description += cut_text.at(1).second;
}
// On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare)
painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width,
2 * opt.fontMetrics.height(), Qt::TextWordWrap, description);
}
painter->restore();
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <QStyledItemDelegate>
/* Custom data types for our custom list models :) */
enum UserDataTypes {
TITLE = 257, // QString
DESCRIPTION = 258, // QString
SELECTED = 259 // bool
};
/** This is an item delegate composed of:
* - An Icon on the left
* - A title
* - A description
* */
class ProjectItemDelegate final : public QStyledItemDelegate {
Q_OBJECT
public:
ProjectItemDelegate(QWidget* parent);
void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override;
};