diff --git a/CHANGELOG.md b/CHANGELOG.md index 7425986c..5118ed95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Geode Changelog +## v3.4.0 + * Add an API for modifying the Geode UI via events; see [the corresponding docs page](https://docs.geode-sdk.org/tutorials/modify-geode) (2a3c35f) + * Add `openInfoPopup` overload that accepts a mod ID and can open both an installed mod page or a server page (028bbf9) + * Add `LoadingSpinner` for creating loading circles easily (5c84012) + * Add `TextInput::focus` and `TextInput::unfocus` (749fdf1) + * MDTextArea changes: hex colors are now formatted as ``; added support for ``, ``, ``, and ``; fixed `mod:` links (028bbf9) + * Deprecate `cc3x` (6080fdb) + * Don't cancel subtasks on `Task` destructor (4b4bc0e) + ## v3.3.1 * Move ObjectDecoder and its delegate to Cocos headers (95f9eeb, dceb91e) * Fix weird behavior with textures, objects and more by changing en-US.utf8 locale to C (2cd1a9e) diff --git a/VERSION b/VERSION index 712bd5a6..fbcbf738 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.1 \ No newline at end of file +3.4.0 \ No newline at end of file diff --git a/installer/windows/Language Files/JapaneseExtra.nsh b/installer/windows/Language Files/JapaneseExtra.nsh index 164df676..4cdb9435 100644 --- a/installer/windows/Language Files/JapaneseExtra.nsh +++ b/installer/windows/Language Files/JapaneseExtra.nsh @@ -8,7 +8,7 @@ ${LangFileString} MUI_UNTEXT_WELCOME_INFO_TEXT "このセットアップは$(^Na ; installer ${LangFileString} GEODE_TEXT_GD_MISSING "$\r$\n$\r$\nこのパスにはGeometry Dashがインストールされていません!" -${LangFileString} GEODE_TEXT_GD_OLD "$\r$\n$\r$\nYour version of Geometry Dash is too old for this version of Geode!" +${LangFileString} GEODE_TEXT_GD_OLD "$\r$\n$\r$\nGeometry DashのバージョンはこのGeodeのバージョンには古すぎます!" ${LangFileString} GEODE_TEXT_MOD_LOADER_ALREADY_INSTALLED "このパスには他のモッドがインストールされています!$\r$\nそれらはGeodeによって上書きされます。(the dll trademark)" ; uninstaller diff --git a/loader/include/Geode/cocos/shaders/CCGLProgram.h b/loader/include/Geode/cocos/shaders/CCGLProgram.h index 7ca4b01e..8aa8f66c 100644 --- a/loader/include/Geode/cocos/shaders/CCGLProgram.h +++ b/loader/include/Geode/cocos/shaders/CCGLProgram.h @@ -107,6 +107,7 @@ public: * @lua NA */ CCGLProgram(); + GEODE_CUSTOM_CONSTRUCTOR_COCOS(CCGLProgram, CCObject); /** * @js NA * @lua NA diff --git a/loader/include/Geode/ui/GeodeUI.hpp b/loader/include/Geode/ui/GeodeUI.hpp index d37ab69e..02417318 100644 --- a/loader/include/Geode/ui/GeodeUI.hpp +++ b/loader/include/Geode/ui/GeodeUI.hpp @@ -1,8 +1,107 @@ #pragma once #include "../loader/Mod.hpp" +#include + +class ModPopup; +class ModItem; +class ModLogoSprite; +class FLAlertLayer; // for macos :3 namespace geode { + /** + * Event posted whenever a popup is opened for a mod. Allows mods to modify + * the Geode UI. See the [tutorial on Geode UI modification](https://docs.geode-sdk.org/tutorials/modify-geode) + * for **very important notes on these events**! + */ + class GEODE_DLL ModPopupUIEvent final : public Event { + private: + class Impl; + std::unique_ptr m_impl; + + friend class ::ModPopup; + + ModPopupUIEvent(std::unique_ptr&& impl); + + public: + virtual ~ModPopupUIEvent(); + + /** + * Get the popup itself + */ + FLAlertLayer* getPopup() const; + /** + * Get the ID of the mod this popup is for + */ + std::string getModID() const; + /** + * If this popup is of an installed mod, get it + */ + std::optional getMod() const; + }; + + /** + * Event posted whenever a logo sprite is created for a mod. Allows mods to modify + * the Geode UI. See the [tutorial on Geode UI modification](https://docs.geode-sdk.org/tutorials/modify-geode) + * for **very important notes on these events**! + */ + class GEODE_DLL ModItemUIEvent final : public Event { + private: + class Impl; + std::unique_ptr m_impl; + + friend class ::ModItem; + + ModItemUIEvent(std::unique_ptr&& impl); + + public: + virtual ~ModItemUIEvent(); + + /** + * Get the item itself + */ + cocos2d::CCNode* getItem() const; + /** + * Get the ID of the mod this logo is for + */ + std::string getModID() const; + /** + * If this logo is of an installed mod, get it + */ + std::optional getMod() const; + }; + + /** + * Event posted whenever a logo sprite is created for a mod. Allows mods to modify + * the Geode UI. See the [tutorial on Geode UI modification](https://docs.geode-sdk.org/tutorials/modify-geode) + * for **very important notes on these events**! + */ + class GEODE_DLL ModLogoUIEvent final : public Event { + private: + class Impl; + std::unique_ptr m_impl; + + friend class ::ModLogoSprite; + + ModLogoUIEvent(std::unique_ptr&& impl); + + public: + virtual ~ModLogoUIEvent(); + + /** + * Get the sprite itself + */ + cocos2d::CCNode* getSprite() const; + /** + * Get the ID of the mod this logo is for + */ + std::string getModID() const; + /** + * If this logo is of an installed mod, get it + */ + std::optional getMod() const; + }; + /** * Open the Geode mods list */ @@ -11,6 +110,15 @@ namespace geode { * Open the info popup for a mod */ GEODE_DLL void openInfoPopup(Mod* mod); + /** + * Open the info popup for a mod based on an ID. If the mod is installed, + * its installed popup is opened. Otherwise will check if the servers + * have this mod, or if not, show an error popup + * @returns A Task that completes to `true` if the mod was found and a + * popup was opened, and `false` otherwise. If you wish to modify the + * created popup, listen for the Geode UI events listed in `GeodeUI.hpp` + */ + GEODE_DLL Task openInfoPopup(std::string const& modID); /** * Open the info popup for a mod on the changelog page */ diff --git a/loader/include/Geode/ui/LoadingSpinner.hpp b/loader/include/Geode/ui/LoadingSpinner.hpp new file mode 100644 index 00000000..c959ea9d --- /dev/null +++ b/loader/include/Geode/ui/LoadingSpinner.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace geode { + /** + * An eternally spinning loading circle. Essentially just a more convenient + * alternative to RobTop's `LoadingCircle` class, as this one respects its + * content size and is a lot more stripped down (not requiring a `show` + * method or anything - it just works!) + */ + class GEODE_DLL LoadingSpinner : public cocos2d::CCNode { + protected: + cocos2d::CCSprite* m_spinner; + + bool init(float size); + + void spin(); + + public: + /** + * Create a loading circle + * @param size The diameter of the circle in Cocos units + */ + static LoadingSpinner* create(float size); + + void setVisible(bool visible) override; + }; +} diff --git a/loader/include/Geode/ui/TextInput.hpp b/loader/include/Geode/ui/TextInput.hpp index 1d65b30d..f771c675 100644 --- a/loader/include/Geode/ui/TextInput.hpp +++ b/loader/include/Geode/ui/TextInput.hpp @@ -138,6 +138,15 @@ namespace geode { */ std::string getString() const; + /** + * Focus this input (activate the cursor) + */ + void focus(); + /** + * Defocus this input (deactivate the cursor) + */ + void defocus(); + CCTextInputNode* getInputNode() const; cocos2d::extension::CCScale9Sprite* getBGSprite() const; }; diff --git a/loader/include/Geode/utils/Task.hpp b/loader/include/Geode/utils/Task.hpp index 8fe407e8..38868bf1 100644 --- a/loader/include/Geode/utils/Task.hpp +++ b/loader/include/Geode/utils/Task.hpp @@ -163,9 +163,12 @@ namespace geode { m_status = Status::Cancelled; // If this task carries extra data, call the extra data's // handling method - if (m_extraData) { - m_extraData->cancel(); - } + // Actually: don't do this! This will cancel tasks even if + // they have other listeners! The extra data's destructor + // will handle cancellation if it has no other listeners! + // if (m_extraData) { + // m_extraData->cancel(); + // } // No need to actually post an event because this Task is // unlisteanable m_finalEventPosted = true; diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp index 851e1451..8500c157 100644 --- a/loader/include/Geode/utils/cocos.hpp +++ b/loader/include/Geode/utils/cocos.hpp @@ -149,16 +149,16 @@ namespace cocos2d { return s1.width != s2.width || s1.height != s2.height; } static bool operator<(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) { - return s1.width < s2.width || s1.height < s2.height; + return s1.width < s2.width && s1.height < s2.height; } static bool operator<=(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) { - return s1.width <= s2.width || s1.height <= s2.height; + return s1.width <= s2.width && s1.height <= s2.height; } static bool operator>(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) { - return s1.width > s2.width || s1.height > s2.height; + return s1.width > s2.width && s1.height > s2.height; } static bool operator>=(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) { - return s1.width >= s2.width || s1.height >= s2.height; + return s1.width >= s2.width && s1.height >= s2.height; } static bool operator==(cocos2d::CCRect const& r1, cocos2d::CCRect const& r2) { return r1.origin == r2.origin && r1.size == r2.size; @@ -861,6 +861,7 @@ namespace geode::cocos { return {color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f}; } + [[deprecated("This function may have unintended behavior, use cc3bFromHexString or manually expand the color instead")]] constexpr cocos2d::ccColor3B cc3x(int hexValue) { if (hexValue <= 0xf) return cocos2d::ccColor3B{ diff --git a/loader/include/Geode/utils/web.hpp b/loader/include/Geode/utils/web.hpp index de85e720..0ad58d45 100644 --- a/loader/include/Geode/utils/web.hpp +++ b/loader/include/Geode/utils/web.hpp @@ -125,11 +125,13 @@ namespace geode::utils::web { WebTask patch(std::string_view url); WebRequest& header(std::string_view name, std::string_view value); + WebRequest& removeHeader(std::string_view name); WebRequest& param(std::string_view name, std::string_view value); template WebRequest& param(std::string_view name, T value) { return this->param(name, std::to_string(value)); } + WebRequest& removeParam(std::string_view name); /** * Sets the request's user agent. diff --git a/loader/src/ui/GeodeUI.cpp b/loader/src/ui/GeodeUI.cpp index 8ad2ca03..bedfa20d 100644 --- a/loader/src/ui/GeodeUI.cpp +++ b/loader/src/ui/GeodeUI.cpp @@ -2,11 +2,78 @@ #include #include #include +#include #include #include #include "mods/GeodeStyle.hpp" #include "mods/settings/ModSettingsPopup.hpp" #include "mods/popups/ModPopup.hpp" +#include "GeodeUIEvent.hpp" + +class LoadServerModLayer : public Popup { +protected: + std::string m_id; + EventListener> m_listener; + + bool setup(std::string const& id) override { + m_closeBtn->setVisible(false); + + this->setTitle("Loading mod..."); + + auto spinner = LoadingSpinner::create(40); + m_mainLayer->addChildAtPosition(spinner, Anchor::Center, ccp(0, -10)); + + m_id = id; + m_listener.bind(this, &LoadServerModLayer::onRequest); + m_listener.setFilter(server::getMod(id)); + + return true; + } + + void onRequest(server::ServerRequest::Event* event) { + if (auto res = event->getValue()) { + if (res->isOk()) { + // Copy info first as onClose may free the listener which will free the event + auto info = **res; + this->onClose(nullptr); + // Run this on next frame because otherwise the popup is unable to call server::getMod for some reason + Loader::get()->queueInMainThread([info = std::move(info)]() mutable { + ModPopup::create(ModSource(std::move(info)))->show(); + }); + } + else { + auto id = m_id; + this->onClose(nullptr); + FLAlertLayer::create( + "Error Loading Mod", + fmt::format("Unable to find mod with the ID {}!", id), + "OK" + )->show(); + } + } + else if (event->isCancelled()) { + this->onClose(nullptr); + } + } + +public: + Task listen() const { + return m_listener.getFilter().map( + [](auto* result) -> bool { return result->isOk(); }, + [](auto) -> std::monostate { return std::monostate(); } + ); + } + + static LoadServerModLayer* create(std::string const& id) { + auto ret = new LoadServerModLayer(); + if (ret && ret->initAnchored(180, 100, id, "square01_001.png", CCRectZero)) { + ret->autorelease(); + return ret; + } + CC_SAFE_RELEASE(ret); + return nullptr; + } +}; void geode::openModsList() { ModsLayer::scene(); @@ -68,6 +135,18 @@ void geode::openSupportPopup(ModMetadata const& metadata) { void geode::openInfoPopup(Mod* mod) { ModPopup::create(mod)->show(); } +Task geode::openInfoPopup(std::string const& modID) { + if (auto mod = Loader::get()->getInstalledMod(modID)) { + openInfoPopup(mod); + return Task::immediate(true); + } + else { + auto popup = LoadServerModLayer::create(modID); + auto task = popup->listen(); + popup->show(); + return task; + } +} void geode::openIndexPopup(Mod* mod) { // deprecated func openInfoPopup(mod); @@ -98,6 +177,9 @@ protected: this->setAnchorPoint({ .5f, .5f }); this->setContentSize({ 50, 50 }); + // This is a default ID, nothing should ever rely on the ID of any ModLogoSprite being this + this->setID(std::string(Mod::get()->expandSpriteName(fmt::format("sprite-{}", id)))); + m_modID = id; m_listener.bind(this, &ModLogoSprite::onFetch); @@ -105,19 +187,22 @@ protected: if (!fetch) { this->setSprite(id == "geode.loader" ? CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) : - CCSprite::create(fmt::format("{}/logo.png", id).c_str()) + CCSprite::create(fmt::format("{}/logo.png", id).c_str()), + false ); } // Asynchronously fetch from server else { - this->setSprite(createLoadingCircle(25)); + this->setSprite(createLoadingCircle(25), false); m_listener.setFilter(server::getModLogo(id)); } + ModLogoUIEvent(std::make_unique(this, id)).post(); + return true; } - void setSprite(CCNode* sprite) { + void setSprite(CCNode* sprite, bool postEvent) { // Remove any existing sprite if (m_sprite) { m_sprite->removeFromParent(); @@ -129,15 +214,20 @@ protected: } // Set sprite and scale it to node size m_sprite = sprite; + m_sprite->setID("sprite"); limitNodeSize(m_sprite, m_obContentSize, 99.f, 0.f); this->addChildAtPosition(m_sprite, Anchor::Center); + + if (postEvent) { + ModLogoUIEvent(std::make_unique(this, m_modID)).post(); + } } void onFetch(server::ServerRequest::Event* event) { if (auto result = event->getValue()) { // Set default sprite on error if (result->isErr()) { - this->setSprite(nullptr); + this->setSprite(nullptr, true); } // Otherwise load downloaded sprite to memory else { @@ -146,11 +236,11 @@ protected: image->initWithImageData(const_cast(data.data()), data.size()); auto texture = CCTextureCache::get()->addUIImage(image, m_modID.c_str()); - this->setSprite(CCSprite::createWithTexture(texture)); + this->setSprite(CCSprite::createWithTexture(texture), true); } } else if (event->isCancelled()) { - this->setSprite(nullptr); + this->setSprite(nullptr, true); } } diff --git a/loader/src/ui/GeodeUIEvent.cpp b/loader/src/ui/GeodeUIEvent.cpp new file mode 100644 index 00000000..3b29d036 --- /dev/null +++ b/loader/src/ui/GeodeUIEvent.cpp @@ -0,0 +1,68 @@ +#include "GeodeUIEvent.hpp" + +ModPopupUIEvent::ModPopupUIEvent(std::unique_ptr&& impl) : m_impl(std::move(impl)) {} +ModPopupUIEvent::~ModPopupUIEvent() = default; + +FLAlertLayer* ModPopupUIEvent::getPopup() const { + return m_impl->popup; +} +std::string ModPopupUIEvent::getModID() const { + return m_impl->popup->getSource().getID(); +} +std::optional ModPopupUIEvent::getMod() const { + auto mod = m_impl->popup->getSource().asMod(); + return mod ? std::optional(mod) : std::nullopt; +} + +ModItemUIEvent::ModItemUIEvent(std::unique_ptr&& impl) : m_impl(std::move(impl)) {} +ModItemUIEvent::~ModItemUIEvent() = default; + +CCNode* ModItemUIEvent::getItem() const { + return m_impl->item; +} +std::string ModItemUIEvent::getModID() const { + return m_impl->item->getSource().getID(); +} +std::optional ModItemUIEvent::getMod() const { + auto mod = m_impl->item->getSource().asMod(); + return mod ? std::optional(mod) : std::nullopt; +} + +ModLogoUIEvent::ModLogoUIEvent(std::unique_ptr&& impl) : m_impl(std::move(impl)) {} +ModLogoUIEvent::~ModLogoUIEvent() = default; + +CCNode* ModLogoUIEvent::getSprite() const { + return m_impl->sprite; +} +std::string ModLogoUIEvent::getModID() const { + return m_impl->modID; +} +std::optional ModLogoUIEvent::getMod() const { + if (auto mod = Loader::get()->getInstalledMod(m_impl->modID)) { + return mod; + } + return std::nullopt; +} + +// $execute { +// new EventListener>(+[](ModLogoUIEvent* event) { +// if (event->getModID() == "geode.loader") { +// auto fart = CCSprite::createWithSpriteFrameName("GJ_demonIcon_001.png"); +// fart->setScaleX(5); +// fart->setScaleY(3); +// event->getSprite()->addChildAtPosition(fart, Anchor::Center); +// } +// return ListenerResult::Propagate; +// }); +// new EventListener>(+[](ModItemUIEvent* event) { +// if (event->getModID() == "geode.loader") { +// auto fart = CCSprite::createWithSpriteFrameName("GJ_demonIcon_001.png"); +// fart->setScaleX(4); +// fart->setScaleY(2); +// if (auto dev = event->getItem()->querySelector("developers-button")) { +// dev->addChildAtPosition(fart, Anchor::Center, ccp(-15, 0)); +// } +// } +// return ListenerResult::Propagate; +// }); +// } diff --git a/loader/src/ui/GeodeUIEvent.hpp b/loader/src/ui/GeodeUIEvent.hpp new file mode 100644 index 00000000..668651b5 --- /dev/null +++ b/loader/src/ui/GeodeUIEvent.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include "mods/popups/ModPopup.hpp" +#include "mods/list/ModItem.hpp" + +using namespace geode::prelude; + +class ModPopupUIEvent::Impl { +public: + ModPopup* popup; + + Impl(ModPopup* popup) + : popup(popup) {} +}; + +class ModItemUIEvent::Impl { +public: + ModItem* item; + + Impl(ModItem* item) + : item(item) {} +}; + +class ModLogoUIEvent::Impl { +public: + CCNode* sprite; + std::string modID; + + Impl(CCNode* sprite, std::string const& modID) + : sprite(sprite), modID(modID) {} +}; diff --git a/loader/src/ui/mods/GeodeStyle.cpp b/loader/src/ui/mods/GeodeStyle.cpp index 497002a5..920244c9 100644 --- a/loader/src/ui/mods/GeodeStyle.cpp +++ b/loader/src/ui/mods/GeodeStyle.cpp @@ -3,6 +3,7 @@ #include #include #include +#include $on_mod(Loaded) { // todo: these names should probably be shorter so they fit in SSO... @@ -134,51 +135,6 @@ void GeodeSquareSprite::setState(bool state) { } } -class LoadingSpinner : public CCNode { -protected: - CCSprite* m_spinner; - - bool init(float sideLength) { - if (!CCNode::init()) - return false; - - this->setID("loading-spinner"); - this->setContentSize({ sideLength, sideLength }); - this->setAnchorPoint({ .5f, .5f }); - - m_spinner = CCSprite::create("loadingCircle.png"); - m_spinner->setBlendFunc({ GL_ONE, GL_ONE }); - limitNodeSize(m_spinner, m_obContentSize, 1.f, .1f); - this->addChildAtPosition(m_spinner, Anchor::Center); - - this->spin(); - - return true; - } - - void spin() { - m_spinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); - } - -public: - static LoadingSpinner* create(float sideLength) { - auto ret = new LoadingSpinner(); - if (ret->init(sideLength)) { - ret->autorelease(); - return ret; - } - delete ret; - return nullptr; - } - - void setVisible(bool visible) override { - CCNode::setVisible(visible); - if (visible) { - this->spin(); - } - } -}; - CCNode* createLoadingCircle(float sideLength, const char* id) { auto spinner = LoadingSpinner::create(sideLength); spinner->setID(id); diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index 05a3fcef..1b83dbcd 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -350,17 +350,17 @@ bool ModsLayer::init() { reloadBtn->setID("reload-button"); actionsMenu->addChild(reloadBtn); - auto themeSpr = createGeodeCircleButton( + auto settingsSpr = createGeodeCircleButton( CCSprite::createWithSpriteFrameName("settings.png"_spr), 1.f, CircleBaseSize::Medium ); - themeSpr->setScale(.8f); - themeSpr->setTopOffset(ccp(.5f, 0)); - auto themeBtn = CCMenuItemSpriteExtra::create( - themeSpr, this, menu_selector(ModsLayer::onSettings) + settingsSpr->setScale(.8f); + settingsSpr->setTopOffset(ccp(.5f, 0)); + auto settingsBtn = CCMenuItemSpriteExtra::create( + settingsSpr, this, menu_selector(ModsLayer::onSettings) ); - themeBtn->setID("theme-button"); - actionsMenu->addChild(themeBtn); + settingsBtn->setID("settings-button"); + actionsMenu->addChild(settingsBtn); auto folderSpr = createGeodeCircleButton( CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png"), 1.f, diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index 08a27fa0..ee85ab85 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -9,6 +9,7 @@ #include "../popups/DevPopup.hpp" #include "ui/mods/popups/ModErrorPopup.hpp" #include "ui/mods/sources/ModSource.hpp" +#include "../../GeodeUIEvent.hpp" bool ModItem::init(ModSource&& source) { if (!CCNode::init()) @@ -407,6 +408,8 @@ void ModItem::updateState() { on->setOpacity(105); } } + + ModItemUIEvent(std::make_unique(this)).post(); } void ModItem::updateSize(float width, bool big) { @@ -521,3 +524,7 @@ ModItem* ModItem::create(ModSource&& source) { delete ret; return nullptr; } + +ModSource& ModItem::getSource() & { + return m_source; +} diff --git a/loader/src/ui/mods/list/ModItem.hpp b/loader/src/ui/mods/list/ModItem.hpp index 91d4008a..717c030c 100644 --- a/loader/src/ui/mods/list/ModItem.hpp +++ b/loader/src/ui/mods/list/ModItem.hpp @@ -56,4 +56,6 @@ public: static ModItem* create(ModSource&& source); void updateSize(float width, bool big); + + ModSource& getSource() &; }; diff --git a/loader/src/ui/mods/popups/ModPopup.cpp b/loader/src/ui/mods/popups/ModPopup.cpp index f81cb7d5..82006b0b 100644 --- a/loader/src/ui/mods/popups/ModPopup.cpp +++ b/loader/src/ui/mods/popups/ModPopup.cpp @@ -8,6 +8,7 @@ #include "ConfirmUninstallPopup.hpp" #include "../settings/ModSettingsPopup.hpp" #include "../../../internal/about.hpp" +#include "../../GeodeUIEvent.hpp" class FetchTextArea : public CCNode { public: @@ -29,6 +30,7 @@ protected: m_noneText = noneText; m_textarea = MDTextArea::create("", size); + m_textarea->setID("textarea"); this->addChildAtPosition(m_textarea, Anchor::Center); m_loading = createLoadingCircle(30); @@ -67,6 +69,8 @@ bool ModPopup::setup(ModSource&& src) { m_source = std::move(src); m_noElasticity = true; + this->setID(std::string(Mod::get()->expandSpriteName(fmt::format("popup-{}", src.getID())))); + if (src.asMod() == Mod::get()) { // Display commit hashes auto loaderHash = about::getLoaderCommitHash(); @@ -97,6 +101,7 @@ bool ModPopup::setup(ModSource&& src) { titleContainer->setAnchorPoint({ .5f, .5f }); auto logo = m_source.createModLogo(); + logo->setID("mod-logo"); limitNodeSize( logo, ccp(titleContainer->getContentHeight(), titleContainer->getContentHeight()), @@ -112,12 +117,14 @@ bool ModPopup::setup(ModSource&& src) { auto title = CCLabelBMFont::create(m_source.getMetadata().getName().c_str(), "bigFont.fnt"); title->limitLabelWidth(titleContainer->getContentWidth() - devAndTitlePos, .45f, .1f); title->setAnchorPoint({ .0f, .5f }); + title->setID("mod-name-label"); titleContainer->addChildAtPosition(title, Anchor::TopLeft, ccp(devAndTitlePos, -titleContainer->getContentHeight() * .25f)); auto by = "By " + m_source.formatDevelopers(); auto dev = CCLabelBMFont::create(by.c_str(), "goldFont.fnt"); dev->limitLabelWidth(titleContainer->getContentWidth() - devAndTitlePos, .35f, .05f); dev->setAnchorPoint({ .0f, .5f }); + dev->setID("mod-developer-label"); titleContainer->addChildAtPosition(dev, Anchor::BottomLeft, ccp(devAndTitlePos, titleContainer->getContentHeight() * .25f)); // Suggestions @@ -170,6 +177,7 @@ bool ModPopup::setup(ModSource&& src) { idLabel->limitLabelWidth(leftColumn->getContentWidth(), .25f, .05f); idLabel->setColor({ 150, 150, 150 }); idLabel->setOpacity(140); + idLabel->setID("mod-id-label"); leftColumn->addChild(idLabel); auto statsContainer = CCNode::create(); @@ -186,6 +194,7 @@ bool ModPopup::setup(ModSource&& src) { m_stats = CCNode::create(); m_stats->setContentSize(statsContainer->getContentSize() - ccp(10, 10)); m_stats->setAnchorPoint({ .5f, .5f }); + m_stats->setID("mod-stats-container"); for (auto stat : std::initializer_list, const char* @@ -264,6 +273,7 @@ bool ModPopup::setup(ModSource&& src) { m_tags->ignoreAnchorPointForPosition(false); m_tags->setContentSize(tagsContainer->getContentSize() - ccp(10, 10)); m_tags->setAnchorPoint({ .5f, .5f }); + m_tags->setID("tags-container"); m_tags->addChild(createLoadingCircle(50)); @@ -438,6 +448,7 @@ bool ModPopup::setup(ModSource&& src) { linksMenu->ignoreAnchorPointForPosition(false); linksMenu->setContentSize(linksContainer->getContentSize() - ccp(10, 10)); linksMenu->setAnchorPoint({ .5f, .5f }); + linksMenu->setID("links-container"); // auto linksLabel = CCLabelBMFont::create("Links", "bigFont.fnt"); // linksLabel->setLayoutOptions( @@ -447,28 +458,29 @@ bool ModPopup::setup(ModSource&& src) { // linksMenu->addChild(linksLabel); for (auto stat : std::initializer_list, SEL_MenuHandler + const char*, const char*, std::optional, SEL_MenuHandler >> { - { "homepage.png"_spr, m_source.getMetadata().getLinks().getHomepageURL(), nullptr }, - { "github.png"_spr, m_source.getMetadata().getLinks().getSourceURL(), nullptr }, - { "gj_discordIcon_001.png", m_source.getMetadata().getLinks().getCommunityURL(), nullptr }, - { "gift.png"_spr, m_source.getMetadata().getSupportInfo(), menu_selector(ModPopup::onSupport) }, + { "homepage", "homepage.png"_spr, m_source.getMetadata().getLinks().getHomepageURL(), nullptr }, + { "github", "github.png"_spr, m_source.getMetadata().getLinks().getSourceURL(), nullptr }, + { "discord", "gj_discordIcon_001.png", m_source.getMetadata().getLinks().getCommunityURL(), nullptr }, + { "support", "gift.png"_spr, m_source.getMetadata().getSupportInfo(), menu_selector(ModPopup::onSupport) }, }) { - auto spr = CCSprite::createWithSpriteFrameName(std::get<0>(stat)); + auto spr = CCSprite::createWithSpriteFrameName(std::get<1>(stat)); spr->setScale(.75f); - if (!std::get<1>(stat).has_value()) { + if (!std::get<2>(stat).has_value()) { spr->setColor({ 155, 155, 155 }); spr->setOpacity(155); } auto btn = CCMenuItemSpriteExtra::create( spr, this, ( - std::get<1>(stat).has_value() ? - (std::get<2>(stat) ? std::get<2>(stat) : menu_selector(ModPopup::onLink)) : + std::get<2>(stat).has_value() ? + (std::get<3>(stat) ? std::get<3>(stat) : menu_selector(ModPopup::onLink)) : nullptr ) ); - if (!std::get<2>(stat) && std::get<1>(stat)) { - btn->setUserObject("url", CCString::create(*std::get<1>(stat))); + btn->setID(std::get<0>(stat)); + if (!std::get<3>(stat) && std::get<2>(stat)) { + btn->setUserObject("url", CCString::create(*std::get<2>(stat))); } linksMenu->addChild(btn); } @@ -510,17 +522,19 @@ bool ModPopup::setup(ModSource&& src) { tabsMenu->setScale(.65f); tabsMenu->setContentWidth(m_rightColumn->getContentWidth() / tabsMenu->getScale()); tabsMenu->setAnchorPoint({ .5f, 1.f }); + tabsMenu->setID("tabs-menu"); - for (auto mdTab : std::initializer_list> { - { "message.png"_spr, "Description", Tab::Details }, - { "changelog.png"_spr, "Changelog", Tab::Changelog } + for (auto mdTab : std::initializer_list> { + { "message.png"_spr, "Description", "description", Tab::Details }, + { "changelog.png"_spr, "Changelog", "changelog", Tab::Changelog } // { "version.png"_spr, "Versions", Tab::Versions }, }) { auto spr = GeodeTabSprite::create(std::get<0>(mdTab), std::get<1>(mdTab), 140, m_source.asServer()); auto btn = CCMenuItemSpriteExtra::create(spr, this, menu_selector(ModPopup::onTab)); - btn->setTag(static_cast(std::get<2>(mdTab))); + btn->setTag(static_cast(std::get<3>(mdTab))); + btn->setID(std::get<2>(mdTab)); tabsMenu->addChild(btn); - m_tabs.insert({ std::get<2>(mdTab), { spr, nullptr } }); + m_tabs.insert({ std::get<3>(mdTab), { spr, nullptr } }); } // placeholder external link until versions tab is implemented @@ -532,7 +546,7 @@ bool ModPopup::setup(ModSource&& src) { auto externalLinkBtn = CCMenuItemSpriteExtra::create(externalLinkSpr, this, menu_selector(ModPopup::onLink)); externalLinkBtn->setUserObject("url", CCString::create(modUrl)); - + externalLinkBtn->setID("mod-online-page-button"); m_buttonMenu->addChildAtPosition(externalLinkBtn, Anchor::TopRight, ccp(-14, -16)); tabsMenu->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::Start)); @@ -548,6 +562,7 @@ bool ModPopup::setup(ModSource&& src) { auto settingsBtn = CCMenuItemSpriteExtra::create( settingsSpr, this, menu_selector(ModPopup::onSettings) ); + settingsBtn->setID("settings-button"); m_buttonMenu->addChildAtPosition(settingsBtn, Anchor::BottomLeft, ccp(28, 25)); if (!m_source.asMod() || !m_source.asMod()->hasSettings()) { @@ -707,6 +722,8 @@ void ModPopup::updateState() { } m_installMenu->updateLayout(); + + ModPopupUIEvent(std::make_unique(this)).post(); } void ModPopup::setStatIcon(CCNode* stat, const char* spr) { @@ -794,6 +811,7 @@ void ModPopup::onLoadServerInfo(typename server::ServerRequestsetStatValue(stat, id.second); } } + ModPopupUIEvent(std::make_unique(this)).post(); } else if (event->isCancelled() || (event->getValue() && event->getValue()->isErr())) { for (auto child : CCArrayExt(m_stats->getChildren())) { @@ -801,6 +819,7 @@ void ModPopup::onLoadServerInfo(typename server::ServerRequestsetStatValue(child, "N/A"); } } + ModPopupUIEvent(std::make_unique(this)).post(); } } @@ -851,6 +870,8 @@ void ModPopup::onLoadTags(typename server::ServerRequestupdateLayout(); + + ModPopupUIEvent(std::make_unique(this)).post(); } else if (event->isCancelled() || (event->getValue() && event->getValue()->isErr())) { m_tags->removeAllChildren(); @@ -860,6 +881,8 @@ void ModPopup::onLoadTags(typename server::ServerRequestaddChild(label); m_tags->updateLayout(); + + ModPopupUIEvent(std::make_unique(this)).post(); } } @@ -888,6 +911,7 @@ void ModPopup::loadTab(ModPopup::Tab tab) { "No description provided", size / mdScale ); + m_currentTabPage->setID("description-container"); m_currentTabPage->setScale(mdScale); } break; @@ -897,12 +921,14 @@ void ModPopup::loadTab(ModPopup::Tab tab) { "No changelog provided", size / mdScale ); + m_currentTabPage->setID("changelog-container"); m_currentTabPage->setScale(mdScale); } break; case Tab::Versions: { m_currentTabPage = CCNode::create(); m_currentTabPage->setContentSize(size); + m_currentTabPage->setID("versions-container"); } break; } m_currentTabPage->setAnchorPoint({ .5f, .0f }); @@ -995,3 +1021,7 @@ ModPopup* ModPopup::create(ModSource&& src) { delete ret; return nullptr; } + +ModSource& ModPopup::getSource() & { + return m_source; +} diff --git a/loader/src/ui/mods/popups/ModPopup.hpp b/loader/src/ui/mods/popups/ModPopup.hpp index a7873083..b91c5092 100644 --- a/loader/src/ui/mods/popups/ModPopup.hpp +++ b/loader/src/ui/mods/popups/ModPopup.hpp @@ -65,4 +65,6 @@ protected: public: void loadTab(Tab tab); static ModPopup* create(ModSource&& src); + + ModSource& getSource() &; }; diff --git a/loader/src/ui/mods/settings/GeodeSettingNode.hpp b/loader/src/ui/mods/settings/GeodeSettingNode.hpp index 56c435d6..d9b44d6a 100644 --- a/loader/src/ui/mods/settings/GeodeSettingNode.hpp +++ b/loader/src/ui/mods/settings/GeodeSettingNode.hpp @@ -150,10 +150,10 @@ protected: virtual void valueChanged(bool updateText = true) { if (this->hasUncommittedChanges()) { - m_nameLabel->setColor(cc3x(0x1d0)); + m_nameLabel->setColor({0x11, 0xdd, 0x00}); } else { - m_nameLabel->setColor(cc3x(0xfff)); + m_nameLabel->setColor({0xff, 0xff, 0xff}); } if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue()); auto isValid = setting()->validate(m_uncommittedValue); diff --git a/loader/src/ui/mods/settings/ModSettingsPopup.cpp b/loader/src/ui/mods/settings/ModSettingsPopup.cpp index c92f7ddf..573218dc 100644 --- a/loader/src/ui/mods/settings/ModSettingsPopup.cpp +++ b/loader/src/ui/mods/settings/ModSettingsPopup.cpp @@ -145,22 +145,22 @@ void ModSettingsPopup::onResetAll(CCObject*) { void ModSettingsPopup::settingValueCommitted(SettingNode*) { if (this->hasUncommitted()) { - m_applyBtnSpr->setColor(cc3x(0xf)); + m_applyBtnSpr->setColor({0xff, 0xff, 0xff}); m_applyBtn->setEnabled(true); } else { - m_applyBtnSpr->setColor(cc3x(0x4)); + m_applyBtnSpr->setColor({0x44, 0x44, 0x44}); m_applyBtn->setEnabled(false); } } void ModSettingsPopup::settingValueChanged(SettingNode*) { if (this->hasUncommitted()) { - m_applyBtnSpr->setColor(cc3x(0xf)); + m_applyBtnSpr->setColor({0xff, 0xff, 0xff}); m_applyBtn->setEnabled(true); } else { - m_applyBtnSpr->setColor(cc3x(0x4)); + m_applyBtnSpr->setColor({0x44, 0x44, 0x44}); m_applyBtn->setEnabled(false); } } diff --git a/loader/src/ui/nodes/LoadingSpinner.cpp b/loader/src/ui/nodes/LoadingSpinner.cpp new file mode 100644 index 00000000..5fabc773 --- /dev/null +++ b/loader/src/ui/nodes/LoadingSpinner.cpp @@ -0,0 +1,43 @@ +#include +#include + +using namespace geode::prelude; + +bool LoadingSpinner::init(float sideLength) { + if (!CCNode::init()) + return false; + + this->setID("loading-spinner"); + this->setContentSize({ sideLength, sideLength }); + this->setAnchorPoint({ .5f, .5f }); + + m_spinner = CCSprite::create("loadingCircle.png"); + m_spinner->setBlendFunc({ GL_ONE, GL_ONE }); + limitNodeSize(m_spinner, m_obContentSize, 1.f, .1f); + this->addChildAtPosition(m_spinner, Anchor::Center); + + this->spin(); + + return true; +} + +void LoadingSpinner::spin() { + m_spinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f))); +} + +LoadingSpinner* LoadingSpinner::create(float sideLength) { + auto ret = new LoadingSpinner(); + if (ret->init(sideLength)) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +void LoadingSpinner::setVisible(bool visible) { + CCNode::setVisible(visible); + if (visible) { + this->spin(); + } +} diff --git a/loader/src/ui/nodes/MDTextArea.cpp b/loader/src/ui/nodes/MDTextArea.cpp index 2023f00f..da766a4f 100644 --- a/loader/src/ui/nodes/MDTextArea.cpp +++ b/loader/src/ui/nodes/MDTextArea.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include using namespace geode::prelude; @@ -22,7 +24,7 @@ static constexpr float g_fontScale = .5f; static constexpr float g_paragraphPadding = 7.f; static constexpr float g_indent = 7.f; static constexpr float g_codeBlockIndent = 8.f; -static constexpr ccColor3B g_linkColor = cc3x(0x7ff4f4); +static constexpr ccColor3B g_linkColor = {0x7f, 0xf4, 0xf4}; TextRenderer::Font g_mdFont = [](int style) -> TextRenderer::Label { if ((style & TextStyleBold) && (style & TextStyleItalic)) { @@ -77,13 +79,12 @@ public: }; Result colorForIdentifier(std::string const& tag) { - if (utils::string::contains(tag, ' ')) { - auto hexStr = utils::string::split(utils::string::normalize(tag), " ").at(1); - auto res = numFromString(hexStr, 16); - if (res.isErr()) { - return Err("Invalid hex"); - } - return Ok(cc3x(res.unwrap())); + if (tag.length() > 2 && tag[1] == '-') { + return cc3bFromHexString(tag.substr(2)); + } + // Support the old form of + else if (tag.find(' ') != std::string::npos) { + return cc3bFromHexString(string::trim(tag.substr(tag.find(' ') + 1))); } else { auto colorText = tag.substr(1); @@ -95,15 +96,19 @@ Result colorForIdentifier(std::string const& tag) { } else { switch (colorText.front()) { - case 'a': return Ok(cc3x(0x9632ff)); break; - case 'b': return Ok(cc3x(0x4a52e1)); break; - case 'g': return Ok(cc3x(0x40e348)); break; - case 'l': return Ok(cc3x(0x60abef)); break; - case 'j': return Ok(cc3x(0x32c8ff)); break; - case 'y': return Ok(cc3x(0xffff00)); break; - case 'o': return Ok(cc3x(0xffa54b)); break; - case 'r': return Ok(cc3x(0xff5a5a)); break; - case 'p': return Ok(cc3x(0xff00ff)); break; + case 'a': return Ok(ccc3(150, 50, 255)); break; + case 'b': return Ok(ccc3(74, 82, 225)); break; + case 'c': return Ok(ccc3(255, 255, 150)); break; + case 'd': return Ok(ccc3(255, 150, 255)); break; + case 'f': return Ok(ccc3(150, 255, 255)); break; + case 'g': return Ok(ccc3(64, 227, 72)); break; + case 'j': return Ok(ccc3(50, 200, 255)); break; + case 'l': return Ok(ccc3(96, 171, 239)); break; + case 'o': return Ok(ccc3(255, 165, 75)); break; + case 'p': return Ok(ccc3(255, 0, 255)); break; + case 'r': return Ok(ccc3(255, 90, 90)); break; + case 's': return Ok(ccc3(255, 220, 65)); break; + case 'y': return Ok(ccc3(255, 255, 0)); break; default: return Err("Unknown color " + colorText); } } @@ -223,44 +228,10 @@ void MDTextArea::onGDLevel(CCObject* pSender) { CCDirector::sharedDirector()->replaceScene(CCTransitionFade::create(0.5f, scene)); } -void MDTextArea::onGeodeMod(CCObject* pSender) { - // TODO - // auto href = as(as(pSender)->getUserObject()); - // auto modString = std::string(href->getCString()); - // modString = modString.substr(modString.find(":") + 1); - // auto loader = Loader::get(); - // auto index = Index::get(); - // Mod* mod; - // bool success = false; - // IndexItemHandle indexItem; - // bool isIndexMod = !loader->isModInstalled(modString); - - // if (isIndexMod) { - // auto indexSearch = index->getItemsByModID(modString); - // if (indexSearch.size() != 0) { - // indexItem = indexSearch.back(); - // Mod mod2 = Mod(indexItem->getMetadata()); - // mod = &mod2; - // auto item = Index::get()->getItem(mod); - // IndexItemInfoPopup::create(item, nullptr)->show(); - // success = true; - // } - // } else { - // mod = loader->getLoadedMod(modString); - // LocalModInfoPopup::create(mod, nullptr)->show(); - // success = true; - // } - - // if (!success) { - // FLAlertLayer::create( - // "Error", - // "Invalid mod ID: " + modString + - // ". This is " - // "probably the mod developers's fault, report the bug to them.", - // "OK" - // ) - // ->show(); - // } +void MDTextArea::onGeodeMod(CCObject* sender) { + auto href = as(as(sender)->getUserObject()); + auto modID = std::string(href->getCString()); + (void)openInfoPopup(modID.substr(modID.find(":") + 1)); } void MDTextArea::FLAlert_Clicked(FLAlertLayer* layer, bool btn) { diff --git a/loader/src/ui/nodes/TextInput.cpp b/loader/src/ui/nodes/TextInput.cpp index fb47ad5b..5c80d308 100644 --- a/loader/src/ui/nodes/TextInput.cpp +++ b/loader/src/ui/nodes/TextInput.cpp @@ -191,6 +191,13 @@ std::string TextInput::getString() const { return m_input->getString(); } +void TextInput::focus() { + m_input->onClickTrackNode(true); +} +void TextInput::defocus() { + m_input->detachWithIME(); +} + CCTextInputNode* TextInput::getInputNode() const { return m_input; } diff --git a/loader/src/utils/web.cpp b/loader/src/utils/web.cpp index bc789c39..3a352edb 100644 --- a/loader/src/utils/web.cpp +++ b/loader/src/utils/web.cpp @@ -505,11 +505,21 @@ WebRequest& WebRequest::header(std::string_view name, std::string_view value) { return *this; } +WebRequest& WebRequest::removeHeader(std::string_view name) { + m_impl->m_headers.erase(std::string(name)); + return *this; +} + WebRequest& WebRequest::param(std::string_view name, std::string_view value) { m_impl->m_urlParameters.insert_or_assign(std::string(name), std::string(value)); return *this; } +WebRequest& WebRequest::removeParam(std::string_view name) { + m_impl->m_urlParameters.erase(std::string(name)); + return *this; +} + WebRequest& WebRequest::userAgent(std::string_view name) { m_impl->m_userAgent = name; return *this;