#include "ResourceModel.h" #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "net/Download.h" #include "net/NetJob.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ProjectItem.h" QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourcePage* parent, ResourceAPI* api) : QAbstractListModel(), m_api(api), m_associated_page(parent) { 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("
")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack.description; } case Qt::DecorationRole: { if (auto icon_or_none = const_cast(this)->getIcon(const_cast(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: return isPackSelected(pack); default: break; } return {}; } 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(); return true; } QString ResourceModel::debugName() const { return m_associated_page->debugName() + " (Model)"; } void ResourceModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; Q_ASSERT(m_next_search_offset != 0); search(); } void ResourceModel::search() { if (!m_current_job.isRunning()) m_current_job.clear(); auto args{ createSearchArguments() }; auto callbacks{ createSearchCallbacks() }; Q_ASSERT(callbacks.on_succeed); // Use defaults if no callbacks are set 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) addActiveJob(job); } void ResourceModel::loadEntry(QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; if (!m_current_job.isRunning()) m_current_job.clear(); if (!pack.versionsLoaded) { auto args{ createVersionsArguments(entry) }; auto callbacks{ createVersionsCallbacks(entry) }; if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) addActiveJob(job); } if (!pack.extraDataLoaded) { auto args{ createInfoArguments(entry) }; auto callbacks{ createInfoCallbacks(entry) }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) addActiveJob(job); } } void ResourceModel::refresh() { if (m_current_job.isRunning()) { m_current_job.abort(); 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(); } std::optional 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( m_associated_page->metaEntryBase(), 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 {}; } bool ResourceModel::isPackSelected(const ModPlatform::IndexedPack& pack) const { return m_associated_page->isPackSelected(pack); } 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"), //: %1 refers to the launcher itself QString("%1 %2") .arg(m_associated_page->displayName()) .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); 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(); }