impl filtering by tags

This commit is contained in:
HJfod 2024-03-30 21:07:29 +02:00
parent a7a4dd9a91
commit 730729ccd5
13 changed files with 242 additions and 99 deletions

View file

@ -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());
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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);

View file

@ -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*) {

View file

@ -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;

View file

@ -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;
}

View file

@ -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);
};

View file

@ -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()) {

View file

@ -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;
}

View file

@ -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);
};

View file

@ -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;
}

View file

@ -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;