feat:refactored modpack ux
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
		| @@ -109,6 +109,8 @@ class ResourceAPI { | ||||
|     }; | ||||
|     struct ProjectInfoCallbacks { | ||||
|         std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed; | ||||
|         std::function<void(QString const& reason)> on_fail; | ||||
|         std::function<void()> on_abort; | ||||
|     }; | ||||
|  | ||||
|     struct DependencySearchArgs { | ||||
|   | ||||
| @@ -72,7 +72,8 @@ Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfo | ||||
|  | ||||
|         callbacks.on_succeed(doc, args.pack); | ||||
|     }); | ||||
|  | ||||
|     QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); | ||||
|     QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); | ||||
|     return job; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -132,6 +132,36 @@ void ResourceModel::search() | ||||
|     if (hasActiveSearchJob()) | ||||
|         return; | ||||
|  | ||||
|     if (m_search_term.startsWith("#")) { | ||||
|         auto projectId = m_search_term.removeFirst(); | ||||
|         if (!projectId.isEmpty()) { | ||||
|             ResourceAPI::ProjectInfoCallbacks callbacks; | ||||
|  | ||||
|             // Use defaults if no callbacks are set | ||||
|             if (!callbacks.on_fail) | ||||
|                 callbacks.on_fail = [this](QString reason) { | ||||
|                     if (!s_running_models.constFind(this).value()) | ||||
|                         return; | ||||
|                     searchRequestFailed(reason, -1); | ||||
|                 }; | ||||
|             if (!callbacks.on_abort) | ||||
|                 callbacks.on_abort = [this] { | ||||
|                     if (!s_running_models.constFind(this).value()) | ||||
|                         return; | ||||
|                     searchRequestAborted(); | ||||
|                 }; | ||||
|  | ||||
|             if (!callbacks.on_succeed) | ||||
|                 callbacks.on_succeed = [this](auto& doc, auto pack) { | ||||
|                     if (!s_running_models.constFind(this).value()) | ||||
|                         return; | ||||
|                     searchRequestForOneSucceeded(doc); | ||||
|                 }; | ||||
|             if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) | ||||
|                 runSearchJob(job); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|     auto args{ createSearchArguments() }; | ||||
|  | ||||
|     auto callbacks{ createSearchCallbacks() }; | ||||
| @@ -194,6 +224,12 @@ void ResourceModel::loadEntry(QModelIndex& entry) | ||||
|                     return; | ||||
|                 infoRequestSucceeded(doc, pack, entry); | ||||
|             }; | ||||
|         if (!callbacks.on_fail) | ||||
|             callbacks.on_fail = [this](QString reason) { | ||||
|                 if (!s_running_models.constFind(this).value()) | ||||
|                     return; | ||||
|                 QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); | ||||
|             }; | ||||
|  | ||||
|         if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) | ||||
|             runInfoJob(job); | ||||
| @@ -372,6 +408,27 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) | ||||
| { | ||||
|     ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>(); | ||||
|  | ||||
|     try { | ||||
|         auto obj = Json::requireObject(doc); | ||||
|         if (obj.contains("data")) | ||||
|             obj = Json::requireObject(obj, "data"); | ||||
|         loadIndexedPack(*pack, obj); | ||||
|     } catch (const JSONValidationError& e) { | ||||
|         qDebug() << doc; | ||||
|         qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); | ||||
|     } | ||||
|  | ||||
|     m_search_state = SearchState::Finished; | ||||
|  | ||||
|     beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); | ||||
|     m_packs.append(pack); | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code) | ||||
| { | ||||
|     switch (network_error_code) { | ||||
|   | ||||
| @@ -149,6 +149,7 @@ class ResourceModel : public QAbstractListModel { | ||||
|    private: | ||||
|     /* Default search request callbacks */ | ||||
|     void searchRequestSucceeded(QJsonDocument&); | ||||
|     void searchRequestForOneSucceeded(QJsonDocument&); | ||||
|     void searchRequestFailed(QString reason, int network_error_code); | ||||
|     void searchRequestAborted(); | ||||
|  | ||||
|   | ||||
| @@ -44,9 +44,6 @@ | ||||
| #include <QKeyEvent> | ||||
|  | ||||
| #include "Markdown.h" | ||||
| #include "ResourceDownloadTask.h" | ||||
|  | ||||
| #include "minecraft/MinecraftInstance.h" | ||||
|  | ||||
| #include "ui/dialogs/ResourceDownloadDialog.h" | ||||
| #include "ui/pages/modplatform/ResourceModel.h" | ||||
|   | ||||
| @@ -67,9 +67,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen | ||||
|     if (searchTerm.isEmpty()) { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); | ||||
|     ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>(); | ||||
|     if (searchTerm.startsWith("#")) | ||||
|         return QString::number(pack.id) == searchTerm.mid(1); | ||||
|     return pack.name.contains(searchTerm, Qt::CaseInsensitive); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
| #include <Json.h> | ||||
|  | ||||
| #include "net/ApiDownload.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| namespace Atl { | ||||
|  | ||||
| @@ -46,11 +47,17 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|     } | ||||
|  | ||||
|     ATLauncher::IndexedPack pack = modpacks.at(pos); | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         return pack.name; | ||||
|     } else if (role == Qt::ToolTipRole) { | ||||
|         return pack.name; | ||||
|     } else if (role == Qt::DecorationRole) { | ||||
|     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 (m_logoMap.contains(pack.safeName)) { | ||||
|                 return (m_logoMap.value(pack.safeName)); | ||||
|             } | ||||
| @@ -60,13 +67,30 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|             ((ListModel*)this)->requestLogo(pack.safeName, url); | ||||
|  | ||||
|             return icon; | ||||
|     } else if (role == Qt::UserRole) { | ||||
|         } | ||||
|         case Qt::UserRole: { | ||||
|             QVariant v; | ||||
|             v.setValue(pack); | ||||
|             return v; | ||||
|         } | ||||
|         case Qt::DisplayRole: | ||||
|             return pack.name; | ||||
|         case Qt::SizeHintRole: | ||||
|             return QSize(0, 58); | ||||
|         // Custom data | ||||
|         case UserDataTypes::TITLE: | ||||
|             return pack.name; | ||||
|         case UserDataTypes::DESCRIPTION: | ||||
|             return pack.description; | ||||
|         case UserDataTypes::SELECTED: | ||||
|             return false; | ||||
|         case UserDataTypes::INSTALLED: | ||||
|             return false; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
|     return {}; | ||||
| } | ||||
|  | ||||
| void ListModel::request() | ||||
|   | ||||
| @@ -35,11 +35,11 @@ | ||||
|  */ | ||||
|  | ||||
| #include "AtlPage.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
| #include "ui_AtlPage.h" | ||||
|  | ||||
| #include "BuildConfig.h" | ||||
|  | ||||
| #include "AtlOptionalModDialog.h" | ||||
| #include "AtlUserInteractionSupportImpl.h" | ||||
| #include "modplatform/atlauncher/ATLPackInstallTask.h" | ||||
| #include "ui/dialogs/NewInstanceDialog.h" | ||||
| @@ -71,6 +71,8 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), | ||||
|     connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); | ||||
|     connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); | ||||
|     connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); | ||||
|  | ||||
|     ui->packView->setItemDelegate(new ProjectItemDelegate(this)); | ||||
| } | ||||
|  | ||||
| AtlPage::~AtlPage() | ||||
|   | ||||
| @@ -11,44 +11,7 @@ | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QGridLayout" name="gridLayout"> | ||||
|    <item row="1" column="0" colspan="2"> | ||||
|     <layout class="QGridLayout" name="gridLayout_3"> | ||||
|      <item row="1" column="0"> | ||||
|       <widget class="QTreeView" name="packView"> | ||||
|        <property name="alternatingRowColors"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="iconSize"> | ||||
|         <size> | ||||
|          <width>96</width> | ||||
|          <height>48</height> | ||||
|         </size> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item row="1" column="1"> | ||||
|       <widget class="QTextBrowser" name="packDescription"> | ||||
|        <property name="openExternalLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="openLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item row="0" column="0" colspan="2"> | ||||
|       <widget class="QLabel" name="label_2"> | ||||
|        <property name="text"> | ||||
|         <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> | ||||
|        </property> | ||||
|        <property name="wordWrap"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="2" column="0" colspan="2"> | ||||
|    <item row="3" 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"/> | ||||
| @@ -68,7 +31,34 @@ | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="0" column="0"> | ||||
|    <item row="2" column="0" colspan="2"> | ||||
|     <layout class="QGridLayout" name="gridLayout_3"> | ||||
|      <item row="1" column="1"> | ||||
|       <widget class="QTextBrowser" name="packDescription"> | ||||
|        <property name="openExternalLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="openLinks"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item row="1" column="0"> | ||||
|       <widget class="QTreeView" name="packView"> | ||||
|        <property name="alternatingRowColors"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|        <property name="iconSize"> | ||||
|         <size> | ||||
|          <width>96</width> | ||||
|          <height>48</height> | ||||
|         </size> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="1" column="0"> | ||||
|     <widget class="QLineEdit" name="searchEdit"> | ||||
|      <property name="placeholderText"> | ||||
|       <string>Search and filter...</string> | ||||
| @@ -78,6 +68,31 @@ | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="1" column="1"> | ||||
|     <widget class="QPushButton" name="pushButton"> | ||||
|      <property name="text"> | ||||
|       <string>Search</string> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="0" column="0" colspan="2"> | ||||
|     <widget class="QLabel" name="label_2"> | ||||
|      <property name="font"> | ||||
|       <font> | ||||
|        <italic>true</italic> | ||||
|       </font> | ||||
|      </property> | ||||
|      <property name="text"> | ||||
|       <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> | ||||
|      </property> | ||||
|      <property name="alignment"> | ||||
|       <set>Qt::AlignCenter</set> | ||||
|      </property> | ||||
|      <property name="wordWrap"> | ||||
|       <bool>true</bool> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <tabstops> | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| #include "FlameModel.h" | ||||
| #include <Json.h> | ||||
| #include "Application.h" | ||||
| #include "modplatform/ResourceAPI.h" | ||||
| #include "modplatform/flame/FlameAPI.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| #include "net/ApiDownload.h" | ||||
| @@ -161,6 +163,25 @@ void ListModel::fetchMore(const QModelIndex& parent) | ||||
|  | ||||
| void ListModel::performPaginatedSearch() | ||||
| { | ||||
|     if (currentSearchTerm.startsWith("#")) { | ||||
|         auto projectId = currentSearchTerm.removeFirst(); | ||||
|         if (!projectId.isEmpty()) { | ||||
|             ResourceAPI::ProjectInfoCallbacks callbacks; | ||||
|  | ||||
|             // Use defaults if no callbacks are set | ||||
|             if (!callbacks.on_fail) | ||||
|                 callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; | ||||
|  | ||||
|             if (!callbacks.on_succeed) | ||||
|                 callbacks.on_succeed = [this](auto& doc, auto pack) { searchRequestForOneSucceeded(doc); }; | ||||
|             static const FlameAPI api; | ||||
|             if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { | ||||
|                 jobPtr = job; | ||||
|                 jobPtr->start(); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|     auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network()); | ||||
|     auto searchUrl = QString( | ||||
|                          "https://api.curseforge.com/v1/mods/search?" | ||||
| @@ -189,23 +210,24 @@ void ListModel::searchWithTerm(const QString& term, int sort) | ||||
|     } | ||||
|     currentSearchTerm = term; | ||||
|     currentSort = sort; | ||||
|     if (jobPtr) { | ||||
|     if (hasActiveSearchJob()) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } else { | ||||
|     } | ||||
|     beginResetModel(); | ||||
|     modpacks.clear(); | ||||
|     endResetModel(); | ||||
|     searchState = None; | ||||
|     } | ||||
|  | ||||
|     nextSearchOffset = 0; | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
|  | ||||
| void Flame::ListModel::searchRequestFinished() | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|     if (hasActiveSearchJob()) | ||||
|         return; | ||||
|  | ||||
|     QJsonParseError parse_error; | ||||
|     QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); | ||||
| @@ -246,6 +268,25 @@ void Flame::ListModel::searchRequestFinished() | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|  | ||||
|     auto packObj = Json::ensureObject(doc.object(), "data"); | ||||
|  | ||||
|     Flame::IndexedPack pack; | ||||
|     try { | ||||
|         Flame::loadIndexedPack(pack, packObj); | ||||
|     } catch (const JSONValidationError& e) { | ||||
|         qWarning() << "Error while loading pack from CurseForge: " << e.cause(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); | ||||
|     modpacks.append({ pack }); | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void Flame::ListModel::searchRequestFailed(QString reason) | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|   | ||||
| @@ -40,6 +40,8 @@ class ListModel : public QAbstractListModel { | ||||
|     void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); | ||||
|     void searchWithTerm(const QString& term, const int sort); | ||||
|  | ||||
|     [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } | ||||
|  | ||||
|    private slots: | ||||
|     void performPaginatedSearch(); | ||||
|  | ||||
| @@ -48,6 +50,7 @@ class ListModel : public QAbstractListModel { | ||||
|  | ||||
|     void searchRequestFinished(); | ||||
|     void searchRequestFailed(QString reason); | ||||
|     void searchRequestForOneSucceeded(QJsonDocument&); | ||||
|  | ||||
|    private: | ||||
|     void requestLogo(QString file, QString url); | ||||
| @@ -63,7 +66,7 @@ class ListModel : public QAbstractListModel { | ||||
|     int currentSort = 0; | ||||
|     int nextSearchOffset = 0; | ||||
|     enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; | ||||
|     NetJob::Ptr jobPtr; | ||||
|     Task::Ptr jobPtr; | ||||
|     std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -61,6 +61,11 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(paren | ||||
|     ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); | ||||
|     ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); | ||||
|  | ||||
|     m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); | ||||
|     m_search_timer.setSingleShot(true); | ||||
|  | ||||
|     connect(&m_search_timer, &QTimer::timeout, this, &FlamePage::triggerSearch); | ||||
|  | ||||
|     // index is used to set the sorting with the curseforge api | ||||
|     ui->sortByBox->addItem(tr("Sort by Featured")); | ||||
|     ui->sortByBox->addItem(tr("Sort by Popularity")); | ||||
| @@ -90,6 +95,11 @@ bool FlamePage::eventFilter(QObject* watched, QEvent* event) | ||||
|             triggerSearch(); | ||||
|             keyEvent->accept(); | ||||
|             return true; | ||||
|         } else { | ||||
|             if (m_search_timer.isActive()) | ||||
|                 m_search_timer.stop(); | ||||
|  | ||||
|             m_search_timer.start(350); | ||||
|         } | ||||
|     } | ||||
|     return QWidget::eventFilter(watched, event); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
|  | ||||
| #include <Application.h> | ||||
| #include <modplatform/flame/FlamePackIndex.h> | ||||
| #include "tasks/Task.h" | ||||
| #include <QTimer> | ||||
| #include "ui/pages/BasePage.h" | ||||
|  | ||||
| namespace Ui { | ||||
| @@ -86,4 +86,7 @@ class FlamePage : public QWidget, public BasePage { | ||||
|     Flame::IndexedPack current; | ||||
|  | ||||
|     int m_selected_version_index = -1; | ||||
|  | ||||
|     // Used to do instant searching with a delay to cache quick changes | ||||
|     QTimer m_search_timer; | ||||
| }; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
|  */ | ||||
|  | ||||
| #include "ImportFTBPage.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
| #include "ui_ImportFTBPage.h" | ||||
|  | ||||
| #include <QWidget> | ||||
| @@ -32,17 +33,30 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg | ||||
|     ui->setupUi(this); | ||||
|  | ||||
|     { | ||||
|         currentModel = new FilterModel(this); | ||||
|         listModel = new ListModel(this); | ||||
|         currentModel->setSourceModel(listModel); | ||||
|  | ||||
|         ui->modpackList->setModel(listModel); | ||||
|         ui->modpackList->setModel(currentModel); | ||||
|         ui->modpackList->setSortingEnabled(true); | ||||
|         ui->modpackList->header()->hide(); | ||||
|         ui->modpackList->setIndentation(0); | ||||
|         ui->modpackList->setIconSize(QSize(42, 42)); | ||||
|  | ||||
|         for (int i = 0; i < currentModel->getAvailableSortings().size(); i++) { | ||||
|             ui->sortByBox->addItem(currentModel->getAvailableSortings().keys().at(i)); | ||||
|         } | ||||
|  | ||||
|         ui->sortByBox->setCurrentText(currentModel->translateCurrentSorting()); | ||||
|     } | ||||
|  | ||||
|     connect(ui->modpackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ImportFTBPage::onPublicPackSelectionChanged); | ||||
|  | ||||
|     connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &ImportFTBPage::onSortingSelectionChanged); | ||||
|  | ||||
|     connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); | ||||
|  | ||||
|     ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); | ||||
|     ui->modpackList->selectionModel()->reset(); | ||||
| } | ||||
|  | ||||
| @@ -86,7 +100,7 @@ void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex pr | ||||
|         onPackSelectionChanged(); | ||||
|         return; | ||||
|     } | ||||
|     Modpack selectedPack = listModel->data(now, Qt::UserRole).value<Modpack>(); | ||||
|     Modpack selectedPack = currentModel->data(now, Qt::UserRole).value<Modpack>(); | ||||
|     onPackSelectionChanged(&selectedPack); | ||||
| } | ||||
|  | ||||
| @@ -101,4 +115,15 @@ void ImportFTBPage::onPackSelectionChanged(Modpack* pack) | ||||
|         dialog->setSuggestedPack(); | ||||
| } | ||||
|  | ||||
| void ImportFTBPage::onSortingSelectionChanged(QString sort) | ||||
| { | ||||
|     FilterModel::Sorting toSet = currentModel->getAvailableSortings().value(sort); | ||||
|     currentModel->setSorting(toSet); | ||||
| } | ||||
|  | ||||
| void ImportFTBPage::triggerSearch() | ||||
| { | ||||
|     currentModel->setSearchTerm(ui->searchEdit->text()); | ||||
| } | ||||
|  | ||||
| }  // namespace FTBImportAPP | ||||
|   | ||||
| @@ -53,12 +53,15 @@ class ImportFTBPage : public QWidget, public BasePage { | ||||
|     void suggestCurrent(); | ||||
|     void onPackSelectionChanged(Modpack* pack = nullptr); | ||||
|    private slots: | ||||
|     void onSortingSelectionChanged(QString data); | ||||
|     void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); | ||||
|     void triggerSearch(); | ||||
|  | ||||
|    private: | ||||
|     bool initialized = false; | ||||
|     Modpack selected; | ||||
|     ListModel* listModel = nullptr; | ||||
|     FilterModel* currentModel = nullptr; | ||||
|  | ||||
|     NewInstanceDialog* dialog = nullptr; | ||||
|     Ui::ImportFTBPage* ui = nullptr; | ||||
|   | ||||
| @@ -10,8 +10,8 @@ | ||||
|     <height>1011</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|    <item> | ||||
|   <layout class="QGridLayout" name="gridLayout"> | ||||
|    <item row="1" column="1"> | ||||
|     <widget class="QTreeView" name="modpackList"> | ||||
|      <property name="maximumSize"> | ||||
|       <size> | ||||
| @@ -21,6 +21,54 @@ | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item row="0" column="1"> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|      <item> | ||||
|       <widget class="QLineEdit" name="searchEdit"> | ||||
|        <property name="placeholderText"> | ||||
|         <string>Search and filter...</string> | ||||
|        </property> | ||||
|        <property name="clearButtonEnabled"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <widget class="QPushButton" name="pushButton"> | ||||
|        <property name="text"> | ||||
|         <string>Search</string> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="2" column="1"> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout_2"> | ||||
|      <item> | ||||
|       <widget class="QComboBox" name="sortByBox"> | ||||
|        <property name="minimumSize"> | ||||
|         <size> | ||||
|          <width>265</width> | ||||
|          <height>0</height> | ||||
|         </size> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <spacer name="horizontalSpacer"> | ||||
|        <property name="orientation"> | ||||
|         <enum>Qt::Horizontal</enum> | ||||
|        </property> | ||||
|        <property name="sizeHint" stdset="0"> | ||||
|         <size> | ||||
|          <width>40</width> | ||||
|          <height>20</height> | ||||
|         </size> | ||||
|        </property> | ||||
|       </spacer> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|   | ||||
| @@ -23,7 +23,9 @@ | ||||
| #include <QIcon> | ||||
| #include <QProcessEnvironment> | ||||
| #include "FileSystem.h" | ||||
| #include "StringUtils.h" | ||||
| #include "modplatform/import_ftb/PackHelpers.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| namespace FTBImportAPP { | ||||
|  | ||||
| @@ -71,18 +73,99 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|     } | ||||
|  | ||||
|     auto pack = modpacks.at(pos); | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         return pack.name; | ||||
|     } else if (role == Qt::DecorationRole) { | ||||
|     if (role == Qt::ToolTipRole) { | ||||
|     } | ||||
|  | ||||
|     switch (role) { | ||||
|         case Qt::ToolTipRole: | ||||
|             return tr("Minecraft %1").arg(pack.mcVersion); | ||||
|         case Qt::DecorationRole: | ||||
|             return pack.icon; | ||||
|     } else if (role == Qt::UserRole) { | ||||
|         case Qt::UserRole: { | ||||
|             QVariant v; | ||||
|             v.setValue(pack); | ||||
|             return v; | ||||
|     } else if (role == Qt::ToolTipRole) { | ||||
|         } | ||||
|         case Qt::DisplayRole: | ||||
|             return pack.name; | ||||
|         case Qt::SizeHintRole: | ||||
|             return QSize(0, 58); | ||||
|         // Custom data | ||||
|         case UserDataTypes::TITLE: | ||||
|             return pack.name; | ||||
|         case UserDataTypes::DESCRIPTION: | ||||
|             return tr("Minecraft %1").arg(pack.mcVersion); | ||||
|         case UserDataTypes::SELECTED: | ||||
|             return false; | ||||
|         case UserDataTypes::INSTALLED: | ||||
|             return false; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
|     return {}; | ||||
| } | ||||
|  | ||||
| FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) | ||||
| { | ||||
|     currentSorting = Sorting::ByGameVersion; | ||||
|     sortings.insert(tr("Sort by Name"), Sorting::ByName); | ||||
|     sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); | ||||
| } | ||||
|  | ||||
| bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const | ||||
| { | ||||
|     Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>(); | ||||
|     Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>(); | ||||
|  | ||||
|     if (currentSorting == Sorting::ByGameVersion) { | ||||
|         Version lv(leftPack.mcVersion); | ||||
|         Version rv(rightPack.mcVersion); | ||||
|         return lv < rv; | ||||
|  | ||||
|     } else if (currentSorting == Sorting::ByName) { | ||||
|         return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; | ||||
|     } | ||||
|  | ||||
|     // UHM, some inavlid value set?! | ||||
|     qWarning() << "Invalid sorting set!"; | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const | ||||
| { | ||||
|     if (searchTerm.isEmpty()) { | ||||
|         return true; | ||||
|     } | ||||
|     QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); | ||||
|     Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>(); | ||||
|     return pack.name.contains(searchTerm, Qt::CaseInsensitive); | ||||
| } | ||||
|  | ||||
| void FilterModel::setSearchTerm(const QString term) | ||||
| { | ||||
|     searchTerm = term.trimmed(); | ||||
|     invalidate(); | ||||
| } | ||||
|  | ||||
| const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() | ||||
| { | ||||
|     return sortings; | ||||
| } | ||||
|  | ||||
| QString FilterModel::translateCurrentSorting() | ||||
| { | ||||
|     return sortings.key(currentSorting); | ||||
| } | ||||
|  | ||||
| void FilterModel::setSorting(Sorting s) | ||||
| { | ||||
|     currentSorting = s; | ||||
|     invalidate(); | ||||
| } | ||||
|  | ||||
| FilterModel::Sorting FilterModel::getCurrentSorting() | ||||
| { | ||||
|     return currentSorting; | ||||
| } | ||||
| }  // namespace FTBImportAPP | ||||
| @@ -20,11 +20,33 @@ | ||||
|  | ||||
| #include <QAbstractListModel> | ||||
| #include <QIcon> | ||||
| #include <QSortFilterProxyModel> | ||||
| #include <QVariant> | ||||
| #include "modplatform/import_ftb/PackHelpers.h" | ||||
|  | ||||
| namespace FTBImportAPP { | ||||
|  | ||||
| class FilterModel : public QSortFilterProxyModel { | ||||
|     Q_OBJECT | ||||
|    public: | ||||
|     FilterModel(QObject* parent = Q_NULLPTR); | ||||
|     enum Sorting { ByName, ByGameVersion }; | ||||
|     const QMap<QString, Sorting> getAvailableSortings(); | ||||
|     QString translateCurrentSorting(); | ||||
|     void setSorting(Sorting sorting); | ||||
|     Sorting getCurrentSorting(); | ||||
|     void setSearchTerm(QString term); | ||||
|  | ||||
|    protected: | ||||
|     bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; | ||||
|     bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; | ||||
|  | ||||
|    private: | ||||
|     QMap<QString, Sorting> sortings; | ||||
|     Sorting currentSorting; | ||||
|     QString searchTerm; | ||||
| }; | ||||
|  | ||||
| class ListModel : public QAbstractListModel { | ||||
|     Q_OBJECT | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,7 @@ | ||||
|  | ||||
| #include <Version.h> | ||||
| #include "StringUtils.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| #include <QLabel> | ||||
| #include <QtMath> | ||||
| @@ -79,7 +80,20 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co | ||||
|  | ||||
| bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const | ||||
| { | ||||
|     if (searchTerm.isEmpty()) { | ||||
|         return true; | ||||
|     } | ||||
|     QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); | ||||
|     Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>(); | ||||
|     if (searchTerm.startsWith("#")) | ||||
|         return pack.packCode == searchTerm.mid(1); | ||||
|     return pack.name.contains(searchTerm, Qt::CaseInsensitive); | ||||
| } | ||||
|  | ||||
| void FilterModel::setSearchTerm(const QString term) | ||||
| { | ||||
|     searchTerm = term.trimmed(); | ||||
|     invalidate(); | ||||
| } | ||||
|  | ||||
| const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() | ||||
| @@ -139,9 +153,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|     } | ||||
|  | ||||
|     Modpack pack = modpacks.at(pos); | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         return pack.name + "\n" + translatePackType(pack.type); | ||||
|     } else if (role == Qt::ToolTipRole) { | ||||
|     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); | ||||
| @@ -149,14 +162,21 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|                 return edit; | ||||
|             } | ||||
|             return pack.description; | ||||
|     } else if (role == Qt::DecorationRole) { | ||||
|         } | ||||
|         case Qt::DecorationRole: { | ||||
|             if (m_logoMap.contains(pack.logo)) { | ||||
|                 return (m_logoMap.value(pack.logo)); | ||||
|             } | ||||
|             QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); | ||||
|             ((ListModel*)this)->requestLogo(pack.logo); | ||||
|             return icon; | ||||
|     } else if (role == Qt::ForegroundRole) { | ||||
|         } | ||||
|         case Qt::UserRole: { | ||||
|             QVariant v; | ||||
|             v.setValue(pack); | ||||
|             return v; | ||||
|         } | ||||
|         case Qt::ForegroundRole: { | ||||
|             if (pack.broken) { | ||||
|                 // FIXME: Hardcoded color | ||||
|                 return QColor(255, 0, 50); | ||||
| @@ -165,13 +185,25 @@ QVariant ListModel::data(const QModelIndex& index, int role) const | ||||
|                 // bugged pack, currently only indicates bugged xml | ||||
|                 return QColor(244, 229, 66); | ||||
|             } | ||||
|     } else if (role == Qt::UserRole) { | ||||
|         QVariant v; | ||||
|         v.setValue(pack); | ||||
|         return v; | ||||
|         } | ||||
|         case Qt::DisplayRole: | ||||
|             return pack.name; | ||||
|         case Qt::SizeHintRole: | ||||
|             return QSize(0, 58); | ||||
|         // Custom data | ||||
|         case UserDataTypes::TITLE: | ||||
|             return pack.name; | ||||
|         case UserDataTypes::DESCRIPTION: | ||||
|             return pack.description; | ||||
|         case UserDataTypes::SELECTED: | ||||
|             return false; | ||||
|         case UserDataTypes::INSTALLED: | ||||
|             return false; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     return QVariant(); | ||||
|     return {}; | ||||
| } | ||||
|  | ||||
| void ListModel::fill(ModpackList modpacks_) | ||||
|   | ||||
| @@ -25,6 +25,7 @@ class FilterModel : public QSortFilterProxyModel { | ||||
|     QString translateCurrentSorting(); | ||||
|     void setSorting(Sorting sorting); | ||||
|     Sorting getCurrentSorting(); | ||||
|     void setSearchTerm(QString term); | ||||
|  | ||||
|    protected: | ||||
|     bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; | ||||
| @@ -33,6 +34,7 @@ class FilterModel : public QSortFilterProxyModel { | ||||
|    private: | ||||
|     QMap<QString, Sorting> sortings; | ||||
|     Sorting currentSorting; | ||||
|     QString searchTerm; | ||||
| }; | ||||
|  | ||||
| class ListModel : public QAbstractListModel { | ||||
|   | ||||
| @@ -35,6 +35,7 @@ | ||||
|  */ | ||||
|  | ||||
| #include "Page.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
| #include "ui_Page.h" | ||||
|  | ||||
| #include <QInputDialog> | ||||
| @@ -110,6 +111,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog | ||||
|     connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); | ||||
|     connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); | ||||
|  | ||||
|     connect(ui->searchEdit, &QLineEdit::textChanged, this, &Page::triggerSearch); | ||||
|  | ||||
|     connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); | ||||
|     connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); | ||||
|     connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); | ||||
| @@ -125,6 +128,9 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog | ||||
|     ui->thirdPartyPackList->selectionModel()->reset(); | ||||
|     ui->privatePackList->selectionModel()->reset(); | ||||
|  | ||||
|     ui->publicPackList->setItemDelegate(new ProjectItemDelegate(this)); | ||||
|     ui->thirdPartyPackList->setItemDelegate(new ProjectItemDelegate(this)); | ||||
|     ui->privatePackList->setItemDelegate(new ProjectItemDelegate(this)); | ||||
|     onTabChanged(ui->tabWidget->currentIndex()); | ||||
| } | ||||
|  | ||||
| @@ -319,6 +325,8 @@ void Page::onTabChanged(int tab) | ||||
|         currentModpackInfo = ui->publicPackDescription; | ||||
|     } | ||||
|  | ||||
|     triggerSearch(); | ||||
|  | ||||
|     currentList->selectionModel()->reset(); | ||||
|     QModelIndex idx = currentList->currentIndex(); | ||||
|     if (idx.isValid()) { | ||||
| @@ -358,4 +366,9 @@ void Page::onRemovePackClicked() | ||||
|     onPackSelectionChanged(); | ||||
| } | ||||
|  | ||||
| void Page::triggerSearch() | ||||
| { | ||||
|     currentModel->setSearchTerm(ui->searchEdit->text()); | ||||
| } | ||||
|  | ||||
| }  // namespace LegacyFTB | ||||
|   | ||||
| @@ -43,7 +43,6 @@ | ||||
| #include "QObjectPtr.h" | ||||
| #include "modplatform/legacy_ftb/PackFetchTask.h" | ||||
| #include "modplatform/legacy_ftb/PackHelpers.h" | ||||
| #include "tasks/Task.h" | ||||
| #include "ui/pages/BasePage.h" | ||||
|  | ||||
| class NewInstanceDialog; | ||||
| @@ -56,8 +55,6 @@ class Page; | ||||
|  | ||||
| class ListModel; | ||||
| class FilterModel; | ||||
| class PrivatePackListModel; | ||||
| class PrivatePackFilterModel; | ||||
| class PrivatePackManager; | ||||
|  | ||||
| class Page : public QWidget, public BasePage { | ||||
| @@ -98,6 +95,8 @@ class Page : public QWidget, public BasePage { | ||||
|     void onAddPackClicked(); | ||||
|     void onRemovePackClicked(); | ||||
|  | ||||
|     void triggerSearch(); | ||||
|  | ||||
|    private: | ||||
|     FilterModel* currentModel = nullptr; | ||||
|     QTreeView* currentList = nullptr; | ||||
|   | ||||
| @@ -10,8 +10,29 @@ | ||||
|     <height>602</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|   <layout class="QGridLayout" name="gridLayout_5"> | ||||
|    <item row="0" column="0"> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|      <item> | ||||
|       <widget class="QLineEdit" name="searchEdit"> | ||||
|        <property name="placeholderText"> | ||||
|         <string>Search and filter...</string> | ||||
|        </property> | ||||
|        <property name="clearButtonEnabled"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <widget class="QPushButton" name="pushButton"> | ||||
|        <property name="text"> | ||||
|         <string>Search</string> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item row="4" column="0"> | ||||
|     <widget class="QTabWidget" name="tabWidget"> | ||||
|      <property name="currentIndex"> | ||||
|       <number>0</number> | ||||
| @@ -113,7 +134,7 @@ | ||||
|      </widget> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|    <item row="5" column="0"> | ||||
|     <layout class="QGridLayout" name="gridLayout_4"> | ||||
|      <item row="0" column="1"> | ||||
|       <widget class="QLabel" name="label"> | ||||
|   | ||||
| @@ -38,8 +38,8 @@ | ||||
|  | ||||
| #include "BuildConfig.h" | ||||
| #include "Json.h" | ||||
| #include "minecraft/MinecraftInstance.h" | ||||
| #include "minecraft/PackProfile.h" | ||||
| #include "modplatform/modrinth/ModrinthAPI.h" | ||||
| #include "net/NetJob.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| #include "net/ApiDownload.h" | ||||
| @@ -130,7 +130,28 @@ bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, | ||||
|  | ||||
| void ModpackListModel::performPaginatedSearch() | ||||
| { | ||||
|     // TODO: Move to standalone API | ||||
|     if (hasActiveSearchJob()) | ||||
|         return; | ||||
|  | ||||
|     if (currentSearchTerm.startsWith("#")) { | ||||
|         auto projectId = currentSearchTerm.removeFirst(); | ||||
|         if (!projectId.isEmpty()) { | ||||
|             ResourceAPI::ProjectInfoCallbacks callbacks; | ||||
|  | ||||
|             // Use defaults if no callbacks are set | ||||
|             if (!callbacks.on_fail) | ||||
|                 callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; | ||||
|  | ||||
|             if (!callbacks.on_succeed) | ||||
|                 callbacks.on_succeed = [this](auto& doc, auto pack) { searchRequestForOneSucceeded(doc); }; | ||||
|             static const ModrinthAPI api; | ||||
|             if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { | ||||
|                 jobPtr = job; | ||||
|                 jobPtr->start(); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|     }  // TODO: Move to standalone API | ||||
|     auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network()); | ||||
|     auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + | ||||
|                                 "/search?" | ||||
| @@ -167,16 +188,17 @@ void ModpackListModel::performPaginatedSearch() | ||||
|  | ||||
| void ModpackListModel::refresh() | ||||
| { | ||||
|     if (jobPtr) { | ||||
|     if (hasActiveSearchJob()) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } else { | ||||
|     } | ||||
|  | ||||
|     beginResetModel(); | ||||
|     modpacks.clear(); | ||||
|     endResetModel(); | ||||
|     searchState = None; | ||||
|     } | ||||
|  | ||||
|     nextSearchOffset = 0; | ||||
|     performPaginatedSearch(); | ||||
| } | ||||
| @@ -307,9 +329,29 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) | ||||
| { | ||||
|     jobPtr.reset(); | ||||
|  | ||||
|     auto packObj = doc.object(); | ||||
|  | ||||
|     Modrinth::Modpack pack; | ||||
|     try { | ||||
|         Modrinth::loadIndexedPack(pack, packObj); | ||||
|         pack.id = Json::ensureString(packObj, "id", pack.id); | ||||
|     } catch (const JSONValidationError& e) { | ||||
|         qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); | ||||
|     modpacks.append({ pack }); | ||||
|     endInsertRows(); | ||||
| } | ||||
|  | ||||
| void ModpackListModel::searchRequestFailed(QString reason) | ||||
| { | ||||
|     auto failed_action = jobPtr->getFailedActions().at(0); | ||||
|     auto failed_action = dynamic_cast<NetJob*>(jobPtr.get())->getFailedActions().at(0); | ||||
|     if (!failed_action->m_reply) { | ||||
|         // Network error | ||||
|         QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); | ||||
|   | ||||
| @@ -73,6 +73,8 @@ class ModpackListModel : public QAbstractListModel { | ||||
|     void refresh(); | ||||
|     void searchWithTerm(const QString& term, const int sort); | ||||
|  | ||||
|     [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } | ||||
|  | ||||
|     void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); | ||||
|  | ||||
|     inline auto canFetchMore(const QModelIndex& parent) const -> bool override | ||||
| @@ -83,6 +85,7 @@ class ModpackListModel : public QAbstractListModel { | ||||
|    public slots: | ||||
|     void searchRequestFinished(QJsonDocument& doc_all); | ||||
|     void searchRequestFailed(QString reason); | ||||
|     void searchRequestForOneSucceeded(QJsonDocument&); | ||||
|  | ||||
|    protected slots: | ||||
|  | ||||
| @@ -111,7 +114,7 @@ class ModpackListModel : public QAbstractListModel { | ||||
|     int nextSearchOffset = 0; | ||||
|     enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; | ||||
|  | ||||
|     NetJob::Ptr jobPtr; | ||||
|     Task::Ptr jobPtr; | ||||
|  | ||||
|     std::shared_ptr<QByteArray> m_all_response = std::make_shared<QByteArray>(); | ||||
|     QByteArray m_specific_response; | ||||
|   | ||||
| @@ -64,6 +64,11 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget | ||||
|     ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); | ||||
|     ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); | ||||
|  | ||||
|     m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); | ||||
|     m_search_timer.setSingleShot(true); | ||||
|  | ||||
|     connect(&m_search_timer, &QTimer::timeout, this, &ModrinthPage::triggerSearch); | ||||
|  | ||||
|     ui->sortByBox->addItem(tr("Sort by Relevance")); | ||||
|     ui->sortByBox->addItem(tr("Sort by Total Downloads")); | ||||
|     ui->sortByBox->addItem(tr("Sort by Follows")); | ||||
| @@ -102,6 +107,11 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) | ||||
|             this->triggerSearch(); | ||||
|             keyEvent->accept(); | ||||
|             return true; | ||||
|         } else { | ||||
|             if (m_search_timer.isActive()) | ||||
|                 m_search_timer.stop(); | ||||
|  | ||||
|             m_search_timer.start(350); | ||||
|         } | ||||
|     } | ||||
|     return QObject::eventFilter(watched, event); | ||||
|   | ||||
| @@ -42,6 +42,7 @@ | ||||
|  | ||||
| #include "modplatform/modrinth/ModrinthPackManifest.h" | ||||
|  | ||||
| #include <QTimer> | ||||
| #include <QWidget> | ||||
|  | ||||
| namespace Ui { | ||||
| @@ -88,4 +89,7 @@ class ModrinthPage : public QWidget, public BasePage { | ||||
|  | ||||
|     Modrinth::Modpack current; | ||||
|     QString selectedVersion; | ||||
|  | ||||
|     // Used to do instant searching with a delay to cache quick changes | ||||
|     QTimer m_search_timer; | ||||
| }; | ||||
|   | ||||
| @@ -39,6 +39,7 @@ | ||||
| #include "Json.h" | ||||
|  | ||||
| #include "net/ApiDownload.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
|  | ||||
| #include <QIcon> | ||||
|  | ||||
| @@ -54,21 +55,47 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const | ||||
|     } | ||||
|  | ||||
|     Modpack pack = modpacks.at(pos); | ||||
|     if (role == Qt::DisplayRole) { | ||||
|         return pack.name; | ||||
|     } else if (role == Qt::DecorationRole) { | ||||
|     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 (m_logoMap.contains(pack.logoName)) { | ||||
|                 return (m_logoMap.value(pack.logoName)); | ||||
|             } | ||||
|             QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); | ||||
|             ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); | ||||
|             return icon; | ||||
|     } else if (role == Qt::UserRole) { | ||||
|         } | ||||
|         case Qt::UserRole: { | ||||
|             QVariant v; | ||||
|             v.setValue(pack); | ||||
|             return v; | ||||
|         } | ||||
|     return QVariant(); | ||||
|         case Qt::DisplayRole: | ||||
|             return pack.name; | ||||
|         case Qt::SizeHintRole: | ||||
|             return QSize(0, 58); | ||||
|         // Custom data | ||||
|         case UserDataTypes::TITLE: | ||||
|             return pack.name; | ||||
|         case UserDataTypes::DESCRIPTION: | ||||
|             return pack.description; | ||||
|         case UserDataTypes::SELECTED: | ||||
|             return false; | ||||
|         case UserDataTypes::INSTALLED: | ||||
|             return false; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     return {}; | ||||
| } | ||||
|  | ||||
| int Technic::ListModel::columnCount(const QModelIndex& parent) const | ||||
| @@ -87,21 +114,25 @@ void Technic::ListModel::searchWithTerm(const QString& term) | ||||
|         return; | ||||
|     } | ||||
|     currentSearchTerm = term; | ||||
|     if (jobPtr) { | ||||
|     if (hasActiveSearchJob()) { | ||||
|         jobPtr->abort(); | ||||
|         searchState = ResetRequested; | ||||
|         return; | ||||
|     } else { | ||||
|     } | ||||
|  | ||||
|     beginResetModel(); | ||||
|     modpacks.clear(); | ||||
|     endResetModel(); | ||||
|     searchState = None; | ||||
|     } | ||||
|  | ||||
|     performSearch(); | ||||
| } | ||||
|  | ||||
| void Technic::ListModel::performSearch() | ||||
| { | ||||
|     if (hasActiveSearchJob()) | ||||
|         return; | ||||
|  | ||||
|     auto netJob = makeShared<NetJob>("Technic::Search", APPLICATION->network()); | ||||
|     QString searchUrl = ""; | ||||
|     if (currentSearchTerm.isEmpty()) { | ||||
| @@ -113,6 +144,9 @@ void Technic::ListModel::performSearch() | ||||
|     } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { | ||||
|         searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); | ||||
|         searchMode = Single; | ||||
|     } else if (currentSearchTerm.startsWith("#")) { | ||||
|         searchUrl = QString("https://api.technicpack.net/modpack/%1?build=%2").arg(currentSearchTerm.mid(1), BuildConfig.TECHNIC_API_BUILD); | ||||
|         searchMode = Single; | ||||
|     } else { | ||||
|         searchUrl = | ||||
|             QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); | ||||
|   | ||||
| @@ -58,6 +58,8 @@ class ListModel : public QAbstractListModel { | ||||
|     void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); | ||||
|     void searchWithTerm(const QString& term); | ||||
|  | ||||
|     [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } | ||||
|  | ||||
|    private slots: | ||||
|     void searchRequestFinished(); | ||||
|     void searchRequestFailed(); | ||||
|   | ||||
| @@ -34,6 +34,7 @@ | ||||
|  */ | ||||
|  | ||||
| #include "TechnicPage.h" | ||||
| #include "ui/widgets/ProjectItem.h" | ||||
| #include "ui_TechnicPage.h" | ||||
|  | ||||
| #include <QKeyEvent> | ||||
| @@ -59,8 +60,15 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(p | ||||
|     model = new Technic::ListModel(this); | ||||
|     ui->packView->setModel(model); | ||||
|  | ||||
|     m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); | ||||
|     m_search_timer.setSingleShot(true); | ||||
|  | ||||
|     connect(&m_search_timer, &QTimer::timeout, this, &TechnicPage::triggerSearch); | ||||
|  | ||||
|     connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); | ||||
|     connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); | ||||
|  | ||||
|     ui->packView->setItemDelegate(new ProjectItemDelegate(this)); | ||||
| } | ||||
|  | ||||
| bool TechnicPage::eventFilter(QObject* watched, QEvent* event) | ||||
| @@ -71,6 +79,11 @@ bool TechnicPage::eventFilter(QObject* watched, QEvent* event) | ||||
|             triggerSearch(); | ||||
|             keyEvent->accept(); | ||||
|             return true; | ||||
|         } else { | ||||
|             if (m_search_timer.isActive()) | ||||
|                 m_search_timer.stop(); | ||||
|  | ||||
|             m_search_timer.start(350); | ||||
|         } | ||||
|     } | ||||
|     return QWidget::eventFilter(watched, event); | ||||
|   | ||||
| @@ -35,12 +35,12 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <QTimer> | ||||
| #include <QWidget> | ||||
|  | ||||
| #include <Application.h> | ||||
| #include "TechnicData.h" | ||||
| #include "net/NetJob.h" | ||||
| #include "tasks/Task.h" | ||||
| #include "ui/pages/BasePage.h" | ||||
|  | ||||
| namespace Ui { | ||||
| @@ -91,4 +91,7 @@ class TechnicPage : public QWidget, public BasePage { | ||||
|  | ||||
|     NetJob::Ptr jobPtr; | ||||
|     std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); | ||||
|  | ||||
|     // Used to do instant searching with a delay to cache quick changes | ||||
|     QTimer m_search_timer; | ||||
| }; | ||||
|   | ||||
| @@ -34,8 +34,8 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o | ||||
|             icon_width = icon_size.width(); | ||||
|             icon_height = icon_size.height(); | ||||
|  | ||||
|             icon_x_margin = (rect.height() - icon_width) / 2; | ||||
|             icon_y_margin = (rect.height() - icon_height) / 2; | ||||
|             icon_x_margin = icon_y_margin;  // use same margins for consistency | ||||
|         } | ||||
|  | ||||
|         // Centralize icon with a margin to separate from the other elements | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Trial97
					Trial97