diff --git a/loader/src/ui/GeodeUI.cpp b/loader/src/ui/GeodeUI.cpp index 40a9daef..0a7c4711 100644 --- a/loader/src/ui/GeodeUI.cpp +++ b/loader/src/ui/GeodeUI.cpp @@ -5,6 +5,7 @@ #include <Geode/ui/MDPopup.hpp> #include <Geode/utils/web.hpp> #include <server/Server.hpp> +#include "mods/GeodeStyle.hpp" void geode::openModsList() { ModsLayer::scene(); @@ -98,9 +99,7 @@ protected: } // Asynchronously fetch from server else { - this->setSprite(CCSprite::create("loadingCircle.png")); - static_cast<CCSprite*>(m_sprite)->setBlendFunc({ GL_ONE, GL_ONE }); - m_sprite->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); + this->setSprite(createLoadingCircle(25)); m_listener.setFilter(server::ServerResultCache<&server::getModLogo>::shared().get(id).listen()); } diff --git a/loader/src/ui/mods/GeodeStyle.cpp b/loader/src/ui/mods/GeodeStyle.cpp index d83d383b..fe609e7c 100644 --- a/loader/src/ui/mods/GeodeStyle.cpp +++ b/loader/src/ui/mods/GeodeStyle.cpp @@ -63,6 +63,21 @@ GeodeSquareSprite* GeodeSquareSprite::createWithSpriteFrameName(const char* top, return nullptr; } +CCNode* createLoadingCircle(float sideLength, const char* id) { + auto spinnerContainer = CCNode::create(); + spinnerContainer->setContentSize({ sideLength, sideLength }); + spinnerContainer->setID(id); + spinnerContainer->setAnchorPoint({ .5f, .5f }); + + auto spinner = CCSprite::create("loadingCircle.png"); + spinner->setBlendFunc({ GL_ONE, GL_ONE }); + spinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); + limitNodeSize(spinner, spinnerContainer->getContentSize(), 1.f, .1f); + spinnerContainer->addChildAtPosition(spinner, Anchor::Center); + + return spinnerContainer; +} + IconButtonSprite* createGeodeButton(CCNode* icon, std::string const& text, std::string const& bg) { return IconButtonSprite::create(bg.c_str(), icon, text.c_str(), "bigFont.fnt"); } @@ -89,10 +104,17 @@ CircleButtonSprite* createGeodeCircleButton(const char* topFrameName) { return CircleButtonSprite::createWithSpriteFrameName(topFrameName, 1.f, CircleBaseColor::DarkPurple); } -ButtonSprite* createGeodeTagLabel(std::string const& text, ccColor3B color, ccColor3B bg) { +ButtonSprite* createGeodeTagLabel(std::string const& text, std::optional<std::pair<ccColor3B, ccColor3B>> const& color) { auto label = ButtonSprite::create(text.c_str(), "bigFont.fnt", "white-square.png"_spr, .8f); - label->m_label->setColor(color); - label->m_BGSprite->setColor(bg); + if (color) { + label->m_label->setColor(color->first); + label->m_BGSprite->setColor(color->second); + } + else { + auto def = geodeTagColor(text); + label->m_label->setColor(def.first); + label->m_BGSprite->setColor(def.second); + } return label; } diff --git a/loader/src/ui/mods/GeodeStyle.hpp b/loader/src/ui/mods/GeodeStyle.hpp index 3dc9d1b5..9e8320e4 100644 --- a/loader/src/ui/mods/GeodeStyle.hpp +++ b/loader/src/ui/mods/GeodeStyle.hpp @@ -19,7 +19,7 @@ protected: // Replace the close button with a Geode style one auto spr = CircleButtonSprite::createWithSpriteFrameName( - "close.png"_spr, 1.f, + "close.png"_spr, .85f, (altBG ? CircleBaseColor::DarkAqua : CircleBaseColor::DarkPurple) ); Popup<Args...>::m_closeBtn->setNormalImage(spr); @@ -48,13 +48,15 @@ public: static GeodeSquareSprite* createWithSpriteFrameName(const char* top, bool* state = nullptr); }; +CCNode* createLoadingCircle(float sideLength, const char* id = "loading-spinner"); + IconButtonSprite* createGeodeButton(CCNode* icon, std::string const& text, std::string const& bg = "GE_button_05.png"_spr); CCNode* createGeodeButton(CCNode* icon, float width, std::string const& text, std::string const& bg = "GE_button_05.png"_spr); ButtonSprite* createGeodeButton(std::string const& text, std::string const& bg = "GE_button_05.png"_spr); CircleButtonSprite* createGeodeCircleButton(const char* topFrameName); -ButtonSprite* createGeodeTagLabel(std::string const& text, ccColor3B color, ccColor3B bg); +ButtonSprite* createGeodeTagLabel(std::string const& text, std::optional<std::pair<ccColor3B, ccColor3B>> const& color = std::nullopt); std::pair<ccColor3B, ccColor3B> geodeTagColor(std::string_view const& text); class GeodeTabSprite : public CCNode { diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index f6ee5b5a..f2fcc16b 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -68,8 +68,10 @@ bool ModItem::init(ModSource&& source) { m_restartRequiredLabel = createGeodeTagLabel( "Restart Required", - to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)), - to3B(ColorProvider::get()->color("mod-list-restart-required-label-bg"_spr)) + {{ + to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)), + to3B(ColorProvider::get()->color("mod-list-restart-required-label-bg"_spr)) + }} ); m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setMaxScale(.75f)); m_infoContainer->addChild(m_restartRequiredLabel); diff --git a/loader/src/ui/mods/list/ModList.cpp b/loader/src/ui/mods/list/ModList.cpp index d4dff567..0e9fb38b 100644 --- a/loader/src/ui/mods/list/ModList.cpp +++ b/loader/src/ui/mods/list/ModList.cpp @@ -1,6 +1,6 @@ #include "ModList.hpp" #include <Geode/utils/ColorProvider.hpp> -#include "../popups/TagsPopup.hpp" +#include "../popups/FiltersPopup.hpp" #include "../GeodeStyle.hpp" #include "../ModsLayer.hpp" @@ -218,9 +218,7 @@ bool ModList::init(ModListSource* src, CCSize const& size) { m_statusDetails->setAlignment(kCCTextAlignmentCenter); m_statusContainer->addChild(m_statusDetails); - m_statusLoadingCircle = CCSprite::create("loadingCircle.png"); - m_statusLoadingCircle->setBlendFunc({ GL_ONE, GL_ONE }); - m_statusLoadingCircle->setScale(.6f); + m_statusLoadingCircle = createLoadingCircle(50); m_statusContainer->addChild(m_statusLoadingCircle); m_statusLoadingBar = Slider::create(this, nullptr); @@ -452,10 +450,6 @@ void ModList::showStatus(ModListStatus status, std::string const& message, std:: m_statusLoadingCircle->setVisible(std::holds_alternative<ModListUnkProgressStatus>(status)); m_statusLoadingBar->setVisible(std::holds_alternative<ModListProgressStatus>(status)); - // The loading circle action gets stopped for some reason so just reactivate it - if (m_statusLoadingCircle->isVisible()) { - m_statusLoadingCircle->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); - } // Update progress bar if (auto per = std::get_if<ModListProgressStatus>(&status)) { m_statusLoadingBar->setValue(per->percentage / 100.f); @@ -467,7 +461,7 @@ void ModList::showStatus(ModListStatus status, std::string const& message, std:: } void ModList::onFilters(CCObject*) { - TagsPopup::create(m_source)->show(); + FiltersPopup::create(m_source)->show(); } void ModList::onClearFilters(CCObject*) { diff --git a/loader/src/ui/mods/list/ModList.hpp b/loader/src/ui/mods/list/ModList.hpp index 6384f539..db86e723 100644 --- a/loader/src/ui/mods/list/ModList.hpp +++ b/loader/src/ui/mods/list/ModList.hpp @@ -25,7 +25,7 @@ protected: CCLabelBMFont* m_statusTitle; SimpleTextArea* m_statusDetails; CCMenuItemSpriteExtra* m_statusDetailsBtn; - CCSprite* m_statusLoadingCircle; + CCNode* m_statusLoadingCircle; Slider* m_statusLoadingBar; ModListSource::PageLoadEventListener m_listener; CCMenuItemSpriteExtra* m_pagePrevBtn; diff --git a/loader/src/ui/mods/popups/FiltersPopup.cpp b/loader/src/ui/mods/popups/FiltersPopup.cpp new file mode 100644 index 00000000..f80880db --- /dev/null +++ b/loader/src/ui/mods/popups/FiltersPopup.cpp @@ -0,0 +1,126 @@ +#include "FiltersPopup.hpp" + +bool FiltersPopup::setup(ModListSource* src) { + m_noElasticity = true; + m_source = src; + m_selectedTags = src->getModTags(); + + this->setTitle("Select Filters"); + + auto tagsContainer = CCNode::create(); + tagsContainer->setContentSize(ccp(220, 80)); + tagsContainer->setAnchorPoint({ .5f, .5f }); + + auto tagsBG = CCScale9Sprite::create("square02b_001.png"); + tagsBG->setColor({ 0, 0, 0 }); + tagsBG->setOpacity(75); + tagsBG->setScale(.3f); + tagsBG->setContentSize(tagsContainer->getContentSize() / tagsBG->getScale()); + tagsContainer->addChildAtPosition(tagsBG, Anchor::Center); + + m_tagsMenu = CCMenu::create(); + m_tagsMenu->setContentSize(tagsContainer->getContentSize() - ccp(10, 10)); + m_tagsMenu->addChild(createLoadingCircle(40)); + m_tagsMenu->setLayout( + RowLayout::create() + ->setDefaultScaleLimits(.1f, 1.f) + ->setGrowCrossAxis(true) + ->setCrossAxisOverflow(false) + ->setAxisAlignment(AxisAlignment::Center) + ->setCrossAxisAlignment(AxisAlignment::Center) + ); + tagsContainer->addChildAtPosition(m_tagsMenu, Anchor::Center); + + auto tagsResetMenu = CCMenu::create(); + tagsResetMenu->setAnchorPoint({ .5f, 1 }); + tagsResetMenu->setContentWidth(tagsContainer->getContentWidth()); + + auto resetSpr = createGeodeButton("Reset Tags"); + auto resetBtn = CCMenuItemSpriteExtra::create( + resetSpr, this, menu_selector(FiltersPopup::onResetTags) + ); + tagsResetMenu->addChild(resetBtn); + + tagsResetMenu->setLayout( + RowLayout::create() + ->setDefaultScaleLimits(.1f, .5f) + ->setAxisAlignment(AxisAlignment::End) + ->setAxisReverse(true) + ); + tagsContainer->addChildAtPosition(tagsResetMenu, Anchor::Bottom, ccp(0, -2)); + + m_mainLayer->addChildAtPosition(tagsContainer, Anchor::Center); + + m_tagsListener.bind(this, &FiltersPopup::onLoadTags); + m_tagsListener.setFilter(server::ServerResultCache<&server::getTags>::shared().get().listen()); + + return true; +} + +void FiltersPopup::onLoadTags(PromiseEvent<std::unordered_set<std::string>, server::ServerError>* event) { + if (auto tags = event->getResolve()) { + m_tagsMenu->removeAllChildren(); + for (auto& tag : *tags) { + auto offSpr = createGeodeTagLabel(tag); + offSpr->m_BGSprite->setOpacity(105); + offSpr->m_label->setOpacity(105); + auto onSpr = createGeodeTagLabel(tag); + auto btn = CCMenuItemToggler::create( + offSpr, onSpr, this, menu_selector(FiltersPopup::onSelectTag) + ); + btn->m_notClickable = true; + btn->setUserObject("tag", CCString::create(tag)); + m_tagsMenu->addChild(btn); + } + m_tagsMenu->updateLayout(); + this->updateTags(); + } + else if (event->getReject()) { + m_tagsMenu->removeAllChildren(); + auto label = CCLabelBMFont::create("Unable to load tags", "bigFont.fnt"); + label->setOpacity(105); + m_tagsMenu->addChild(label); + m_tagsMenu->updateLayout(); + } +} + +void FiltersPopup::updateTags() { + for (auto node : CCArrayExt<CCNode*>(m_tagsMenu->getChildren())) { + if (auto toggle = typeinfo_cast<CCMenuItemToggler*>(node)) { + auto tag = static_cast<CCString*>(toggle->getUserObject("tag"))->getCString(); + toggle->toggle(m_selectedTags.contains(tag)); + } + } +} + +void FiltersPopup::onSelectTag(CCObject* sender) { + auto toggle = static_cast<CCMenuItemToggler*>(sender); + auto tag = static_cast<CCString*>(toggle->getUserObject("tag"))->getCString(); + if (m_selectedTags.contains(tag)) { + m_selectedTags.erase(tag); + } + else { + m_selectedTags.insert(tag); + } + this->updateTags(); +} + +void FiltersPopup::onResetTags(CCObject*) { + m_selectedTags.clear(); + this->updateTags(); +} + +void FiltersPopup::onClose(CCObject* sender) { + m_source->setModTags(m_selectedTags); + Popup::onClose(sender); +} + +FiltersPopup* FiltersPopup::create(ModListSource* src) { + auto ret = new FiltersPopup(); + if (ret && ret->init(310, 250, src)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/mods/popups/FiltersPopup.hpp b/loader/src/ui/mods/popups/FiltersPopup.hpp new file mode 100644 index 00000000..5b693f92 --- /dev/null +++ b/loader/src/ui/mods/popups/FiltersPopup.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <Geode/ui/Popup.hpp> +#include "../sources/ModListSource.hpp" +#include "../GeodeStyle.hpp" +#include <server/Server.hpp> + +using namespace geode::prelude; + +class FiltersPopup : public GeodePopup<ModListSource*> { +protected: + ModListSource* m_source; + CCMenu* m_tagsMenu; + std::unordered_set<std::string> m_selectedTags; + EventListener<PromiseEventFilter<std::unordered_set<std::string>, server::ServerError>> m_tagsListener; + + bool setup(ModListSource* src) override; + void updateTags(); + + void onLoadTags(PromiseEvent<std::unordered_set<std::string>, server::ServerError>* event); + void onResetTags(CCObject*); + void onSelectTag(CCObject* sender); + void onClose(CCObject* sender) override; + +public: + static FiltersPopup* create(ModListSource* src); +}; diff --git a/loader/src/ui/mods/popups/ModPopup.cpp b/loader/src/ui/mods/popups/ModPopup.cpp index 2af8b78d..224e3cd4 100644 --- a/loader/src/ui/mods/popups/ModPopup.cpp +++ b/loader/src/ui/mods/popups/ModPopup.cpp @@ -109,21 +109,7 @@ bool ModPopup::setup(ModSource&& src) { valueLabel->setID("value-label"); labelContainer->addChild(valueLabel); - // todo: refactor these spinners into a reusable class that's not the ass LoadingCircle is - auto spinnerContainer = CCNode::create(); - spinnerContainer->setContentSize({ - container->getContentHeight() / labelContainer->getScale(), - container->getContentHeight() / labelContainer->getScale() - }); - spinnerContainer->setID("loading-spinner"); - - auto spinner = CCSprite::create("loadingCircle.png"); - spinner->setBlendFunc({ GL_ONE, GL_ONE }); - spinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); - limitNodeSize(spinner, spinnerContainer->getContentSize(), 1.f, .1f); - spinnerContainer->addChildAtPosition(spinner, Anchor::Center); - - labelContainer->addChild(spinnerContainer); + labelContainer->addChild(createLoadingCircle(container->getContentHeight() / labelContainer->getScale())); this->setStatIcon(container, std::get<0>(stat)); this->setStatLabel(container, std::get<1>(stat)); @@ -165,18 +151,7 @@ bool ModPopup::setup(ModSource&& src) { m_tags->setContentSize(tagsContainer->getContentSize() - ccp(10, 10)); m_tags->setAnchorPoint({ .5f, .5f }); - // todo: refactor these spinners into a reusable class that's not the ass LoadingCircle is - auto tagsSpinnerContainer = CCNode::create(); - tagsSpinnerContainer->setContentSize({ 50, 50 }); - tagsSpinnerContainer->setID("loading-spinner"); - - auto tagsSpinner = CCSprite::create("loadingCircle.png"); - tagsSpinner->setBlendFunc({ GL_ONE, GL_ONE }); - tagsSpinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); - limitNodeSize(tagsSpinner, tagsSpinnerContainer->getContentSize(), 1.f, .1f); - tagsSpinnerContainer->addChildAtPosition(tagsSpinner, Anchor::Center); - - m_tags->addChild(tagsSpinnerContainer); + m_tags->addChild(createLoadingCircle(50)); m_tags->setLayout( RowLayout::create() @@ -202,8 +177,10 @@ bool ModPopup::setup(ModSource&& src) { m_restartRequiredLabel = createGeodeTagLabel( "Restart Required", - to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)), - to3B(ColorProvider::get()->color("mod-list-restart-required-label-bg"_spr)) + {{ + to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)), + to3B(ColorProvider::get()->color("mod-list-restart-required-label-bg"_spr)) + }} ); m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setMaxScale(.75f)); m_restartRequiredLabel->setScale(.3f); @@ -578,7 +555,7 @@ void ModPopup::onLoadTags(PromiseEvent<std::unordered_set<std::string>, server:: auto readable = tag; readable[0] = std::toupper(readable[0]); auto colors = geodeTagColor(tag); - m_tags->addChild(createGeodeTagLabel(readable, colors.first, colors.second)); + m_tags->addChild(createGeodeTagLabel(readable)); } if (data->empty()) { diff --git a/loader/src/ui/mods/popups/TagsPopup.cpp b/loader/src/ui/mods/popups/TagsPopup.cpp deleted file mode 100644 index 296e80d9..00000000 --- a/loader/src/ui/mods/popups/TagsPopup.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "TagsPopup.hpp" - -bool TagsPopup::setup(ModListSource* src) { - m_noElasticity = true; - m_source = src; - - this->setTitle("Select Tags"); - - // todo: need a "get available tags" endpoint first... - - return true; -} - -void TagsPopup::onClose(CCObject* sender) { - InvalidateCacheEvent(m_source).post(); - Popup::onClose(sender); -} - -TagsPopup* TagsPopup::create(ModListSource* src) { - auto ret = new TagsPopup(); - if (ret && ret->init(260, 200, src)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} diff --git a/loader/src/ui/mods/popups/TagsPopup.hpp b/loader/src/ui/mods/popups/TagsPopup.hpp deleted file mode 100644 index 067a4710..00000000 --- a/loader/src/ui/mods/popups/TagsPopup.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include <Geode/ui/Popup.hpp> -#include "../sources/ModListSource.hpp" -#include "../GeodeStyle.hpp" - -using namespace geode::prelude; - -class TagsPopup : public GeodePopup<ModListSource*> { -protected: - ModListSource* m_source; - - bool setup(ModListSource* src) override; - - void onClose(CCObject*) override; - -public: - static TagsPopup* create(ModListSource* src); -}; diff --git a/loader/src/ui/mods/sources/ModListSource.cpp b/loader/src/ui/mods/sources/ModListSource.cpp index cdeb8a3e..b7a24f65 100644 --- a/loader/src/ui/mods/sources/ModListSource.cpp +++ b/loader/src/ui/mods/sources/ModListSource.cpp @@ -33,6 +33,15 @@ static void filterModsWithQuery(InstalledModListSource::ProvidedMods& mods, Inst if (auto updates = src.hasUpdates(); query.onlyUpdates && !(updates && updates->hasUpdateForInstalledMod())) { addToList = false; } + // If some tags are provided, only return mods that match + if (addToList && query.tags.size()) { + auto compare = mod->getMetadata().getTags(); + for (auto& tag : query.tags) { + if (!compare.contains(tag)) { + addToList = false; + } + } + } // Don't bother with unnecessary fuzzy match calculations if this mod isn't going to be added anyway if (addToList && query.query) { // By default don't add anything @@ -221,6 +230,14 @@ void InstalledModListSource::setSearchQuery(std::string const& query) { m_query.query = query.size() ? std::optional(query) : std::nullopt; } +std::unordered_set<std::string> InstalledModListSource::getModTags() const { + return m_query.tags; +} +void InstalledModListSource::setModTags(std::unordered_set<std::string> const& tags) { + m_query.tags = tags; + this->clearCache(); +} + InstalledModsQuery const& InstalledModListSource::getQuery() const { return m_query; } @@ -323,6 +340,14 @@ void ServerModListSource::setSearchQuery(std::string const& query) { m_query.query = query.size() ? std::optional(query) : std::nullopt; } +std::unordered_set<std::string> ServerModListSource::getModTags() const { + return m_query.tags; +} +void ServerModListSource::setModTags(std::unordered_set<std::string> const& tags) { + m_query.tags = tags; + this->clearCache(); +} + server::ModsQuery const& ServerModListSource::getQuery() const { return m_query; } @@ -355,6 +380,11 @@ ModPackListSource* ModPackListSource::get() { void ModPackListSource::setSearchQuery(std::string const& query) {} +std::unordered_set<std::string> ModPackListSource::getModTags() const { + return {}; +} +void ModPackListSource::setModTags(std::unordered_set<std::string> const& set) {} + bool ModPackListSource::isInstalledMods() const { return false; } diff --git a/loader/src/ui/mods/sources/ModListSource.hpp b/loader/src/ui/mods/sources/ModListSource.hpp index 70301494..1486e35d 100644 --- a/loader/src/ui/mods/sources/ModListSource.hpp +++ b/loader/src/ui/mods/sources/ModListSource.hpp @@ -30,6 +30,7 @@ public: struct InstalledModsQuery final { std::optional<std::string> query; bool onlyUpdates = false; + std::unordered_set<std::string> tags = {}; size_t page = 0; size_t pageSize = 10; }; @@ -77,6 +78,9 @@ public: void clearCache(); void search(std::string const& query); + virtual std::unordered_set<std::string> getModTags() const = 0; + virtual void setModTags(std::unordered_set<std::string> const& tags) = 0; + // Load page, uses cache if possible unless `update` is true PagePromise loadPage(size_t page, bool update = false); std::optional<size_t> getPageCount() const; @@ -112,13 +116,15 @@ protected: void resetQuery() override; ProviderPromise fetchPage(size_t page, size_t pageSize) override; + void setSearchQuery(std::string const& query) override; InstalledModListSource(bool onlyUpdates); public: static InstalledModListSource* get(bool onlyUpdates); - void setSearchQuery(std::string const& query) override; + std::unordered_set<std::string> getModTags() const override; + void setModTags(std::unordered_set<std::string> const& tags) override; InstalledModsQuery const& getQuery() const; InvalidateQueryAfter<InstalledModsQuery> getQueryMut(); @@ -141,13 +147,15 @@ protected: void resetQuery() override; ProviderPromise fetchPage(size_t page, size_t pageSize) override; + void setSearchQuery(std::string const& query) override; ServerModListSource(ServerModListType type); public: static ServerModListSource* get(ServerModListType type); - void setSearchQuery(std::string const& query) override; + std::unordered_set<std::string> getModTags() const override; + void setModTags(std::unordered_set<std::string> const& tags) override; server::ModsQuery const& getQuery() const; InvalidateQueryAfter<server::ModsQuery> getQueryMut(); @@ -160,13 +168,15 @@ class ModPackListSource : public ModListSource { protected: void resetQuery() override; ProviderPromise fetchPage(size_t page, size_t pageSize) override; + void setSearchQuery(std::string const& query) override; ModPackListSource(); public: static ModPackListSource* get(); - void setSearchQuery(std::string const& query) override; + std::unordered_set<std::string> getModTags() const override; + void setModTags(std::unordered_set<std::string> const& tags) override; bool isInstalledMods() const override; bool wantsRestart() const override;