refactor a bit; works like a charm :3

This commit is contained in:
HJfod 2024-02-27 00:55:33 +02:00
parent 2689320116
commit 708d1ec873
10 changed files with 266 additions and 153 deletions

View file

@ -36,7 +36,9 @@ namespace geode {
*/
GEODE_DLL cocos2d::CCNode* createModLogo(Mod* mod);
/**
* Create a logo sprite for an index item
* Create a logo sprite for a mod downloaded from the Geode servers. The
* logo is initially a loading circle, with the actual sprite downloaded
* asynchronously
*/
// GEODE_DLL cocos2d::CCNode* createIndexItemLogo(IndexItemHandle item);
GEODE_DLL cocos2d::CCNode* createServerModLogo(std::string const& id);
}

BIN
loader/resources/globe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -9,6 +9,32 @@ using namespace server;
#define GEODE_GD_VERSION_STRINGIFY_2(version) GEODE_GD_VERSION_STRINGIFY(version)
#define GEODE_GD_VERSION_STR GEODE_GD_VERSION_STRINGIFY_2(GEODE_GD_VERSION)
static void parseServerError(auto reject, auto error) {
// The server should return errors as `{ "error": "...", "payload": "" }`
if (auto json = error.json()) {
reject(ServerError(
"Error code: {}; details: {}",
error.code(), json.unwrap().template get<std::string>("error")
));
}
// But if we get something else for some reason, return that
else {
reject(ServerError(
"Error code: {}; details: {}",
error.code(), error.string().unwrapOr("Unknown (not a valid string)")
));
}
}
static void parseServerProgress(auto progress, auto prog, auto msg) {
if (auto per = prog.downloadProgress()) {
progress({ msg, static_cast<uint8_t>(*per) });
}
else {
progress({ msg });
}
}
const char* server::sortToString(ModsSort sorting) {
switch (sorting) {
default:
@ -112,12 +138,14 @@ Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
root.has("about").into(res.about);
root.has("changelog").into(res.changelog);
std::vector<std::string> developerNames;
for (auto item : root.needs("developers").iterate()) {
auto obj = item.obj();
auto dev = ServerDeveloper();
obj.needs("username").into(dev.username);
obj.needs("display_name").into(dev.displayName);
res.developers.push_back(dev);
developerNames.push_back(dev.displayName);
}
for (auto item : root.needs("versions").iterate()) {
auto versionRes = ServerModVersion::parse(item.json());
@ -125,6 +153,7 @@ Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
auto version = versionRes.unwrap();
version.metadata.setDetails(res.about);
version.metadata.setChangelog(res.changelog);
version.metadata.setDevelopers(developerNames);
res.versions.push_back(version);
}
else {
@ -197,7 +226,9 @@ ServerPromise<ServerModsList> server::getMods(ModsQuery query) {
if (query.tags.size()) {
req.param("tags", ranges::join(query.tags, ","));
}
req.param("featured", query.featuredOnly ? "true" : "false");
if (query.featured) {
req.param("featured", query.featured.value() ? "true" : "false");
}
req.param("sort", sortToString(query.sorting));
if (query.developer) {
req.param("developer", *query.developer);
@ -228,28 +259,28 @@ ServerPromise<ServerModsList> server::getMods(ModsQuery query) {
if (error.code() == 404) {
return resolve(ServerModsList());
}
// The server should return errors as `{ "error": "...", "payload": "" }`
if (auto json = error.json()) {
reject(ServerError(
"Error code: {}; details: {}",
error.code(), json.unwrap().template get<std::string>("error")
));
}
// But if we get something else for some reason, return that
else {
reject(ServerError(
"Error code: {}; details: {}",
error.code(), error.string().unwrapOr("Unknown (not a valid string)")
));
}
parseServerError(reject, error);
})
.progress([progress](auto prog) {
if (auto per = prog.downloadProgress()) {
progress({ "Downloading mods", static_cast<uint8_t>(*per) });
}
else {
progress({ "Downloading mods" });
}
parseServerProgress(progress, prog, "Downloading mods");
})
.link(cancel);
});
}
ServerPromise<ByteVector> server::getModLogo(std::string const& id) {
auto req = web::WebRequest();
req.param("id", id);
return ServerPromise<ByteVector>([req = std::move(req), id](auto resolve, auto reject, auto progress, auto cancel) mutable {
req.get(getServerAPIBaseURL() + "/mods/" + id + "/logo")
.then([resolve](auto response) {
resolve(response.data());
})
.expect([reject](auto error) {
parseServerError(reject, error);
})
.progress([progress, id](auto prog) {
parseServerProgress(progress, prog, "Downloading logo for " + id);
})
.link(cancel);
});

View file

@ -54,7 +54,7 @@ namespace server {
std::optional<std::string> query;
std::unordered_set<PlatformID> platforms = { GEODE_PLATFORM_TARGET };
std::unordered_set<std::string> tags;
bool featuredOnly = false;
std::optional<bool> featured;
ModsSort sorting = ModsSort::Downloads;
std::optional<std::string> developer;
size_t page = 0;
@ -77,4 +77,5 @@ namespace server {
std::string getServerAPIBaseURL();
ServerPromise<ServerModsList> getMods(ModsQuery query);
ServerPromise<ByteVector> getModLogo(std::string const& id);
}

View file

@ -1,10 +1,10 @@
#include "mods/ModsLayer.hpp"
#include <Geode/loader/Dirs.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/utils/web.hpp>
#include <server/Server.hpp>
void geode::openModsList() {
ModsLayer::scene();
@ -65,26 +65,95 @@ void geode::openSettingsPopup(Mod* mod) {
}
}
CCNode* geode::createDefaultLogo() {
CCNode* spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) {
spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
class ModLogoSprite : public CCNode {
protected:
std::string m_modID;
CCNode* m_sprite = nullptr;
EventListener<PromiseEventFilter<ByteVector, server::ServerError>> m_listener;
bool init(std::string const& id, bool fetch) {
if (!CCNode::init())
return false;
this->setAnchorPoint({ .5f, .5f });
this->setContentSize({ 50, 50 });
m_modID = id;
m_listener.bind(this, &ModLogoSprite::onFetch);
// Load from Resources
if (!fetch) {
this->setSprite(id == "geode.loader" ?
CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) :
CCSprite::create(fmt::format("{}/logo.png", id).c_str())
);
}
// Asynchronously fetch from server
else {
this->setSprite(CCSprite::create("loadingCircle.png"));
static_cast<CCSprite*>(m_sprite)->setBlendFunc({ GL_ONE, GL_ONE });
m_sprite->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f)));
m_listener.setFilter(server::getModLogo(id).listen());
}
return true;
}
return spr;
void setSprite(CCNode* sprite) {
// Remove any existing sprite
if (m_sprite) {
m_sprite->removeFromParent();
}
// Fallback to default logo if the sprite is null
if (!sprite) {
sprite = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}
// Fallback to lobotomy if Geode sprites are missing
if (!sprite) {
sprite = CCSprite::createWithSpriteFrameName("difficulty_02_btn_001.png");
}
// Set sprite and scale it to node size
m_sprite = sprite;
limitNodeSize(m_sprite, m_obContentSize, 99.f, .05f);
this->addChildAtPosition(m_sprite, Anchor::Center);
}
void onFetch(PromiseEvent<ByteVector, server::ServerError>* event) {
// Set default sprite on error
if (event->getReject()) {
this->setSprite(nullptr);
}
else if (auto data = event->getResolve()) {
auto image = Ref(new CCImage());
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));
}
}
public:
static ModLogoSprite* create(std::string const& id, bool fetch) {
auto ret = new ModLogoSprite();
if (ret && ret->init(id, fetch)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
CCNode* geode::createDefaultLogo() {
return ModLogoSprite::create("", false);
}
CCNode* geode::createModLogo(Mod* mod) {
CCNode* ret = nullptr;
if (mod == Mod::get()) {
ret = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
ret = CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str());
}
if (!ret) {
ret = createDefaultLogo();
}
return ret;
return ModLogoSprite::create(mod->getID(), false);
}
CCNode* geode::createServerModLogo(std::string const& id) {
return ModLogoSprite::create(id, true);
}
// CCNode* geode::createIndexItemLogo(IndexItemHandle item) {

View file

@ -138,7 +138,7 @@ ModMetadata ServerModItem::getMetadata() const {
}
CCNode* ServerModItem::createModLogo() const {
return CCSprite::create("loadingCircle.png");
return createServerModLogo(m_metadata.id);
}
bool ServerModItem::wantsRestart() const {

View file

@ -8,11 +8,57 @@ static size_t ceildiv(size_t a, size_t b) {
return a / b + (a % b != 0);
}
#define GEODE_GD_VERSION_STRINGIFY(version) # version
#define GEODE_GD_VERSION_STRINGIFY_2(version) GEODE_GD_VERSION_STRINGIFY(version)
#define GEODE_GD_VERSION_STR GEODE_GD_VERSION_STRINGIFY_2(GEODE_GD_VERSION)
static auto loadInstalledModsPage(size_t page) {
return ModListSource::ProviderPromise([page](auto resolve, auto, auto, auto const&) {
Loader::get()->queueInMainThread([page, resolve = std::move(resolve)] {
auto content = ModListSource::Page();
auto all = Loader::get()->getAllMods();
for (
size_t i = page * PAGE_SIZE;
i < all.size() && i < (page + 1) * PAGE_SIZE;
i += 1
) {
content.push_back(InstalledModItem::create(all.at(i)));
}
resolve({ content, all.size() });
});
});
}
static auto loadServerModsPage(size_t page, bool featuredOnly) {
return ModListSource::ProviderPromise([page, featuredOnly](auto resolve, auto reject, auto progress, auto cancelled) {
server::getMods(server::ModsQuery {
.featured = featuredOnly ? std::optional(true) : std::nullopt,
.page = page,
.pageSize = PAGE_SIZE,
})
.then([resolve, reject](server::ServerModsList list) {
if (list.totalModCount == 0) {
return reject(ModListSource::LoadPageError("No mods found :("));
}
auto content = ModListSource::Page();
for (auto mod : list.mods) {
content.push_back(ServerModItem::create(mod));
}
resolve({ content, list.totalModCount });
})
.expect([reject](auto error) {
reject(ModListSource::LoadPageError("Error loading mods", error.details));
})
.progress([progress](auto prog) {
progress(prog.percentage);
})
.link(cancelled);
});
}
typename ModListSource::PagePromise ModListSource::loadPage(size_t page, bool update) {
// Return a generic "Coming soon" message if there's no provider set
if (!m_provider) {
return PagePromise([this, page](auto, auto reject) {
reject("Coming soon! ;)");
});
}
if (!update && m_cachedPages.contains(page)) {
return PagePromise([this, page](auto resolve, auto) {
Loader::get()->queueInMainThread([this, page, resolve] {
@ -22,10 +68,11 @@ typename ModListSource::PagePromise ModListSource::loadPage(size_t page, bool up
}
m_cachedPages.erase(page);
return PagePromise([this, page](auto resolve, auto reject, auto progress, auto cancelled) {
this->reloadPage(page)
m_provider(page)
.then([page, this, resolve = std::move(resolve)](auto data) {
m_cachedPages.insert({ page, data });
resolve(data);
m_cachedItemCount = data.second;
m_cachedPages.insert({ page, data.first });
resolve(data.first);
})
.expect([this, reject = std::move(reject)](auto error) {
reject(error);
@ -45,69 +92,43 @@ std::optional<size_t> ModListSource::getItemCount() const {
return m_cachedItemCount;
}
typename ModListSource::PagePromise InstalledModsList::reloadPage(size_t page) {
m_cachedItemCount = Loader::get()->getAllMods().size();
return PagePromise([page](auto resolve, auto, auto, auto const&) {
Loader::get()->queueInMainThread([page, resolve = std::move(resolve)] {
auto content = Page();
auto all = Loader::get()->getAllMods();
for (
size_t i = page * PAGE_SIZE;
i < all.size() && i < (page + 1) * PAGE_SIZE;
i += 1
) {
content.push_back(InstalledModItem::create(all.at(i)));
}
resolve(content);
});
});
ModListSource* ModListSource::create(Provider* provider) {
auto ret = new ModListSource();
ret->m_provider = provider;
ret->autorelease();
return ret;
}
InstalledModsList* InstalledModsList::get() {
static auto inst = new InstalledModsList();
return inst;
}
ModListSource* ModListSource::get(ModListSourceType type) {
switch (type) {
default:
case ModListSourceType::Installed: {
static auto inst = ModListSource::create(loadInstalledModsPage);
return inst;
} break;
typename ModListSource::PagePromise FeaturedModsList::reloadPage(size_t page) {
return PagePromise([this, page](auto resolve, auto reject, auto progress, auto cancelled) {
server::getMods(server::ModsQuery {
.page = page,
.pageSize = PAGE_SIZE,
})
.then([this, resolve, reject](server::ServerModsList list) {
m_cachedItemCount = list.totalModCount;
if (list.totalModCount == 0) {
return reject(LoadPageError("No mods found :("));
}
auto content = Page();
for (auto mod : list.mods) {
content.push_back(ServerModItem::create(mod));
}
resolve(content);
})
.expect([reject](auto error) {
reject(LoadPageError("Error loading mods", error.details));
})
.progress([progress](auto prog) {
progress(prog.percentage);
})
.link(cancelled);
});
}
case ModListSourceType::Featured: {
static auto inst = ModListSource::create(+[](size_t page) {
return loadServerModsPage(page, true);
});
return inst;
} break;
FeaturedModsList* FeaturedModsList::get() {
static auto inst = new FeaturedModsList();
return inst;
}
case ModListSourceType::Trending: {
static auto inst = ModListSource::create(nullptr);
return inst;
} break;
typename ModListSource::PagePromise ModPacksModsList::reloadPage(size_t page) {
m_cachedItemCount = 0;
return PagePromise([](auto, auto reject) {
reject(LoadPageError("Coming soon! ;)"));
});
}
case ModListSourceType::ModPacks: {
static auto inst = ModListSource::create(nullptr);
return inst;
} break;
ModPacksModsList* ModPacksModsList::get() {
static auto inst = new ModPacksModsList();
return inst;
case ModListSourceType::All: {
static auto inst = ModListSource::create(+[](size_t page) {
return loadServerModsPage(page, false);
});
return inst;
} break;
}
}

View file

@ -6,8 +6,16 @@
using namespace geode::prelude;
enum class ModListSourceType {
Installed,
Featured,
Trending,
ModPacks,
All,
};
// Handles loading the entries for the mods list
class ModListSource {
class ModListSource : public CCObject {
public:
struct LoadPageError {
std::string message;
@ -24,40 +32,23 @@ public:
using PageLoadEventListener = EventListener<PageLoadEventFilter>;
using PagePromise = Promise<Page, LoadPageError, std::optional<uint8_t>>;
using ProviderPromise = Promise<std::pair<Page, size_t>, LoadPageError, std::optional<uint8_t>>;
using Provider = ProviderPromise(size_t page);
protected:
std::unordered_map<size_t, Page> m_cachedPages;
std::optional<size_t> m_cachedItemCount;
// Load/reload a page. This should also set/update the page count
virtual PagePromise reloadPage(size_t page) = 0;
Provider* m_provider = nullptr;
public:
// Create a new source with an arbitary provider
static ModListSource* create(Provider* provider);
// Get a standard source (lazily created static instance)
static ModListSource* get(ModListSourceType type);
// Load page, uses cache if possible unless `update` is true
PagePromise loadPage(size_t page, bool update = false);
std::optional<size_t> getPageCount() const;
std::optional<size_t> getItemCount() const;
};
class InstalledModsList : public ModListSource {
protected:
PagePromise reloadPage(size_t page) override;
public:
static InstalledModsList* get();
};
class FeaturedModsList : public ModListSource {
protected:
PagePromise reloadPage(size_t page) override;
public:
static FeaturedModsList* get();
};
class ModPacksModsList : public ModListSource {
protected:
PagePromise reloadPage(size_t page) override;
public:
static ModPacksModsList* get();
};

View file

@ -365,12 +365,12 @@ bool ModsLayer::init() {
mainTabs->setAnchorPoint({ .5f, .0f });
mainTabs->setPosition(m_frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10)));
for (auto item : std::initializer_list<std::tuple<const char*, const char*, ModListSource*>> {
{ "download.png"_spr, "Installed", InstalledModsList::get() },
{ "GJ_bigStar_noShadow_001.png", "Featured", FeaturedModsList::get() },
{ "GJ_sTrendingIcon_001.png", "Trending", nullptr },
{ "gj_folderBtn_001.png", "Mod Packs", ModPacksModsList::get() },
{ "search.png"_spr, "Search", nullptr },
for (auto item : std::initializer_list<std::tuple<const char*, const char*, ModListSourceType>> {
{ "download.png"_spr, "Installed", ModListSourceType::Installed },
{ "GJ_bigStar_noShadow_001.png", "Featured", ModListSourceType::Featured },
{ "GJ_sTrendingIcon_001.png", "Trending", ModListSourceType::Trending },
{ "gj_folderBtn_001.png", "Mod Packs", ModListSourceType::ModPacks },
{ "globe.png"_spr, "All Mods", ModListSourceType::All },
}) {
const CCSize itemSize { 100, 35 };
const CCSize iconSize { 18, 18 };
@ -403,7 +403,7 @@ bool ModsLayer::init() {
spr->addChildAtPosition(title, Anchor::Left, ccp(28, 0), false);
auto btn = CCMenuItemSpriteExtra::create(spr, this, menu_selector(ModsLayer::onTab));
btn->setUserData(std::get<2>(item));
btn->setTag(static_cast<int>(std::get<2>(item)));
mainTabs->addChild(btn);
m_tabs.push_back(btn);
}
@ -411,7 +411,7 @@ bool ModsLayer::init() {
mainTabs->setLayout(RowLayout::create());
this->addChild(mainTabs);
this->gotoTab();
this->gotoTab(ModListSourceType::Installed);
this->setKeypadEnabled(true);
cocos::handleTouchPriority(this, true);
@ -419,19 +419,17 @@ bool ModsLayer::init() {
return true;
}
void ModsLayer::gotoTab(ModListSource* src) {
// Default to installed mods
if (!src) {
src = InstalledModsList::get();
}
void ModsLayer::gotoTab(ModListSourceType type) {
// Update selected tab
for (auto tab : m_tabs) {
auto selected = tab->getUserData() == src;
auto selected = tab->getTag() == static_cast<int>(type);
tab->getNormalImage()->getChildByID("disabled-bg")->setVisible(!selected);
tab->getNormalImage()->getChildByID("enabled-bg")->setVisible(selected);
tab->setEnabled(!selected);
}
auto src = ModListSource::get(type);
// Remove current list from UI (it's Ref'd so it stays in memory)
if (m_currentSource) {
m_lists.at(m_currentSource)->removeFromParent();
@ -453,7 +451,7 @@ void ModsLayer::gotoTab(ModListSource* src) {
}
void ModsLayer::onTab(CCObject* sender) {
this->gotoTab(static_cast<ModListSource*>(static_cast<CCNode*>(sender)->getUserData()));
this->gotoTab(static_cast<ModListSourceType>(sender->getTag()));
}
void ModsLayer::keyBackClicked() {

View file

@ -17,7 +17,7 @@ using ModListStatus = std::variant<ModListErrorStatus, ModListUnkProgressStatus,
class ModList : public CCNode, public SetTextPopupDelegate {
protected:
ModListSource* m_source;
Ref<ModListSource> m_source;
size_t m_page = 0;
ScrollLayer* m_list;
CCMenu* m_statusContainer;
@ -70,5 +70,5 @@ public:
void onBack(CCObject*);
void onRefreshList(CCObject*);
void gotoTab(ModListSource* src = nullptr);
void gotoTab(ModListSourceType type);
};