new mods list design is done :3 ready to impl new index!!

This commit is contained in:
HJfod 2024-02-24 02:09:02 +02:00
parent e82ea3eddd
commit 2db556dc20
4 changed files with 355 additions and 141 deletions

View file

@ -2,43 +2,59 @@
static size_t INSTALLED_MODS_PAGE_SIZE = 10;
struct InstalledModsList : ModListSource {
std::string_view getID() const override {
return "installed";
Promise<typename ModListSource::Page> ModListSource::loadPage(size_t page, bool update) {
if (!update && m_cachedPages.contains(page)) {
return Promise<Page>([this, page](auto resolve, auto) { resolve(m_cachedPages.at(page)); });
}
Promise<Page> loadNewPage(size_t page, bool) {
// You can't load new mods at runtime so update does nothing
return Promise([](auto resolve, auto _reject, auto _progress) {
std::thread([res = std::move(resolve)] {
std::this_thread::sleep_for(std::chrono::seconds(3));
auto page = Page();
auto all = Loader::get()->getAllMods();
for (size_t i = page * INSTALLED_MODS_PAGE_SIZE; i < all.size(); i += 1) {
page.push_back(InstalledModItem::create(all.at(i)));
}
res(page);
}).detach();
});
}
Promise<size_t> loadTotalPageCount() const {
return Promise([](auto resolve, auto _reject, auto _progress) {
resolve(Loader::get()->getAllMods().size() / INSTALLED_MODS_PAGE_SIZE + 1);
});
}
static InstalledModsList& get() {
static auto inst = InstalledModsList();
return inst;
}
};
std::optional<std::reference_wrapper<ModListSource>> ModListSource::get(std::string_view id) {
switch (hash(id)) {
case hash("installed"): {
return InstalledModsList::get();
} break;
}
return std::nullopt;
m_cachedPages.erase(page);
return Promise<Page>([this, page](auto resolve, auto reject, auto progress, auto const&) {
this->reloadPage(page)
.then([page, this, resolve = std::move(resolve)](auto data) {
m_cachedPages.insert({ page, data });
resolve(data);
})
.expect([this, reject = std::move(reject)](auto error) {
reject(error);
})
.progress([this, progress = std::move(progress)](auto prog) {
progress(prog);
});
});
}
std::optional<size_t> ModListSource::getPageCount() const {
return m_cachedPageCount;
}
Promise<typename ModListSource::Page> InstalledModsList::reloadPage(size_t page) {
m_cachedPageCount = Loader::get()->getAllMods().size() / INSTALLED_MODS_PAGE_SIZE + 1;
return Promise<Page>([page](auto resolve, auto, auto, auto const&) {
auto content = Page();
auto all = Loader::get()->getAllMods();
for (
size_t i = page * INSTALLED_MODS_PAGE_SIZE;
i < all.size() && i < (page + 1) * INSTALLED_MODS_PAGE_SIZE;
i += 1
) {
content.push_back(InstalledModItem::create(all.at(i)));
}
resolve(content);
});
}
InstalledModsList* InstalledModsList::get() {
static auto inst = new InstalledModsList();
return inst;
}
Promise<typename ModListSource::Page> ModPacksModsList::reloadPage(size_t page) {
m_cachedPageCount = 0;
return Promise<Page>([](auto, auto reject) {
reject("Coming soon! ;)");
});
}
ModPacksModsList* ModPacksModsList::get() {
static auto inst = new ModPacksModsList();
return inst;
}

View file

@ -7,18 +7,36 @@
using namespace geode::prelude;
// Handles loading the entries for the mods list
struct ModListSource {
class ModListSource {
public:
using Page = std::vector<Ref<BaseModItem>>;
// ID of the list source
virtual std::string_view getID() const = 0;
using PageLoadEventListener = EventListener<PromiseEventFilter<Page>>;
// Load a page. Use update to force a reload on existing page. Up to the
// source impl to cache this
virtual Promise<Page> loadNewPage(size_t page, bool update = false) = 0;
protected:
std::unordered_map<size_t, Page> m_cachedPages;
std::optional<size_t> m_cachedPageCount;
// Get the total number of available pages
virtual Promise<size_t> loadTotalPageCount() const = 0;
// Load/reload a page. This should also set/update the page count
virtual Promise<Page> reloadPage(size_t page) = 0;
static std::optional<std::reference_wrapper<ModListSource>> get(std::string_view id);
public:
// Load page, uses cache if possible unless `update` is true
Promise<Page> loadPage(size_t page, bool update = false);
std::optional<size_t> getPageCount() const;
};
class InstalledModsList : public ModListSource {
protected:
Promise<Page> reloadPage(size_t page) override;
public:
static InstalledModsList* get();
};
class ModPacksModsList : public ModListSource {
protected:
Promise<Page> reloadPage(size_t page) override;
public:
static ModPacksModsList* get();
};

View file

@ -3,6 +3,201 @@
static bool BIG_VIEW = false;
bool ModList::init(ModListSource* src, CCSize const& size) {
if (!CCNode::init())
return false;
this->setContentSize(size);
this->setAnchorPoint({ .5f, .5f });
m_source = src;
m_list = ScrollLayer::create(size);
m_list->m_contentLayer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::End)
->setAutoGrowAxis(size.height)
);
this->addChildAtPosition(m_list, Anchor::Center, -m_list->getScaledContentSize() / 2);
auto pageLeftMenu = CCMenu::create();
pageLeftMenu->setContentWidth(30.f);
pageLeftMenu->setAnchorPoint({ 1.f, .5f });
m_pagePrevBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png"),
this, menu_selector(ModList::onPage)
);
m_pagePrevBtn->setTag(-1);
pageLeftMenu->addChild(m_pagePrevBtn);
pageLeftMenu->setLayout(
RowLayout::create()
->setAxisAlignment(AxisAlignment::End)
->setAxisReverse(true)
);
this->addChildAtPosition(pageLeftMenu, Anchor::Left, ccp(-5, 0));
auto pageRightMenu = CCMenu::create();
pageRightMenu->setContentWidth(30.f);
pageRightMenu->setAnchorPoint({ 0.f, .5f });
auto pageNextSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png");
pageNextSpr->setFlipX(true);
m_pageNextBtn = CCMenuItemSpriteExtra::create(
pageNextSpr,
this, menu_selector(ModList::onPage)
);
m_pageNextBtn->setTag(1);
pageRightMenu->addChild(m_pageNextBtn);
pageRightMenu->setLayout(
RowLayout::create()
->setAxisAlignment(AxisAlignment::Start)
);
this->addChildAtPosition(pageRightMenu, Anchor::Right, ccp(5, 0));
m_pageLabel = CCLabelBMFont::create("", "bigFont.fnt");
m_pageLabel->setAnchorPoint({ .5f, 1.f });
this->addChildAtPosition(m_pageLabel, Anchor::Bottom, ccp(0, -5));
m_statusText = SimpleTextArea::create("", "bigFont.fnt", .6f);
m_statusText->setAlignment(kCCTextAlignmentCenter);
this->addChildAtPosition(m_statusText, Anchor::Center, ccp(0, 40));
m_statusLoadingCircle = CCSprite::create("loadingCircle.png");
m_statusLoadingCircle->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f)));
m_statusLoadingCircle->setScale(.6f);
this->addChildAtPosition(m_statusLoadingCircle, Anchor::Center, ccp(0, -40));
m_listener.bind(this, &ModList::onPromise);
this->gotoPage(0);
return true;
}
void ModList::onPromise(PromiseEvent<typename ModListSource::Page>* event) {
if (auto resolved = event->getResolve()) {
// Hide status
m_statusText->setVisible(false);
m_statusLoadingCircle->setVisible(false);
// Create items
for (auto item : *resolved) {
m_list->m_contentLayer->addChild(item);
item->updateSize(m_list->getContentWidth(), BIG_VIEW);
}
// Auto-grow the size of the list content
m_list->m_contentLayer->updateLayout();
// Scroll list to top
auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight();
m_list->m_contentLayer->setPositionY(listTopScrollPos);
// Update page UI
this->updatePageUI();
}
else if (auto progress = event->getProgress()) {
// todo: percentage in a loading bar
this->showStatus(progress->message, true);
}
else if (auto rejected = event->getReject()) {
this->showStatus(*rejected, false);
this->updatePageUI(true);
}
if (event->isFinally()) {
// Clear listener
m_listener.setFilter(PromiseEventFilter<ModListSource::Page>());
}
}
void ModList::onPage(CCObject* sender) {
// If no page count has been loaded yet, we can't do anything
if (!m_source->getPageCount()) return;
auto pageCount = m_source->getPageCount().value();
// Make sure you can't go beyond the limits
if (sender->getTag() < 0 && m_page >= -sender->getTag()) {
m_page += sender->getTag();
}
// Ig this can technically overflow, but why would there be over 4 billion pages
// (and why would someone manually scroll that far)
else if (sender->getTag() > 0 && m_page + sender->getTag() < m_source->getPageCount()) {
m_page += sender->getTag();
}
// Load new page
this->gotoPage(m_page);
}
void ModList::updatePageUI(bool hide) {
auto pageCount = m_source->getPageCount();
// Clamp page count in case the max amount has changed for some reason
if (pageCount && m_page >= pageCount.value()) {
auto count = pageCount.value();
m_page = count > 0 ? count - 1 : 0;
}
// Hide if page count hasn't been loaded
if (!pageCount) {
hide = true;
}
m_pagePrevBtn->setVisible(!hide && m_page > 0);
m_pageNextBtn->setVisible(!hide && m_page < pageCount.value() - 1);
m_pageLabel->setVisible(!hide);
if (pageCount > 0u) {
auto fmt = fmt::format("Page {}/{}", m_page + 1, pageCount.value());
m_pageLabel->setString(fmt.c_str());
m_pageLabel->limitLabelWidth(100.f, .35f, .1f);
}
}
void ModList::reloadPage() {
// Just force an update on the current page
this->gotoPage(m_page, true);
}
void ModList::gotoPage(size_t page, bool update) {
// Clear list contents
m_list->m_contentLayer->removeAllChildren();
m_page = page;
// Start loading new page with generic loading message
this->showStatus("Loading...", true);
m_listener.setFilter(m_source->loadPage(page, update).listen());
// Do initial eager update on page UI (to prevent user spamming arrows
// to access invalid pages)
this->updatePageUI();
}
void ModList::showStatus(std::string const& status, bool loading) {
// Clear list contents
m_list->m_contentLayer->removeAllChildren();
// Update status
m_statusText->setText(status);
m_statusText->updateAnchoredPosition(Anchor::Center, (loading ? ccp(0, 40) : ccp(0, 0)));
// Make status visible
m_statusText->setVisible(true);
m_statusLoadingCircle->setVisible(loading);
}
ModList* ModList::create(ModListSource* src, CCSize const& size) {
auto ret = new ModList();
if (ret && ret->init(src, size)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
bool ModsLayer::init() {
if (!CCLayer::init())
return false;
@ -49,54 +244,45 @@ bool ModsLayer::init() {
);
this->addChildAtPosition(actionsMenu, Anchor::BottomLeft, ccp(35, 12), false);
auto frame = CCNode::create();
frame->setAnchorPoint({ .5f, .5f });
frame->setContentSize({ 380, 205 });
m_frame = CCNode::create();
m_frame->setAnchorPoint({ .5f, .5f });
m_frame->setContentSize({ 380, 205 });
auto frameBG = CCLayerColor::create({ 25, 17, 37, 255 });
frameBG->setContentSize(frame->getContentSize());
frameBG->setContentSize(m_frame->getContentSize());
frameBG->ignoreAnchorPointForPosition(false);
frame->addChildAtPosition(frameBG, Anchor::Center);
m_frame->addChildAtPosition(frameBG, Anchor::Center);
auto tabsTop = CCSprite::createWithSpriteFrameName("mods-list-top.png"_spr);
tabsTop->setAnchorPoint({ .5f, .0f });
frame->addChildAtPosition(tabsTop, Anchor::Top, ccp(0, -2));
m_frame->addChildAtPosition(tabsTop, Anchor::Top, ccp(0, -2));
auto tabsLeft = CCSprite::createWithSpriteFrameName("mods-list-side.png"_spr);
tabsLeft->setScaleY(frame->getContentHeight() / tabsLeft->getContentHeight());
frame->addChildAtPosition(tabsLeft, Anchor::Left, ccp(6, 0));
tabsLeft->setScaleY(m_frame->getContentHeight() / tabsLeft->getContentHeight());
m_frame->addChildAtPosition(tabsLeft, Anchor::Left, ccp(6, 0));
auto tabsRight = CCSprite::createWithSpriteFrameName("mods-list-side.png"_spr);
tabsRight->setFlipX(true);
tabsRight->setScaleY(frame->getContentHeight() / tabsRight->getContentHeight());
frame->addChildAtPosition(tabsRight, Anchor::Right, ccp(-6, 0));
tabsRight->setScaleY(m_frame->getContentHeight() / tabsRight->getContentHeight());
m_frame->addChildAtPosition(tabsRight, Anchor::Right, ccp(-6, 0));
auto tabsBottom = CCSprite::createWithSpriteFrameName("mods-list-bottom.png"_spr);
tabsBottom->setAnchorPoint({ .5f, 1.f });
frame->addChildAtPosition(tabsBottom, Anchor::Bottom, ccp(0, 2));
m_frame->addChildAtPosition(tabsBottom, Anchor::Bottom, ccp(0, 2));
this->addChildAtPosition(frame, Anchor::Center, ccp(0, -10), false);
m_list = ScrollLayer::create(frame->getContentSize() - ccp(24, 0));
m_list->m_contentLayer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::End)
->setAutoGrowAxis(frame->getContentHeight())
);
this->addChildAtPosition(m_list, Anchor::Center, -m_list->getScaledContentSize() / 2 - ccp(0, 10), false);
this->addChildAtPosition(m_frame, Anchor::Center, ccp(0, -10), false);
auto mainTabs = CCMenu::create();
mainTabs->setContentWidth(tabsTop->getContentWidth() - 45);
mainTabs->setAnchorPoint({ .5f, .0f });
mainTabs->setPosition(frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10)));
mainTabs->setPosition(m_frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10)));
for (auto item : std::initializer_list<std::tuple<const char*, const char*, const char*>> {
{ "download.png"_spr, "Installed", "installed" },
{ "GJ_bigStar_noShadow_001.png", "Featured", "featured" },
{ "GJ_sTrendingIcon_001.png", "Trending", "trending" },
{ "gj_folderBtn_001.png", "Mod Packs", "mod-packs" },
{ "search.png"_spr, "Search", "search" },
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", nullptr },
{ "GJ_sTrendingIcon_001.png", "Trending", nullptr },
{ "gj_folderBtn_001.png", "Mod Packs", ModPacksModsList::get() },
{ "search.png"_spr, "Search", nullptr },
}) {
const CCSize itemSize { 100, 35 };
const CCSize iconSize { 18, 18 };
@ -129,7 +315,7 @@ bool ModsLayer::init() {
spr->addChildAtPosition(title, Anchor::Left, ccp(28, 0), false);
auto btn = CCMenuItemSpriteExtra::create(spr, this, menu_selector(ModsLayer::onTab));
btn->setID(std::get<2>(item));
btn->setUserData(std::get<2>(item));
mainTabs->addChild(btn);
m_tabs.push_back(btn);
}
@ -137,7 +323,7 @@ bool ModsLayer::init() {
mainTabs->setLayout(RowLayout::create());
this->addChild(mainTabs);
this->gotoTab("installed");
this->gotoTab();
this->setKeypadEnabled(true);
cocos::handleTouchPriority(this, true);
@ -145,65 +331,41 @@ bool ModsLayer::init() {
return true;
}
void ModsLayer::loadList(std::string const& id) {
if (!m_currentListID.empty()) {
m_listPosCaches[m_currentListID].scrollPosition = m_list->m_contentLayer->getPositionY();
m_listPosCaches[m_currentListID].page = m_currentPage;
void ModsLayer::gotoTab(ModListSource* src) {
// Default to installed mods
if (!src) {
src = InstalledModsList::get();
}
if (!m_listPosCaches.contains(id)) {
m_listPosCaches.insert_or_assign(id, ListPosCache {
.page = 0,
.scrollPosition = std::numeric_limits<float>::max(),
});
}
m_currentListID = id;
if (auto srcOpt = ModListSource::get(id)) {
auto& src = srcOpt.value().get();
src.loadTotalPageCount()
.then([this](size_t count) {
// todo: show count in UI
this->loadPage(m_listPosCaches.at(id).page);
});
}
else {
}
}
void ModsLayer::loadPage(size_t page, bool update) {
m_list->m_contentLayer->removeAllChildren();
if (auto srcOpt = ModListSource::get(m_currentListID)) {
auto& src = srcOpt.value().get();
src.loadNewPage();
}
else {
}
for (auto item : cache.source.loadNewPage(m_currentPage, update)) {
m_list->m_contentLayer->addChild(item);
item->updateSize(m_list->getContentWidth(), BIG_VIEW);
}
m_list->m_contentLayer->updateLayout();
auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight();
if (cache.scrollPosition > 0.f || cache.scrollPosition < listTopScrollPos) {
cache.scrollPosition = listTopScrollPos;
}
m_list->m_contentLayer->setPositionY(cache.scrollPosition);
m_currentList = &cache;
}
void ModsLayer::gotoTab(std::string const& id) {
// Update selected tab
for (auto tab : m_tabs) {
auto selected = tab->getID() == id;
auto selected = tab->getUserData() == src;
tab->getNormalImage()->getChildByID("disabled-bg")->setVisible(!selected);
tab->getNormalImage()->getChildByID("enabled-bg")->setVisible(selected);
tab->setEnabled(!selected);
}
this->loadList(id);
// Remove current list from UI (it's Ref'd so it stays in memory)
if (m_currentSource) {
m_lists.at(m_currentSource)->removeFromParent();
}
// Update current source
m_currentSource = src;
// Lazily create new list and add it to UI
if (!m_lists.contains(src)) {
auto list = ModList::create(src, m_frame->getContentSize() - ccp(24, 0));
list->setPosition(m_frame->getPosition());
this->addChild(list);
m_lists.emplace(src, list);
}
// Add list to UI
else {
this->addChild(m_lists.at(src));
}
}
void ModsLayer::onTab(CCObject* sender) {
this->gotoTab(static_cast<CCNode*>(sender)->getID());
this->gotoTab(static_cast<ModListSource*>(static_cast<CCNode*>(sender)->getUserData()));
}
void ModsLayer::keyBackClicked() {
@ -211,9 +373,7 @@ void ModsLayer::keyBackClicked() {
}
void ModsLayer::onRefreshList(CCObject*) {
if (m_currentList) {
this->loadList(m_currentList->id, true);
}
m_lists.at(m_currentSource)->reloadPage();
}
void ModsLayer::onBack(CCObject*) {

View file

@ -2,32 +2,50 @@
#include <Geode/ui/General.hpp>
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/ui/TextArea.hpp>
#include "ModItem.hpp"
#include "ModListSource.hpp"
using namespace geode::prelude;
// Stores the current page and scroll position for a given list
struct ListPosCache {
size_t page;
float scrollPosition;
class ModList : public CCNode {
protected:
ModListSource* m_source;
size_t m_page = 0;
ScrollLayer* m_list;
SimpleTextArea* m_statusText;
CCSprite* m_statusLoadingCircle;
ModListSource::PageLoadEventListener m_listener;
CCMenuItemSpriteExtra* m_pagePrevBtn;
CCMenuItemSpriteExtra* m_pageNextBtn;
CCLabelBMFont* m_pageLabel;
bool init(ModListSource* src, CCSize const& size);
void onPromise(PromiseEvent<ModListSource::Page>* event);
void onPage(CCObject*);
void updatePageUI(bool hide = false);
public:
static ModList* create(ModListSource* src, CCSize const& size);
void reloadPage();
void gotoPage(size_t page, bool update = false);
void showStatus(std::string const& status, bool loading);
};
class ModsLayer : public CCLayer {
protected:
CCNode* m_frame;
std::vector<CCMenuItemSpriteExtra*> m_tabs;
ScrollLayer* m_list;
std::string m_currentListID;
size_t m_currentPage = 0;
std::unordered_map<std::string, ListPosCache> m_listPosCaches;
ModListSource* m_currentSource = nullptr;
std::unordered_map<ModListSource*, Ref<ModList>> m_lists;
bool init();
void keyBackClicked() override;
void onTab(CCObject* sender);
void gotoTab(std::string const& id);
void loadList(std::string const& id);
void loadPage(size_t page, bool update = false);
public:
static ModsLayer* create();
@ -35,4 +53,6 @@ public:
void onBack(CCObject*);
void onRefreshList(CCObject*);
void gotoTab(ModListSource* src = nullptr);
};