Merge branch 'main' into macos-is-mean-yet-again

This commit is contained in:
Chloe 2024-07-29 17:49:56 -07:00
commit 97b6a68b7d
No known key found for this signature in database
GPG key ID: D2D404DD810FE0E3
25 changed files with 525 additions and 145 deletions

View file

@ -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 `<c-XXXXXX></c>`; added support for `<cc>`, `<cd>`, `<cf>`, and `<cs>`; 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)

View file

@ -1 +1 @@
3.3.1
3.4.0

View file

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

View file

@ -107,6 +107,7 @@ public:
* @lua NA
*/
CCGLProgram();
GEODE_CUSTOM_CONSTRUCTOR_COCOS(CCGLProgram, CCObject);
/**
* @js NA
* @lua NA

View file

@ -1,8 +1,107 @@
#pragma once
#include "../loader/Mod.hpp"
#include <Geode/binding/FLAlertLayer.hpp>
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<Impl> m_impl;
friend class ::ModPopup;
ModPopupUIEvent(std::unique_ptr<Impl>&& 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<Mod*> 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<Impl> m_impl;
friend class ::ModItem;
ModItemUIEvent(std::unique_ptr<Impl>&& 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<Mod*> 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<Impl> m_impl;
friend class ::ModLogoSprite;
ModLogoUIEvent(std::unique_ptr<Impl>&& 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<Mod*> 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<bool> openInfoPopup(std::string const& modID);
/**
* Open the info popup for a mod on the changelog page
*/

View file

@ -0,0 +1,29 @@
#pragma once
#include <cocos2d.h>
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;
};
}

View file

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

View file

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

View file

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

View file

@ -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 <std::integral T>
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.

View file

@ -2,11 +2,78 @@
#include <Geode/loader/Dirs.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/ui/LoadingSpinner.hpp>
#include <Geode/utils/web.hpp>
#include <server/Server.hpp>
#include "mods/GeodeStyle.hpp"
#include "mods/settings/ModSettingsPopup.hpp"
#include "mods/popups/ModPopup.hpp"
#include "GeodeUIEvent.hpp"
class LoadServerModLayer : public Popup<std::string const&> {
protected:
std::string m_id;
EventListener<server::ServerRequest<server::ServerModMetadata>> 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<server::ServerModMetadata>::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 <cr>{}</c>!", id),
"OK"
)->show();
}
}
else if (event->isCancelled()) {
this->onClose(nullptr);
}
}
public:
Task<bool> 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<bool> geode::openInfoPopup(std::string const& modID) {
if (auto mod = Loader::get()->getInstalledMod(modID)) {
openInfoPopup(mod);
return Task<bool>::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<ModLogoUIEvent::Impl>(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<ModLogoUIEvent::Impl>(this, m_modID)).post();
}
}
void onFetch(server::ServerRequest<ByteVector>::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<uint8_t*>(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);
}
}

View file

@ -0,0 +1,68 @@
#include "GeodeUIEvent.hpp"
ModPopupUIEvent::ModPopupUIEvent(std::unique_ptr<Impl>&& 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<Mod*> ModPopupUIEvent::getMod() const {
auto mod = m_impl->popup->getSource().asMod();
return mod ? std::optional(mod) : std::nullopt;
}
ModItemUIEvent::ModItemUIEvent(std::unique_ptr<Impl>&& 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<Mod*> ModItemUIEvent::getMod() const {
auto mod = m_impl->item->getSource().asMod();
return mod ? std::optional(mod) : std::nullopt;
}
ModLogoUIEvent::ModLogoUIEvent(std::unique_ptr<Impl>&& 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<Mod*> ModLogoUIEvent::getMod() const {
if (auto mod = Loader::get()->getInstalledMod(m_impl->modID)) {
return mod;
}
return std::nullopt;
}
// $execute {
// new EventListener<EventFilter<ModLogoUIEvent>>(+[](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<EventFilter<ModItemUIEvent>>(+[](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;
// });
// }

View file

@ -0,0 +1,32 @@
#pragma once
#include <Geode/ui/GeodeUI.hpp>
#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) {}
};

View file

@ -3,6 +3,7 @@
#include <Geode/utils/ColorProvider.hpp>
#include <Geode/loader/SettingEvent.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/ui/LoadingSpinner.hpp>
$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);

View file

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

View file

@ -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<ModItemUIEvent::Impl>(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;
}

View file

@ -56,4 +56,6 @@ public:
static ModItem* create(ModSource&& source);
void updateSize(float width, bool big);
ModSource& getSource() &;
};

