diff --git a/loader/src/ui/mods/ModItem.cpp b/loader/src/ui/mods/ModItem.cpp index 149e417b..314be9e3 100644 --- a/loader/src/ui/mods/ModItem.cpp +++ b/loader/src/ui/mods/ModItem.cpp @@ -2,12 +2,13 @@ #include #include #include "GeodeStyle.hpp" +#include "ModPopup.hpp" -bool BaseModItem::init() { +bool ModItem::init(ModSource&& source) { if (!CCNode::init()) return false; - auto meta = this->getMetadata(); + m_source = std::move(source); m_bg = CCScale9Sprite::create("square02b_small.png"); m_bg->setOpacity(0); @@ -16,7 +17,7 @@ bool BaseModItem::init() { m_bg->setScale(.7f); this->addChild(m_bg); - m_logo = this->createModLogo(); + m_logo = m_source.createModLogo(); this->addChild(m_logo); m_infoContainer = CCNode::create(); @@ -38,7 +39,7 @@ bool BaseModItem::init() { ->setAxisAlignment(AxisAlignment::Start) ); - m_titleLabel = CCLabelBMFont::create(meta.getName().c_str(), "bigFont.fnt"); + m_titleLabel = CCLabelBMFont::create(m_source.getMetadata().getName().c_str(), "bigFont.fnt"); m_titleLabel->setAnchorPoint({ .0f, .5f }); m_titleLabel->setLayoutOptions( AxisLayoutOptions::create() @@ -52,7 +53,7 @@ bool BaseModItem::init() { m_developers->ignoreAnchorPointForPosition(false); m_developers->setAnchorPoint({ .0f, .5f }); - auto by = "By " + ModMetadata::formatDeveloperDisplayString(this->getMetadata().getDevelopers()); + auto by = "By " + ModMetadata::formatDeveloperDisplayString(m_source.getMetadata().getDevelopers()); m_developerLabel = CCLabelBMFont::create(by.c_str(), "goldFont.fnt"); auto developersBtn = CCMenuItemSpriteExtra::create( m_developerLabel, this, nullptr @@ -83,7 +84,8 @@ bool BaseModItem::init() { m_viewMenu->setScale(.55f); auto viewBtn = CCMenuItemSpriteExtra::create( - createGeodeButton("View"), this, nullptr + createGeodeButton("View"), + this, menu_selector(ModItem::onView) ); m_viewMenu->addChild(viewBtn); @@ -95,19 +97,64 @@ bool BaseModItem::init() { ); this->addChildAtPosition(m_viewMenu, Anchor::Right, ccp(-10, 0)); + // Handle source-specific stuff + m_source.visit(makeVisitor { + [this](Mod* mod) { + // Add an enable button if the mod is enablable + if (!mod->isInternal()) { + m_enableToggle = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(ModItem::onEnable), 1.f + ); + // Manually handle toggle state + m_enableToggle->m_notClickable = true; + m_viewMenu->addChild(m_enableToggle); + m_viewMenu->updateLayout(); + } + }, + [this](server::ServerModMetadata const& metadata) { + if (metadata.featured) { + m_checkmark = CCScale9Sprite::createWithSpriteFrameName("GJ_colorBtn_001.png"); + m_checkmark->setContentSize({ 50, 38 }); + m_checkmark->setColor({ 255, 255, 120 }); + m_checkmark->setOpacity(45); + + auto tick = CCSprite::createWithSpriteFrameName("GJ_starsIcon_001.png"); + m_checkmark->addChildAtPosition(tick, Anchor::Center); + + m_titleContainer->addChild(m_checkmark); + } + }, + }); + + this->updateState(); + return true; } -void BaseModItem::updateState() { - auto wantsRestart = this->wantsRestart(); +void ModItem::updateState() { + auto wantsRestart = m_source.wantsRestart(); m_restartRequiredLabel->setVisible(wantsRestart); m_developers->setVisible(!wantsRestart); m_infoContainer->updateLayout(); - // Set default color to BG to start off with + // Set default colors based on source to start off with // (possibly overriding later based on state) - m_bg->setColor(to3B(m_defaultBG)); - m_bg->setOpacity(m_defaultBG.a); + m_source.visit(makeVisitor { + [this](Mod* mod) { + m_bg->setColor({ 255, 255, 255 }); + m_bg->setOpacity(mod->isOrWillBeEnabled() ? 25 : 10); + m_titleLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155); + m_developerLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155); + }, + [this](server::ServerModMetadata const& metadata) { + m_bg->setColor({ 255, 255, 255 }); + m_bg->setOpacity(25); + if (metadata.featured && m_checkmark) { + m_bg->setColor(m_checkmark->getColor()); + m_bg->setOpacity(40); + } + }, + }); // Highlight item via BG if it wants to restart for extra UI attention if (wantsRestart) { @@ -119,9 +166,14 @@ void BaseModItem::updateState() { if (m_updateParentState) { m_updateParentState(); } + + // Update enable toggle state + if (m_enableToggle && m_source.asMod()) { + m_enableToggle->toggle(m_source.asMod()->isOrWillBeEnabled()); + } } -void BaseModItem::updateSize(float width, bool big) { +void ModItem::updateSize(float width, bool big) { this->setContentSize({ width, big ? 40.f : 30.f }); m_bg->setContentSize((m_obContentSize - ccp(6, 0)) / m_bg->getScale()); @@ -154,135 +206,37 @@ void BaseModItem::updateSize(float width, bool big) { this->updateLayout(); } -void BaseModItem::onUpdateParentState(MiniFunction listener) { +void ModItem::onUpdateParentState(MiniFunction listener) { m_updateParentState = listener; } -bool InstalledModItem::init(Mod* mod) { - m_mod = mod; - - if (!BaseModItem::init()) - return false; - - // Add an enable button if the mod is enablable - if (!mod->isInternal()) { - m_enableToggle = CCMenuItemToggler::createWithStandardSprites( - this, menu_selector(InstalledModItem::onEnable), 1.f - ); - // Manually handle toggle state - m_enableToggle->m_notClickable = true; - m_viewMenu->addChild(m_enableToggle); - m_viewMenu->updateLayout(); +ModItem* ModItem::create(ModSource&& source) { + auto ret = new ModItem(); + if (ret && ret->init(std::move(source))) { + ret->autorelease(); + return ret; } - - this->updateState(); - - return true; + CC_SAFE_DELETE(ret); + return nullptr; } -void InstalledModItem::updateState() { - m_defaultBG.a = m_mod->isOrWillBeEnabled() ? 25 : 10; - m_titleLabel->setOpacity(m_mod->isOrWillBeEnabled() ? 255 : 155); - m_developerLabel->setOpacity(m_mod->isOrWillBeEnabled() ? 255 : 155); - - BaseModItem::updateState(); - - // Update enable toggle state - if (m_enableToggle) { - m_enableToggle->toggle(m_mod->isOrWillBeEnabled()); - } +void ModItem::onView(CCObject*) { + ModPopup::create(ModSource(m_source))->show(); } -void InstalledModItem::onEnable(CCObject*) { - // Toggle the mod state - auto res = m_mod->isOrWillBeEnabled() ? m_mod->disable() : m_mod->enable(); - if (!res) { - FLAlertLayer::create( - "Error Toggling Mod", - res.unwrapErr(), - "OK" - )->show(); +void ModItem::onEnable(CCObject*) { + if (auto mod = m_source.asMod()) { + // Toggle the mod state + auto res = mod->isOrWillBeEnabled() ? mod->disable() : mod->enable(); + if (!res) { + FLAlertLayer::create( + "Error Toggling Mod", + res.unwrapErr(), + "OK" + )->show(); + } } // Update whole state of the mod item this->updateState(); } - -InstalledModItem* InstalledModItem::create(Mod* mod) { - auto ret = new InstalledModItem(); - if (ret && ret->init(mod)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -ModMetadata InstalledModItem::getMetadata() const { - return m_mod->getMetadata(); -} - -CCNode* InstalledModItem::createModLogo() const { - return geode::createModLogo(m_mod); -} - -bool InstalledModItem::wantsRestart() const { - return m_mod->getRequestedAction() != ModRequestedAction::None; -} - -bool ServerModItem::init(server::ServerModMetadata const& metadata) { - m_metadata = metadata; - - if (!BaseModItem::init()) - return false; - - if (metadata.featured) { - m_checkmark = CCScale9Sprite::createWithSpriteFrameName("GJ_colorBtn_001.png"); - m_checkmark->setContentSize({ 50, 38 }); - m_checkmark->setColor({ 255, 255, 120 }); - m_checkmark->setOpacity(45); - - auto tick = CCSprite::createWithSpriteFrameName("GJ_starsIcon_001.png"); - m_checkmark->addChildAtPosition(tick, Anchor::Center); - - m_titleContainer->addChild(m_checkmark); - } - - this->updateState(); - - return true; -} - -void ServerModItem::updateState() { - BaseModItem::updateState(); - - // Update BG color unless we want to restart (more important color to show) - // and if the mod is featured - if (!this->wantsRestart() && m_metadata.featured && m_checkmark) { - m_bg->setColor(m_checkmark->getColor()); - m_bg->setOpacity(40); - } -} - -ServerModItem* ServerModItem::create(server::ServerModMetadata const& metadata) { - auto ret = new ServerModItem(); - if (ret && ret->init(metadata)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -ModMetadata ServerModItem::getMetadata() const { - return m_metadata.versions.front().metadata; -} - -CCNode* ServerModItem::createModLogo() const { - return createServerModLogo(m_metadata.id); -} - -bool ServerModItem::wantsRestart() const { - // todo: request restart after install - return false; -} diff --git a/loader/src/ui/mods/ModItem.hpp b/loader/src/ui/mods/ModItem.hpp index e1524937..8c64e55b 100644 --- a/loader/src/ui/mods/ModItem.hpp +++ b/loader/src/ui/mods/ModItem.hpp @@ -2,13 +2,14 @@ #include #include +#include "ModSource.hpp" using namespace geode::prelude; -class BaseModItem : public CCNode { +class ModItem : public CCNode { protected: + ModSource m_source; CCScale9Sprite* m_bg; - ccColor4B m_defaultBG = { 255, 255, 255, 25 }; CCNode* m_logo; CCNode* m_infoContainer; CCNode* m_titleContainer; @@ -18,65 +19,26 @@ protected: ButtonSprite* m_restartRequiredLabel = nullptr; CCMenu* m_viewMenu; MiniFunction m_updateParentState = nullptr; + CCMenuItemToggler* m_enableToggle = nullptr; + CCScale9Sprite* m_checkmark = nullptr; /** * @warning Make sure `getMetadata` and `createModLogo` are callable * before calling `init`! */ - bool init(); + bool init(ModSource&& source); // This should never be exposed outside, so the parent can't call this and // cause an infinite loop during state updating - virtual void updateState(); + void updateState(); + + void onEnable(CCObject*); + void onView(CCObject*); public: - virtual ModMetadata getMetadata() const = 0; - virtual CCNode* createModLogo() const = 0; - virtual bool wantsRestart() const = 0; + static ModItem* create(ModSource&& source); - virtual void updateSize(float width, bool big); + void updateSize(float width, bool big); void onUpdateParentState(MiniFunction listener); }; - -class InstalledModItem : public BaseModItem { -protected: - Mod* m_mod; - CCMenuItemToggler* m_enableToggle = nullptr; - - bool init(Mod* mod); - - void onEnable(CCObject*); - - void updateState() override; - -public: - /** - * @note Make sure to call `updateSize` afterwards - */ - static InstalledModItem* create(Mod* mod); - - ModMetadata getMetadata() const override; - CCNode* createModLogo() const override; - bool wantsRestart() const override; -}; - -class ServerModItem : public BaseModItem { -protected: - server::ServerModMetadata m_metadata; - CCScale9Sprite* m_checkmark = nullptr; - - bool init(server::ServerModMetadata const& metadata); - - void updateState() override; - -public: - /** - * @note Make sure to call `updateSize` afterwards - */ - static ServerModItem* create(server::ServerModMetadata const& metadata); - - ModMetadata getMetadata() const override; - CCNode* createModLogo() const override; - bool wantsRestart() const override; -}; diff --git a/loader/src/ui/mods/ModList.cpp b/loader/src/ui/mods/ModList.cpp index e3c3c1ca..4ef60ff2 100644 --- a/loader/src/ui/mods/ModList.cpp +++ b/loader/src/ui/mods/ModList.cpp @@ -294,7 +294,7 @@ void ModList::updateSize(bool big) { // Update all BaseModItems that are children of the list // There may be non-BaseModItems there (like separators) so gotta be type-safe for (auto& node : CCArrayExt(m_list->m_contentLayer->getChildren())) { - if (auto item = typeinfo_cast(node)) { + if (auto item = typeinfo_cast(node)) { item->updateSize(m_list->getContentWidth(), big); } } diff --git a/loader/src/ui/mods/ModListSource.cpp b/loader/src/ui/mods/ModListSource.cpp index 678fda59..3da5ceec 100644 --- a/loader/src/ui/mods/ModListSource.cpp +++ b/loader/src/ui/mods/ModListSource.cpp @@ -70,19 +70,20 @@ static std::pair, size_t> getModsWithQuery(server::ModsQuery c ) { result.push_back(mods.at(i).first); } - // Return paged mods & total count of matching mods + // Return paged mods and total count of matches return { result, mods.size() }; } static auto loadInstalledModsPage(server::ModsQuery&& query) { return ModListSource::ProviderPromise([query = std::move(query)](auto resolve, auto, auto, auto const&) { Loader::get()->queueInMainThread([query = std::move(query), resolve = std::move(resolve)] { - auto content = ModListSource::Page(); - auto mods = getModsWithQuery(query); - for (auto& mod : mods.first) { - content.push_back(InstalledModItem::create(mod)); + auto content = ModListSource::ProvidedMods(); + auto paged = getModsWithQuery(query); + for (auto& mod : std::move(paged.first)) { + content.mods.push_back(ModSource(mod)); } - resolve({ content, mods.second }); + content.totalModCount = paged.second; + resolve(content); }); }); } @@ -91,11 +92,12 @@ static auto loadServerModsPage(server::ModsQuery&& query) { return ModListSource::ProviderPromise([query = std::move(query)](auto resolve, auto reject, auto progress, auto cancelled) { server::getMods(query) .then([resolve, reject](server::ServerModsList list) { - auto content = ModListSource::Page(); - for (auto& mod : list.mods) { - content.push_back(ServerModItem::create(mod)); + auto content = ModListSource::ProvidedMods(); + for (auto&& mod : std::move(list.mods)) { + content.mods.push_back(ModSource(std::move(mod))); } - resolve({ content, list.totalModCount }); + content.totalModCount = list.totalModCount; + resolve(content); }) .expect([reject](auto error) { reject(ModListSource::LoadPageError("Error loading mods", error.details)); @@ -130,12 +132,16 @@ typename ModListSource::PagePromise ModListSource::loadPage(size_t page, bool up .pageSize = PER_PAGE, }) .then([page, this, resolve, reject](auto data) { - if (data.second == 0 || data.first.empty()) { + if (data.totalModCount == 0 || data.mods.empty()) { return reject(ModListSource::LoadPageError("No mods found :(")); } - m_cachedItemCount = data.second; - m_cachedPages.insert({ page, data.first }); - resolve(data.first); + auto pageData = Page(); + for (auto mod : std::move(data.mods)) { + pageData.push_back(ModItem::create(std::move(mod))); + } + m_cachedItemCount = data.totalModCount; + m_cachedPages.insert({ page, pageData }); + resolve(pageData); }) .expect([this, reject = reject](auto error) { reject(error); diff --git a/loader/src/ui/mods/ModListSource.hpp b/loader/src/ui/mods/ModListSource.hpp index f2ec30a3..96deca71 100644 --- a/loader/src/ui/mods/ModListSource.hpp +++ b/loader/src/ui/mods/ModListSource.hpp @@ -28,13 +28,17 @@ public: LoadPageError(auto msg, auto details) : message(msg), details(details) {} }; - using Page = std::vector>; + using Page = std::vector>; using PageLoadEvent = PromiseEvent>; using PageLoadEventFilter = PromiseEventFilter>; using PageLoadEventListener = EventListener; using PagePromise = Promise>; - using ProviderPromise = Promise, LoadPageError, std::optional>; + struct ProvidedMods { + std::vector mods; + size_t totalModCount; + }; + using ProviderPromise = Promise>; struct Provider { ProviderPromise(*get)(server::ModsQuery&& query) = nullptr; diff --git a/loader/src/ui/mods/ModPopup.cpp b/loader/src/ui/mods/ModPopup.cpp new file mode 100644 index 00000000..9eb1c36e --- /dev/null +++ b/loader/src/ui/mods/ModPopup.cpp @@ -0,0 +1,18 @@ +#include "ModPopup.hpp" + +bool ModPopup::setup(ModSource&& src) { + m_source = std::move(src); + m_noElasticity = true; + + return true; +} + +ModPopup* ModPopup::create(ModSource&& src) { + auto ret = new ModPopup(); + if (ret && ret->init(358.f, 250.f, std::move(src))) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/mods/ModPopup.hpp b/loader/src/ui/mods/ModPopup.hpp new file mode 100644 index 00000000..99fff6b0 --- /dev/null +++ b/loader/src/ui/mods/ModPopup.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "ModSource.hpp" + +using namespace geode::prelude; + +class ModPopup : public Popup { +protected: + ModSource m_source; + + bool setup(ModSource&& src) override; + +public: + static ModPopup* create(ModSource&& src); +}; diff --git a/loader/src/ui/mods/ModSource.cpp b/loader/src/ui/mods/ModSource.cpp new file mode 100644 index 00000000..9c945fed --- /dev/null +++ b/loader/src/ui/mods/ModSource.cpp @@ -0,0 +1,46 @@ +#include "ModSource.hpp" +#include + +ModSource::ModSource(Mod* mod) : m_value(mod) {} +ModSource::ModSource(server::ServerModMetadata&& metadata) : m_value(metadata) {} + +ModMetadata ModSource::getMetadata() const { + return std::visit(makeVisitor { + [](Mod* mod) { + return mod->getMetadata(); + }, + [](server::ServerModMetadata const& metadata) { + // Versions should be guaranteed to have at least one item + return metadata.versions.front().metadata; + } + }, m_value); +} +CCNode* ModSource::createModLogo() const { + return std::visit(makeVisitor { + [](Mod* mod) { + return geode::createModLogo(mod); + }, + [](server::ServerModMetadata const& metadata) { + return createServerModLogo(metadata.id); + } + }, m_value); +} +bool ModSource::wantsRestart() const { + return std::visit(makeVisitor { + [](Mod* mod) { + return mod->getRequestedAction() != ModRequestedAction::None; + }, + [](server::ServerModMetadata const& metdata) { + // todo: check if the mod has been installed + return false; + } + }, m_value); +} + +Mod* ModSource::asMod() const { + auto mod = std::get_if(&m_value); + return mod ? *mod : nullptr; +} +server::ServerModMetadata const* ModSource::asServer() const { + return std::get_if(&m_value); +} diff --git a/loader/src/ui/mods/ModSource.hpp b/loader/src/ui/mods/ModSource.hpp new file mode 100644 index 00000000..21a6746f --- /dev/null +++ b/loader/src/ui/mods/ModSource.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +using namespace geode::prelude; + +class ModSource final { +private: + std::variant m_value; + +public: + ModSource() = default; + ModSource(Mod* mod); + ModSource(server::ServerModMetadata&& metadata); + + ModMetadata getMetadata() const; + CCNode* createModLogo() const; + bool wantsRestart() const; + + auto visit(auto&& func) { + return std::visit(func, m_value); + } + + Mod* asMod() const; + server::ServerModMetadata const* asServer() const; +};