many changes for updating Index

- events now follow a public const member pattern instead of getters
 - more file utils
 - ModInfoLayer split into LocalModInfoPopup and IndexItemInfoPopup to reduce uses of ugly variant
 - same with ModCell
 - lots of restructuring related to UI anyway
 - Index also got tons of redesigning
 - much more i've forgotten
 - this commit compiles, surprisingly enough, but it's not usable as installing mods through index was removed
 - remove duplicate LevelSearchLayer ids
This commit is contained in:
HJfod 2022-12-06 21:22:03 +02:00
parent 08934132d8
commit 956ad1d6d5
25 changed files with 1854 additions and 1901 deletions

View file

@ -93,19 +93,19 @@ namespace geode {
}; };
class GEODE_DLL Event { class GEODE_DLL Event {
private:
static std::unordered_set<EventListenerProtocol*> s_listeners; static std::unordered_set<EventListenerProtocol*> s_listeners;
Mod* m_sender;
friend EventListenerProtocol; friend EventListenerProtocol;
public:
void postFrom(Mod* sender);
public:
Mod* sender;
void postFrom(Mod* sender);
template<class = void> template<class = void>
void post() { void post() {
postFrom(Mod::get()); postFrom(Mod::get());
} }
Mod* getSender();
virtual ~Event(); virtual ~Event();
}; };
} }

View file