View file

@ -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<std::tuple<
const char*, const char*, const char*, std::optional<std::string>, 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<std::tuple<
const char*, std::optional<std::string>, SEL_MenuHandler
const char*, const char*, std::optional<std::string>, 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<std::tuple<const char*, const char*, Tab>> {
{ "message.png"_spr, "Description", Tab::Details },
{ "changelog.png"_spr, "Changelog", Tab::Changelog }
for (auto mdTab : std::initializer_list<std::tuple<const char*, const char*, const char*, Tab>> {
{ "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<int>(std::get<2>(mdTab)));
btn->setTag(static_cast<int>(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<ModPopupUIEvent::Impl>(this)).post();
}
void ModPopup::setStatIcon(CCNode* stat, const char* spr) {
@ -794,6 +811,7 @@ void ModPopup::onLoadServerInfo(typename server::ServerRequest<server::ServerMod
this->setStatValue(stat, id.second);
}
}
ModPopupUIEvent(std::make_unique<ModPopupUIEvent::Impl>(this)).post();
}
else if (event->isCancelled() || (event->getValue() && event->getValue()->isErr())) {
for (auto child : CCArrayExt<CCNode*>(m_stats->getChildren())) {
@ -801,6 +819,7 @@ void ModPopup::onLoadServerInfo(typename server::ServerRequest<server::ServerMod
this->setStatValue(child, "N/A");
}
}
ModPopupUIEvent(std::make_unique<ModPopupUIEvent::Impl>(this)).post();
}
}
@ -851,6 +870,8 @@ void ModPopup::onLoadTags(typename server::ServerRequest<std::unordered_set<std:
}
m_tags->updateLayout();
ModPopupUIEvent(std::make_unique<ModPopupUIEvent::Impl>(this)).post();
}
else if (event->isCancelled() || (event->getValue() && event->getValue()->isErr())) {
m_tags->removeAllChildren();
@ -860,6 +881,8 @@ void ModPopup::onLoadTags(typename server::ServerRequest<std::unordered_set<std:
m_tags->addChild(label);
m_tags->updateLayout();
ModPopupUIEvent(std::make_unique<ModPopupUIEvent::Impl>(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;
}

View file

@ -65,4 +65,6 @@ protected:
public:
void loadTab(Tab tab);
static ModPopup* create(ModSource&& src);
ModSource& getSource() &;
};

View file

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

View file

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

View file

@ -0,0 +1,43 @@
#include <Geode/ui/LoadingSpinner.hpp>
#include <Geode/utils/cocos.hpp>
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();
}
}

View file

@ -15,6 +15,8 @@
#include <md4c.h>
#include <charconv>
#include <Geode/loader/Log.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <server/Server.hpp>
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<ccColor3B> colorForIdentifier(std::string const& tag) {
if (utils::string::contains(tag, ' ')) {
auto hexStr = utils::string::split(utils::string::normalize(tag), " ").at(1);
auto res = numFromString<uint32_t>(hexStr, 16);
if (res.isErr()) {
return Err("Invalid hex");
if (tag.length() > 2 && tag[1] == '-') {
return cc3bFromHexString(tag.substr(2));
}
return Ok(cc3x(res.unwrap()));
// Support the old form of <carbitaryletters hex>
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<ccColor3B> 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<CCString*>(as<CCNode*>(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: <cr>" + modString +
// "</c>. This is "
// "probably the mod developers's fault, report the bug to them.",
// "OK"
// )
// ->show();
// }
void MDTextArea::onGeodeMod(CCObject* sender) {
auto href = as<CCString*>(as<CCNode*>(sender)->getUserObject());
auto modID = std::string(href->getCString());
(void)openInfoPopup(modID.substr(modID.find(":") + 1));
}
void MDTextArea::FLAlert_Clicked(FLAlertLayer* layer, bool btn) {

View file

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

View file

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