2022-11-25 12:23:46 +00:00
|
|
|
#include "ResourceModel.h"
|
|
|
|
|
|
|
|
#include <QCryptographicHash>
|
|
|
|
#include <QIcon>
|
|
|
|
#include <QMessageBox>
|
|
|
|
#include <QPixmapCache>
|
|
|
|
#include <QUrl>
|
|
|
|
|
|
|
|
#include "Application.h"
|
|
|
|
#include "BuildConfig.h"
|
2022-12-23 21:18:20 +00:00
|
|
|
#include "Json.h"
|
2022-11-25 12:23:46 +00:00
|
|
|
|
|
|
|
#include "net/Download.h"
|
|
|
|
#include "net/NetJob.h"
|
|
|
|
|
|
|
|
#include "modplatform/ModIndex.h"
|
|
|
|
|
|
|
|
#include "ui/widgets/ProjectItem.h"
|
|
|
|
|
2022-12-16 22:03:52 +00:00
|
|
|
namespace ResourceDownload {
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
QHash<ResourceModel*, bool> ResourceModel::s_running_models;
|
|
|
|
|
2023-01-03 15:48:22 +00:00
|
|
|
ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api)
|
2022-11-25 12:23:46 +00:00
|
|
|
{
|
|
|
|
s_running_models.insert(this, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
ResourceModel::~ResourceModel()
|
|
|
|
{
|
|
|
|
s_running_models.find(this).value() = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
|
|
|
|
{
|
|
|
|
int pos = index.row();
|
|
|
|
if (pos >= m_packs.size() || pos < 0 || !index.isValid()) {
|
|
|
|
return QString("INVALID INDEX %1").arg(pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
auto pack = m_packs.at(pos);
|
|
|
|
switch (role) {
|
|
|
|
case 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;
|
|
|
|
}
|
|
|
|
case Qt::DecorationRole: {
|
|
|
|
if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack.logoUrl);
|
|
|
|
icon_or_none.has_value())
|
|
|
|
return icon_or_none.value();
|
|
|
|
|
|
|
|
return APPLICATION->getThemedIcon("screenshot-placeholder");
|
|
|
|
}
|
|
|
|
case Qt::SizeHintRole:
|
|
|
|
return QSize(0, 58);
|
|
|
|
case Qt::UserRole: {
|
|
|
|
QVariant v;
|
|
|
|
v.setValue(pack);
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
// Custom data
|
|
|
|
case UserDataTypes::TITLE:
|
|
|
|
return pack.name;
|
|
|
|
case UserDataTypes::DESCRIPTION:
|
|
|
|
return pack.description;
|
|
|
|
case UserDataTypes::SELECTED:
|
2022-12-18 18:41:46 +00:00
|
|
|
return pack.isAnyVersionSelected();
|
2022-11-25 12:23:46 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-12-18 19:55:09 +00:00
|
|
|
QHash<int, QByteArray> ResourceModel::roleNames() const
|
|
|
|
{
|
|
|
|
QHash<int, QByteArray> roles;
|
|
|
|
|
|
|
|
roles[Qt::ToolTipRole] = "toolTip";
|
|
|
|
roles[Qt::DecorationRole] = "decoration";
|
|
|
|
roles[Qt::SizeHintRole] = "sizeHint";
|
|
|
|
roles[Qt::UserRole] = "pack";
|
|
|
|
roles[UserDataTypes::TITLE] = "title";
|
|
|
|
roles[UserDataTypes::DESCRIPTION] = "description";
|
|
|
|
roles[UserDataTypes::SELECTED] = "selected";
|
|
|
|
|
|
|
|
return roles;
|
|
|
|
}
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
|
|
|
{
|
|
|
|
int pos = index.row();
|
|
|
|
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
m_packs[pos] = value.value<ModPlatform::IndexedPack>();
|
2022-12-18 18:41:46 +00:00
|
|
|
emit dataChanged(index, index);
|
2022-11-25 12:23:46 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ResourceModel::debugName() const
|
|
|
|
{
|
2022-12-18 18:41:46 +00:00
|
|
|
return "ResourceDownload (Model)";
|
2022-11-25 12:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::fetchMore(const QModelIndex& parent)
|
|
|
|
{
|
2023-01-03 19:26:07 +00:00
|
|
|
if (parent.isValid() || m_search_state == SearchState::Finished)
|
2022-11-25 12:23:46 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
search();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::search()
|
|
|
|
{
|
2022-12-20 20:14:17 +00:00
|
|
|
if (hasActiveSearchJob())
|
|
|
|
return;
|
2022-11-25 12:23:46 +00:00
|
|
|
|
|
|
|
auto args{ createSearchArguments() };
|
|
|
|
|
|
|
|
auto callbacks{ createSearchCallbacks() };
|
|
|
|
|
|
|
|
// Use defaults if no callbacks are set
|
2022-12-23 21:18:20 +00:00
|
|
|
if (!callbacks.on_succeed)
|
|
|
|
callbacks.on_succeed = [this](auto& doc) {
|
|
|
|
if (!s_running_models.constFind(this).value())
|
|
|
|
return;
|
|
|
|
searchRequestSucceeded(doc);
|
|
|
|
};
|
2022-11-25 12:23:46 +00:00
|
|
|
if (!callbacks.on_fail)
|
|
|
|
callbacks.on_fail = [this](QString reason, int network_error_code) {
|
|
|
|
if (!s_running_models.constFind(this).value())
|
|
|
|
return;
|
|
|
|
searchRequestFailed(reason, network_error_code);
|
|
|
|
};
|
|
|
|
if (!callbacks.on_abort)
|
|
|
|
callbacks.on_abort = [this] {
|
|
|
|
if (!s_running_models.constFind(this).value())
|
|
|
|
return;
|
|
|
|
searchRequestAborted();
|
|
|
|
};
|
|
|
|
|
|
|
|
if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
|
2022-12-20 20:14:17 +00:00
|
|
|
runSearchJob(job);
|
2022-11-25 12:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::loadEntry(QModelIndex& entry)
|
|
|
|
{
|
|
|
|
auto const& pack = m_packs[entry.row()];
|
|
|
|
|
2022-12-20 20:14:17 +00:00
|
|
|
if (!hasActiveInfoJob())
|
|
|
|
m_current_info_job.clear();
|
2022-11-25 12:23:46 +00:00
|
|
|
|
|
|
|
if (!pack.versionsLoaded) {
|
|
|
|
auto args{ createVersionsArguments(entry) };
|
|
|
|
auto callbacks{ createVersionsCallbacks(entry) };
|
|
|
|
|
2022-12-23 21:18:20 +00:00
|
|
|
// Use default if no callbacks are set
|
|
|
|
if (!callbacks.on_succeed)
|
|
|
|
callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
|
|
|
|
if (!s_running_models.constFind(this).value())
|
|
|
|
return;
|
|
|
|
versionRequestSucceeded(doc, pack, entry);
|
|
|
|
};
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
|
2022-12-20 20:14:17 +00:00
|
|
|
runInfoJob(job);
|
2022-11-25 12:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!pack.extraDataLoaded) {
|
|
|
|
auto args{ createInfoArguments(entry) };
|
|
|
|
auto callbacks{ createInfoCallbacks(entry) };
|
|
|
|
|
2022-12-23 21:18:20 +00:00
|
|
|
// Use default if no callbacks are set
|
|
|
|
if (!callbacks.on_succeed)
|
|
|
|
callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
|
|
|
|
if (!s_running_models.constFind(this).value())
|
|
|
|
return;
|
|
|
|
infoRequestSucceeded(doc, pack, entry);
|
|
|
|
};
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
|
2022-12-20 20:14:17 +00:00
|
|
|
runInfoJob(job);
|
2022-11-25 12:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::refresh()
|
|
|
|
{
|
2022-12-20 20:14:17 +00:00
|
|
|
bool reset_requested = false;
|
|
|
|
|
|
|
|
if (hasActiveInfoJob()) {
|
|
|
|
m_current_info_job.abort();
|
|
|
|
reset_requested = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasActiveSearchJob()) {
|
|
|
|
m_current_search_job->abort();
|
|
|
|
reset_requested = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reset_requested) {
|
2022-11-25 12:23:46 +00:00
|
|
|
m_search_state = SearchState::ResetRequested;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
clearData();
|
|
|
|
m_search_state = SearchState::None;
|
|
|
|
|
|
|
|
m_next_search_offset = 0;
|
|
|
|
search();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::clearData()
|
|
|
|
{
|
|
|
|
beginResetModel();
|
|
|
|
m_packs.clear();
|
|
|
|
endResetModel();
|
|
|
|
}
|
|
|
|
|
2023-01-03 16:58:27 +00:00
|
|
|
void ResourceModel::runSearchJob(Task::Ptr ptr)
|
2022-12-20 20:14:17 +00:00
|
|
|
{
|
|
|
|
m_current_search_job = ptr;
|
|
|
|
m_current_search_job->start();
|
|
|
|
}
|
|
|
|
void ResourceModel::runInfoJob(Task::Ptr ptr)
|
|
|
|
{
|
|
|
|
if (!m_current_info_job.isRunning())
|
|
|
|
m_current_info_job.clear();
|
|
|
|
|
|
|
|
m_current_info_job.addTask(ptr);
|
|
|
|
|
|
|
|
if (!m_current_info_job.isRunning())
|
|
|
|
m_current_info_job.run();
|
|
|
|
}
|
|
|
|
|
2022-12-23 20:28:42 +00:00
|
|
|
std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const
|
|
|
|
{
|
|
|
|
std::optional<ResourceAPI::SortingMethod> sort{};
|
|
|
|
|
2022-12-23 21:18:20 +00:00
|
|
|
{ // Find sorting method by ID
|
2022-12-23 20:28:42 +00:00
|
|
|
auto sorting_methods = getSortingMethods();
|
|
|
|
auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(),
|
2022-12-23 21:18:20 +00:00
|
|
|
[this](auto const& e) { return m_current_sort_index == e.index; });
|
2022-12-23 20:28:42 +00:00
|
|
|
if (method != sorting_methods.constEnd())
|
|
|
|
sort = *method;
|
|
|
|
}
|
|
|
|
|
|
|
|
return sort;
|
|
|
|
}
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
|
|
|
|
{
|
|
|
|
QPixmap pixmap;
|
|
|
|
if (QPixmapCache::find(url.toString(), &pixmap))
|
|
|
|
return { pixmap };
|
|
|
|
|
|
|
|
if (!m_current_icon_job)
|
|
|
|
m_current_icon_job = new NetJob("IconJob", APPLICATION->network());
|
|
|
|
|
|
|
|
if (m_currently_running_icon_actions.contains(url))
|
|
|
|
return {};
|
|
|
|
if (m_failed_icon_actions.contains(url))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
auto cache_entry = APPLICATION->metacache()->resolveEntry(
|
2022-12-18 18:41:46 +00:00
|
|
|
metaEntryBase(),
|
2022-11-25 12:23:46 +00:00
|
|
|
QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
|
|
|
|
auto icon_fetch_action = Net::Download::makeCached(url, cache_entry);
|
|
|
|
|
|
|
|
auto full_file_path = cache_entry->getFullPath();
|
|
|
|
connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
|
|
|
|
auto icon = QIcon(full_file_path);
|
|
|
|
QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
|
|
|
|
|
|
|
|
m_currently_running_icon_actions.remove(url);
|
|
|
|
|
|
|
|
emit dataChanged(index, index, { Qt::DecorationRole });
|
|
|
|
});
|
|
|
|
connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
|
|
|
|
m_currently_running_icon_actions.remove(url);
|
|
|
|
m_failed_icon_actions.insert(url);
|
|
|
|
});
|
|
|
|
|
|
|
|
m_currently_running_icon_actions.insert(url);
|
|
|
|
|
|
|
|
m_current_icon_job->addNetAction(icon_fetch_action);
|
|
|
|
if (!m_current_icon_job->isRunning())
|
|
|
|
QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start);
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-12-23 21:18:20 +00:00
|
|
|
// No 'forgor to implement' shall pass here :blobfox_knife:
|
|
|
|
#define NEED_FOR_CALLBACK_ASSERT(name) \
|
|
|
|
Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
|
|
|
|
|
|
|
|
QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const
|
|
|
|
{
|
|
|
|
NEED_FOR_CALLBACK_ASSERT("documentToArray");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
|
|
|
|
{
|
|
|
|
NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
|
|
|
|
}
|
|
|
|
void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
|
|
|
|
{
|
|
|
|
NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
|
|
|
|
}
|
|
|
|
void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
|
|
|
|
{
|
|
|
|
NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Default callbacks */
|
|
|
|
|
|
|
|
void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
|
|
|
|
{
|
|
|
|
QList<ModPlatform::IndexedPack> newList;
|
|
|
|
auto packs = documentToArray(doc);
|
|
|
|
|
|
|
|
for (auto packRaw : packs) {
|
|
|
|
auto packObj = packRaw.toObject();
|
|
|
|
|
|
|
|
ModPlatform::IndexedPack pack;
|
|
|
|
try {
|
|
|
|
loadIndexedPack(pack, packObj);
|
|
|
|
newList.append(pack);
|
|
|
|
} catch (const JSONValidationError& e) {
|
|
|
|
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (packs.size() < 25) {
|
|
|
|
m_search_state = SearchState::Finished;
|
|
|
|
} else {
|
|
|
|
m_next_search_offset += 25;
|
|
|
|
m_search_state = SearchState::CanFetchMore;
|
|
|
|
}
|
|
|
|
|
|
|
|
// When you have a Qt build with assertions turned on, proceeding here will abort the application
|
|
|
|
if (newList.size() == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
|
|
|
|
m_packs.append(newList);
|
|
|
|
endInsertRows();
|
|
|
|
}
|
|
|
|
|
2022-11-25 12:23:46 +00:00
|
|
|
void ResourceModel::searchRequestFailed(QString reason, int network_error_code)
|
|
|
|
{
|
|
|
|
switch (network_error_code) {
|
|
|
|
default:
|
|
|
|
// Network error
|
|
|
|
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
|
|
|
|
break;
|
|
|
|
case 409:
|
|
|
|
// 409 Gone, notify user to update
|
|
|
|
QMessageBox::critical(nullptr, tr("Error"),
|
2022-12-23 21:18:20 +00:00
|
|
|
QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
|
2022-11-25 12:23:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_search_state = SearchState::Finished;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::searchRequestAborted()
|
|
|
|
{
|
|
|
|
if (m_search_state != SearchState::ResetRequested)
|
|
|
|
qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!";
|
|
|
|
|
|
|
|
// Retry fetching
|
|
|
|
clearData();
|
|
|
|
|
|
|
|
m_next_search_offset = 0;
|
|
|
|
search();
|
|
|
|
}
|
2022-12-16 22:03:52 +00:00
|
|
|
|
2022-12-23 21:18:20 +00:00
|
|
|
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
|
|
|
|
{
|
|
|
|
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
|
|
|
|
|
|
|
|
// Check if the index is still valid for this resource or not
|
|
|
|
if (pack.addonId != current_pack.addonId)
|
|
|
|
return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
|
|
|
|
loadIndexedPackVersions(current_pack, arr);
|
|
|
|
} catch (const JSONValidationError& e) {
|
|
|
|
qDebug() << doc;
|
|
|
|
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache info :^)
|
|
|
|
QVariant new_pack;
|
|
|
|
new_pack.setValue(current_pack);
|
|
|
|
if (!setData(index, new_pack, Qt::UserRole)) {
|
|
|
|
qWarning() << "Failed to cache resource versions!";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
emit versionListUpdated();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
|
|
|
|
{
|
|
|
|
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
|
|
|
|
|
|
|
|
// Check if the index is still valid for this resource or not
|
|
|
|
if (pack.addonId != current_pack.addonId)
|
|
|
|
return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
auto obj = Json::requireObject(doc);
|
|
|
|
loadExtraPackInfo(current_pack, obj);
|
|
|
|
} catch (const JSONValidationError& e) {
|
|
|
|
qDebug() << doc;
|
|
|
|
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache info :^)
|
|
|
|
QVariant new_pack;
|
|
|
|
new_pack.setValue(current_pack);
|
|
|
|
if (!setData(index, new_pack, Qt::UserRole)) {
|
|
|
|
qWarning() << "Failed to cache resource info!";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
emit projectInfoUpdated();
|
|
|
|
}
|
|
|
|
|
2022-12-16 22:03:52 +00:00
|
|
|
} // namespace ResourceDownload
|