@ -3,6 +3,7 @@
#include "Types.hpp" #include "Types.hpp"
#include "ModInfo.hpp" #include "ModInfo.hpp"
#include "Event.hpp" #include "Event.hpp"
#include "../utils/Result.hpp"
#include <unordered_set> #include <unordered_set>
namespace geode { namespace geode {
@ -11,18 +12,25 @@ namespace geode {
using UpdateError = std::string; using UpdateError = std::string;
using UpdateStatus = std::variant<UpdateFinished, UpdateProgress, UpdateError>; using UpdateStatus = std::variant<UpdateFinished, UpdateProgress, UpdateError>;
class GEODE_DLL IndexUpdateEvent : public Event { struct GEODE_DLL ModInstallEvent : public Event {
const std::string modID;
const UpdateStatus status;
};
class GEODE_DLL ModInstallFilter : public EventFilter<ModInstallEvent> {
protected: protected:
std::string m_sourceRepository; std::string m_id;
UpdateStatus m_status;
public: public:
IndexUpdateEvent( using Callback = void(ModInstallEvent*);
std::string const& src,
UpdateStatus status ListenerResult handle(std::function<Callback> fn, ModInstallEvent* event);
); ModInstallFilter(std::string const& id);
std::string getSource() const; };
UpdateStatus getStatus() const;
struct GEODE_DLL IndexUpdateEvent : public Event {
const UpdateStatus status;
IndexUpdateEvent(const UpdateStatus status);
}; };
class GEODE_DLL IndexUpdateFilter : public EventFilter<IndexUpdateEvent> { class GEODE_DLL IndexUpdateFilter : public EventFilter<IndexUpdateEvent> {
@ -33,31 +41,34 @@ namespace geode {
IndexUpdateFilter(); IndexUpdateFilter();
}; };
class GEODE_DLL ModInstallEvent : public Event { namespace impl {
protected: // The reason sources have private implementation events that are
std::string m_id; // turned into the global IndexUpdateEvent is because it makes it much
UpdateStatus m_status; // simpler to keep track of progress, what errors were received, etc.
// without having to store a ton of members
public: struct GEODE_DLL IndexSource final {
ModInstallEvent( std::string repository;
std::string const& id, bool isUpToDate = false;
UpdateStatus status
); std::string dirname() const;
std::string getModID() const;
UpdateStatus getStatus() const;
}; };
class GEODE_DLL ModInstallFilter : public EventFilter<IndexUpdateEvent> { struct GEODE_DLL SourceUpdateEvent : public Event {
protected: const IndexSource& source;
std::string m_id; const UpdateStatus status;
SourceUpdateEvent(IndexSource const& src, const UpdateStatus status);
public:
using Callback = void(ModInstallEvent*);
ListenerResult handle(std::function<Callback> fn, ModInstallEvent* event);
ModInstallFilter(std::string const& id);
}; };
class GEODE_DLL SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
public:
using Callback = void(SourceUpdateEvent*);
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event);
SourceUpdateFilter();
};
}
struct GEODE_DLL IndexItem { struct GEODE_DLL IndexItem {
std::string sourceRepository; std::string sourceRepository;
ghc::filesystem::path path; ghc::filesystem::path path;
@ -68,29 +79,34 @@ namespace geode {
std::unordered_set<PlatformID> platforms; std::unordered_set<PlatformID> platforms;
} download; } download;
bool isFeatured; bool isFeatured;
};
struct GEODE_DLL IndexSource final { /**
std::string repository; * Create IndexItem from a directory
bool isUpToDate = false; */
static Result<std::shared_ptr<IndexItem>> createFromDir(
std::string dirname() const; std::string const& sourceRepository,
ghc::filesystem::path const& dir
);
}; };
using IndexItemHandle = std::shared_ptr<IndexItem>;
class GEODE_DLL Index final { class GEODE_DLL Index final {
protected: protected:
// for once, the fact that std::map is ordered is useful (this makes // for once, the fact that std::map is ordered is useful (this makes
// getting the latest version of a mod as easy as items.rbegin()) // getting the latest version of a mod as easy as items.rbegin())
using ItemVersions = std::map<size_t, IndexItem>; using ItemVersions = std::map<size_t, IndexItemHandle>;
std::vector<IndexSource> m_sources; std::vector<impl::IndexSource> m_sources;
std::unordered_map<std::string, UpdateStatus> m_sourceStatuses;
std::atomic<bool> m_triedToUpdate = false;
std::unordered_map<std::string, ItemVersions> m_items; std::unordered_map<std::string, ItemVersions> m_items;
Index(); Index();
void checkSourceUpdates(IndexSource& src); void onSourceUpdate(impl::SourceUpdateEvent* event);
void downloadSource(IndexSource& src); void checkSourceUpdates(impl::IndexSource& src);
void updateSourceFromLocal(IndexSource& src); void downloadSource(impl::IndexSource& src);
void updateSourceFromLocal(impl::IndexSource& src);
void cleanupItems(); void cleanupItems();
public: public:
@ -98,12 +114,16 @@ namespace geode {
void addSource(std::string const& repository); void addSource(std::string const& repository);
void removeSource(std::string const& repository); void removeSource(std::string const& repository);
std::vector<IndexSource> getSources() const; std::vector<impl::IndexSource> getSources() const;
std::vector<IndexItem> getItems() const; std::vector<IndexItemHandle> getItems() const;
bool isKnownItem(std::string const& id, std::optional<size_t> version) const; bool isKnownItem(std::string const& id, std::optional<size_t> version) const;
std::optional<IndexItem> getItem(std::string const& id, std::optional<size_t> version) const; IndexItemHandle getItem(std::string const& id, std::optional<size_t> version) const;
IndexItemHandle getItem(ModInfo const& info) const;
IndexItemHandle getItem(Mod* mod) const;
bool updateAvailable(IndexItemHandle item) const;
bool hasTriedToUpdate() const;
bool isUpToDate() const; bool isUpToDate() const;
void update(bool force = false); void update(bool force = false);
}; };

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "../loader/Mod.hpp" #include "../loader/Mod.hpp"
#include "../loader/Index.hpp"
namespace geode { namespace geode {
/** /**
@ -11,6 +12,10 @@ namespace geode {
* Open the info popup for a mod * Open the info popup for a mod
*/ */
GEODE_DLL void openInfoPopup(Mod* mod); GEODE_DLL void openInfoPopup(Mod* mod);
/**
* Open the issue report popup for a mod
*/
GEODE_DLL void openIssueReportPopup(Mod* mod);
/** /**
* Open the store page for a mod (if it exists) * Open the store page for a mod (if it exists)
*/ */
@ -19,4 +24,25 @@ namespace geode {
* Open the settings popup for a mod (if it has any settings) * Open the settings popup for a mod (if it has any settings)
*/ */
GEODE_DLL void openSettingsPopup(Mod* mod); GEODE_DLL void openSettingsPopup(Mod* mod);
/**
* Create a default logo sprite
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createDefaultLogo(
cocos2d::CCSize const& size
);
/**
* Create a logo sprite for a mod
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createModLogo(
Mod* mod, cocos2d::CCSize const& size
);
/**
* Create a logo sprite for an index item
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createIndexItemLogo(
IndexItemHandle item, cocos2d::CCSize const& size
);
} }

View file

@ -3,6 +3,7 @@
#include "Result.hpp" #include "Result.hpp"
#include "general.hpp" #include "general.hpp"
#include "../external/json/json.hpp"
#include <Geode/DefaultInclude.hpp> #include <Geode/DefaultInclude.hpp>
#include <fs/filesystem.hpp> #include <fs/filesystem.hpp>
#include <string> #include <string>
@ -10,6 +11,7 @@
namespace geode::utils::file { namespace geode::utils::file {
GEODE_DLL Result<std::string> readString(ghc::filesystem::path const& path); GEODE_DLL Result<std::string> readString(ghc::filesystem::path const& path);
GEODE_DLL Result<nlohmann::json> readJson(ghc::filesystem::path const& path);
GEODE_DLL Result<byte_array> readBinary(ghc::filesystem::path const& path); GEODE_DLL Result<byte_array> readBinary(ghc::filesystem::path const& path);
GEODE_DLL Result<> writeString(ghc::filesystem::path const& path, std::string const& data); GEODE_DLL Result<> writeString(ghc::filesystem::path const& path, std::string const& data);

View file

@ -41,6 +41,16 @@ namespace geode {
using TypeIdentityType = typename TypeIdentity<T>::type; using TypeIdentityType = typename TypeIdentity<T>::type;
namespace utils { namespace utils {
// helper for std::visit
template<class... Ts> struct makeVisitor : Ts... { using Ts::operator()...; };
template<class... Ts> makeVisitor(Ts...) -> makeVisitor<Ts...>;
template<class T, class ... Args>
constexpr T getOr(std::variant<Args...> const& variant, T const& defValue) {
return std::holds_alternative<T>(variant) ?
std::get<T>(variant) : defValue;
}
constexpr unsigned int hash(char const* str, int h = 0) { constexpr unsigned int hash(char const* str, int h = 0) {
return !str[h] ? 5381 : (hash(str, h + 1) * 33) ^ str[h]; return !str[h] ? 5381 : (hash(str, h + 1) * 33) ^ str[h];
} }

View file

@ -1,104 +0,0 @@
#include "../ui/internal/info/ModInfoLayer.hpp"
#include "../ui/internal/list/ModListLayer.hpp"
#include <Geode/utils/cocos.hpp>
#include <Index.hpp>
#include <InternalLoader.hpp>
#include <InternalMod.hpp>
USE_GEODE_NAMESPACE();
#pragma warning(disable : 4217)
template <class T = CCNode>
requires std::is_base_of_v<CCNode, T>
T* setIDSafe(CCNode* node, int index, char const* id) {
if constexpr (std::is_same_v<CCNode, T>) {
if (auto child = getChild(node, index)) {
child->setID(id);
return child;
}
}
else {
if (auto child = getChildOfType<T>(node, index)) {
child->setID(id);
return child;
}
}
return nullptr;
}
// clang-format off
#include <Geode/modify/LevelSearchLayer.hpp>
struct LevelSearchLayerIDs : Modify<LevelSearchLayerIDs, LevelSearchLayer> {
bool init() {
if (!LevelSearchLayer::init())
return false;
// set the funny ids
this->setID("creator-layer");
setIDSafe(this, 0, "creator-layer-bg");
getChildOfType<CCTextInputNode>(this, 0)->setID("search-bar");
getChildOfType<CCScale9Sprite>(this, 0)->setID("level-search-bg");
getChildOfType<CCScale9Sprite>(this, 1)->setID("level-search-bar-bg");
getChildOfType<CCScale9Sprite>(this, 2)->setID("quick-search-bg");
getChildOfType<CCScale9Sprite>(this, 3)->setID("difficulty-filters-bg");
getChildOfType<CCScale9Sprite>(this, 4)->setID("length-filters-bg");
getChildOfType<CCLabelBMFont>(this, 0)->setID("quick-search-title");
getChildOfType<CCLabelBMFont>(this, 1)->setID("filters-title");
getChildOfType<CCSprite>(this, 1)->setID("left-corner");
getChildOfType<CCSprite>(this, 2)->setID("right-corner");
if (auto filtermenu = getChildOfType<CCMenu>(this, 0)) {
filtermenu->setID("other-filter-menu");
setIDSafe(filtermenu, 0, "clear-filters-button");
setIDSafe(filtermenu, 1, "advanced-filters-button");
}
if (auto searchmenu = getChildOfType<CCMenu>(this, 1)) {
searchmenu->setID("search-button-menu");
setIDSafe(searchmenu, 0, "search-level-button");
setIDSafe(searchmenu, 1, "search-user-button");
}
if (auto quickmenu = getChildOfType<CCMenu>(this, 2)) {
quickmenu->setID("quick-search-menu");
setIDSafe(quickmenu, 0, "most-downloaded-button");
setIDSafe(quickmenu, 1, "most-liked-button");
setIDSafe(quickmenu, 2, "trending-button");
setIDSafe(quickmenu, 3, "recent-button");
setIDSafe(quickmenu, 4, "magic-button");
setIDSafe(quickmenu, 5, "awarded-button");
setIDSafe(quickmenu, 6, "followed-button");
setIDSafe(quickmenu, 7, "friends-button");
}
if (auto filtersmenu = getChildOfType<CCMenu>(this, 3)) {
filtersmenu->setID("difficulty-filter-menu");
setIDSafe(filtersmenu, 0, "na-filter-button");
setIDSafe(filtersmenu, 1, "easy-filter-button");
setIDSafe(filtersmenu, 2, "normal-filter-button");
setIDSafe(filtersmenu, 3, "hard-filter-button");
setIDSafe(filtersmenu, 4, "harder-filter-button");
setIDSafe(filtersmenu, 5, "insane-filter-button");
setIDSafe(filtersmenu, 6, "demon-filter-button");
setIDSafe(filtersmenu, 7, "auto-filter-button");
setIDSafe(filtersmenu, 8, "demon-type-filter-button");
}
if (auto filtersmenu = getChildOfType<CCMenu>(this, 4)) {
filtersmenu->setID("length-filter-menu");
setIDSafe(filtersmenu, 0, "clock-icon");
setIDSafe(filtersmenu, 1, "tiny-filter-button");
setIDSafe(filtersmenu, 2, "short-filter-button");
setIDSafe(filtersmenu, 3, "medium-filter-button");
setIDSafe(filtersmenu, 4, "long-filter-button");
setIDSafe(filtersmenu, 5, "xl-filter-button");
setIDSafe(filtersmenu, 6, "star-filter-button");
}
if (auto backmenu = getChildOfType<CCMenu>(this, 5)) {
backmenu->setID("exit-menu");
setIDSafe(backmenu, 0, "exit-button");
}
return true;
}
};
// clang-format on

View file

@ -44,32 +44,25 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
void setUpdateText(std::string const& text) { void setUpdateText(std::string const& text) {
m_textArea->setString(text.c_str()); m_textArea->setString(text.c_str());
// m_fields->m_updatingResources->setString(text.c_str());
// m_fields->m_updatingResourcesBG->setContentSize({
// m_fields->m_updatingResources->getScaledContentSize().width + 30.f,
// 50.f
// });
// m_fields->m_updatingResources->setPosition(
// m_fields->m_updatingResourcesBG->getContentSize() / 2
// );
} }
void updateResourcesProgress(ResourceDownloadEvent* event) { void updateResourcesProgress(ResourceDownloadEvent* event) {
auto status = event->getStatus(); std::visit(makeVisitor {
if (std::holds_alternative<UpdateProgress>(status)) { [&](UpdateProgress const& progress) {
auto prog = std::get<UpdateProgress>(status); this->setUpdateText(fmt::format(
this->setUpdateText("Downloading Resources: " + std::to_string(prog.first) + "%"); "Downloading Resources: {}%", progress.first
} ));
else if (std::holds_alternative<UpdateFinished>(status)) { },
[&](UpdateFinished) {
this->setUpdateText("Resources Downloaded"); this->setUpdateText("Resources Downloaded");
m_fields->m_updatingResources = false; m_fields->m_updatingResources = false;
this->loadAssets(); this->loadAssets();
} },
else { [&](UpdateError const& error) {
InternalLoader::platformMessageBox( InternalLoader::platformMessageBox(
"Error updating resources", "Error updating resources",
"Unable to update Geode resources: " + "Unable to update Geode resources: " +
std::get<UpdateError>(status) + ".\n" error + ".\n"
"The game will be loaded as normal, but please be aware " "The game will be loaded as normal, but please be aware "
"that it may very likely crash." "that it may very likely crash."
); );
@ -77,6 +70,7 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
m_fields->m_updatingResources = false; m_fields->m_updatingResources = false;
this->loadAssets(); this->loadAssets();
} }
}, event->status);
} }
void loadAssets() { void loadAssets() {

View file

@ -1,16 +1,17 @@
#include <Geode/utils/cocos.hpp> #include <Geode/utils/cocos.hpp>
#include "../ui/internal/info/ModInfoLayer.hpp"
#include "../ui/internal/list/ModListLayer.hpp" #include "../ui/internal/list/ModListLayer.hpp"
#include <Geode/ui/BasedButtonSprite.hpp> #include <Geode/ui/BasedButtonSprite.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/ui/Notification.hpp> #include <Geode/ui/Notification.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/Popup.hpp>
#include <Geode/utils/cocos.hpp> #include <Geode/utils/cocos.hpp>
#include <Index.hpp> #include <Geode/loader/Index.hpp>
#include <InternalLoader.hpp> #include <InternalLoader.hpp>
#include "../ids/AddIDs.hpp" #include "../ids/AddIDs.hpp"
#include <InternalMod.hpp> #include <InternalMod.hpp>
#include <Geode/modify/Modify.hpp> #include <Geode/modify/Modify.hpp>
#include <Geode/modify/MenuLayer.hpp>
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
@ -21,39 +22,6 @@ class CustomMenuLayer;
static Ref<Notification> g_indexUpdateNotif = nullptr; static Ref<Notification> g_indexUpdateNotif = nullptr;
static Ref<CCSprite> g_geodeButton = nullptr; static Ref<CCSprite> g_geodeButton = nullptr;
static void addUpdateIcon(char const* icon = "updates-available.png"_spr) {
if (g_geodeButton && Index::get()->areUpdatesAvailable()) {
auto updateIcon = CCSprite::createWithSpriteFrameName(icon);
updateIcon->setPosition(g_geodeButton->getContentSize() - CCSize { 10.f, 10.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
g_geodeButton->addChild(updateIcon);
}
}
static void updateIndexProgress(UpdateStatus status, std::string const& info, uint8_t progress) {
if (status == UpdateStatus::Failed) {
g_indexUpdateNotif->setIcon(NotificationIcon::Error);
g_indexUpdateNotif->setString("Index update failed");
g_indexUpdateNotif->setTime(2.f);
g_indexUpdateNotif = nullptr;
addUpdateIcon("updates-failed.png"_spr);
}
if (status == UpdateStatus::Finished) {
g_indexUpdateNotif->setIcon(NotificationIcon::Success);
if (Index::get()->areUpdatesAvailable()) {
g_indexUpdateNotif->setString("Updates Available");
addUpdateIcon();
} else {
g_indexUpdateNotif->setString("Everything Up-to-Date");
}
g_indexUpdateNotif->setTime(2.f);
g_indexUpdateNotif = nullptr;
}
}
#include <Geode/modify/MenuLayer.hpp>
struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
void destructor() { void destructor() {
g_geodeButton = nullptr; g_geodeButton = nullptr;
@ -81,8 +49,6 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
)) ))
.orMake<ButtonSprite>("!!"); .orMake<ButtonSprite>("!!");
addUpdateIcon();
auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu")); auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
auto btn = CCMenuItemSpriteExtra::create( auto btn = CCMenuItemSpriteExtra::create(
@ -122,9 +88,7 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
"No", "Send", "No", "Send",
[](auto, bool btn2) { [](auto, bool btn2) {
if (btn2) { if (btn2) {
ModInfoLayer::showIssueReportPopup( geode::openIssueReportPopup(InternalMod::get());
InternalMod::get()->getModInfo()
);
} }
}, },
false false
@ -135,13 +99,13 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
} }
// update mods index // update mods index
if (!g_indexUpdateNotif && !Index::get()->isIndexUpdated()) { if (!g_indexUpdateNotif && !Index::get()->hasTriedToUpdate()) {
g_indexUpdateNotif = Notification::create( g_indexUpdateNotif = Notification::create(
"Updating Index", NotificationIcon::Loading, 0 "Updating Index", NotificationIcon::Loading, 0
); );
g_indexUpdateNotif->show(); g_indexUpdateNotif->show();
Index::get()->updateIndex(updateIndexProgress); Index::get()->update();
} }
return true; return true;

View file

@ -17,6 +17,10 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
ResourceDownloadEvent::ResourceDownloadEvent(
UpdateStatus const& status
) : status(status) {}
ListenerResult ResourceDownloadFilter::handle( ListenerResult ResourceDownloadFilter::handle(
std::function<Callback> fn, std::function<Callback> fn,
ResourceDownloadEvent* event ResourceDownloadEvent* event

View file

@ -17,13 +17,9 @@
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
class ResourceDownloadEvent : public Event { struct ResourceDownloadEvent : public Event {
protected: const UpdateStatus status;
UpdateStatus m_status; ResourceDownloadEvent(UpdateStatus const& status);
public:
ResourceDownloadEvent(UpdateStatus status);
UpdateStatus getStatus() const;
}; };
class GEODE_DLL ResourceDownloadFilter : public EventFilter<ResourceDownloadEvent> { class GEODE_DLL ResourceDownloadFilter : public EventFilter<ResourceDownloadEvent> {

View file

@ -19,7 +19,7 @@ EventListenerProtocol::~EventListenerProtocol() {
Event::~Event() {} Event::~Event() {}
void Event::postFrom(Mod* m) { void Event::postFrom(Mod* m) {
if (m) m_sender = m; if (m) this->sender = m;
for (auto h : Event::s_listeners) { for (auto h : Event::s_listeners) {
if (h->passThrough(this) == ListenerResult::Stop) { if (h->passThrough(this) == ListenerResult::Stop) {
@ -27,7 +27,3 @@ void Event::postFrom(Mod* m) {
} }
} }
} }
Mod* Event::getSender() {
return m_sender;
}

View file

@ -7,6 +7,7 @@
#include <Geode/utils/map.hpp> #include <Geode/utils/map.hpp>
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
using namespace geode::impl;
struct IndexSourceSaveData { struct IndexSourceSaveData {
std::string downloadedCommitSHA; std::string downloadedCommitSHA;
@ -22,20 +23,41 @@ std::string IndexSource::dirname() const {
return string::replace(this->repository, "/", "_"); return string::replace(this->repository, "/", "_");
} }
// SourceUpdateEvent
SourceUpdateEvent::SourceUpdateEvent(
IndexSource const& src,
const UpdateStatus status
) : source(src), status(status) {}
ListenerResult SourceUpdateFilter::handle(
std::function<Callback> fn,
SourceUpdateEvent* event
) {
fn(event);
return ListenerResult::Propagate;
}
SourceUpdateFilter::SourceUpdateFilter() {}
// ModInstallEvent
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
if (m_id == event->modID) {
fn(event);
}
return ListenerResult::Propagate;
}
ModInstallFilter::ModInstallFilter(
std::string const& id
) : m_id(id) {}
// IndexUpdateEvent // IndexUpdateEvent
IndexUpdateEvent::IndexUpdateEvent( IndexUpdateEvent::IndexUpdateEvent(
std::string const& src, const UpdateStatus status
UpdateStatus status ) : status(status) {}
) : m_sourceRepository(src), m_status(status) {}
std::string IndexUpdateEvent::getSource() const {
return m_sourceRepository;
}
UpdateStatus IndexUpdateEvent::getStatus() const {
return m_status;
}
ListenerResult IndexUpdateFilter::handle( ListenerResult IndexUpdateFilter::handle(
std::function<Callback> fn, std::function<Callback> fn,
@ -47,35 +69,53 @@ ListenerResult IndexUpdateFilter::handle(
IndexUpdateFilter::IndexUpdateFilter() {} IndexUpdateFilter::IndexUpdateFilter() {}
// ModInstallEvent // IndexItem
ModInstallEvent::ModInstallEvent( Result<IndexItemHandle> IndexItem::createFromDir(
std::string const& id, std::string const& sourceRepository,
UpdateStatus status ghc::filesystem::path const& dir
) : m_id(id), m_status(status) {} ) {
GEODE_UNWRAP_INTO(
auto entry, file::readJson(dir / "entry.json")
.expect("Unable to read entry.json")
);
GEODE_UNWRAP_INTO(
auto info, ModInfo::createFromFile(dir / "mod.json")
.expect("Unable to read mod.json: {error}")
);
std::string ModInstallEvent::getModID() const { JsonChecker checker(entry);
return m_id; auto root = checker.root("[entry.json]").obj();
std::unordered_set<PlatformID> platforms;
for (auto& plat : root.has("platforms").iterate()) {
platforms.insert(PlatformID::from(plat.template get<std::string>()));
} }
UpdateStatus ModInstallEvent::getStatus() const { auto item = std::make_shared<IndexItem>(IndexItem {
return m_status; .sourceRepository = sourceRepository,
.path = dir,
.info = info,
.download = {
.url = root.has("mod").obj().has("download").template get<std::string>(),
.hash = root.has("mod").obj().has("hash").template get<std::string>(),
.platforms = platforms,
},
.isFeatured = root.has("is-featured").template get<bool>(),
});
if (checker.isError()) {
return Err(checker.getError());
} }
return Ok(item);
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
if (m_id == event->getModID()) {
fn(event);
} }
return ListenerResult::Propagate;
}
ModInstallFilter::ModInstallFilter(
std::string const& id
) : m_id(id) {}
// Index // Index
Index::Index() { Index::Index() {
new EventListener(
std::bind(&Index::onSourceUpdate, this, std::placeholders::_1),
SourceUpdateFilter()
);
this->addSource("https://github.com/geode-sdk/index-test"); this->addSource("https://github.com/geode-sdk/index-test");
} }
@ -100,20 +140,74 @@ std::vector<IndexSource> Index::getSources() const {
return m_sources; return m_sources;
} }
bool Index::isUpToDate() const { void Index::onSourceUpdate(SourceUpdateEvent* event) {
for (auto& source : m_sources) { // save status for aggregating SourceUpdateEvents to a single global
if (!source.isUpToDate) { // IndexUpdateEvent
return false; m_sourceStatuses[event->source.repository] = event->status;
// figure out aggregate event
enum { Finished, Progress, Failed, } whatToPost = Finished;
for (auto& [src, status] : m_sourceStatuses) {
// if some source is still updating, post progress
if (std::holds_alternative<UpdateProgress>(status)) {
whatToPost = Progress;
break;
}
// otherwise, if some source failed, then post failed
else if (std::holds_alternative<UpdateError>(status)) {
if (whatToPost != Progress) {
whatToPost = Failed;
} }
} }
return true; // otherwise if all are finished, whatToPost is already set to that
}
switch (whatToPost) {
case Finished: {
// clear source statuses to allow updating index again
m_sourceStatuses.clear();
// post finish event
IndexUpdateEvent(UpdateFinished()).post();
} break;
case Progress: {
// get total progress
size_t total = 0;
for (auto& [src, status] : m_sourceStatuses) {
if (std::holds_alternative<UpdateProgress>(status)) {
total += std::get<UpdateProgress>(status).first;
} else {
total += 100;
}
}
IndexUpdateEvent(
UpdateProgress(
static_cast<uint8_t>(total / m_sourceStatuses.size()),
"Downloading"
)
).post();
} break;
case Failed: {
std::string info = "";
for (auto& [src, status] : m_sourceStatuses) {
if (std::holds_alternative<UpdateError>(status)) {
info += src + ": " + std::get<UpdateError>(status) + "\n";
}
}
// clear source statuses to allow updating index again
m_sourceStatuses.clear();
// post finish event
IndexUpdateEvent(UpdateError(info)).post();
} break;
}
} }
void Index::checkSourceUpdates(IndexSource& src) { void Index::checkSourceUpdates(IndexSource& src) {
if (src.isUpToDate) { if (src.isUpToDate) {
return this->updateSourceFromLocal(src); return this->updateSourceFromLocal(src);
} }
IndexUpdateEvent(src.repository, UpdateProgress(0, "Checking status")).post(); SourceUpdateEvent(src, UpdateProgress(0, "Checking status")).post();
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index"); auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
auto oldSHA = data.sources[src.repository].downloadedCommitSHA; auto oldSHA = data.sources[src.repository].downloadedCommitSHA;
web::AsyncWebRequest() web::AsyncWebRequest()
@ -136,15 +230,15 @@ void Index::checkSourceUpdates(IndexSource& src) {
} }
}) })
.expect([&src](std::string const& err) { .expect([&src](std::string const& err) {
IndexUpdateEvent( SourceUpdateEvent(
src.repository, src,
UpdateError(fmt::format("Error checking for updates: {}", err)) UpdateError(fmt::format("Error checking for updates: {}", err))
).post(); ).post();
}); });
} }
void Index::downloadSource(IndexSource& src) { void Index::downloadSource(IndexSource& src) {
IndexUpdateEvent(src.repository, UpdateProgress(0, "Beginning download")).post(); SourceUpdateEvent(src, UpdateProgress(0, "Beginning download")).post();
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname()); auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname());
@ -161,18 +255,16 @@ void Index::downloadSource(IndexSource& src) {
} }
} }
catch(...) { catch(...) {
return IndexUpdateEvent( return SourceUpdateEvent(
src.repository, src, UpdateError("Unable to clear cached index")
UpdateError("Unable to clear cached index")
).post(); ).post();
} }
// unzip new index // unzip new index
auto unzip = file::Unzip::intoDir(targetFile, targetDir, true); auto unzip = file::Unzip::intoDir(targetFile, targetDir, true);
if (!unzip) { if (!unzip) {
return IndexUpdateEvent( return SourceUpdateEvent(
src.repository, src, UpdateError("Unable to unzip new index")
UpdateError("Unable to unzip new index")
).post(); ).post();
} }
@ -180,25 +272,27 @@ void Index::downloadSource(IndexSource& src) {
this->updateSourceFromLocal(src); this->updateSourceFromLocal(src);
}) })
.expect([&src](std::string const& err) { .expect([&src](std::string const& err) {
IndexUpdateEvent( SourceUpdateEvent(
src.repository, src, UpdateError(fmt::format("Error downloading: {}", err))
UpdateError(fmt::format("Error downloading: {}", err))
).post(); ).post();
}) })
.progress([&src](auto&, double now, double total) { .progress([&src](auto&, double now, double total) {
IndexUpdateEvent( SourceUpdateEvent(
src.repository, src,
UpdateProgress(static_cast<uint8_t>(now / total * 100.0), "Downloading") UpdateProgress(
static_cast<uint8_t>(now / total * 100.0),
"Downloading"
)
).post(); ).post();
}); });
} }
void Index::updateSourceFromLocal(IndexSource& src) { void Index::updateSourceFromLocal(IndexSource& src) {
IndexUpdateEvent(src.repository, UpdateProgress(100, "Updating local cache")).post(); SourceUpdateEvent(src, UpdateProgress(100, "Updating local cache")).post();
// delete old items from this url if such exist // delete old items from this url if such exist
for (auto& [_, versions] : m_items) { for (auto& [_, versions] : m_items) {
for (auto it = versions.begin(); it != versions.end(); ) { for (auto it = versions.begin(); it != versions.end(); ) {
if (it->second.sourceRepository == src.repository) { if (it->second->sourceRepository == src.repository) {
it = versions.erase(it); it = versions.erase(it);
} else { } else {
++it; ++it;
@ -207,10 +301,32 @@ void Index::updateSourceFromLocal(IndexSource& src) {
} }
this->cleanupItems(); this->cleanupItems();
// todo: add shit // read directory and add new items
for (auto& dir : ghc::filesystem::directory_iterator(src.dirname())) {
auto addRes = IndexItem::createFromDir(src.repository, dir);
if (!addRes) {
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
continue;
}
auto add = addRes.unwrap();
// check if this major version of this item has already been added
if (m_items[add->info.m_id].count(add->info.m_version.getMajor())) {
log::warn(
"Item {}@{} has already been added, skipping",
add->info.m_id, add->info.m_version
);
continue;
}
// add new major version of this item
m_items[add->info.m_id].insert({
add->info.m_version.getMajor(),
add
});
}
// mark source as finished
src.isUpToDate = true; src.isUpToDate = true;
IndexUpdateEvent(src.repository, UpdateFinished()).post(); SourceUpdateEvent(src, UpdateFinished()).post();
} }
void Index::cleanupItems() { void Index::cleanupItems() {
@ -224,12 +340,33 @@ void Index::cleanupItems() {
} }
} }
bool Index::isUpToDate() const {
for (auto& source : m_sources) {
if (!source.isUpToDate) {
return false;
}
}
return true;
}
bool Index::hasTriedToUpdate() const {
return m_triedToUpdate;
}
void Index::update(bool force) { void Index::update(bool force) {
// create index dir if it doesn't exist // create index dir if it doesn't exist
(void)file::createDirectoryAll(dirs::getIndexDir()); (void)file::createDirectoryAll(dirs::getIndexDir());
m_triedToUpdate = true;
// update all sources in GD thread // update all sources in GD thread
Loader::get()->queueInGDThread([force, this]() { Loader::get()->queueInGDThread([force, this]() {
// check if some sources are already being updated
if (m_sourceStatuses.size()) {
return;
}
// update sources
for (auto& src : m_sources) { for (auto& src : m_sources) {
if (force) { if (force) {
this->downloadSource(src); this->downloadSource(src);
@ -240,8 +377,8 @@ void Index::update(bool force) {
}); });
} }
std::vector<IndexItem> Index::getItems() const { std::vector<IndexItemHandle> Index::getItems() const {
std::vector<IndexItem> res; std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_items)) { for (auto& items : map::values(m_items)) {
if (items.size()) { if (items.size()) {
res.push_back(items.rbegin()->second); res.push_back(items.rbegin()->second);
@ -265,7 +402,7 @@ bool Index::isKnownItem(
} }
} }
std::optional<IndexItem> Index::getItem( IndexItemHandle Index::getItem(
std::string const& id, std::string const& id,
std::optional<size_t> version std::optional<size_t> version
) const { ) const {
@ -281,5 +418,21 @@ std::optional<IndexItem> Index::getItem(
} }
} }
} }
return std::nullopt; return nullptr;
}
IndexItemHandle Index::getItem(ModInfo const& info) const {
return this->getItem(info.m_id, info.m_version.getMajor());
}
IndexItemHandle Index::getItem(Mod* mod) const {
return this->getItem(mod->getID(), mod->getVersion().getMajor());
}
bool Index::updateAvailable(IndexItemHandle item) const {
auto installed = Loader::get()->getInstalledMod(item->info.m_id);
if (!installed) {
return false;
}
return item->info.m_version > installed->getVersion();
} }

View file

@ -1,24 +1,58 @@
#include <Geode/loader/Index.hpp> #include <Geode/loader/Index.hpp>
#include "info/ModInfoLayer.hpp" #include <Geode/loader/Dirs.hpp>
#include "info/ModInfoPopup.hpp"
#include "list/ModListLayer.hpp" #include "list/ModListLayer.hpp"
#include "settings/ModSettingsPopup.hpp" #include "settings/ModSettingsPopup.hpp"
#include <Geode/ui/MDPopup.hpp>
#include <Geode/ui/GeodeUI.hpp> #include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/web.hpp>
void geode::openModsList() { void geode::openModsList() {
ModListLayer::scene(); ModListLayer::scene();
} }
void geode::openIssueReportPopup(Mod* mod) {
if (mod->getModInfo().m_issues) {
MDPopup::create(
"Issue Report",
mod->getModInfo().m_issues.value().m_info +
"\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
dirs::getCrashlogsDir().string() + "`",
"OK", (mod->getModInfo().m_issues.value().m_url ? "Open URL" : ""),
[mod](bool btn2) {
if (btn2) {
web::openLinkInBrowser(
mod->getModInfo().m_issues.value().m_url.value()
);
}
}
)->show();
}
else {
MDPopup::create(
"Issue Report",
"Please report your issue on the "
"[#support](https://discord.com/channels/911701438269386882/979352389985390603) "
"channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" + dirs::getCrashlogsDir().string() + "`",
"OK"
)->show();
}
}
void geode::openInfoPopup(Mod* mod) { void geode::openInfoPopup(Mod* mod) {
ModInfoLayer::create(mod, nullptr)->show(); LocalModInfoPopup::create(mod, nullptr)->show();
} }
void geode::openIndexPopup(Mod* mod) { void geode::openIndexPopup(Mod* mod) {
if (Index::get()->isKnownItem(mod->getID())) { if (auto item = Index::get()->getItem(
ModInfoLayer::create( mod->getID(), mod->getVersion().getMajor()
new ModObject(Index::get()->getKnownItem(mod->getID())) )) {
)->show(); IndexItemInfoPopup::create(item, nullptr)->show();
} }
} }
@ -27,3 +61,61 @@ void geode::openSettingsPopup(Mod* mod) {
ModSettingsPopup::create(mod)->show(); ModSettingsPopup::create(mod)->show();
} }
} }
CCNode* geode::createDefaultLogo(CCSize const& size) {
CCNode* spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) {
spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
}
limitNodeSize(spr, size, 1.f, .1f);
return spr;
}
CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
CCNode* spr = nullptr;
if (mod == Loader::getInternalMod()) {
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
spr = CCSprite::create(
fmt::format("{}/logo.png", mod->getID()).c_str()
);
}
if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
limitNodeSize(spr, size, 1.f, .1f);
return spr;
}
CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) {
CCNode* spr = nullptr;
auto logoPath = ghc::filesystem::absolute(item->path / "logo.png");
spr = CCSprite::create(logoPath.string().c_str());
if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}
if (!spr) {
spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
}
if (item->isFeatured) {
auto glowSize = size + CCSize(4.f, 4.f);
auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr);
logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width);
logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height);
// i dont know why + 1 is needed and its too late for me to figure out why
spr->setPosition(
logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2
);
// scary mathematics
spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX());
spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY());
logoGlow->addChild(spr);
spr = logoGlow;
}
else {
limitNodeSize(spr, size, 1.f, .1f);
}
return spr;
}

View file

@ -0,0 +1,53 @@
#include "ModInfoPopup.hpp"
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/binding/SliderThumb.hpp>
bool DownloadStatusNode::init() {
if (!CCNode::init()) return false;
this->setContentSize({ 150.f, 25.f });
auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setScale(.33f);
bg->setColor({ 0, 0, 0 });
bg->setOpacity(75);
bg->setContentSize(m_obContentSize * 3);
this->addChild(bg);
m_bar = Slider::create(this, nullptr, .6f);
m_bar->setValue(.0f);
m_bar->updateBar();
m_bar->setPosition(0.f, -5.f);
m_bar->m_touchLogic->m_thumb->setVisible(false);
this->addChild(m_bar);
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setAnchorPoint({ .0f, .5f });
m_label->setScale(.45f);
m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f);
this->addChild(m_label);
return true;
}
DownloadStatusNode* DownloadStatusNode::create() {
auto ret = new DownloadStatusNode();
if (ret && ret->init()) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void DownloadStatusNode::setProgress(uint8_t progress) {
m_bar->setValue(progress / 100.f);
m_bar->updateBar();
}
void DownloadStatusNode::setStatus(std::string const& text) {
m_label->setString(text.c_str());
m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f);
}

View file

@ -1,832 +0,0 @@
#include "ModInfoLayer.hpp"
#include "../dev/HookListLayer.hpp"
#include "../list/ModListView.hpp"
#include "../settings/ModSettingsPopup.hpp"
#include "../settings/AdvancedSettingsPopup.hpp"
#include <InternalLoader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCTextInputNode.hpp>
#include <Geode/binding/GJListLayer.hpp>
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderThumb.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/ranges.hpp>
#include <InternalLoader.hpp>
// TODO: die
#undef min
#undef max
static constexpr int const TAG_CONFIRM_UNINSTALL = 5;
static constexpr int const TAG_DELETE_SAVEDATA = 6;
bool DownloadStatusNode::init() {
if (!CCNode::init()) return false;
this->setContentSize({ 150.f, 25.f });
auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setScale(.33f);
bg->setColor({ 0, 0, 0 });
bg->setOpacity(75);
bg->setContentSize(m_obContentSize * 3);
this->addChild(bg);
m_bar = Slider::create(this, nullptr, .6f);
m_bar->setValue(.0f);
m_bar->updateBar();
m_bar->setPosition(0.f, -5.f);
m_bar->m_touchLogic->m_thumb->setVisible(false);
this->addChild(m_bar);
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setAnchorPoint({ .0f, .5f });
m_label->setScale(.45f);
m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f);
this->addChild(m_label);
return true;
}
DownloadStatusNode* DownloadStatusNode::create() {
auto ret = new DownloadStatusNode();
if (ret && ret->init()) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void DownloadStatusNode::setProgress(uint8_t progress) {
m_bar->setValue(progress / 100.f);
m_bar->updateBar();
}
void DownloadStatusNode::setStatus(std::string const& text) {
m_label->setString(text.c_str());
m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f);
}
void ModInfoLayer::onChangelog(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto winSize = CCDirector::get()->getWinSize();
m_detailsArea->setVisible(toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_detailsArea->setPositionX(
toggle->isToggled() ? winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2
: -5000.f
);
m_changelogArea->setVisible(!toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_changelogArea->setPositionX(
!toggle->isToggled() ? winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2
: -5000.f
);
}
bool ModInfoLayer::init(ModObject* obj, ModListView* list) {
m_noElasticity = true;
m_list = list;
m_mod = obj->m_mod;
bool isInstalledMod;
switch (obj->m_type) {
case ModObjectType::Mod:
{
m_info = obj->m_mod->getModInfo();
isInstalledMod = true;
}
break;
case ModObjectType::Index:
{
m_info = obj->m_index.m_info;
isInstalledMod = false;
}
break;
default: return false;
}
auto winSize = CCDirector::sharedDirector()->getWinSize();
CCSize size { 440.f, 290.f };
if (!this->initWithColor({ 0, 0, 0, 105 })) return false;
m_mainLayer = CCLayer::create();
this->addChild(m_mainLayer);
auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setContentSize(size);
bg->setPosition(winSize.width / 2, winSize.height / 2);
bg->setZOrder(-10);
m_mainLayer->addChild(bg);
m_buttonMenu = CCMenu::create();
m_mainLayer->addChild(m_buttonMenu);
constexpr float logoSize = 40.f;
constexpr float logoOffset = 10.f;
auto nameLabel = CCLabelBMFont::create(m_info.m_name.c_str(), "bigFont.fnt");
nameLabel->setAnchorPoint({ .0f, .5f });
nameLabel->limitLabelWidth(200.f, .7f, .1f);
m_mainLayer->addChild(nameLabel, 2);
auto logoSpr = this->createLogoSpr(obj, { logoSize, logoSize });
m_mainLayer->addChild(logoSpr);
auto developerStr = "by " + m_info.m_developer;
auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt");
developerLabel->setScale(.5f);
developerLabel->setAnchorPoint({ .0f, .5f });
m_mainLayer->addChild(developerLabel);
auto logoTitleWidth =
std::max(
nameLabel->getScaledContentSize().width, developerLabel->getScaledContentSize().width
) +
logoSize + logoOffset;
nameLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 125.f
);
logoSpr->setPosition({ winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2,
winSize.height / 2 + 115.f });
developerLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 105.f
);
auto versionLabel = CCLabelBMFont::create(m_info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.4f);
versionLabel->setPosition(
nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f,
winSize.height / 2 + 125.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
this->registerWithTouchDispatcher();
m_detailsArea = MDTextArea::create(
m_info.m_details ? m_info.m_details.value() : "### No description provided.",
{ 350.f, 137.5f }
);
m_detailsArea->setPosition(
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2,
winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f
);
m_mainLayer->addChild(m_detailsArea);
m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer());
m_scrollbar->setPosition(
winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f,
winSize.height / 2 - 20.f
);
m_mainLayer->addChild(m_scrollbar);
// changelog
if (m_info.m_changelog) {
m_changelogArea = MDTextArea::create(m_info.m_changelog.value(), { 350.f, 137.5f });
m_changelogArea->setPosition(
-5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f
);
m_changelogArea->setVisible(false);
m_mainLayer->addChild(m_changelogArea);
auto changelogBtnOffSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f,
"GJ_button_01.png", 1.f
);
changelogBtnOffSpr->setScale(.65f);
auto changelogBtnOnSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f,
"GJ_button_02.png", 1.f
);
changelogBtnOnSpr->setScale(.65f);
auto changelogBtn = CCMenuItemToggler::create(
changelogBtnOffSpr, changelogBtnOnSpr, this, menu_selector(ModInfoLayer::onChangelog)
);
changelogBtn->setPosition(-size.width / 2 + 21.5f, .0f);
m_buttonMenu->addChild(changelogBtn);
}
// mod info
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.85f);
auto infoBtn =
CCMenuItemSpriteExtra::create(infoSpr, this, menu_selector(ModInfoLayer::onInfo));
infoBtn->setPosition(size.width / 2 - 25.f, size.height / 2 - 25.f);
m_buttonMenu->addChild(infoBtn);
// issue report button
if (m_info.m_issues) {
auto issuesBtnSpr =
ButtonSprite::create("Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f);
issuesBtnSpr->setScale(.75f);
auto issuesBtn = CCMenuItemSpriteExtra::create(
issuesBtnSpr, this, menu_selector(ModInfoLayer::onIssues)
);
issuesBtn->setPosition(0.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(issuesBtn);
}
if (isInstalledMod) {
// mod settings
auto settingsSpr = CCSprite::createWithSpriteFrameName(
"GJ_optionsBtn_001.png"
);
settingsSpr->setScale(.65f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
settingsSpr, this, menu_selector(ModInfoLayer::onSettings)
);
settingsBtn->setPosition(-size.width / 2 + 25.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(settingsBtn);
// Check if a config directory for the mod exists
// Mod::getConfigDir auto-creates the directory for user convenience, so
// have to do it manually
if (ghc::filesystem::exists(m_mod->getConfigDir(false))) {
auto configSpr = CircleButtonSprite::createWithSpriteFrameName(
"pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2
);
configSpr->setScale(.65f);
auto configBtn = CCMenuItemSpriteExtra::create(
configSpr, this, menu_selector(ModInfoLayer::onOpenConfigDir)
);
configBtn->setPosition(-size.width / 2 + 65.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(configBtn);
}
if (!m_mod->hasSettings()) {
settingsSpr->setColor({ 150, 150, 150 });
settingsBtn->setTarget(this, menu_selector(ModInfoLayer::onNoSettings));
}
if (m_mod->getModInfo().m_repository) {
auto repoBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("github.png"_spr), this,
menu_selector(ModInfoLayer::onRepository)
);
repoBtn->setPosition(size.width / 2 - 25.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(repoBtn);
}
if (m_mod->getModInfo().m_supportInfo) {
auto supportBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("gift.png"_spr), this,
menu_selector(ModInfoLayer::onSupport)
);
supportBtn->setPosition(size.width / 2 - 60.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(supportBtn);
}
auto enableBtnSpr = ButtonSprite::create("Enable", "bigFont.fnt", "GJ_button_01.png", .6f);
enableBtnSpr->setScale(.6f);
auto disableBtnSpr =
ButtonSprite::create("Disable", "bigFont.fnt", "GJ_button_06.png", .6f);
disableBtnSpr->setScale(.6f);
auto enableBtn = CCMenuItemToggler::create(
disableBtnSpr, enableBtnSpr, this, menu_selector(ModInfoLayer::onEnableMod)
);
enableBtn->setPosition(-155.f, 75.f);
enableBtn->toggle(!obj->m_mod->isEnabled());
m_buttonMenu->addChild(enableBtn);
if (!m_info.m_supportsDisabling) {
enableBtn->setTarget(this, menu_selector(ModInfoLayer::onDisablingNotSupported));
enableBtnSpr->setColor({ 150, 150, 150 });
disableBtnSpr->setColor({ 150, 150, 150 });
}
if (
m_mod != Loader::get()->getInternalMod() &&
m_mod != Mod::get()
) {
// advanced settings
auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png");
advSettSpr->setScale(.65f);
auto advSettBtn = CCMenuItemSpriteExtra::create(
advSettSpr, this, menu_selector(ModInfoLayer::onAdvancedSettings)
);
advSettBtn->setPosition(
infoBtn->getPositionX() - 30.f,
infoBtn->getPositionY()
);
m_buttonMenu->addChild(advSettBtn);
auto uninstallBtnSpr = ButtonSprite::create(
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
);
uninstallBtnSpr->setScale(.6f);
auto uninstallBtn = CCMenuItemSpriteExtra::create(
uninstallBtnSpr, this, menu_selector(ModInfoLayer::onUninstall)
);
uninstallBtn->setPosition(-85.f, 75.f);
m_buttonMenu->addChild(uninstallBtn);
// todo: show update button on loader that invokes the installer
if (Index::get()->isUpdateAvailableForItem(m_info.m_id)) {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Update", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod)
);
m_installBtn->setPosition(-8.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(winSize.width / 2 + 105.f, winSize.height / 2 + 75.f);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
auto incomingVersion =
Index::get()->getKnownItem(m_info.m_id).m_info.m_version.toString();
m_updateVersionLabel =
CCLabelBMFont::create(("Available: " + incomingVersion).c_str(), "bigFont.fnt");
m_updateVersionLabel->setScale(.35f);
m_updateVersionLabel->setAnchorPoint({ .0f, .5f });
m_updateVersionLabel->setColor({ 94, 219, 255 });
m_updateVersionLabel->setPosition(
winSize.width / 2 + 35.f, winSize.height / 2 + 75.f
);
m_mainLayer->addChild(m_updateVersionLabel);
}
}
}
else {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Install", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod)
);
m_installBtn->setPosition(-143.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(winSize.width / 2 - 25.f, winSize.height / 2 + 75.f);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
}
// check if this mod is being installed/updated, and if so, update UI
if (auto handle = Index::get()->isInstallingItem(m_info.m_id)) {
m_installation = handle;
this->install();
}
auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
closeSpr->setScale(.8f);
auto closeBtn =
CCMenuItemSpriteExtra::create(closeSpr, this, menu_selector(ModInfoLayer::onClose));
closeBtn->setPosition(-size.width / 2 + 3.f, size.height / 2 - 3.f);
m_buttonMenu->addChild(closeBtn);
this->setKeypadEnabled(true);
this->setTouchEnabled(true);
return true;
}
void ModInfoLayer::onIssues(CCObject*) {
ModInfoLayer::showIssueReportPopup(m_info);
}
void ModInfoLayer::onSupport(CCObject*) {
MDPopup::create("Support " + m_mod->getName(), m_mod->getModInfo().m_supportInfo.value(), "OK")
->show();
}
void ModInfoLayer::onEnableMod(CCObject* pSender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"You may still see some effects of the mod left, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)
->show();
if (m_list) m_list->updateAllStates(nullptr);
return;
}
if (as<CCMenuItemToggler*>(pSender)->isToggled()) {
auto res = m_mod->loadBinary();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Loading Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
else {
auto res = m_mod->disable();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Disabling Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
if (m_list) m_list->updateAllStates(nullptr);
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void ModInfoLayer::onRepository(CCObject*) {
web::openLinkInBrowser(m_mod->getModInfo().m_repository.value());
}
void ModInfoLayer::onInstallMod(CCObject*) {
auto ticketRes = Index::get()->installItem(Index::get()->getKnownItem(m_info.m_id));
if (!ticketRes) {
return FLAlertLayer::create(
"Unable to install", ticketRes.unwrapErr(), "OK"
)->show();
}
m_installation = ticketRes.unwrap();
createQuickPopup(
"Install",
"The following <cb>mods</c> will be installed: " +
ranges::join(m_installation->toInstall(), ",") + ".",
"Cancel", "OK",
[this](FLAlertLayer*, bool btn2) {
if (btn2) {
this->install();
}
else {
this->updateInstallStatus("", 0);
}
}
);
}
void ModInfoLayer::onCancelInstall(CCObject*) {
m_installBtn->setEnabled(false);
m_installBtnSpr->setString("Cancelling");
m_installation->cancel();
m_installation = nullptr;
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(true);
}
}
void ModInfoLayer::onOpenConfigDir(CCObject*) {
file::openFolder(m_mod->getConfigDir());
}
void ModInfoLayer::onUninstall(CCObject*) {
auto layer = FLAlertLayer::create(
this, "Confirm Uninstall",
"Are you sure you want to uninstall <cr>" + m_info.m_name + "</c>?", "Cancel", "OK"
);
layer->setTag(TAG_CONFIRM_UNINSTALL);
layer->show();
}
void ModInfoLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
switch (layer->getTag()) {
case TAG_CONFIRM_UNINSTALL:
{
if (btn2) {
this->uninstall();
}
}
break;
case TAG_DELETE_SAVEDATA:
{
if (btn2) {
if (ghc::filesystem::remove_all(m_mod->getSaveDir())) {
FLAlertLayer::create("Deleted", "The mod's save data was deleted.", "OK")
->show();
}
else {
FLAlertLayer::create(
"Error", "Unable to delete mod's save directory!", "OK"
)
->show();
}
}
if (m_list) m_list->refreshList();
this->onClose(nullptr);
}
break;
}
}
void ModInfoLayer::updateInstallStatus(std::string const& status, uint8_t progress) {
if (status.size()) {
m_installStatus->setVisible(true);
m_installStatus->setStatus(status);
m_installStatus->setProgress(progress);
}
else {
m_installStatus->setVisible(false);
}
}
void ModInfoLayer::modInstallProgress(
InstallHandle, UpdateStatus status, std::string const& info, uint8_t percentage
) {
switch (status) {
case UpdateStatus::Failed:
{
FLAlertLayer::create("Installation failed :(", info, "OK")->show();
this->updateInstallStatus("", 0);
m_installBtn->setEnabled(true);
m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onInstallMod));
m_installBtnSpr->setString("Install");
m_installBtnSpr->setBG("GE_button_01.png"_spr, false);
m_installation = nullptr;
}
break;
case UpdateStatus::Finished:
{
this->updateInstallStatus("", 100);
FLAlertLayer::create(
"Install complete",
"Mod succesfully installed! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect)",
"OK"
)
->show();
m_installation = nullptr;
if (m_list) m_list->refreshList();
this->onClose(nullptr);
}
break;
default:
{
this->updateInstallStatus(info, percentage);
}
break;
}
}
void ModInfoLayer::install() {
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(false);
}
this->updateInstallStatus("Starting install", 0);
m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onCancelInstall));
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
m_callbackID = m_installation->start(std::bind(
&ModInfoLayer::modInstallProgress, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4
));
}
void ModInfoLayer::uninstall() {
auto res = m_mod->uninstall();
if (!res) {
return FLAlertLayer::create(
"Uninstall failed :(", res.unwrapErr(), "OK"
)->show();
}
auto layer = FLAlertLayer::create(
this, "Uninstall complete",
"Mod was succesfully uninstalled! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect). "
"<co>Would you also like to delete the mod's "
"save data?</c>",
"Cancel", "Delete", 350.f
);
layer->setTag(TAG_DELETE_SAVEDATA);
layer->show();
}
void ModInfoLayer::onDisablingNotSupported(CCObject* pSender) {
FLAlertLayer::create("Unsupported", "<cr>Disabling</c> is not supported for this mod.", "OK")
->show();
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void ModInfoLayer::onHooks(CCObject*) {
auto layer = HookListLayer::create(this->m_mod);
this->addChild(layer);
layer->showLayer(false);
}
void ModInfoLayer::onSettings(CCObject*) {
ModSettingsPopup::create(m_mod)->show();
}
void ModInfoLayer::onNoSettings(CCObject*) {
FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK")
->show();
}
void ModInfoLayer::onAdvancedSettings(CCObject*) {
AdvancedSettingsPopup::create(m_mod)->show();
}
void ModInfoLayer::onInfo(CCObject*) {
FLAlertLayer::create(
nullptr, ("About " + m_info.m_name).c_str(),
"<cr>ID: " + m_info.m_id +
"</c>\n"
"<cg>Version: " +
m_info.m_version.toString() +
"</c>\n"
"<cp>Developer: " +
m_info.m_developer +
"</c>\n"
"<cb>Path: " +
m_info.m_path.string() + "</c>\n",
"OK", nullptr, 400.f
)
->show();
}
void ModInfoLayer::keyDown(enumKeyCodes key) {
if (key == KEY_Escape) return this->onClose(nullptr);
if (key == KEY_Space) return;
return FLAlertLayer::keyDown(key);
}
void ModInfoLayer::onClose(CCObject* pSender) {
this->setKeyboardEnabled(false);
this->removeFromParentAndCleanup(true);
if (m_installation) {
m_installation->leave(m_callbackID);
}
};
ModInfoLayer* ModInfoLayer::create(Mod* mod, ModListView* list) {
auto ret = new ModInfoLayer;
if (ret && ret->init(new ModObject(mod), list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
ModInfoLayer* ModInfoLayer::create(ModObject* obj, ModListView* list) {
auto ret = new ModInfoLayer;
if (ret && ret->init(obj, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
CCNode* ModInfoLayer::createLogoSpr(ModObject* modObj, CCSize const& size) {
switch (modObj->m_type) {
case ModObjectType::Mod:
{
return ModInfoLayer::createLogoSpr(modObj->m_mod, size);
}
break;
case ModObjectType::Index:
{
return ModInfoLayer::createLogoSpr(modObj->m_index, size);
}
break;
default:
{
auto spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
if (!spr) {
return CCLabelBMFont::create("OwO", "goldFont.fnt");
}
return spr;
}
break;
}
}
CCNode* ModInfoLayer::createLogoSpr(Mod* mod, CCSize const& size) {
CCNode* spr = nullptr;
if (mod == Loader::getInternalMod()) {
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
spr = CCSprite::create(
fmt::format("{}/logo.png", mod->getID()).c_str()
);
}
if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
return spr;
}
CCNode* ModInfoLayer::createLogoSpr(IndexItem const& item, CCSize const& size) {
CCNode* spr = nullptr;
auto logoPath = ghc::filesystem::absolute(item.m_path / "logo.png");
spr = CCSprite::create(logoPath.string().c_str());
if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}
if (!spr) {
spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
}
if (Index::get()->isFeaturedItem(item.m_info.m_id)) {
auto glowSize = size + CCSize(4.f, 4.f);
auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr);
logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width);
logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height);
// i dont know why + 1 is needed and its too late for me to figure out why
spr->setPosition(
logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2
);
// scary mathematics
spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX());
spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY());
logoGlow->addChild(spr);
spr = logoGlow;
}
else {
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
}
return spr;
}
void ModInfoLayer::showIssueReportPopup(ModInfo const& info) {
if (info.m_issues) {
MDPopup::create(
"Issue Report",
info.m_issues.value().m_info +
"\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
dirs::getCrashlogsDir().string() + "`",
"OK", (info.m_issues.value().m_url ? "Open URL" : ""),
[info](bool btn2) {
if (btn2) {
web::openLinkInBrowser(info.m_issues.value().m_url.value());
}
}
)->show();
}
else {
MDPopup::create(
"Issue Report",
"Please report your issue on the "
"[#support](https://discord.com/channels/911701438269386882/979352389985390603) "
"channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" + dirs::getCrashlogsDir().string() + "`",
"OK"
)
->show();
}
}

View file

@ -0,0 +1,620 @@
#include "ModInfoPopup.hpp"
#include "../dev/HookListLayer.hpp"
#include "../list/ModListView.hpp"
#include "../settings/ModSettingsPopup.hpp"
#include "../settings/AdvancedSettingsPopup.hpp"
#include <InternalLoader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCTextInputNode.hpp>
#include <Geode/binding/GJListLayer.hpp>
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderThumb.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/web.hpp>
#include <InternalLoader.hpp>
// TODO: die
#undef min
#undef max
static constexpr int const TAG_CONFIRM_UNINSTALL = 5;
static constexpr int const TAG_DELETE_SAVEDATA = 6;
static const CCSize LAYER_SIZE = { 440.f, 290.f };
bool ModInfoPopup::init(ModInfo const& info, ModListView* list) {
m_noElasticity = true;
m_list = list;
auto winSize = CCDirector::sharedDirector()->getWinSize();
if (!this->initWithColor({ 0, 0, 0, 105 })) return false;
m_mainLayer = CCLayer::create();
this->addChild(m_mainLayer);
auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setContentSize(LAYER_SIZE);
bg->setPosition(winSize.width / 2, winSize.height / 2);
bg->setZOrder(-10);
m_mainLayer->addChild(bg);
m_buttonMenu = CCMenu::create();
m_mainLayer->addChild(m_buttonMenu);
constexpr float logoSize = 40.f;
constexpr float logoOffset = 10.f;
auto nameLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
nameLabel->setAnchorPoint({ .0f, .5f });
nameLabel->limitLabelWidth(200.f, .7f, .1f);
m_mainLayer->addChild(nameLabel, 2);
auto logoSpr = this->createLogo({ logoSize, logoSize });
m_mainLayer->addChild(logoSpr);
auto developerStr = "by " + info.m_developer;
auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt");
developerLabel->setScale(.5f);
developerLabel->setAnchorPoint({ .0f, .5f });
m_mainLayer->addChild(developerLabel);
auto logoTitleWidth =
std::max(
nameLabel->getScaledContentSize().width,
developerLabel->getScaledContentSize().width
) +
logoSize + logoOffset;
nameLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset,
winSize.height / 2 + 125.f
);
logoSpr->setPosition({
winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2,
winSize.height / 2 + 115.f
});
developerLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset,
winSize.height / 2 + 105.f
);
auto versionLabel = CCLabelBMFont::create(
info.m_version.toString().c_str(),
"bigFont.fnt"
);
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.4f);
versionLabel->setPosition(
nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f,
winSize.height / 2 + 125.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
this->registerWithTouchDispatcher();
m_detailsArea = MDTextArea::create(
(info.m_details ? info.m_details.value() : "### No description provided."),
{ 350.f, 137.5f }
);
m_detailsArea->setPosition(
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2,
winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f
);
m_mainLayer->addChild(m_detailsArea);
m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer());
m_scrollbar->setPosition(
winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f,
winSize.height / 2 - 20.f
);
m_mainLayer->addChild(m_scrollbar);
// changelog
if (info.m_changelog) {
m_changelogArea = MDTextArea::create(info.m_changelog.value(), { 350.f, 137.5f });
m_changelogArea->setPosition(
-5000.f, winSize.height / 2 -
m_changelogArea->getScaledContentSize().height / 2 - 20.f
);
m_changelogArea->setVisible(false);
m_mainLayer->addChild(m_changelogArea);
auto changelogBtnOffSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr),
0x20, true, 32.f, "GJ_button_01.png", 1.f
);
changelogBtnOffSpr->setScale(.65f);
auto changelogBtnOnSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr),
0x20, true, 32.f, "GJ_button_02.png", 1.f
);
changelogBtnOnSpr->setScale(.65f);
auto changelogBtn = CCMenuItemToggler::create(
changelogBtnOffSpr, changelogBtnOnSpr,
this, menu_selector(ModInfoPopup::onChangelog)
);
changelogBtn->setPosition(-LAYER_SIZE.width / 2 + 21.5f, .0f);
m_buttonMenu->addChild(changelogBtn);
}
// mod info
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.85f);
m_infoBtn = CCMenuItemSpriteExtra::create(
infoSpr, this, menu_selector(ModInfoPopup::onInfo)
);
m_infoBtn->setPosition(
LAYER_SIZE.width / 2 - 25.f,
LAYER_SIZE.height / 2 - 25.f
);
m_buttonMenu->addChild(m_infoBtn);
// repo button
if (info.m_repository) {
auto repoBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("github.png"_spr), this,
menu_selector(ModInfoPopup::onRepository)
);
repoBtn->setPosition(
LAYER_SIZE.width / 2 - 25.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(repoBtn);
}
// support button
if (info.m_supportInfo) {
auto supportBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("gift.png"_spr), this,
menu_selector(ModInfoPopup::onSupport)
);
supportBtn->setPosition(
LAYER_SIZE.width / 2 - 60.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(supportBtn);
}
auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
closeSpr->setScale(.8f);
auto closeBtn = CCMenuItemSpriteExtra::create(
closeSpr, this, menu_selector(ModInfoPopup::onClose)
);
closeBtn->setPosition(
-LAYER_SIZE.width / 2 + 3.f,
LAYER_SIZE.height / 2 - 3.f
);
m_buttonMenu->addChild(closeBtn);
this->setKeypadEnabled(true);
this->setTouchEnabled(true);
return true;
}
void ModInfoPopup::onSupport(CCObject*) {
MDPopup::create(
"Support " + this->getModInfo().m_name,
this->getModInfo().m_supportInfo.value(),
"OK"
)->show();
}
void ModInfoPopup::onRepository(CCObject*) {
web::openLinkInBrowser(this->getModInfo().m_repository.value());
}
void ModInfoPopup::onInfo(CCObject*) {
auto info = this->getModInfo();
FLAlertLayer::create(
nullptr,
("About " + info.m_name).c_str(),
fmt::format(
"<cr>ID: {}</c>\n"
"<cg>Version: {}</c>\n"
"<cp>Developer: {}</c>\n"
"<cb>Path: {}</c>\n",
info.m_id,
info.m_version.toString(),
info.m_developer,
info.m_path.string()
),
"OK", nullptr, 400.f
)->show();
}
void ModInfoPopup::onChangelog(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto winSize = CCDirector::get()->getWinSize();
m_detailsArea->setVisible(toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_detailsArea->setPositionX(
toggle->isToggled() ?
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2 :
-5000.f
);
m_changelogArea->setVisible(!toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_changelogArea->setPositionX(
!toggle->isToggled() ?
winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2 :
-5000.f
);
}
void ModInfoPopup::keyDown(enumKeyCodes key) {
if (key == KEY_Escape) return this->onClose(nullptr);
if (key == KEY_Space) return;
return FLAlertLayer::keyDown(key);
}
void ModInfoPopup::onClose(CCObject* pSender) {
this->setKeyboardEnabled(false);
this->removeFromParentAndCleanup(true);
};
// LocalModInfoPopup
bool LocalModInfoPopup::init(Mod* mod, ModListView* list) {
m_mod = mod;
if (!ModInfoPopup::init(mod->getModInfo(), list))
return false;
auto winSize = CCDirector::sharedDirector()->getWinSize();
// mod settings
auto settingsSpr = CCSprite::createWithSpriteFrameName(
"GJ_optionsBtn_001.png"
);
settingsSpr->setScale(.65f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
settingsSpr, this, menu_selector(LocalModInfoPopup::onSettings)
);
settingsBtn->setPosition(
-LAYER_SIZE.width / 2 + 25.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(settingsBtn);
// Check if a config directory for the mod exists
if (ghc::filesystem::exists(mod->getConfigDir(false))) {
auto configSpr = CircleButtonSprite::createWithSpriteFrameName(
"pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2
);
configSpr->setScale(.65f);
auto configBtn = CCMenuItemSpriteExtra::create(
configSpr, this, menu_selector(LocalModInfoPopup::onOpenConfigDir)
);
configBtn->setPosition(
-LAYER_SIZE.width / 2 + 65.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(configBtn);
}
if (!mod->hasSettings()) {
settingsSpr->setColor({ 150, 150, 150 });
settingsBtn->setTarget(this, menu_selector(LocalModInfoPopup::onNoSettings));
}
auto enableBtnSpr = ButtonSprite::create(
"Enable", "bigFont.fnt", "GJ_button_01.png", .6f
);
enableBtnSpr->setScale(.6f);
auto disableBtnSpr = ButtonSprite::create(
"Disable", "bigFont.fnt", "GJ_button_06.png", .6f
);
disableBtnSpr->setScale(.6f);
auto enableBtn = CCMenuItemToggler::create(
disableBtnSpr, enableBtnSpr,
this, menu_selector(LocalModInfoPopup::onEnableMod)
);
enableBtn->setPosition(-155.f, 75.f);
enableBtn->toggle(!mod->isEnabled());
m_buttonMenu->addChild(enableBtn);
if (!mod->supportsDisabling()) {
enableBtn->setTarget(this, menu_selector(LocalModInfoPopup::onDisablingNotSupported));
enableBtnSpr->setColor({ 150, 150, 150 });
disableBtnSpr->setColor({ 150, 150, 150 });
}
if (mod != Loader::get()->getInternalMod()) {
// advanced settings
auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png");
advSettSpr->setScale(.65f);
auto advSettBtn = CCMenuItemSpriteExtra::create(
advSettSpr, this, menu_selector(LocalModInfoPopup::onAdvancedSettings)
);
advSettBtn->setPosition(
m_infoBtn->getPositionX() - 30.f,
m_infoBtn->getPositionY()
);
m_buttonMenu->addChild(advSettBtn);
auto uninstallBtnSpr = ButtonSprite::create(
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
);
uninstallBtnSpr->setScale(.6f);
auto uninstallBtn = CCMenuItemSpriteExtra::create(
uninstallBtnSpr, this, menu_selector(LocalModInfoPopup::onUninstall)
);
uninstallBtn->setPosition(-85.f, 75.f);
m_buttonMenu->addChild(uninstallBtn);
auto indexItem = Index::get()->getItem(mod->getModInfo());
// todo: show update button on loader that invokes the installer
if (indexItem && Index::get()->updateAvailable(indexItem)) {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr,
CCSprite::createWithSpriteFrameName("install.png"_spr),
"Update", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, nullptr
);
m_installBtn->setPosition(-8.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(
winSize.width / 2 + 105.f,
winSize.height / 2 + 75.f
);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
m_updateVersionLabel = CCLabelBMFont::create(
("Available: " + indexItem->info.m_version.toString()).c_str(),
"bigFont.fnt"
);
m_updateVersionLabel->setScale(.35f);
m_updateVersionLabel->setAnchorPoint({ .0f, .5f });
m_updateVersionLabel->setColor({ 94, 219, 255 });
m_updateVersionLabel->setPosition(
winSize.width / 2 + 35.f, winSize.height / 2 + 75.f
);
m_mainLayer->addChild(m_updateVersionLabel);
}
}
// issue report button
if (mod->getModInfo().m_issues) {
auto issuesBtnSpr = ButtonSprite::create(
"Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f
);
issuesBtnSpr->setScale(.75f);
auto issuesBtn = CCMenuItemSpriteExtra::create(
issuesBtnSpr, this, menu_selector(LocalModInfoPopup::onIssues)
);
issuesBtn->setPosition(0.f, -LAYER_SIZE.height / 2 + 25.f);
m_buttonMenu->addChild(issuesBtn);
}
return true;
}
CCNode* LocalModInfoPopup::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
ModInfo LocalModInfoPopup::getModInfo() const {
return m_mod->getModInfo();
}
void LocalModInfoPopup::onIssues(CCObject*) {
geode::openIssueReportPopup(m_mod);
}
void LocalModInfoPopup::onUninstall(CCObject*) {
auto layer = FLAlertLayer::create(
this, "Confirm Uninstall",
fmt::format(
"Are you sure you want to uninstall <cr>{}</c>?",
m_mod->getName()
),
"Cancel", "OK"
);
layer->setTag(TAG_CONFIRM_UNINSTALL);
layer->show();
}
void LocalModInfoPopup::onEnableMod(CCObject* sender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"You may still see some effects of the mod left, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)
->show();
if (m_list) m_list->updateAllStates(nullptr);
return;
}
if (as<CCMenuItemToggler*>(sender)->isToggled()) {
auto res = m_mod->loadBinary();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Loading Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
else {
auto res = m_mod->disable();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Disabling Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
if (m_list) m_list->updateAllStates(nullptr);
as<CCMenuItemToggler*>(sender)->toggle(m_mod->isEnabled());
}
void LocalModInfoPopup::onOpenConfigDir(CCObject*) {
file::openFolder(m_mod->getConfigDir());
}
void LocalModInfoPopup::onDisablingNotSupported(CCObject* pSender) {
FLAlertLayer::create(
"Unsupported",
"<cr>Disabling</c> is not supported for this mod.",
"OK"
)->show();
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void LocalModInfoPopup::onSettings(CCObject*) {
ModSettingsPopup::create(m_mod)->show();
}
void LocalModInfoPopup::onNoSettings(CCObject*) {
FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK")
->show();
}
void LocalModInfoPopup::onAdvancedSettings(CCObject*) {
AdvancedSettingsPopup::create(m_mod)->show();
}
void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
switch (layer->getTag()) {
case TAG_CONFIRM_UNINSTALL: {
if (btn2) {
this->uninstall();
}
} break;
case TAG_DELETE_SAVEDATA: {
if (btn2) {
if (ghc::filesystem::remove_all(m_mod->getSaveDir())) {
FLAlertLayer::create(
"Deleted", "The mod's save data was deleted.", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Error", "Unable to delete mod's save directory!", "OK"
)->show();
}
}
if (m_list) m_list->refreshList();
this->onClose(nullptr);
} break;
}
}
void LocalModInfoPopup::uninstall() {
auto res = m_mod->uninstall();
if (!res) {
return FLAlertLayer::create(
"Uninstall failed :(", res.unwrapErr(), "OK"
)->show();
}
auto layer = FLAlertLayer::create(
this, "Uninstall complete",
"Mod was succesfully uninstalled! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect). "
"<co>Would you also like to delete the mod's "
"save data?</c>",
"Cancel", "Delete", 350.f
);
layer->setTag(TAG_DELETE_SAVEDATA);
layer->show();
}
LocalModInfoPopup* LocalModInfoPopup::create(Mod* mod, ModListView* list) {
auto ret = new LocalModInfoPopup;
if (ret && ret->init(mod, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// IndexItemInfoPopup
bool IndexItemInfoPopup::init(IndexItemHandle item, ModListView* list) {
m_item = item;
auto winSize = CCDirector::sharedDirector()->getWinSize();
if (!ModInfoPopup::init(item->info, list))
return false;
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Install", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, nullptr
);
m_installBtn->setPosition(-143.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(
winSize.width / 2 - 25.f,
winSize.height / 2 + 75.f
);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
return true;
}
CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
ModInfo IndexItemInfoPopup::getModInfo() const {
return m_item->info;
}
IndexItemInfoPopup* IndexItemInfoPopup::create(
IndexItemHandle item, ModListView* list
) {
auto ret = new IndexItemInfoPopup;
if (ret && ret->init(item, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -26,58 +26,66 @@ public:
void setStatus(std::string const& text); void setStatus(std::string const& text);
}; };
class ModInfoLayer : public FLAlertLayer, public FLAlertLayerProtocol { class ModInfoPopup : public FLAlertLayer {
protected: protected:
Mod* m_mod = nullptr;
ModInfo m_info;
bool m_isIndexMod = false;
ModListView* m_list = nullptr; ModListView* m_list = nullptr;
DownloadStatusNode* m_installStatus = nullptr; DownloadStatusNode* m_installStatus = nullptr;
IconButtonSprite* m_installBtnSpr; IconButtonSprite* m_installBtnSpr;
CCMenuItemSpriteExtra* m_installBtn; CCMenuItemSpriteExtra* m_installBtn;
CCMenuItemSpriteExtra* m_infoBtn;
CCLabelBMFont* m_updateVersionLabel = nullptr; CCLabelBMFont* m_updateVersionLabel = nullptr;
InstallHandle m_installation;
InstallItems::CallbackID m_callbackID;
MDTextArea* m_detailsArea; MDTextArea* m_detailsArea;
MDTextArea* m_changelogArea; MDTextArea* m_changelogArea;
Scrollbar* m_scrollbar; Scrollbar* m_scrollbar;
void onHooks(CCObject*);
void onSettings(CCObject*);
void onNoSettings(CCObject*);
void onInfo(CCObject*);
void onEnableMod(CCObject*);
void onInstallMod(CCObject*);
void onCancelInstall(CCObject*);
void onUninstall(CCObject*);
void onDisablingNotSupported(CCObject*);
void onChangelog(CCObject*); void onChangelog(CCObject*);
void onIssues(CCObject*);
void onRepository(CCObject*); void onRepository(CCObject*);
void onSupport(CCObject*); void onSupport(CCObject*);
void onOpenConfigDir(CCObject*); void onInfo(CCObject*);
void onAdvancedSettings(CCObject*);
void install();
void uninstall();
void updateInstallStatus(std::string const& status, uint8_t progress);
void modInstallProgress( bool init(ModInfo const& info, ModListView* list);
InstallHandle handle, UpdateStatus status, std::string const& info, uint8_t percentage
);
void FLAlert_Clicked(FLAlertLayer*, bool) override;
bool init(ModObject* obj, ModListView* list);
void keyDown(cocos2d::enumKeyCodes) override; void keyDown(cocos2d::enumKeyCodes) override;
void onClose(cocos2d::CCObject*); void onClose(cocos2d::CCObject*);
public: virtual CCNode* createLogo(CCSize const& size) = 0;
static ModInfoLayer* create(Mod* mod, ModListView* list); virtual ModInfo getModInfo() const = 0;
static ModInfoLayer* create(ModObject* obj, ModListView* list); };
static void showIssueReportPopup(ModInfo const& info); class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
protected:
static CCNode* createLogoSpr(ModObject* modObj, CCSize const& size); Mod* m_mod;
static CCNode* createLogoSpr(Mod* mod, CCSize const& size);
static CCNode* createLogoSpr(IndexItem const& item, CCSize const& size); bool init(Mod* mod, ModListView* list);
void onIssues(CCObject*);
void onSettings(CCObject*);
void onNoSettings(CCObject*);
void onDisablingNotSupported(CCObject*);
void onEnableMod(CCObject*);
void onUninstall(CCObject*);
void onOpenConfigDir(CCObject*);
void onAdvancedSettings(CCObject*);
void uninstall();
void FLAlert_Clicked(FLAlertLayer*, bool) override;
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
public:
static LocalModInfoPopup* create(Mod* mod, ModListView* list);
};
class IndexItemInfoPopup : public ModInfoPopup {
protected:
IndexItemHandle m_item;
bool init(IndexItemHandle item, ModListView* list);
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
public:
static IndexItemInfoPopup* create(IndexItemHandle item, ModListView* list);
}; };

View file

@ -0,0 +1,400 @@
#include "ModListCell.hpp"
#include "ModListView.hpp"
#include "../info/ModInfoPopup.hpp"
#include <Geode/binding/StatsCell.hpp>
#include <Geode/binding/FLAlertLayer.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <InternalLoader.hpp>
template <class T>
static bool tryOrAlert(Result<T> const& res, char const* title) {
if (!res) {
FLAlertLayer::create(title, res.unwrapErr(), "OK")->show();
}
return res.isOk();
}
ModListCell::ModListCell(char const* name, CCSize const& size)
: TableViewCell(name, size.width, size.height) {}
void ModListCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
void ModListCell::setupInfo(ModInfo const& info, bool spaceForCategories) {
m_mainLayer->setVisible(true);
m_backgroundLayer->setOpacity(255);
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
m_mainLayer->addChild(m_menu);
auto logoSize = m_height / 1.5f;
auto logoSpr = this->createLogo({ logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
m_mainLayer->addChild(logoSpr);
bool hasDesc =
m_display == ModListDisplay::Expanded &&
info.m_description.has_value();
auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && spaceForCategories) {
titleLabel->setPositionY(m_height / 2 + 20.f);
}
else if (hasDesc || spaceForCategories) {
titleLabel->setPositionY(m_height / 2 + 15.f);
}
else {
titleLabel->setPositionY(m_height / 2 + 7.f);
}
titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f);
m_mainLayer->addChild(titleLabel);
auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.3f);
versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f,
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
auto creatorStr = "by " + info.m_developer;
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setAnchorPoint({ .0f, .5f });
creatorLabel->setScale(.43f);
creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && spaceForCategories) {
creatorLabel->setPositionY(m_height / 2 + 7.5f);
}
else if (hasDesc || spaceForCategories) {
creatorLabel->setPositionY(m_height / 2);
}
else {
creatorLabel->setPositionY(m_height / 2 - 7.f);
}
m_mainLayer->addChild(creatorLabel);
if (hasDesc) {
auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
descBG->setColor({ 0, 0, 0 });
descBG->setOpacity(90);
descBG->setContentSize({ m_width * 2, 60.f });
descBG->setAnchorPoint({ .0f, .5f });
descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (spaceForCategories) {
descBG->setPositionY(m_height / 2 - 7.5f);
}
else {
descBG->setPositionY(m_height / 2 - 17.f);
}
descBG->setScale(.25f);
m_mainLayer->addChild(descBG);
auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt");
descText->setAnchorPoint({ .0f, .5f });
descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY());
descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f);
m_mainLayer->addChild(descText);
}
}
void ModListCell::updateBGColor(int index) {
if (index % 2) {
m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e));
}
else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c));
m_backgroundLayer->setOpacity(0xff);
}
bool ModListCell::init(ModListView* list, ModListDisplay display) {
m_list = list;
m_display = display;
return true;
}
// ModCell
ModCell::ModCell(const char* name, CCSize const& size)
: ModListCell(name, size) {}
ModCell* ModCell::create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
) {
auto ret = new ModCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void ModCell::onEnable(CCObject* sender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and "
"calls its user-defined disable function if one exists. You may "
"still see some effects of the mod left however, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)->show();
m_list->updateAllStates(this);
return;
}
if (!as<CCMenuItemToggler*>(sender)->isToggled()) {
tryOrAlert(m_mod->enable(), "Error enabling mod");
}
else {
tryOrAlert(m_mod->disable(), "Error disabling mod");
}
m_list->updateAllStates(this);
}
void ModCell::onUnresolvedInfo(CCObject*) {
std::string info =
"This mod has the following "
"<cr>unresolved dependencies</c>: ";
for (auto const& dep : m_mod->getUnresolvedDependencies()) {
info += fmt::format(
"<cg>{}</c> (<cy>{}</c>), ",
dep.m_id, dep.m_version.toString()
);
}
info.pop_back();
info.pop_back();
FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show();
}
void ModCell::onInfo(CCObject*) {
LocalModInfoPopup::create(m_mod, m_list)->show();
}
void ModCell::updateState() {
bool unresolved = m_mod->hasUnresolvedDependencies();
if (m_enableToggle) {
m_enableToggle->toggle(m_mod->isEnabled());
m_enableToggle->setEnabled(!unresolved);
m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255));
m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255));
}
m_unresolvedExMark->setVisible(unresolved);
}
void ModCell::loadFromMod(Mod* mod) {
this->setupInfo(mod->getModInfo(), false);
auto viewSpr = ButtonSprite::create(
"View", "bigFont.fnt", "GJ_button_01.png", .8f
);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this, menu_selector(ModCell::onInfo)
);
m_menu->addChild(viewBtn);
if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) {
m_enableToggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(ModCell::onEnable), .7f
);
m_enableToggle->setPosition(-45.f, 0.f);
m_menu->addChild(m_enableToggle);
}
auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png");
exMark->setScale(.5f);
m_unresolvedExMark = CCMenuItemSpriteExtra::create(
exMark, this, menu_selector(ModCell::onUnresolvedInfo)
);
m_unresolvedExMark->setPosition(-80.f, 0.f);
m_unresolvedExMark->setVisible(false);
m_menu->addChild(m_unresolvedExMark);
// if (m_mod->wasSuccesfullyLoaded()) {
// if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) {
// viewSpr->updateBGImage("GE_button_01.png"_spr);
// auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
// updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
// updateIcon->setZOrder(99);
// updateIcon->setScale(.5f);
// viewSpr->addChild(updateIcon);
// }
// }
this->updateState();
}
CCNode* ModCell::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
// IndexItemCell
IndexItemCell::IndexItemCell(char const* name, CCSize const& size)
: ModListCell(name, size) {}
void IndexItemCell::onInfo(CCObject*) {
IndexItemInfoPopup::create(m_item, m_list)->show();
}
IndexItemCell* IndexItemCell::create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
) {
auto ret = new IndexItemCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void IndexItemCell::loadFromItem(IndexItemHandle item) {
this->setupInfo(item->info, true);
auto viewSpr = ButtonSprite::create(
"View", "bigFont.fnt", "GJ_button_01.png", .8f
);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this, menu_selector(IndexItemCell::onInfo)
);
m_menu->addChild(viewBtn);
// if (hasCategories) {
// float x = m_height / 2 + logoSize / 2 + 13.f;
// for (auto& category : modobj->m_index.m_categories) {
// auto node = CategoryNode::create(category);
// node->setAnchorPoint({ .0f, .5f });
// node->setPositionX(x);
// node->setScale(.3f);
// if (hasDesc) {
// node->setPositionY(m_height / 2 - 23.f);
// }
// else {
// node->setPositionY(m_height / 2 - 17.f);
// }
// m_mainLayer->addChild(node);
// x += node->getScaledContentSize().width + 5.f;
// }
// }
this->updateState();
}
void IndexItemCell::updateState() {}
CCNode* IndexItemCell::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
// InvalidGeodeFileCell
InvalidGeodeFileCell::InvalidGeodeFileCell(const char* name, CCSize const& size)
: ModListCell(name, size) {}
void InvalidGeodeFileCell::onInfo(CCObject*) {
FLAlertLayer::create(
this, "Error Info",
m_info.m_reason,
"OK", "Remove file", 360.f
)->show();
}
void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
try {
if (ghc::filesystem::remove(m_info.m_path)) {
FLAlertLayer::create(
"File removed", "Removed <cy>" + m_info.m_path.string() + "</c>", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_info.m_path.string() + "</c>", "OK"
)->show();
}
}
catch (std::exception& e) {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_info.m_path.string() + "</c>: <cr>" +
std::string(e.what()) + "</c>",
"OK"
)->show();
}
(void)Loader::get()->refreshModsList();
m_list->refreshList();
}
}
InvalidGeodeFileCell* InvalidGeodeFileCell::create(
ModListView* list, ModListDisplay display,
char const* key, CCSize const& size
) {
auto ret = new InvalidGeodeFileCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void InvalidGeodeFileCell::loadFromInfo(InvalidGeodeFile const& info) {
m_info = info;
m_mainLayer->setVisible(true);
auto menu = CCMenu::create();
menu->setPosition(m_width - m_height, m_height / 2);
m_mainLayer->addChild(menu);
auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setScale(.5f);
titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f);
m_mainLayer->addChild(titleLabel);
auto pathLabel = CCLabelBMFont::create(
m_info.m_path.string().c_str(),
"chatFont.fnt"
);
pathLabel->setAnchorPoint({ .0f, .5f });
pathLabel->setScale(.43f);
pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f);
pathLabel->setColor({ 255, 255, 0 });
m_mainLayer->addChild(pathLabel);
auto whySpr = ButtonSprite::create(
"Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f
);
whySpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
whySpr, this, menu_selector(InvalidGeodeFileCell::onInfo)
);
menu->addChild(viewBtn);
}
void InvalidGeodeFileCell::updateState() {}
CCNode* InvalidGeodeFileCell::createLogo(CCSize const& size) {
return nullptr;
}

View file

@ -0,0 +1,91 @@
#pragma once
#include <Geode/binding/TableViewCell.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModInfo.hpp>
#include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE();
class ModListView;
enum class ModListDisplay;
class ModListCell : public TableViewCell {
protected:
ModListView* m_list;
CCMenu* m_menu;
CCMenuItemToggler* m_enableToggle = nullptr;
CCMenuItemSpriteExtra* m_unresolvedExMark;
ModListDisplay m_display;
ModListCell(char const* name, CCSize const& size);
bool init(ModListView* list, ModListDisplay display);
void setupInfo(ModInfo const& info, bool spaceForCategories);
void draw() override;
public:
void updateBGColor(int index);
virtual void updateState() = 0;
virtual CCNode* createLogo(CCSize const& size) = 0;
};
class ModCell : public ModListCell {
protected:
Mod* m_mod;
ModCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
void onEnable(CCObject*);
void onUnresolvedInfo(CCObject*);
public:
static ModCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromMod(Mod* mod);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};
class IndexItemCell : public ModListCell {
protected:
IndexItemHandle m_item;
IndexItemCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
public:
static IndexItemCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromItem(IndexItemHandle item);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};
class InvalidGeodeFileCell : public ModListCell, public FLAlertLayerProtocol {
protected:
InvalidGeodeFile m_info;
InvalidGeodeFileCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
void FLAlert_Clicked(FLAlertLayer*, bool btn2) override;
public:
static InvalidGeodeFileCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromInfo(InvalidGeodeFile const& file);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};

View file

@ -205,40 +205,6 @@ void ModListLayer::createSearchControl() {
this->addChild(m_searchInput); this->addChild(m_searchInput);
} }
void ModListLayer::indexUpdateProgress(
UpdateStatus status, std::string const& info, uint8_t percentage
) {
// if we have a check for updates button
// visible, disable it from being clicked
// again
if (m_checkForUpdatesBtn) {
m_checkForUpdatesBtn->setEnabled(false);
as<ButtonSprite*>(m_checkForUpdatesBtn->getNormalImage())->setString("Updating Index");
}
// if finished, refresh list
if (status == UpdateStatus::Finished) {
m_indexUpdateLabel->setVisible(false);
this->reloadList();
// make sure to release global instance
// and set it back to null
CC_SAFE_RELEASE_NULL(g_instance);
}
else {
m_indexUpdateLabel->setVisible(true);
m_indexUpdateLabel->setString(info.c_str());
}
if (status == UpdateStatus::Failed) {
FLAlertLayer::create("Error Updating Index", info, "OK")->show();
// make sure to release global instance
// and set it back to null
CC_SAFE_RELEASE(g_instance);
}
}
void ModListLayer::reloadList() { void ModListLayer::reloadList() {
auto winSize = CCDirector::sharedDirector()->getWinSize(); auto winSize = CCDirector::sharedDirector()->getWinSize();
@ -249,25 +215,21 @@ void ModListLayer::reloadList() {
} }
// create new list // create new list
m_query.m_searchFilter = auto list = ModListView::create(g_tab, m_display);
m_searchInput && m_searchInput->getString() && strlen(m_searchInput->getString())
? std::optional<std::string>(m_searchInput->getString())
: std::nullopt;
auto list = ModListView::create(g_tab, m_expandedList, 358.f, 190.f, m_query);
list->setLayer(this); list->setLayer(this);
// set list status // set list status
auto status = list->getStatusAsString(); // auto status = list->getStatusAsString();
if (status.size()) { // if (status.size()) {
m_listLabel->setVisible(true); // m_listLabel->setVisible(true);
m_listLabel->setString(status.c_str()); // m_listLabel->setString(status.c_str());
} // }
else { // else {
m_listLabel->setVisible(false); m_listLabel->setVisible(false);
} // }
// update index if needed // update index if needed
if (g_tab == ModListType::Download && !Index::get()->isIndexUpdated()) { if (g_tab == ModListType::Download && !Index::get()->isUpToDate()) {
m_listLabel->setString("Updating index..."); m_listLabel->setString("Updating index...");
if (!m_loadingCircle) { if (!m_loadingCircle) {
m_loadingCircle = LoadingCircle::create(); m_loadingCircle = LoadingCircle::create();
@ -311,9 +273,9 @@ void ModListLayer::reloadList() {
// check if the user has searched something, // check if the user has searched something,
// and show visual indicator if so // and show visual indicator if so
auto hasQuery = m_query.m_searchFilter.has_value(); // auto hasQuery = m_query.m_searchFilter.has_value();
m_searchBtn->setVisible(!hasQuery); // m_searchBtn->setVisible(!hasQuery);
m_searchClearBtn->setVisible(hasQuery); // m_searchClearBtn->setVisible(hasQuery);
// add/remove "Check for Updates" button // add/remove "Check for Updates" button
if ( if (
@ -321,7 +283,7 @@ void ModListLayer::reloadList() {
g_tab == ModListType::Installed && g_tab == ModListType::Installed &&
// check if index is updated, and if not // check if index is updated, and if not
// add button if it doesn't exist yet // add button if it doesn't exist yet
!Index::get()->isIndexUpdated() !Index::get()->isUpToDate()
) { ) {
if (!m_checkForUpdatesBtn) { if (!m_checkForUpdatesBtn) {
auto checkSpr = ButtonSprite::create("Check for Updates"); auto checkSpr = ButtonSprite::create("Check for Updates");
@ -351,11 +313,7 @@ void ModListLayer::onCheckForUpdates(CCObject*) {
g_instance->retain(); g_instance->retain();
// update index // update index
Index::get()->updateIndex( Index::get()->update();
[](UpdateStatus status, std::string const& info, uint8_t progress) -> void {
g_instance->indexUpdateProgress(status, info, progress);
}
);
} }
void ModListLayer::textChanged(CCTextInputNode* input) { void ModListLayer::textChanged(CCTextInputNode* input) {
@ -374,7 +332,9 @@ void ModListLayer::onReload(CCObject*) {
} }
void ModListLayer::onExpand(CCObject* sender) { void ModListLayer::onExpand(CCObject* sender) {
m_expandedList = !static_cast<CCMenuItemToggler*>(sender)->isToggled(); m_display = static_cast<CCMenuItemToggler*>(sender)->isToggled() ?
ModListDisplay::Concise :
ModListDisplay::Expanded;
this->reloadList(); this->reloadList();
} }
@ -443,5 +403,5 @@ ModListLayer* ModListLayer::scene() {
} }
ModListLayer::~ModListLayer() { ModListLayer::~ModListLayer() {
removeAllChildrenWithCleanup(true); this->removeAllChildrenWithCleanup(true);
} }

View file

@ -25,9 +25,8 @@ protected:
CCNode* m_searchBG = nullptr; CCNode* m_searchBG = nullptr;
CCTextInputNode* m_searchInput = nullptr; CCTextInputNode* m_searchInput = nullptr;
LoadingCircle* m_loadingCircle = nullptr; LoadingCircle* m_loadingCircle = nullptr;
ModListQuery m_query;
CCMenuItemSpriteExtra* m_filterBtn; CCMenuItemSpriteExtra* m_filterBtn;
bool m_expandedList = false; ModListDisplay m_display = ModListDisplay::Concise;
virtual ~ModListLayer(); virtual ~ModListLayer();
@ -43,7 +42,6 @@ protected:
void onFilters(CCObject*); void onFilters(CCObject*);
void keyDown(enumKeyCodes) override; void keyDown(enumKeyCodes) override;
void textChanged(CCTextInputNode*) override; void textChanged(CCTextInputNode*) override;
void indexUpdateProgress(UpdateStatus status, std::string const& info, uint8_t percentage);
void createSearchControl(); void createSearchControl();
friend class SearchFilterPopup; friend class SearchFilterPopup;

View file

@ -1,369 +1,30 @@
#include "ModListView.hpp" #include "ModListView.hpp"
#include "../info/CategoryNode.hpp" #include "../info/CategoryNode.hpp"
#include "../info/ModInfoLayer.hpp"
#include "ModListLayer.hpp" #include "ModListLayer.hpp"
#include "ModListCell.hpp"
#include <Geode/binding/ButtonSprite.hpp> #include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp> #include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/StatsCell.hpp>
#include <Geode/binding/TableView.hpp> #include <Geode/binding/TableView.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp> #include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/loader/Mod.hpp> #include <Geode/loader/Mod.hpp>
#include <Geode/utils/casts.hpp> #include <Geode/utils/casts.hpp>
#include <Geode/utils/cocos.hpp> #include <Geode/utils/cocos.hpp>
#include <Geode/utils/string.hpp> #include <Geode/utils/string.hpp>
#include <Index.hpp> #include <Geode/loader/Index.hpp>
#include <InternalLoader.hpp> #include <InternalLoader.hpp>
template <class T> void ModListView::updateAllStates(ModListCell* toggled) {
static bool tryOrAlert(Result<T> const& res, char const* title) { for (auto cell : CCArrayExt<ModListCell>(m_tableView->m_cellArray)) {
if (!res) { if (toggled != cell) {
FLAlertLayer::create(title, res.unwrapErr(), "OK")->show(); cell->updateState();
} }
return res.isOk();
}
ModCell::ModCell(char const* name, CCSize size) : TableViewCell(name, size.width, size.height) {}
void ModCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
void ModCell::onFailedInfo(CCObject*) {
FLAlertLayer::create(
this, "Error Info",
m_obj->m_info.m_reason.size() ?
m_obj->m_info.m_reason :
"Unable to load mod",
"OK", "Remove file", 360.f
)->show();
}
void ModCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
try {
if (ghc::filesystem::remove(m_obj->m_info.m_path)) {
FLAlertLayer::create(
"File removed", "Removed <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
}
catch (std::exception& e) {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>: <cr>" +
std::string(e.what()) + "</c>",
"OK"
)->show();
}
(void)Loader::get()->refreshModsList();
m_list->refreshList();
}
}
void ModCell::setupUnloaded() {
m_mainLayer->setVisible(true);
auto menu = CCMenu::create();
menu->setPosition(m_width - m_height, m_height / 2);
m_mainLayer->addChild(menu);
auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setScale(.5f);
titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f);
m_mainLayer->addChild(titleLabel);
auto pathLabel = CCLabelBMFont::create(
m_obj->m_info.m_path.string().c_str(),
"chatFont.fnt"
);
pathLabel->setAnchorPoint({ .0f, .5f });
pathLabel->setScale(.43f);
pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f);
pathLabel->setColor({ 255, 255, 0 });
m_mainLayer->addChild(pathLabel);
auto whySpr = ButtonSprite::create("Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f);
whySpr->setScale(.65f);
auto viewBtn =
CCMenuItemSpriteExtra::create(whySpr, this, menu_selector(ModCell::onFailedInfo));
menu->addChild(viewBtn);
}
void ModCell::setupLoadedButtons() {
auto viewSpr = m_obj->m_mod->wasSuccesfullyLoaded()
? ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f)
: ButtonSprite::create("Why", "bigFont.fnt", "GJ_button_06.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this,
m_obj->m_mod->wasSuccesfullyLoaded() ? menu_selector(ModCell::onInfo)
: menu_selector(ModCell::onFailedInfo)
);
m_menu->addChild(viewBtn);
if (m_obj->m_mod->wasSuccesfullyLoaded() && m_obj->m_mod->supportsDisabling()) {
m_enableToggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(ModCell::onEnable), .7f
);
m_enableToggle->setPosition(-45.f, 0.f);
m_menu->addChild(m_enableToggle);
}
auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png");
exMark->setScale(.5f);
m_unresolvedExMark =
CCMenuItemSpriteExtra::create(exMark, this, menu_selector(ModCell::onUnresolvedInfo));
m_unresolvedExMark->setPosition(-80.f, 0.f);
m_unresolvedExMark->setVisible(false);
m_menu->addChild(m_unresolvedExMark);
if (m_obj->m_mod->wasSuccesfullyLoaded()) {
if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) {
viewSpr->updateBGImage("GE_button_01.png"_spr);
auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
viewSpr->addChild(updateIcon);
}
}
}
void ModCell::setupIndexButtons() {
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo));
m_menu->addChild(viewBtn);
}
void ModCell::loadFromObject(ModObject* modobj) {
m_obj = modobj;
if (modobj->m_type == ModObjectType::Unloaded) {
return this->setupUnloaded();
}
m_mainLayer->setVisible(true);
m_backgroundLayer->setOpacity(255);
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
m_mainLayer->addChild(m_menu);
auto logoSize = m_height / 1.5f;
auto logoSpr = ModInfoLayer::createLogoSpr(modobj, { logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
m_mainLayer->addChild(logoSpr);
bool hasCategories = false;
ModInfo info;
switch (modobj->m_type) {
case ModObjectType::Mod: info = modobj->m_mod->getModInfo(); break;
case ModObjectType::Index:
info = modobj->m_index.m_info;
hasCategories = m_expanded && modobj->m_index.m_categories.size();
break;
default: return;
}
bool hasDesc = m_expanded && info.m_description.has_value();
auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && hasCategories) {
titleLabel->setPositionY(m_height / 2 + 20.f);
}
else if (hasDesc || hasCategories) {
titleLabel->setPositionY(m_height / 2 + 15.f);
}
else {
titleLabel->setPositionY(m_height / 2 + 7.f);
}
titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f);
m_mainLayer->addChild(titleLabel);
auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.3f);
versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f,
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
auto creatorStr = "by " + info.m_developer;
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setAnchorPoint({ .0f, .5f });
creatorLabel->setScale(.43f);
creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && hasCategories) {
creatorLabel->setPositionY(m_height / 2 + 7.5f);
}
else if (hasDesc || hasCategories) {
creatorLabel->setPositionY(m_height / 2);
}
else {
creatorLabel->setPositionY(m_height / 2 - 7.f);
}
m_mainLayer->addChild(creatorLabel);
if (hasDesc) {
auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
descBG->setColor({ 0, 0, 0 });
descBG->setOpacity(90);
descBG->setContentSize({ m_width * 2, 60.f });
descBG->setAnchorPoint({ .0f, .5f });
descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasCategories) {
descBG->setPositionY(m_height / 2 - 7.5f);
}
else {
descBG->setPositionY(m_height / 2 - 17.f);
}
descBG->setScale(.25f);
m_mainLayer->addChild(descBG);
auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt");
descText->setAnchorPoint({ .0f, .5f });
descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY());
descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f);
m_mainLayer->addChild(descText);
}
if (hasCategories) {
float x = m_height / 2 + logoSize / 2 + 13.f;
for (auto& category : modobj->m_index.m_categories) {
auto node = CategoryNode::create(category);
node->setAnchorPoint({ .0f, .5f });
node->setPositionX(x);
node->setScale(.3f);
if (hasDesc) {
node->setPositionY(m_height / 2 - 23.f);
}
else {
node->setPositionY(m_height / 2 - 17.f);
}
m_mainLayer->addChild(node);
x += node->getScaledContentSize().width + 5.f;
}
}
switch (modobj->m_type) {
case ModObjectType::Mod: this->setupLoadedButtons(); break;
case ModObjectType::Index: this->setupIndexButtons(); break;
default: break;
}
this->updateState();
}
void ModCell::onInfo(CCObject*) {
ModInfoLayer::create(m_obj, m_list)->show();
}
void ModCell::updateBGColor(int index) {
if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e));
else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c));
m_backgroundLayer->setOpacity(0xff);
}
void ModCell::onEnable(CCObject* pSender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and "
"calls its user-defined disable function if one exists. You may "
"still see some effects of the mod left however, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)->show();
m_list->updateAllStates(this);
return;
}
if (!as<CCMenuItemToggler*>(pSender)->isToggled()) {
tryOrAlert(m_obj->m_mod->enable(), "Error enabling mod");
}
else {
tryOrAlert(m_obj->m_mod->disable(), "Error disabling mod");
}
m_list->updateAllStates(this);
}
void ModCell::onUnresolvedInfo(CCObject* pSender) {
std::string info =
"This mod has the following "
"<cr>unresolved dependencies</c>: ";
for (auto const& dep : m_obj->m_mod->getUnresolvedDependencies()) {
info += "<cg>" + dep.m_id +
"</c> "
"(<cy>" +
dep.m_version.toString() + "</c>), ";
}
info.pop_back();
info.pop_back();
FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show();
}
bool ModCell::init(ModListView* list, bool expanded) {
m_list = list;
m_expanded = expanded;
return true;
}
void ModCell::updateState(bool invert) {
if (m_obj->m_type == ModObjectType::Mod) {
bool unresolved = m_obj->m_mod->hasUnresolvedDependencies();
if (m_enableToggle) {
m_enableToggle->toggle(m_obj->m_mod->isEnabled() ^ invert);
m_enableToggle->setEnabled(!unresolved);
m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255));
m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255));
}
m_unresolvedExMark->setVisible(unresolved);
}
}
ModCell* ModCell::create(ModListView* list, bool expanded, char const* key, CCSize size) {
auto pRet = new ModCell(key, size);
if (pRet && pRet->init(list, expanded)) {
return pRet;
}
CC_SAFE_DELETE(pRet);
return nullptr;
}
void ModListView::updateAllStates(ModCell* toggled) {
for (auto cell : CCArrayExt<ModCell>(m_tableView->m_cellArray)) {
cell->updateState(toggled == cell);
} }
} }
void ModListView::setupList() { void ModListView::setupList() {
m_itemSeparation = m_expandedList ? 60.f : 40.0f; m_itemSeparation = m_display == ModListDisplay::Expanded ? 60.f : 40.0f;
if (!m_entries->count()) return; if (!m_entries->count()) return;
@ -372,8 +33,10 @@ void ModListView::setupList() {
// fix content layer content size so the // fix content layer content size so the
// list is properly aligned to the top // list is properly aligned to the top
auto coverage = calculateChildCoverage(m_tableView->m_contentLayer); auto coverage = calculateChildCoverage(m_tableView->m_contentLayer);
m_tableView->m_contentLayer->setContentSize({ -coverage.origin.x + coverage.size.width, m_tableView->m_contentLayer->setContentSize({
-coverage.origin.y + coverage.size.height }); -coverage.origin.x + coverage.size.width,
-coverage.origin.y + coverage.size.height
});
if (m_entries->count() == 1) { if (m_entries->count() == 1) {
m_tableView->moveToTopWithOffset(m_itemSeparation * 2); m_tableView->moveToTopWithOffset(m_itemSeparation * 2);
@ -387,72 +50,21 @@ void ModListView::setupList() {
} }
TableViewCell* ModListView::getListCell(char const* key) { TableViewCell* ModListView::getListCell(char const* key) {
return ModCell::create(this, m_expandedList, key, { m_width, m_itemSeparation }); return ModCell::create(this, m_display, key, { m_width, m_itemSeparation });
} }
void ModListView::loadCell(TableViewCell* cell, unsigned int index) { void ModListView::loadCell(TableViewCell* cell, unsigned int index) {
auto obj = as<ModObject*>(m_entries->objectAtIndex(index)); auto obj = m_entries->objectAtIndex(index);
as<ModCell*>(cell)->loadFromObject(obj); if (auto mod = typeinfo_cast<ModObject*>(obj)) {
if (obj->m_type == ModObjectType::Mod) { as<ModCell*>(cell)->loadFromMod(mod->mod);
if (obj->m_mod->wasSuccesfullyLoaded()) {
as<ModCell*>(cell)->updateBGColor(index);
} }
else { if (auto mod = typeinfo_cast<IndexItemObject*>(obj)) {
cell->m_backgroundLayer->setOpacity(255); // as<IndexItemCell*>(cell)->loadFromItem(mod->item);
cell->m_backgroundLayer->setColor({ 153, 0, 0 });
} }
if (obj->m_mod->isUninstalled()) { if (auto failed = typeinfo_cast<InvalidGeodeFileObject*>(obj)) {
cell->m_backgroundLayer->setColor({ 50, 50, 50 }); as<InvalidGeodeFileCell*>(cell)->loadFromInfo(failed->info);
} }
} as<ModListCell*>(cell)->updateBGColor(index);
else {
as<ModCell*>(cell)->updateBGColor(index);
}
}
bool ModListView::filter(ModInfo const& info, ModListQuery const& query) {
// the UI for this functionality has been removed, however
// the code has been kept in case we want to add it back at
// some point.
if (!query.m_searchFilter) return true;
auto check = [query](SearchFlags flag, std::string const& name) -> bool {
if (!(query.m_searchFlags & flag)) return false;
return utils::string::contains(
utils::string::toLower(name), utils::string::toLower(query.m_searchFilter.value())
);
};
if (check(SearchFlag::Name, info.m_name)) return true;
if (check(SearchFlag::ID, info.m_id)) return true;
if (check(SearchFlag::Developer, info.m_developer)) return true;
if (check(SearchFlag::Description, info.m_description.value_or(""))) return true;
if (check(SearchFlag::Details, info.m_details.value_or(""))) return true;
return false;
}
bool ModListView::filter(IndexItem const& item, ModListQuery const& query) {
if (!query.m_showInstalled) {
if (Loader::get()->isModInstalled(item.m_info.m_id)) {
return false;
}
}
if (query.m_categories.size()) {
bool found = false;
for (auto& cat : query.m_categories) {
if (item.m_categories.count(cat)) {
found = true;
}
}
if (!found) {
return false;
}
}
for (auto& plat : query.m_platforms) {
if (item.m_download.m_platforms.count(plat)) {
return filter(item.m_info, query);
}
}
return false;
} }
static void sortInstalledMods(std::vector<Mod*>& mods) { static void sortInstalledMods(std::vector<Mod*>& mods) {
@ -462,7 +74,8 @@ static void sortInstalledMods(std::vector<Mod*>& mods) {
auto front = mods.front(); auto front = mods.front();
for (auto mod = mods.begin(); mod != mods.end(); mod++) { for (auto mod = mods.begin(); mod != mods.end(); mod++) {
// move mods with updates to front // move mods with updates to front
if (Index::get()->isUpdateAvailableForItem((*mod)->getID())) { if (auto item = Index::get()->getItem(*mod)) {
if (Index::get()->updateAvailable(item)) {
// swap first object and updatable mod // swap first object and updatable mod
// if the updatable mod is the first object, // if the updatable mod is the first object,
// nothing changes // nothing changes
@ -475,6 +88,7 @@ static void sortInstalledMods(std::vector<Mod*>& mods) {
} }
} }
} }
}
static std::vector<Mod*> sortedInstalledMods() { static std::vector<Mod*> sortedInstalledMods() {
auto mods = Loader::get()->getAllMods(); auto mods = Loader::get()->getAllMods();
@ -482,81 +96,50 @@ static std::vector<Mod*> sortedInstalledMods() {
return std::move(mods); return std::move(mods);
} }
bool ModListView::init( bool ModListView::init(CCArray* mods, ModListDisplay display) {
CCArray* mods, ModListType type, bool expanded, float width, float height, ModListQuery query m_display = display;
) { return CustomListView::init(mods, BoomListType::Default, 358.f, 190.f);
m_expandedList = expanded; }
if (!mods) {
CCArray* ModListView::getModsForType(ModListType type) {
auto mods = CCArray::create();
switch (type) { switch (type) {
case ModListType::Installed: default:
{ case ModListType::Installed: {
mods = CCArray::create();
// failed mods first // failed mods first
for (auto const& mod : Loader::get()->getFailedMods()) { for (auto const& mod : Loader::get()->getFailedMods()) {
mods->addObject(new ModObject(mod)); mods->addObject(new InvalidGeodeFileObject(mod));
} }
// internal geode representation always at the top // internal geode representation always at the top
auto imod = Loader::getInternalMod(); auto imod = Loader::getInternalMod();
if (this->filter(imod->getModInfo(), query)) {
mods->addObject(new ModObject(imod)); mods->addObject(new ModObject(imod));
}
// then other mods // then other mods
for (auto const& mod : sortedInstalledMods()) { for (auto const& mod : sortedInstalledMods()) {
// if the mod is no longer installed nor // if the mod is no longer installed nor
// loaded, it's as good as not existing // loaded, it's as good as not existing
// (because it doesn't) // (because it doesn't)
if (mod->isUninstalled() && !mod->isLoaded()) continue; if (mod->isUninstalled() && !mod->isLoaded()) continue;
if (this->filter(mod->getModInfo(), query)) {
mods->addObject(new ModObject(mod)); mods->addObject(new ModObject(mod));
} }
} } break;
if (!mods->count()) {
m_status = Status::SearchEmpty;
}
}
break;
case ModListType::Download: case ModListType::Download: {
{
mods = CCArray::create();
for (auto const& item : Index::get()->getItems()) { for (auto const& item : Index::get()->getItems()) {
if (this->filter(item, query)) { mods->addObject(new IndexItemObject(item));
mods->addObject(new ModObject(item));
} }
} } break;
if (!mods->count()) {
m_status = Status::NoModsFound;
}
}
break;
case ModListType::Featured: case ModListType::Featured: {
{ } break;
mods = CCArray::create();
for (auto const& item : Index::get()->getFeaturedItems()) {
if (this->filter(item, query)) {
mods->addObject(new ModObject(item));
} }
} return mods;
if (!mods->count()) {
m_status = Status::NoModsFound;
}
}
break;
default: return false;
}
}
return CustomListView::init(mods, BoomListType::Default, width, height);
} }
ModListView* ModListView::create( ModListView* ModListView::create(CCArray* mods, ModListDisplay display) {
CCArray* mods, ModListType type, bool expanded, float width, float height,
ModListQuery const& query
) {
auto pRet = new ModListView; auto pRet = new ModListView;
if (pRet) { if (pRet) {
if (pRet->init(mods, type, expanded, width, height, query)) { if (pRet->init(mods, display)) {
pRet->autorelease(); pRet->autorelease();
return pRet; return pRet;
} }
@ -565,24 +148,8 @@ ModListView* ModListView::create(
return nullptr; return nullptr;
} }
ModListView* ModListView::create( ModListView* ModListView::create(ModListType type, ModListDisplay display) {
ModListType type, bool expanded, float width, float height, ModListQuery const& query return ModListView::create(getModsForType(type), display);
) {
return ModListView::create(nullptr, type, expanded, width, height, query);
}
ModListView::Status ModListView::getStatus() const {
return m_status;
}
std::string ModListView::getStatusAsString() const {
switch (m_status) {
case Status::OK: return "";
case Status::Unknown: return "Unknown Issue";
case Status::NoModsFound: return "No Mods Found";
case Status::SearchEmpty: return "No Mods Match Search Query";
}
return "Unrecorded Status";
} }
void ModListView::setLayer(ModListLayer* layer) { void ModListView::setLayer(ModListLayer* layer) {

View file

@ -9,143 +9,59 @@
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
struct ModListQuery;
enum class ModListType { enum class ModListType {
Installed, Installed,
Download, Download,
Featured, Featured,
}; };
enum class ModObjectType { enum class ModListDisplay {
Mod, Concise,
Invalid, Expanded,
Index,
}; };
class ModListLayer; class ModListLayer;
class ModListCell;
// for passing invalid files as CCObject
struct InvalidGeodeFileObject : public CCObject {
InvalidGeodeFile info;
inline InvalidGeodeFileObject(InvalidGeodeFile const& info) : info(info) {
this->autorelease();
}
};
// Wrapper so you can pass Mods in a CCArray
struct ModObject : public CCObject { struct ModObject : public CCObject {
ModObjectType m_type; Mod* mod;
std::variant<Mod*, InvalidGeodeFile, IndexItem> m_data; inline ModObject(Mod* mod) : mod(mod) {
inline ModObject(Mod* mod)
: m_data(mod), m_type(ModObjectType::Mod)
{
this->autorelease();
}
inline ModObject(InvalidGeodeFile const& info)
: m_data(info), m_type(ModObjectType::Invalid)
{
this->autorelease();
}
inline ModObject(IndexItem const& index)
: m_data(index), m_type(ModObjectType::Index)
{
this->autorelease(); this->autorelease();
} }
}; };
class ModListView; struct IndexItemObject : public CCObject {
IndexItemHandle item;
class ModCell : public TableViewCell, public FLAlertLayerProtocol { inline IndexItemObject(IndexItemHandle item) : item(item) {
protected: this->autorelease();
ModListView* m_list; }
ModObject* m_obj;
CCMenu* m_menu;
CCMenuItemToggler* m_enableToggle = nullptr;
CCMenuItemSpriteExtra* m_unresolvedExMark;
bool m_expanded;
ModCell(char const* name, CCSize size);
void draw() override;
void onInfo(CCObject*);
void onFailedInfo(CCObject*);
void onEnable(CCObject*);
void onUnresolvedInfo(CCObject*);
void setupUnloaded();
void setupLoadedButtons();
void setupIndexButtons();
void FLAlert_Clicked(FLAlertLayer*, bool btn2) override;
bool init(ModListView* list, bool expanded);
public:
void updateBGColor(int index);
void loadFromObject(ModObject*);
void updateState(bool invert = false);
static ModCell* create(ModListView* list, bool expanded, char const* key, CCSize size);
};
struct SearchFlag {
enum : int {
Name = 0b1,
ID = 0b10,
Developer = 0b100,
Credits = 0b1000,
Description = 0b10000,
Details = 0b100000,
};
};
using SearchFlags = int;
static constexpr SearchFlags ALL_FLAGS = SearchFlag::Name | SearchFlag::ID | SearchFlag::Developer |
SearchFlag::Credits | SearchFlag::Description | SearchFlag::Details;
struct ModListQuery {
std::optional<std::string> m_searchFilter = std::nullopt;
int m_searchFlags = ALL_FLAGS;
bool m_showInstalled = false;
std::unordered_set<PlatformID> m_platforms { GEODE_PLATFORM_TARGET };
std::unordered_set<std::string> m_categories {};
}; };
class ModListView : public CustomListView { class ModListView : public CustomListView {
protected: protected:
enum class Status {
OK,
Unknown,
NoModsFound,
SearchEmpty,
};
Status m_status = Status::OK;
ModListLayer* m_layer = nullptr; ModListLayer* m_layer = nullptr;
bool m_expandedList; ModListDisplay m_display;
void setupList() override; void setupList() override;
TableViewCell* getListCell(char const* key) override; TableViewCell* getListCell(char const* key) override;
void loadCell(TableViewCell* cell, unsigned int index) override; void loadCell(TableViewCell* cell, unsigned int index) override;
bool init( bool init(CCArray* mods, ModListDisplay display);
CCArray* mods, ModListType type, bool expanded, float width, float height,
ModListQuery query
);
bool filter(ModInfo const& info, ModListQuery const& query);
bool filter(IndexItem const& item, ModListQuery const& query);
public: public:
static ModListView* create( static ModListView* create(CCArray* mods, ModListDisplay display);
CCArray* mods, ModListType type = ModListType::Installed, bool expanded = false, static ModListView* create(ModListType type, ModListDisplay display);
float width = 358.f, float height = 220.f, ModListQuery const& query = ModListQuery() static CCArray* getModsForType(ModListType type);
);
static ModListView* create(
ModListType type, bool expanded = false, float width = 358.f, float height = 220.f,
ModListQuery const& query = ModListQuery()
);
void updateAllStates(ModCell* toggled = nullptr); void updateAllStates(ModListCell* except = nullptr);
void setLayer(ModListLayer* layer); void setLayer(ModListLayer* layer);
void refreshList(); void refreshList();
Status getStatus() const;
std::string getStatusAsString() const;
}; };

View file

@ -5,6 +5,7 @@
#include "ModListView.hpp" #include "ModListView.hpp"
#include <Geode/binding/GameToolbox.hpp> #include <Geode/binding/GameToolbox.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/SelectList.hpp> #include <Geode/ui/SelectList.hpp>
bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
@ -53,10 +54,10 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f }; pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f };
this->addToggle( // this->addToggle(
"Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), // "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled),
layer->m_query.m_showInstalled, 0, pos // layer->m_query.m_showInstalled, 0, pos
); // );
// categories // categories
@ -75,23 +76,23 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f }; pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f };
for (auto& category : Index::get()->getCategories()) { // for (auto& category : Index::get()->getCategories()) {
auto toggle = CCMenuItemToggler::createWithStandardSprites( // auto toggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(SearchFilterPopup::onCategory), .5f // this, menu_selector(SearchFilterPopup::onCategory), .5f
); // );
toggle->toggle(m_modLayer->m_query.m_categories.count(category)); // toggle->toggle(m_modLayer->m_query.m_categories.count(category));
toggle->setPosition(pos - winSize / 2); // toggle->setPosition(pos - winSize / 2);
toggle->setUserObject(CCString::create(category)); // toggle->setUserObject(CCString::create(category));
m_buttonMenu->addChild(toggle); // m_buttonMenu->addChild(toggle);
auto label = CategoryNode::create(category, CategoryNodeStyle::Dot); // auto label = CategoryNode::create(category, CategoryNodeStyle::Dot);
label->setScale(.4f); // label->setScale(.4f);
label->setAnchorPoint({ .0f, .5f }); // label->setAnchorPoint({ .0f, .5f });
label->setPosition(pos.x + 10.f, pos.y); // label->setPosition(pos.x + 10.f, pos.y);
m_mainLayer->addChild(label); // m_mainLayer->addChild(label);
pos.y -= 22.5f; // pos.y -= 22.5f;
} // }
return true; return true;
} }
@ -99,13 +100,13 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
void SearchFilterPopup::onCategory(CCObject* sender) { void SearchFilterPopup::onCategory(CCObject* sender) {
try { try {
auto toggle = static_cast<CCMenuItemToggler*>(sender); auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto category = static_cast<CCString*>(toggle->getUserObject())->getCString(); // auto category = static_cast<CCString*>(toggle->getUserObject())->getCString();
if (!toggle->isToggled()) { // if (!toggle->isToggled()) {
m_modLayer->m_query.m_categories.insert(category); // m_modLayer->m_query.m_categories.insert(category);
} // }
else { // else {
m_modLayer->m_query.m_categories.erase(category); // m_modLayer->m_query.m_categories.erase(category);
} // }
} }
catch (...) { catch (...) {
} }
@ -113,7 +114,7 @@ void SearchFilterPopup::onCategory(CCObject* sender) {
void SearchFilterPopup::onShowInstalled(CCObject* sender) { void SearchFilterPopup::onShowInstalled(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender); auto toggle = static_cast<CCMenuItemToggler*>(sender);
m_modLayer->m_query.m_showInstalled = !toggle->isToggled(); // m_modLayer->m_query.m_showInstalled = !toggle->isToggled();
} }
void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) { void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) {
@ -137,37 +138,39 @@ CCMenuItemToggler* SearchFilterPopup::addToggle(
} }
CCMenuItemToggler* SearchFilterPopup::addSearchMatch(char const* title, int flag, CCPoint& pos) { CCMenuItemToggler* SearchFilterPopup::addSearchMatch(char const* title, int flag, CCPoint& pos) {
return this->addToggle( // return this->addToggle(
title, menu_selector(SearchFilterPopup::onSearchToggle), // title, menu_selector(SearchFilterPopup::onSearchToggle),
m_modLayer->m_query.m_searchFlags & flag, flag, pos // m_modLayer->m_query.m_searchFlags & flag, flag, pos
); // );
return nullptr;
} }
CCMenuItemToggler* SearchFilterPopup::addPlatformToggle( CCMenuItemToggler* SearchFilterPopup::addPlatformToggle(
char const* title, PlatformID id, CCPoint& pos char const* title, PlatformID id, CCPoint& pos
) { ) {
return this->addToggle( // return this->addToggle(
title, menu_selector(SearchFilterPopup::onPlatformToggle), // title, menu_selector(SearchFilterPopup::onPlatformToggle),
m_modLayer->m_query.m_platforms.count(id), id.to<int>(), pos // m_modLayer->m_query.m_platforms.count(id), id.to<int>(), pos
); // );
return nullptr;
} }
void SearchFilterPopup::onSearchToggle(CCObject* sender) { void SearchFilterPopup::onSearchToggle(CCObject* sender) {
if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) { // if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
m_modLayer->m_query.m_searchFlags &= ~sender->getTag(); // m_modLayer->m_query.m_searchFlags &= ~sender->getTag();
} // }
else { // else {
m_modLayer->m_query.m_searchFlags |= sender->getTag(); // m_modLayer->m_query.m_searchFlags |= sender->getTag();
} // }
} }
void SearchFilterPopup::onPlatformToggle(CCObject* sender) { void SearchFilterPopup::onPlatformToggle(CCObject* sender) {
if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) { // if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag())); // m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag()));
} // }
else { // else {
m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag())); // m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag()));
} // }
} }
void SearchFilterPopup::onClose(CCObject* sender) { void SearchFilterPopup::onClose(CCObject* sender) {

View file

@ -28,6 +28,22 @@ Result<std::string> utils::file::readString(ghc::filesystem::path const& path) {
return Err("Unable to open file"); return Err("Unable to open file");
} }
Result<nlohmann::json> utils::file::readJson(ghc::filesystem::path const& path) {
#if _WIN32
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);
#else
std::ifstream in(path.string(), std::ios::in | std::ios::binary);
#endif
if (in) {
try {
return Ok(nlohmann::json::parse(in));
} catch(std::exception const& e) {
return Err("Unable to parse JSON: " + std::string(e.what()));
}
}
return Err("Unable to open file");
}
Result<byte_array> utils::file::readBinary(ghc::filesystem::path const& path) { Result<byte_array> utils::file::readBinary(ghc::filesystem::path const& path) {
#if _WIN32 #if _WIN32
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary); std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);