mirror of
https://github.com/geode-sdk/geode.git
synced 2024-11-14 19:15:05 -05:00
recommended mods list
This commit is contained in:
parent
8a7a7a40cc
commit
beeb7ca1f8
36 changed files with 1203 additions and 425 deletions
|
@ -73,6 +73,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
|
|||
src/utils/*.cpp
|
||||
src/ui/*.cpp
|
||||
src/ui/nodes/*.cpp
|
||||
src/ui/other/*.cpp
|
||||
src/ui/mods/*.cpp
|
||||
src/ui/mods/list/*.cpp
|
||||
src/ui/mods/popups/*.cpp
|
||||
|
|
|
@ -91,7 +91,9 @@ namespace geode {
|
|||
bool isModLoaded(std::string const& id) const;
|
||||
Mod* getLoadedMod(std::string const& id) const;
|
||||
std::vector<Mod*> getAllMods();
|
||||
std::vector<LoadProblem> getAllProblems() const;
|
||||
std::vector<LoadProblem> getProblems() const;
|
||||
std::vector<LoadProblem> getRecommendations() const;
|
||||
|
||||
/**
|
||||
* Returns the available launch argument names.
|
||||
|
|
|
@ -435,6 +435,9 @@ namespace geode {
|
|||
void setLoggingEnabled(bool enabled);
|
||||
|
||||
bool hasProblems() const;
|
||||
std::vector<LoadProblem> getAllProblems() const;
|
||||
std::vector<LoadProblem> getProblems() const;
|
||||
std::vector<LoadProblem> getRecommendations() const;
|
||||
bool shouldLoad() const;
|
||||
bool isCurrentlyLoading() const;
|
||||
|
||||
|
|
|
@ -49,12 +49,44 @@ namespace geode {
|
|||
|
||||
class Handle final {
|
||||
private:
|
||||
// Handles may contain extra data, for example for holding ownership
|
||||
// of other Tasks for `Task::map` and `Task::all`. This struct
|
||||
// provides type erasure for that extra data
|
||||
struct ExtraData final {
|
||||
// Pointer to the owned extra data
|
||||
void* ptr;
|
||||
// Pointer to a function that deletes that extra data
|
||||
// The function MUST have a static lifetime
|
||||
void(*onDestroy)(void*);
|
||||
// Pointer to a function that handles cancelling any tasks within
|
||||
// that extra data when this task is cancelled. Note that the
|
||||
// task may not free up the memory associated with itself here
|
||||
// and this function may not be called if the user uses
|
||||
// `Task::shallowCancel`. However, this pointer *must* always be
|
||||
// valid
|
||||
// The function MUST have a static lifetime
|
||||
void(*onCancelled)(void*);
|
||||
|
||||
ExtraData(void* ptr, void(*onDestroy)(void*), void(*onCancelled)(void*))
|
||||
: ptr(ptr), onDestroy(onDestroy), onCancelled(onCancelled)
|
||||
{}
|
||||
ExtraData(ExtraData const&) = delete;
|
||||
ExtraData(ExtraData&&) = delete;
|
||||
|
||||
~ExtraData() {
|
||||
onDestroy(ptr);
|
||||
}
|
||||
void cancel() {
|
||||
onCancelled(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
std::recursive_mutex m_mutex;
|
||||
Status m_status = Status::Pending;
|
||||
std::optional<T> m_resultValue;
|
||||
bool m_finalEventPosted = false;
|
||||
std::unique_ptr<void, void(*)(void*)> m_mapListener = { nullptr, +[](void*) {} };
|
||||
std::string m_name;
|
||||
std::unique_ptr<ExtraData> m_extraData = nullptr;
|
||||
|
||||
class PrivateMarker final {};
|
||||
|
||||
|
@ -140,6 +172,8 @@ namespace geode {
|
|||
handle->m_status = Status::Finished;
|
||||
handle->m_resultValue.emplace(std::move(value));
|
||||
Loader::get()->queueInMainThread([handle, value = &*handle->m_resultValue]() mutable {
|
||||
// SAFETY: Task::all() depends on the lifetime of the value pointer
|
||||
// being as long as the lifetime of the task itself
|
||||
Event::createFinished(handle, value).post();
|
||||
std::unique_lock<std::recursive_mutex> lock(handle->m_mutex);
|
||||
handle->m_finalEventPosted = true;
|
||||
|
@ -155,11 +189,16 @@ namespace geode {
|
|||
});
|
||||
}
|
||||
}
|
||||
static void cancel(std::shared_ptr<Handle> handle) {
|
||||
static void cancel(std::shared_ptr<Handle> handle, bool shallow = false) {
|
||||
if (!handle) return;
|
||||
std::unique_lock<std::recursive_mutex> lock(handle->m_mutex);
|
||||
if (handle->m_status == Status::Pending) {
|
||||
handle->m_status = Status::Cancelled;
|
||||
// If this task carries extra data, call the extra data's handling method
|
||||
// (unless shallow cancelling was specifically requested)
|
||||
if (!shallow && handle->m_extraData) {
|
||||
handle->m_extraData->cancel();
|
||||
}
|
||||
Loader::get()->queueInMainThread([handle]() mutable {
|
||||
Event::createCancelled(handle).post();
|
||||
std::unique_lock<std::recursive_mutex> lock(handle->m_mutex);
|
||||
|
@ -213,6 +252,17 @@ namespace geode {
|
|||
void cancel() {
|
||||
Task::cancel(m_handle);
|
||||
}
|
||||
/**
|
||||
* If this is a Task that owns other Task(s) (for example created
|
||||
* through `Task::map` or `Task::all`), then this method cancels *only*
|
||||
* this Task and *not* any of the Task(s) it is built on top of.
|
||||
* Ownership of the other Task(s) will be released, so if this is the
|
||||
* only Task listening to them, they will still be destroyed due to a
|
||||
* lack of listeners
|
||||
*/
|
||||
void shallowCancel() {
|
||||
Task::cancel(m_handle, true);
|
||||
}
|
||||
bool isPending() const {
|
||||
return m_handle && m_handle->is(Status::Pending);
|
||||
}
|
||||
|
@ -275,18 +325,101 @@ namespace geode {
|
|||
// The task has been cancelled if the user has explicitly cancelled it,
|
||||
// or if there is no one listening anymore
|
||||
auto lock = handle.lock();
|
||||
return !(lock && lock->is(Status::Pending));
|
||||
return !lock || lock->is(Status::Cancelled);
|
||||
}
|
||||
);
|
||||
}).detach();
|
||||
return task;
|
||||
}
|
||||
/**
|
||||
* @warning The result vector may contain nulls if any of the tasks
|
||||
* were cancelled!
|
||||
*/
|
||||
template <std::move_constructible NP>
|
||||
static Task<std::vector<T*>, std::monostate> all(std::vector<Task<T, NP>>&& tasks, std::string const& name = "<Multiple Tasks>") {
|
||||
using AllTask = Task<std::vector<T*>, std::monostate>;
|
||||
|
||||
template <class ResultMapper, class ProgressMapper>
|
||||
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "<Mapping Task>") {
|
||||
// Create a new supervising task for all of the provided tasks
|
||||
auto task = AllTask(AllTask::Handle::create(name));
|
||||
|
||||
// Storage for storing the results received so far & keeping
|
||||
// ownership of the running tasks
|
||||
struct Waiting final {
|
||||
std::vector<T*> taskResults;
|
||||
std::vector<Task<std::monostate>> taskListeners;
|
||||
size_t taskCount;
|
||||
};
|
||||
task.m_handle->m_extraData = std::make_unique<AllTask::Handle::ExtraData>(
|
||||
// Create the data
|
||||
static_cast<void*>(new Waiting()),
|
||||
// When the task is destroyed
|
||||
+[](void* ptr) {
|
||||
delete static_cast<Waiting*>(ptr);
|
||||
},
|
||||
// If the task is cancelled
|
||||
+[](void* ptr) {
|
||||
// The move clears the `taskListeners` vector (important!)
|
||||
for (auto task : std::move(static_cast<Waiting*>(ptr)->taskListeners)) {
|
||||
task.cancel();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Store the task count in case some tasks finish immediately during the loop
|
||||
static_cast<Waiting*>(task.m_handle->m_extraData->ptr)->taskCount = tasks.size();
|
||||
|
||||
// Make sure to only give a weak pointer to avoid circular references!
|
||||
// (Tasks should NEVER own themselves!!)
|
||||
auto markAsDone = [handle = std::weak_ptr(task.m_handle)](T* result) {
|
||||
auto lock = handle.lock();
|
||||
|
||||
// If this task handle has expired, consider the task cancelled
|
||||
// (We don't have to do anything because the lack of a handle
|
||||
// means all the memory has been freed or is managed by
|
||||
// something else)
|
||||
if (!lock) return;
|
||||
|
||||
// Get the waiting handle from the task handle
|
||||
auto waiting = static_cast<Waiting*>(lock->m_extraData->ptr);
|
||||
|
||||
// SAFETY: The lifetime of result pointer is the same as the task that
|
||||
// produced that pointer, so as long as we have an owning reference to
|
||||
// the tasks through `taskListeners` we can be sure `result` is valid
|
||||
waiting->taskResults.push_back(result);
|
||||
|
||||
// If all tasks are done, finish
|
||||
log::debug("waiting for {}/{} tasks", waiting->taskResults.size(), waiting->taskCount);
|
||||
if (waiting->taskResults.size() >= waiting->taskCount) {
|
||||
// SAFETY: The task results' lifetimes are tied to the tasks
|
||||
// which could have their only owner be `waiting->taskListeners`,
|
||||
// but since Waiting is owned by the returned AllTask it should
|
||||
// be safe to access as long as it's accessible
|
||||
AllTask::finish(lock, std::move(waiting->taskResults));
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate the tasks & start listening to them using
|
||||
for (auto& taskToWait : tasks) {
|
||||
static_cast<Waiting*>(task.m_handle->m_extraData->ptr)->taskListeners.emplace_back(taskToWait.map(
|
||||
[markAsDone](auto* result) {
|
||||
markAsDone(result);
|
||||
return std::monostate();
|
||||
},
|
||||
[](auto*) { return std::monostate(); },
|
||||
[markAsDone]() { markAsDone(nullptr); }
|
||||
));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
template <class ResultMapper, class ProgressMapper, class OnCancelled>
|
||||
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, OnCancelled&& onCancelled, std::string const& name = "<Mapping Task>") const {
|
||||
using T2 = decltype(resultMapper(std::declval<T*>()));
|
||||
using P2 = decltype(progressMapper(std::declval<P*>()));
|
||||
|
||||
static_assert(std::is_move_constructible_v<T2>, "The type being mapped to must be move-constructible!");
|
||||
static_assert(std::is_move_constructible_v<P2>, "The type being mapped to must be move-constructible!");
|
||||
|
||||
auto task = Task<T2, P2>(Task<T2, P2>::Handle::create(fmt::format("{} <= {}", name, m_handle->m_name)));
|
||||
|
||||
// Lock the current task until we have managed to create our new one
|
||||
|
@ -294,6 +427,7 @@ namespace geode {
|
|||
|
||||
// If the current task is cancelled, cancel the new one immediately
|
||||
if (m_handle->m_status == Status::Cancelled) {
|
||||
onCancelled();
|
||||
Task<T2, P2>::cancel(task.m_handle);
|
||||
}
|
||||
// If the current task is finished, immediately map the value and post that
|
||||
|
@ -302,13 +436,14 @@ namespace geode {
|
|||
}
|
||||
// Otherwise start listening and waiting for the current task to finish
|
||||
else {
|
||||
task.m_handle->m_mapListener = std::unique_ptr<void, void(*)(void*)>(
|
||||
task.m_handle->m_extraData = std::make_unique<Task<T2, P2>::Handle::ExtraData>(
|
||||
static_cast<void*>(new EventListener<Task>(
|
||||
[
|
||||
handle = std::weak_ptr(task.m_handle),
|
||||
resultMapper = std::move(resultMapper),
|
||||
progressMapper = std::move(progressMapper)
|
||||
](Event* event) {
|
||||
progressMapper = std::move(progressMapper),
|
||||
onCancelled = std::move(onCancelled)
|
||||
](Event* event) mutable {
|
||||
if (auto v = event->getValue()) {
|
||||
Task<T2, P2>::finish(handle.lock(), std::move(resultMapper(v)));
|
||||
}
|
||||
|
@ -316,6 +451,7 @@ namespace geode {
|
|||
Task<T2, P2>::progress(handle.lock(), std::move(progressMapper(p)));
|
||||
}
|
||||
else if (event->isCancelled()) {
|
||||
onCancelled();
|
||||
Task<T2, P2>::cancel(handle.lock());
|
||||
}
|
||||
},
|
||||
|
@ -323,15 +459,24 @@ namespace geode {
|
|||
)),
|
||||
+[](void* ptr) {
|
||||
delete static_cast<EventListener<Task>*>(ptr);
|
||||
},
|
||||
+[](void* ptr) {
|
||||
// Cancel the mapped task too
|
||||
static_cast<EventListener<Task>*>(ptr)->getFilter().cancel();
|
||||
}
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
template <class ResultMapper, class ProgressMapper>
|
||||
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "<Mapping Task>") const {
|
||||
return this->map(std::move(resultMapper), std::move(progressMapper), +[]() {}, name);
|
||||
}
|
||||
|
||||
template <class ResultMapper>
|
||||
requires std::copy_constructible<P>
|
||||
auto map(ResultMapper&& resultMapper, std::string const& name = "<Mapping Task>") {
|
||||
auto map(ResultMapper&& resultMapper, std::string const& name = "<Mapping Task>") const {
|
||||
return this->map(std::move(resultMapper), +[](P* p) -> P { return *p; }, name);
|
||||
}
|
||||
|
||||
|
|
|
@ -186,8 +186,11 @@ namespace geode {
|
|||
std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
|
||||
}
|
||||
|
||||
[[deprecated("Use toNonVString or toVString instead")]]
|
||||
std::string toString(bool includeTag = true) const;
|
||||
|
||||
std::string toVString(bool includeTag = true) const;
|
||||
std::string toNonVString(bool includeTag = true) const;
|
||||
|
||||
friend GEODE_DLL std::string format_as(VersionInfo const& version);
|
||||
};
|
||||
|
||||
|
|
BIN
loader/resources/exclamation.png
Normal file
BIN
loader/resources/exclamation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
loader/resources/images/GE_square03.png
Normal file
BIN
loader/resources/images/GE_square03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -14,6 +14,7 @@
|
|||
#include <loader/updater.hpp>
|
||||
#include <Geode/binding/ButtonSprite.hpp>
|
||||
#include <Geode/modify/LevelSelectLayer.hpp>
|
||||
#include <ui/other/FixIssuesPopup.hpp>
|
||||
|
||||
using namespace geode::prelude;
|
||||
|
||||
|
@ -82,17 +83,8 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
|
|||
}
|
||||
|
||||
// show if some mods failed to load
|
||||
static bool shownFailedNotif = false;
|
||||
if (!shownFailedNotif) {
|
||||
shownFailedNotif = true;
|
||||
auto problems = Loader::get()->getProblems();
|
||||
if (std::any_of(problems.begin(), problems.end(), [&](auto& item) {
|
||||
return item.type != LoadProblem::Type::Suggestion && item.type != LoadProblem::Type::Recommendation;
|
||||
})) {
|
||||
Notification::create("There were problems loading some mods", NotificationIcon::Error)->show();
|
||||
}
|
||||
}
|
||||
|
||||
checkLoadingIssues(this);
|
||||
|
||||
// show if the user tried to be naughty and load arbitrary DLLs
|
||||
static bool shownTriedToLoadDlls = false;
|
||||
if (!shownTriedToLoadDlls) {
|
||||
|
|
|
@ -65,9 +65,40 @@ std::vector<Mod*> Loader::getAllMods() {
|
|||
return m_impl->getAllMods();
|
||||
}
|
||||
|
||||
std::vector<LoadProblem> Loader::getProblems() const {
|
||||
std::vector<LoadProblem> Loader::getAllProblems() const {
|
||||
return m_impl->getProblems();
|
||||
}
|
||||
std::vector<LoadProblem> Loader::getProblems() const {
|
||||
std::vector<LoadProblem> result;
|
||||
for (auto problem : this->getAllProblems()) {
|
||||
if (
|
||||
problem.type != LoadProblem::Type::Recommendation &&
|
||||
problem.type != LoadProblem::Type::Suggestion
|
||||
) {
|
||||
result.push_back(problem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return ranges::filter(
|
||||
m_impl->getProblems(),
|
||||
[](auto const& problem) {
|
||||
return problem.type != LoadProblem::Type::Recommendation &&
|
||||
problem.type != LoadProblem::Type::Suggestion;
|
||||
}
|
||||
);
|
||||
}
|
||||
std::vector<LoadProblem> Loader::getRecommendations() const {
|
||||
std::vector<LoadProblem> result;
|
||||
for (auto problem : this->getAllProblems()) {
|
||||
if (
|
||||
problem.type == LoadProblem::Type::Recommendation ||
|
||||
problem.type == LoadProblem::Type::Suggestion
|
||||
) {
|
||||
result.push_back(problem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Loader::queueInMainThread(ScheduledFunction func) {
|
||||
return m_impl->queueInMainThread(std::move(func));
|
||||
|
|
|
@ -521,22 +521,35 @@ void Loader::Impl::findProblems() {
|
|||
for (auto const& dep : mod->getMetadata().getDependencies()) {
|
||||
if (dep.mod && dep.mod->isEnabled() && dep.version.compare(dep.mod->getVersion()))
|
||||
continue;
|
||||
|
||||
auto dismissKey = fmt::format("dismiss-optional-dependency-{}-for-{}", dep.id, id);
|
||||
|
||||
switch(dep.importance) {
|
||||
case ModMetadata::Dependency::Importance::Suggested:
|
||||
this->addProblem({
|
||||
LoadProblem::Type::Suggestion,
|
||||
mod,
|
||||
fmt::format("{} {}", dep.id, dep.version.toString())
|
||||
});
|
||||
log::info("{} suggests {} {}", id, dep.id, dep.version);
|
||||
if (!Mod::get()->template getSavedValue<bool>(dismissKey)) {
|
||||
this->addProblem({
|
||||
LoadProblem::Type::Suggestion,
|
||||
mod,
|
||||
fmt::format("{} {}", dep.id, dep.version.toString())
|
||||
});
|
||||
log::info("{} suggests {} {}", id, dep.id, dep.version);
|
||||
}
|
||||
else {
|
||||
log::info("{} suggests {} {}, but that suggestion was dismissed", id, dep.id, dep.version);
|
||||
}
|
||||
break;
|
||||
case ModMetadata::Dependency::Importance::Recommended:
|
||||
this->addProblem({
|
||||
LoadProblem::Type::Recommendation,
|
||||
mod,
|
||||
fmt::format("{} {}", dep.id, dep.version.toString())
|
||||
});
|
||||
log::warn("{} recommends {} {}", id, dep.id, dep.version);
|
||||
if (!Mod::get()->template getSavedValue<bool>(dismissKey)) {
|
||||
this->addProblem({
|
||||
LoadProblem::Type::Recommendation,
|
||||
mod,
|
||||
fmt::format("{} {}", dep.id, dep.version.toString())
|
||||
});
|
||||
log::warn("{} recommends {} {}", id, dep.id, dep.version);
|
||||
}
|
||||
else {
|
||||
log::warn("{} recommends {} {}, but that suggestion was dismissed", id, dep.id, dep.version);
|
||||
}
|
||||
break;
|
||||
case ModMetadata::Dependency::Importance::Required:
|
||||
if(m_mods.find(dep.id) == m_mods.end()) {
|
||||
|
|
|
@ -248,11 +248,30 @@ bool Mod::hasSavedValue(std::string_view const key) {
|
|||
bool Mod::hasProblems() const {
|
||||
return m_impl->hasProblems();
|
||||
}
|
||||
|
||||
std::vector<LoadProblem> Mod::getAllProblems() const {
|
||||
return m_impl->getProblems();
|
||||
}
|
||||
std::vector<LoadProblem> Mod::getProblems() const {
|
||||
return ranges::filter(
|
||||
this->getAllProblems(),
|
||||
[](auto const& problem) {
|
||||
return problem.type != LoadProblem::Type::Recommendation &&
|
||||
problem.type != LoadProblem::Type::Suggestion;
|
||||
}
|
||||
);
|
||||
}
|
||||
std::vector<LoadProblem> Mod::getRecommendations() const {
|
||||
return ranges::filter(
|
||||
this->getAllProblems(),
|
||||
[](auto const& problem) {
|
||||
return problem.type == LoadProblem::Type::Recommendation ||
|
||||
problem.type == LoadProblem::Type::Suggestion;
|
||||
}
|
||||
);
|
||||
}
|
||||
bool Mod::shouldLoad() const {
|
||||
return m_impl->shouldLoad();
|
||||
}
|
||||
|
||||
bool Mod::isCurrentlyLoading() const {
|
||||
return m_impl->isCurrentlyLoading();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -678,7 +678,7 @@ std::vector<std::pair<std::string, std::optional<std::string>*>> ModMetadata::ge
|
|||
|
||||
ModMetadata::ModMetadata() : m_impl(std::make_unique<Impl>()) {}
|
||||
ModMetadata::ModMetadata(std::string id) : m_impl(std::make_unique<Impl>()) { m_impl->m_id = std::move(id); }
|
||||
ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(std::make_unique<Impl>(*other.m_impl)) {}
|
||||
ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(other.m_impl ? std::make_unique<Impl>(*other.m_impl) : std::make_unique<Impl>()) {}
|
||||
ModMetadata::ModMetadata(ModMetadata&& other) noexcept : m_impl(std::move(other.m_impl)) {}
|
||||
|
||||
ModMetadata& ModMetadata::operator=(ModMetadata const& other) {
|
||||
|
|
|
@ -598,7 +598,8 @@ ServerRequest<ServerModVersion> server::getModVersion(std::string const& id, std
|
|||
auto req = web::WebRequest();
|
||||
req.userAgent(getServerUserAgent());
|
||||
|
||||
auto versionURL = version ? version->toString(false) : "latest";
|
||||
auto versionURL = version ? version->toNonVString(false) : "latest";
|
||||
log::info("{}", getServerAPIBaseURL() + "/mods/" + id + "/versions/" + versionURL);
|
||||
return req.get(getServerAPIBaseURL() + "/mods/" + id + "/versions/" + versionURL).map(
|
||||
[](web::WebResponse* response) -> Result<ServerModVersion, ServerError> {
|
||||
if (response->ok()) {
|
||||
|
|
|
@ -12,11 +12,16 @@ $execute {
|
|||
ColorProvider::get()->define("mod-list-search-bg"_spr, { 83, 65, 109, 255 });
|
||||
ColorProvider::get()->define("mod-list-updates-available-bg"_spr, { 139, 89, 173, 255 });
|
||||
ColorProvider::get()->define("mod-list-updates-available-bg-2"_spr, { 45, 110, 222, 255 });
|
||||
ColorProvider::get()->define("mod-list-errors-found"_spr, { 235, 35, 112, 255 });
|
||||
ColorProvider::get()->define("mod-list-errors-found-2"_spr, { 245, 27, 27, 255 });
|
||||
ColorProvider::get()->define("mod-list-tab-selected-bg"_spr, { 168, 147, 185, 255 });
|
||||
ColorProvider::get()->define("mod-list-tab-selected-bg-alt"_spr, { 147, 163, 185, 255 });
|
||||
ColorProvider::get()->define("mod-list-featured-color"_spr, { 255, 255, 120, 255 });
|
||||
ColorProvider::get()->define("mod-list-enabled"_spr, { 120, 255, 100, 255 });
|
||||
ColorProvider::get()->define("mod-list-disabled"_spr, { 255, 120, 100, 255 });
|
||||
ColorProvider::get()->define("mod-list-recommended-bg"_spr, ccc3(25, 255, 167));
|
||||
ColorProvider::get()->define("mod-list-recommended-by"_spr, ccc3(25, 255, 167));
|
||||
ColorProvider::get()->define("mod-list-recommended-by-2"_spr, ccc3(47, 255, 255));
|
||||
}
|
||||
|
||||
bool GeodeSquareSprite::init(CCSprite* top, bool* state) {
|
||||
|
|
|
@ -6,17 +6,30 @@
|
|||
|
||||
using namespace geode::prelude;
|
||||
|
||||
enum class GeodePopupStyle {
|
||||
Default,
|
||||
Alt,
|
||||
Alt2,
|
||||
};
|
||||
|
||||
template <class... Args>
|
||||
class GeodePopup : public Popup<Args...> {
|
||||
protected:
|
||||
bool init(float width, float height, Args... args, bool altBG = false) {
|
||||
if (!Popup<Args...>::initAnchored(width, height, std::forward<Args>(args)..., (altBG ? "GE_square02.png"_spr : "GE_square01.png"_spr)))
|
||||
bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default) {
|
||||
const char* bg;
|
||||
switch (style) {
|
||||
default:
|
||||
case GeodePopupStyle::Default: bg = "GE_square01.png"_spr; break;
|
||||
case GeodePopupStyle::Alt: bg = "GE_square02.png"_spr; break;
|
||||
case GeodePopupStyle::Alt2: bg = "GE_square03.png"_spr; break;
|
||||
}
|
||||
if (!Popup<Args...>::initAnchored(width, height, std::forward<Args>(args)..., bg))
|
||||
return false;
|
||||
|
||||
this->setCloseButtonSpr(
|
||||
CircleButtonSprite::createWithSpriteFrameName(
|
||||
"close.png"_spr, .85f,
|
||||
(altBG ? CircleBaseColor::DarkAqua : CircleBaseColor::DarkPurple)
|
||||
(style == GeodePopupStyle::Default ? CircleBaseColor::DarkPurple : CircleBaseColor::DarkAqua)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ void ModsStatusNode::updateState() {
|
|||
switch (state) {
|
||||
// If there are no downloads happening, just show the restart button if needed
|
||||
case DownloadState::None: {
|
||||
m_restartBtn->setVisible(isRestartRequired());
|
||||
m_restartBtn->setVisible(ModListSource::isRestartRequired());
|
||||
} break;
|
||||
|
||||
// If some downloads were cancelled, show the restart button normally
|
||||
|
@ -135,7 +135,7 @@ void ModsStatusNode::updateState() {
|
|||
m_status->setColor(ccWHITE);
|
||||
m_status->setVisible(true);
|
||||
|
||||
m_restartBtn->setVisible(isRestartRequired());
|
||||
m_restartBtn->setVisible(ModListSource::isRestartRequired());
|
||||
} break;
|
||||
|
||||
// If all downloads were finished, show the restart button normally
|
||||
|
@ -151,7 +151,7 @@ void ModsStatusNode::updateState() {
|
|||
m_status->setVisible(true);
|
||||
m_statusBG->setVisible(true);
|
||||
|
||||
m_restartBtn->setVisible(isRestartRequired());
|
||||
m_restartBtn->setVisible(ModListSource::isRestartRequired());
|
||||
} break;
|
||||
|
||||
case DownloadState::SomeErrored: {
|
||||
|
@ -349,8 +349,8 @@ bool ModsLayer::init() {
|
|||
mainTabs->setPosition(m_frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10)));
|
||||
|
||||
for (auto item : std::initializer_list<std::tuple<const char*, const char*, ModListSource*>> {
|
||||
{ "download.png"_spr, "Installed", InstalledModListSource::get(false) },
|
||||
{ "GJ_timeIcon_001.png", "Updates", InstalledModListSource::get(true) },
|
||||
{ "download.png"_spr, "Installed", InstalledModListSource::get(InstalledModListType::All) },
|
||||
{ "GJ_starsIcon_001.png", "Recommended", SuggestedModListSource::get() },
|
||||
{ "globe.png"_spr, "Download", ServerModListSource::get(ServerModListType::Download) },
|
||||
{ "GJ_sTrendingIcon_001.png", "Trending", ServerModListSource::get(ServerModListType::Trending) },
|
||||
{ "gj_folderBtn_001.png", "Mod Packs", ModPackListSource::get() },
|
||||
|
@ -416,7 +416,7 @@ bool ModsLayer::init() {
|
|||
this->addChildAtPosition(m_pageMenu, Anchor::TopRight, ccp(-5, -5), false);
|
||||
|
||||
// Go to installed mods list
|
||||
this->gotoTab(InstalledModListSource::get(false));
|
||||
this->gotoTab(InstalledModListSource::get(InstalledModListType::All));
|
||||
|
||||
this->setKeypadEnabled(true);
|
||||
cocos::handleTouchPriority(this, true);
|
||||
|
@ -517,7 +517,7 @@ void ModsLayer::onBack(CCObject*) {
|
|||
|
||||
// To avoid memory overloading, clear caches after leaving the layer
|
||||
server::clearServerCaches(true);
|
||||
clearAllModListSourceCaches();
|
||||
ModListSource::clearAllCaches();
|
||||
}
|
||||
|
||||
void ModsLayer::onGoToPage(CCObject*) {
|
||||
|
|
|
@ -122,6 +122,7 @@ bool ModItem::init(ModSource&& source) {
|
|||
->setAxisAlignment(AxisAlignment::End)
|
||||
->setGap(10)
|
||||
);
|
||||
m_viewMenu->getLayout()->ignoreInvisibleChildren(true);
|
||||
this->addChildAtPosition(m_viewMenu, Anchor::Right, ccp(-10, 0));
|
||||
|
||||
// Handle source-specific stuff
|
||||
|
@ -137,6 +138,16 @@ bool ModItem::init(ModSource&& source) {
|
|||
m_viewMenu->addChild(m_enableToggle);
|
||||
m_viewMenu->updateLayout();
|
||||
}
|
||||
if (mod->hasProblems()) {
|
||||
auto viewErrorSpr = CircleButtonSprite::createWithSpriteFrameName(
|
||||
"exclamation.png"_spr, 1.f,
|
||||
CircleBaseColor::DarkPurple, CircleBaseSize::Small
|
||||
);
|
||||
auto viewErrorBtn = CCMenuItemSpriteExtra::create(
|
||||
viewErrorSpr, this, menu_selector(ModItem::onViewError)
|
||||
);
|
||||
m_viewMenu->addChild(viewErrorBtn);
|
||||
}
|
||||
},
|
||||
[this](server::ServerModMetadata const& metadata) {
|
||||
if (metadata.featured) {
|
||||
|
@ -151,6 +162,25 @@ bool ModItem::init(ModSource&& source) {
|
|||
m_titleContainer->addChild(starBG);
|
||||
}
|
||||
},
|
||||
[this](ModSuggestion const& suggestion) {
|
||||
m_recommendedBy = CCNode::create();
|
||||
m_recommendedBy->setContentWidth(225);
|
||||
|
||||
auto byLabel = CCLabelBMFont::create("Recommended by ", "bigFont.fnt");
|
||||
byLabel->setColor("mod-list-recommended-by"_cc3b);
|
||||
m_recommendedBy->addChild(byLabel);
|
||||
|
||||
auto nameLabel = CCLabelBMFont::create(suggestion.forMod->getName().c_str(), "bigFont.fnt");
|
||||
nameLabel->setColor("mod-list-recommended-by-2"_cc3b);
|
||||
m_recommendedBy->addChild(nameLabel);
|
||||
|
||||
m_recommendedBy->setLayout(
|
||||
RowLayout::create()
|
||||
->setDefaultScaleLimits(.1f, 1.f)
|
||||
->setAxisAlignment(AxisAlignment::Start)
|
||||
);
|
||||
m_infoContainer->addChild(m_recommendedBy);
|
||||
},
|
||||
});
|
||||
|
||||
auto updateSpr = CircleButtonSprite::createWithSpriteFrameName(
|
||||
|
@ -160,7 +190,6 @@ bool ModItem::init(ModSource&& source) {
|
|||
updateSpr, this, menu_selector(ModItem::onInstall)
|
||||
);
|
||||
m_viewMenu->addChild(m_updateBtn);
|
||||
m_updateBtn->setVisible(false);
|
||||
|
||||
if (m_source.asMod()) {
|
||||
m_checkUpdateListener.bind(this, &ModItem::onCheckUpdates);
|
||||
|
@ -217,20 +246,24 @@ void ModItem::updateState() {
|
|||
// (possibly overriding later based on state)
|
||||
m_source.visit(makeVisitor {
|
||||
[this](Mod* mod) {
|
||||
m_bg->setColor({ 255, 255, 255 });
|
||||
m_bg->setColor(ccWHITE);
|
||||
m_bg->setOpacity(mod->isOrWillBeEnabled() ? 25 : 10);
|
||||
m_titleLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155);
|
||||
m_versionLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155);
|
||||
m_developerLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155);
|
||||
},
|
||||
[this](server::ServerModMetadata const& metadata) {
|
||||
m_bg->setColor({ 255, 255, 255 });
|
||||
m_bg->setColor(ccWHITE);
|
||||
m_bg->setOpacity(25);
|
||||
if (metadata.featured) {
|
||||
m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-featured-color"_spr)));
|
||||
m_bg->setColor("mod-list-featured-color"_cc3b);
|
||||
m_bg->setOpacity(40);
|
||||
}
|
||||
},
|
||||
[this](ModSuggestion const& suggestion) {
|
||||
m_bg->setColor("mod-list-recommended-bg"_cc3b);
|
||||
m_bg->setOpacity(25);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
|
@ -253,6 +286,12 @@ void ModItem::updateState() {
|
|||
m_viewMenu->updateLayout();
|
||||
m_titleContainer->updateLayout();
|
||||
|
||||
// If there were problems, tint the BG red
|
||||
if (m_source.asMod() && m_source.asMod()->hasProblems()) {
|
||||
m_bg->setColor("mod-list-errors-found"_cc3b);
|
||||
m_bg->setOpacity(40);
|
||||
}
|
||||
|
||||
// Highlight item via BG if it wants to restart for extra UI attention
|
||||
if (wantsRestart) {
|
||||
m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)));
|
||||
|
@ -299,11 +338,16 @@ void ModItem::updateSize(float width, bool big) {
|
|||
m_developers->setContentWidth(titleSpace.width / m_infoContainer->getScale());
|
||||
m_developers->updateLayout();
|
||||
|
||||
if (m_recommendedBy) {
|
||||
m_recommendedBy->setContentWidth(titleSpace.width / m_infoContainer->getScale());
|
||||
m_recommendedBy->updateLayout();
|
||||
}
|
||||
|
||||
m_infoContainer->setPosition(m_obContentSize.height + 10, m_obContentSize.height / 2);
|
||||
m_infoContainer->setContentSize(ccp(titleSpace.width, titleSpace.height) / m_infoContainer->getScale());
|
||||
m_infoContainer->updateLayout();
|
||||
|
||||
m_viewMenu->setContentWidth(m_obContentSize.width / 2 - 20);
|
||||
m_viewMenu->setContentWidth(m_obContentSize.width / m_viewMenu->getScaleX() / 2 - 20);
|
||||
m_viewMenu->updateLayout();
|
||||
|
||||
this->updateLayout();
|
||||
|
@ -317,7 +361,21 @@ void ModItem::onCheckUpdates(typename server::ServerRequest<std::optional<server
|
|||
|
||||
void ModItem::onView(CCObject*) {
|
||||
// Always open up the popup for the installed mod page if that is possible
|
||||
ModPopup::create(m_source.tryConvertToMod())->show();
|
||||
ModPopup::create(m_source.convertForPopup())->show();
|
||||
}
|
||||
|
||||
void ModItem::onViewError(CCObject*) {
|
||||
if (auto mod = m_source.asMod()) {
|
||||
std::vector<std::string> problems;
|
||||
for (auto problem : mod->getProblems()) {
|
||||
problems.push_back(fmt::format("{} (code {})", problem.message, static_cast<int>(problem.type)));
|
||||
}
|
||||
FLAlertLayer::create(
|
||||
fmt::format("Errors with {}", mod->getName()).c_str(),
|
||||
ranges::join(problems, "\n"),
|
||||
"OK"
|
||||
)->show();
|
||||
}
|
||||
}
|
||||
|
||||
void ModItem::onEnable(CCObject*) {
|
||||
|
|
|
@ -18,6 +18,7 @@ protected:
|
|||
CCLabelBMFont* m_titleLabel;
|
||||
CCLabelBMFont* m_versionLabel;
|
||||
CCNode* m_developers;
|
||||
CCNode* m_recommendedBy;
|
||||
CCLabelBMFont* m_developerLabel;
|
||||
ButtonSprite* m_restartRequiredLabel;
|
||||
CCNode* m_downloadWaiting;
|
||||
|
@ -43,6 +44,7 @@ protected:
|
|||
|
||||
void onEnable(CCObject*);
|
||||
void onView(CCObject*);
|
||||
void onViewError(CCObject*);
|
||||
void onInstall(CCObject*);
|
||||
|
||||
public:
|
||||
|
|
|
@ -95,12 +95,62 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
|
|||
m_updateAllMenu->setLayout(
|
||||
RowLayout::create()
|
||||
->setAxisAlignment(AxisAlignment::End)
|
||||
->setDefaultScaleLimits(.1f, .75f)
|
||||
->setDefaultScaleLimits(.1f, .6f)
|
||||
);
|
||||
m_updateAllMenu->getLayout()->ignoreInvisibleChildren(true);
|
||||
m_updateAllContainer->addChildAtPosition(m_updateAllMenu, Anchor::Right, ccp(-10, 0));
|
||||
|
||||
m_topContainer->addChild(m_updateAllContainer);
|
||||
|
||||
if (Loader::get()->getProblems().size()) {
|
||||
m_errorsContainer = CCNode::create();
|
||||
m_errorsContainer->ignoreAnchorPointForPosition(false);
|
||||
m_errorsContainer->setContentSize({ size.width, 30 });
|
||||
m_errorsContainer->setVisible(false);
|
||||
|
||||
auto errorsBG = CCLayerGradient::create(
|
||||
"mod-list-errors-found"_cc4b,
|
||||
"mod-list-errors-found-2"_cc4b,
|
||||
ccp(1, -.5f)
|
||||
);
|
||||
errorsBG->setContentSize(m_errorsContainer->getContentSize());
|
||||
errorsBG->ignoreAnchorPointForPosition(false);
|
||||
|
||||
m_errorsContainer->addChildAtPosition(errorsBG, Anchor::Center);
|
||||
|
||||
auto errorsLabel = TextArea::create(
|
||||
"There were <cy>errors</c> loading some mods",
|
||||
"bigFont.fnt", .35f, size.width / 2 - 30, ccp(0, 1), 12.f, false
|
||||
);
|
||||
m_errorsContainer->addChildAtPosition(errorsLabel, Anchor::Left, ccp(10, 0), ccp(0, 0));
|
||||
|
||||
auto errorsMenu = CCMenu::create();
|
||||
errorsMenu->setContentWidth(size.width / 2);
|
||||
errorsMenu->setAnchorPoint({ 1, .5f });
|
||||
|
||||
auto showErrorsSpr = createGeodeButton(
|
||||
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
|
||||
"Show Errors Only", "GJ_button_06.png"
|
||||
);
|
||||
auto hideErrorsSpr = createGeodeButton(
|
||||
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
|
||||
"Hide Errors Only", "GE_button_05.png"_spr
|
||||
);
|
||||
m_toggleErrorsOnlyBtn = CCMenuItemToggler::create(
|
||||
showErrorsSpr, hideErrorsSpr, this, menu_selector(ModList::onToggleErrors)
|
||||
);
|
||||
m_toggleErrorsOnlyBtn->m_notClickable = true;
|
||||
errorsMenu->addChild(m_toggleErrorsOnlyBtn);
|
||||
|
||||
errorsMenu->setLayout(
|
||||
RowLayout::create()
|
||||
->setAxisAlignment(AxisAlignment::End)
|
||||
->setDefaultScaleLimits(.1f, .6f)
|
||||
);
|
||||
m_errorsContainer->addChildAtPosition(errorsMenu, Anchor::Right, ccp(-10, 0));
|
||||
|
||||
m_topContainer->addChild(m_errorsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
m_searchMenu = CCNode::create();
|
||||
|
@ -418,6 +468,12 @@ void ModList::updateTopContainer() {
|
|||
m_updateAllMenu->updateLayout();
|
||||
}
|
||||
|
||||
// If there are errors, show the error banner
|
||||
if (m_errorsContainer) {
|
||||
auto noErrors = Loader::get()->getProblems().empty();
|
||||
m_errorsContainer->setVisible(!noErrors);
|
||||
}
|
||||
|
||||
// ModList uses an anchor layout, so this puts the list in the right place
|
||||
this->updateLayout();
|
||||
}
|
||||
|
@ -449,11 +505,14 @@ void ModList::updateSize(bool big) {
|
|||
}
|
||||
|
||||
void ModList::updateState() {
|
||||
// Update the "Show Updates" button on the updates available banner
|
||||
if (m_toggleUpdatesOnlyBtn) {
|
||||
auto src = typeinfo_cast<InstalledModListSource*>(m_source);
|
||||
if (src) {
|
||||
m_toggleUpdatesOnlyBtn->toggle(src->getQuery().onlyUpdates);
|
||||
// Update the "Show Updates" and "Show Errors" buttons on
|
||||
// the updates available / errors banners
|
||||
if (auto src = typeinfo_cast<InstalledModListSource*>(m_source)) {
|
||||
if (m_toggleUpdatesOnlyBtn) {
|
||||
m_toggleUpdatesOnlyBtn->toggle(src->getQuery().type == InstalledModListType::OnlyUpdates);
|
||||
}
|
||||
if (m_toggleErrorsOnlyBtn) {
|
||||
m_toggleErrorsOnlyBtn->toggle(src->getQuery().type == InstalledModListType::OnlyErrors);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -525,7 +584,19 @@ void ModList::onClearFilters(CCObject*) {
|
|||
|
||||
void ModList::onToggleUpdates(CCObject*) {
|
||||
if (auto src = typeinfo_cast<InstalledModListSource*>(m_source)) {
|
||||
src->getQueryMut()->onlyUpdates = !src->getQuery().onlyUpdates;
|
||||
auto mut = src->getQueryMut();
|
||||
mut->type = mut->type == InstalledModListType::OnlyUpdates ?
|
||||
InstalledModListType::All :
|
||||
InstalledModListType::OnlyUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
void ModList::onToggleErrors(CCObject*) {
|
||||
if (auto src = typeinfo_cast<InstalledModListSource*>(m_source)) {
|
||||
auto mut = src->getQueryMut();
|
||||
mut->type = mut->type == InstalledModListType::OnlyErrors ?
|
||||
InstalledModListType::All :
|
||||
InstalledModListType::OnlyErrors;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@ protected:
|
|||
CCMenuItemSpriteExtra* m_updateAllBtn = nullptr;
|
||||
CCNode* m_updateAllLoadingCircle = nullptr;
|
||||
CCMenuItemToggler* m_toggleUpdatesOnlyBtn = nullptr;
|
||||
CCNode* m_errorsContainer = nullptr;
|
||||
CCMenuItemToggler* m_toggleErrorsOnlyBtn = nullptr;
|
||||
TextArea* m_updateCountLabel = nullptr;
|
||||
TextInput* m_searchInput;
|
||||
EventListener<InvalidateCacheFilter> m_invalidateCacheListener;
|
||||
|
@ -59,6 +61,7 @@ protected:
|
|||
void onSort(CCObject*);
|
||||
void onClearFilters(CCObject*);
|
||||
void onToggleUpdates(CCObject*);
|
||||
void onToggleErrors(CCObject*);
|
||||
void onUpdateAll(CCObject*);
|
||||
|
||||
public:
|
||||
|
|
97
loader/src/ui/mods/list/ModProblemItem.cpp
Normal file
97
loader/src/ui/mods/list/ModProblemItem.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include "ModProblemItem.hpp"
|
||||
#include <Geode/utils/ColorProvider.hpp>
|
||||
#include <Geode/ui/GeodeUI.hpp>
|
||||
|
||||
bool ModProblemItem::init() {
|
||||
if (!CCNode::init())
|
||||
return false;
|
||||
|
||||
this->setContentSize({ 250, 28 });
|
||||
|
||||
auto bg = CCScale9Sprite::create("square02b_small.png");
|
||||
bg->setOpacity(20);
|
||||
bg->ignoreAnchorPointForPosition(false);
|
||||
bg->setAnchorPoint({ .5f, .5f });
|
||||
bg->setScale(.7f);
|
||||
bg->setContentSize(m_obContentSize / bg->getScale());
|
||||
this->addChildAtPosition(bg, Anchor::Center);
|
||||
|
||||
m_menu = CCMenu::create();
|
||||
m_menu->setContentWidth(m_obContentSize.width / 2 - 10);
|
||||
m_menu->setLayout(
|
||||
RowLayout::create()
|
||||
->setDefaultScaleLimits(.1f, .35f)
|
||||
->setAxisAlignment(AxisAlignment::End)
|
||||
);
|
||||
this->addChildAtPosition(m_menu, Anchor::Right, ccp(-5, 0), ccp(1, .5f));
|
||||
|
||||
this->setLoading();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModProblemItem::clearState() {
|
||||
this->removeChildByID("loading-spinner");
|
||||
this->removeChildByID("error-label");
|
||||
}
|
||||
void ModProblemItem::setLoading() {
|
||||
this->clearState();
|
||||
|
||||
auto loadingCircle = createLoadingCircle(20);
|
||||
this->addChildAtPosition(loadingCircle, Anchor::Center);
|
||||
}
|
||||
void ModProblemItem::setError(std::string const& error) {
|
||||
this->clearState();
|
||||
|
||||
auto label = CCLabelBMFont::create(error.c_str(), "bigFont.fnt");
|
||||
label->setColor("mod-list-errors-found"_cc3b);
|
||||
label->limitLabelWidth(m_obContentSize.width, .5f, .1f);
|
||||
label->setID("error-label");
|
||||
this->addChildAtPosition(label, Anchor::Center);
|
||||
}
|
||||
void ModProblemItem::setMod(ModMetadata const& metadata) {
|
||||
this->clearState();
|
||||
|
||||
auto h = m_obContentSize.height;
|
||||
|
||||
auto logo = createServerModLogo(metadata.getID());
|
||||
limitNodeSize(logo, { h / 1.4f, h / 1.4f }, 5.f, .1f);
|
||||
this->addChildAtPosition(logo, Anchor::Left, ccp(h / 2, 0));
|
||||
|
||||
auto title = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
|
||||
title->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f);
|
||||
this->addChildAtPosition(title, Anchor::Left, ccp(h, h / 5), ccp(0, .5f));
|
||||
|
||||
auto versionLabel = CCLabelBMFont::create(
|
||||
metadata.getVersion().toVString().c_str(),
|
||||
"bigFont.fnt"
|
||||
);
|
||||
versionLabel->setColor("mod-list-version-label"_cc3b);
|
||||
versionLabel->limitLabelWidth(m_obContentSize.width / 2, .25f, .1f);
|
||||
this->addChildAtPosition(
|
||||
versionLabel, Anchor::Left,
|
||||
ccp(h + title->getScaledContentWidth() + 3, h / 5), ccp(0, .5f)
|
||||
);
|
||||
|
||||
auto developer = CCLabelBMFont::create(
|
||||
fmt::format("By {}", metadata.getDeveloper()).c_str(),
|
||||
"goldFont.fnt"
|
||||
);
|
||||
developer->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f);
|
||||
this->addChildAtPosition(developer, Anchor::Left, ccp(h, -h / 5), ccp(0, .5f));
|
||||
}
|
||||
|
||||
ModProblemItem* ModProblemItem::create() {
|
||||
auto ret = new ModProblemItem();
|
||||
if (ret && ret->init()) {
|
||||
ret->setError("Unknown Type");
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ModProblemItem* ModProblemItem::parse(LoadProblem const& problem) {
|
||||
return ModProblemItem::create();
|
||||
}
|
25
loader/src/ui/mods/list/ModProblemItem.hpp
Normal file
25
loader/src/ui/mods/list/ModProblemItem.hpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <Geode/DefaultInclude.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <server/Server.hpp>
|
||||
#include "../GeodeStyle.hpp"
|
||||
|
||||
using namespace geode::prelude;
|
||||
using VersionDownload = server::ServerRequest<server::ServerModVersion>;
|
||||
|
||||
class ModProblemItem : public CCNode {
|
||||
protected:
|
||||
CCMenu* m_menu;
|
||||
|
||||
bool init();
|
||||
|
||||
void clearState();
|
||||
void setLoading();
|
||||
void setError(std::string const& error);
|
||||
void setMod(ModMetadata const& metadata);
|
||||
|
||||
public:
|
||||
static ModProblemItem* create();
|
||||
static ModProblemItem* parse(LoadProblem const& problem);
|
||||
};
|
33
loader/src/ui/mods/list/ModProblemItemList.cpp
Normal file
33
loader/src/ui/mods/list/ModProblemItemList.cpp
Normal file
|
@ -0,0 +1,33 @@
|
|||
#include "ModProblemItemList.hpp"
|
||||
#include "ModProblemItem.hpp"
|
||||
|
||||
bool ModProblemItemList::init(float height) {
|
||||
if (!CCNode::init())
|
||||
return false;
|
||||
|
||||
this->setContentSize({ 250, height });
|
||||
|
||||
m_scrollLayer = ScrollLayer::create({ m_obContentSize.width, height });
|
||||
for (auto problem : Loader::get()->getProblems()) {
|
||||
m_scrollLayer->m_contentLayer->addChild(ModProblemItem::parse(problem));
|
||||
}
|
||||
m_scrollLayer->m_contentLayer->setLayout(
|
||||
ColumnLayout::create()
|
||||
->setAutoGrowAxis(height)
|
||||
->setAxisReverse(true)
|
||||
->setAxisAlignment(AxisAlignment::End)
|
||||
);
|
||||
this->addChildAtPosition(m_scrollLayer, Anchor::BottomLeft);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ModProblemItemList* ModProblemItemList::create(float height) {
|
||||
auto ret = new ModProblemItemList();
|
||||
if (ret && ret->init(height)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
16
loader/src/ui/mods/list/ModProblemItemList.hpp
Normal file
16
loader/src/ui/mods/list/ModProblemItemList.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <Geode/DefaultInclude.hpp>
|
||||
#include <Geode/ui/ScrollLayer.hpp>
|
||||
|
||||
using namespace geode::prelude;
|
||||
|
||||
class ModProblemItemList : public CCNode {
|
||||
protected:
|
||||
ScrollLayer* m_scrollLayer;
|
||||
|
||||
bool init(float height);
|
||||
|
||||
public:
|
||||
static ModProblemItemList* create(float height);
|
||||
};
|
|
@ -104,6 +104,30 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
dev->setAnchorPoint({ .0f, .5f });
|
||||
titleContainer->addChildAtPosition(dev, Anchor::BottomLeft, ccp(devAndTitlePos, titleContainer->getContentHeight() * .25f));
|
||||
|
||||
if (auto suggestion = m_source.asSuggestion()) {
|
||||
title->updateAnchoredPosition(Anchor::TopLeft, ccp(devAndTitlePos, -2));
|
||||
dev->updateAnchoredPosition(Anchor::Left, ccp(devAndTitlePos, 0));
|
||||
|
||||
auto recommendedBy = CCNode::create();
|
||||
recommendedBy->setContentWidth(titleContainer->getContentWidth() - devAndTitlePos);
|
||||
recommendedBy->setAnchorPoint({ .0f, .5f });
|
||||
|
||||
auto byLabel = CCLabelBMFont::create("Recommended by ", "bigFont.fnt");
|
||||
byLabel->setColor("mod-list-recommended-by"_cc3b);
|
||||
recommendedBy->addChild(byLabel);
|
||||
|
||||
auto nameLabel = CCLabelBMFont::create(suggestion->forMod->getName().c_str(), "bigFont.fnt");
|
||||
nameLabel->setColor("mod-list-recommended-by-2"_cc3b);
|
||||
recommendedBy->addChild(nameLabel);
|
||||
|
||||
recommendedBy->setLayout(
|
||||
RowLayout::create()
|
||||
->setDefaultScaleLimits(.1f, 1.f)
|
||||
->setAxisAlignment(AxisAlignment::Start)
|
||||
);
|
||||
titleContainer->addChildAtPosition(recommendedBy, Anchor::BottomLeft, ccp(devAndTitlePos, 4));
|
||||
}
|
||||
|
||||
leftColumn->addChild(titleContainer);
|
||||
|
||||
auto idStr = "(ID: " + m_source.getMetadata().getID() + ")";
|
||||
|
@ -512,7 +536,6 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
|
||||
void ModPopup::updateState() {
|
||||
auto asMod = m_source.asMod();
|
||||
auto asServer = m_source.asServer();
|
||||
auto wantsRestart = m_source.wantsRestart();
|
||||
|
||||
m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0));
|
||||
|
@ -543,7 +566,7 @@ void ModPopup::updateState() {
|
|||
m_reenableBtn->setVisible(asMod && modRequestedActionIsToggle(asMod->getRequestedAction()));
|
||||
|
||||
m_updateBtn->setVisible(m_source.hasUpdates().has_value() && asMod->getRequestedAction() == ModRequestedAction::None);
|
||||
m_installBtn->setVisible(asServer);
|
||||
m_installBtn->setVisible(m_source.asServer() || m_source.asSuggestion());
|
||||
m_uninstallBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None);
|
||||
|
||||
if (asMod && modRequestedActionIsUninstall(asMod->getRequestedAction())) {
|
||||
|
@ -878,8 +901,14 @@ void ModPopup::onSupport(CCObject*) {
|
|||
|
||||
ModPopup* ModPopup::create(ModSource&& src) {
|
||||
auto ret = new ModPopup();
|
||||
bool isServer = src.asServer();
|
||||
if (ret && ret->init(440, 280, std::move(src), isServer)) {
|
||||
GeodePopupStyle style = GeodePopupStyle::Default;
|
||||
if (src.asServer()) {
|
||||
style = GeodePopupStyle::Alt;
|
||||
}
|
||||
else if (src.asSuggestion()) {
|
||||
style = GeodePopupStyle::Alt2;
|
||||
}
|
||||
if (ret && ret->init(440, 280, std::move(src), style)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
|
|
112
loader/src/ui/mods/sources/InstalledModListSource.cpp
Normal file
112
loader/src/ui/mods/sources/InstalledModListSource.cpp
Normal file
|
@ -0,0 +1,112 @@
|
|||
#include "ModListSource.hpp"
|
||||
|
||||
bool InstalledModsQuery::preCheck(ModSource const& src) const {
|
||||
// If we only want mods with updates, then only give mods with updates
|
||||
// NOTE: The caller of filterModsWithQuery() should have ensured that
|
||||
// `src.checkUpdates()` has been called and has finished
|
||||
if (
|
||||
auto updates = src.hasUpdates();
|
||||
type == InstalledModListType::OnlyUpdates &&
|
||||
!(updates && updates->hasUpdateForInstalledMod())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// If only errors requested, only show mods with errors (duh)
|
||||
if (type == InstalledModListType::OnlyErrors) {
|
||||
return src.asMod()->hasProblems();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool InstalledModsQuery::queryCheck(ModSource const& src, double& weighted) const {
|
||||
auto addToList = modFuzzyMatch(src.asMod()->getMetadata(), *query, weighted);
|
||||
// Loader gets boost to ensure it's normally always top of the list
|
||||
if (addToList && src.asMod()->getID() == "geode.loader") {
|
||||
weighted += 5;
|
||||
}
|
||||
// todo: favorites
|
||||
return addToList;
|
||||
}
|
||||
|
||||
InstalledModListSource::InstalledModListSource(InstalledModListType type)
|
||||
: m_type(type)
|
||||
{
|
||||
this->resetQuery();
|
||||
}
|
||||
|
||||
InstalledModListSource* InstalledModListSource::get(InstalledModListType type) {
|
||||
switch (type) {
|
||||
default:
|
||||
case InstalledModListType::All: {
|
||||
static auto inst = new InstalledModListSource(InstalledModListType::All);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case InstalledModListType::OnlyUpdates: {
|
||||
static auto inst = new InstalledModListSource(InstalledModListType::OnlyUpdates);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case InstalledModListType::OnlyErrors: {
|
||||
static auto inst = new InstalledModListSource(InstalledModListType::OnlyErrors);
|
||||
return inst;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledModListSource::resetQuery() {
|
||||
m_query = InstalledModsQuery {
|
||||
.type = m_type,
|
||||
};
|
||||
}
|
||||
|
||||
InstalledModListSource::ProviderTask InstalledModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
m_query.page = page;
|
||||
m_query.pageSize = pageSize;
|
||||
|
||||
auto content = ModListSource::ProvidedMods();
|
||||
for (auto& mod : Loader::get()->getAllMods()) {
|
||||
content.mods.push_back(ModSource(mod));
|
||||
}
|
||||
// If we're only checking mods that have updates, we first have to run
|
||||
// update checks every mod...
|
||||
if (m_query.type == InstalledModListType::OnlyUpdates && content.mods.size()) {
|
||||
using UpdateTask = server::ServerRequest<std::optional<server::ServerModUpdate>>;
|
||||
std::vector<UpdateTask> tasks;
|
||||
for (auto& src : content.mods) {
|
||||
tasks.push_back(src.checkUpdates());
|
||||
}
|
||||
return UpdateTask::all(std::move(tasks)).map(
|
||||
[content = std::move(content), query = m_query](auto*) mutable -> ProviderTask::Value {
|
||||
// Filter the results based on the current search
|
||||
// query and return them
|
||||
filterModsWithLocalQuery(content, query);
|
||||
return Ok(content);
|
||||
},
|
||||
[](auto*) -> ProviderTask::Progress { return std::nullopt; }
|
||||
);
|
||||
}
|
||||
// Otherwise simply construct the result right away
|
||||
else {
|
||||
filterModsWithLocalQuery(content, m_query);
|
||||
return ProviderTask::immediate(Ok(content));
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledModListSource::setSearchQuery(std::string const& query) {
|
||||
m_query.query = query.size() ? std::optional(query) : std::nullopt;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> InstalledModListSource::getModTags() const {
|
||||
return m_query.tags;
|
||||
}
|
||||
void InstalledModListSource::setModTags(std::unordered_set<std::string> const& tags) {
|
||||
m_query.tags = tags;
|
||||
this->clearCache();
|
||||
}
|
||||
|
||||
InstalledModsQuery const& InstalledModListSource::getQuery() const {
|
||||
return m_query;
|
||||
}
|
||||
InvalidateQueryAfter<InstalledModsQuery> InstalledModListSource::getQueryMut() {
|
||||
return InvalidateQueryAfter(m_query, this);
|
||||
}
|
|
@ -5,95 +5,13 @@
|
|||
#include <Geode/external/fts/fts_fuzzy_match.h>
|
||||
|
||||
static constexpr size_t PER_PAGE = 10;
|
||||
static std::vector<ModListSource*> ALL_EXTANT_SOURCES {};
|
||||
|
||||
static size_t ceildiv(size_t a, size_t b) {
|
||||
// https://stackoverflow.com/questions/2745074/fast-ceiling-of-an-integer-division-in-c-c
|
||||
return a / b + (a % b != 0);
|
||||
}
|
||||
|
||||
static bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out) {
|
||||
int score;
|
||||
if (fts::fuzzy_match(kw.c_str(), str.c_str(), score)) {
|
||||
out = std::max(out, score * weight);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void filterModsWithQuery(InstalledModListSource::ProvidedMods& mods, InstalledModsQuery const& query) {
|
||||
std::vector<std::pair<Mod*, double>> filtered;
|
||||
|
||||
// Filter installed mods based on query
|
||||
for (auto& src : mods.mods) {
|
||||
auto mod = src.asMod();
|
||||
double weighted = 0;
|
||||
bool addToList = true;
|
||||
// If we only want mods with updates, then only give mods with updates
|
||||
// NOTE: The caller of filterModsWithQuery() should have ensured that
|
||||
// `src.checkUpdates()` has been called and has finished
|
||||
if (auto updates = src.hasUpdates(); query.onlyUpdates && !(updates && updates->hasUpdateForInstalledMod())) {
|
||||
addToList = false;
|
||||
}
|
||||
// If some tags are provided, only return mods that match
|
||||
if (addToList && query.tags.size()) {
|
||||
auto compare = mod->getMetadata().getTags();
|
||||
for (auto& tag : query.tags) {
|
||||
if (!compare.contains(tag)) {
|
||||
addToList = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't bother with unnecessary fuzzy match calculations if this mod isn't going to be added anyway
|
||||
if (addToList && query.query) {
|
||||
// By default don't add anything
|
||||
addToList = false;
|
||||
addToList |= weightedFuzzyMatch(mod->getName(), *query.query, 1, weighted);
|
||||
addToList |= weightedFuzzyMatch(mod->getID(), *query.query, 0.5, weighted);
|
||||
for (auto& dev : mod->getDevelopers()) {
|
||||
addToList |= weightedFuzzyMatch(dev, *query.query, 0.25, weighted);
|
||||
}
|
||||
if (auto details = mod->getDetails()) {
|
||||
addToList |= weightedFuzzyMatch(*details, *query.query, 0.005, weighted);
|
||||
}
|
||||
if (auto desc = mod->getDescription()) {
|
||||
addToList |= weightedFuzzyMatch(*desc, *query.query, 0.02, weighted);
|
||||
}
|
||||
if (weighted < 2) {
|
||||
addToList = false;
|
||||
}
|
||||
}
|
||||
// Loader gets boost to ensure it's normally always top of the list
|
||||
if (addToList && mod->getID() == "geode.loader") {
|
||||
weighted += 5;
|
||||
}
|
||||
if (addToList) {
|
||||
filtered.push_back({ mod, weighted });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort list based on score
|
||||
std::sort(filtered.begin(), filtered.end(), [](auto a, auto b) {
|
||||
// Sort primarily by score
|
||||
if (a.second != b.second) {
|
||||
return a.second > b.second;
|
||||
}
|
||||
// Sort secondarily alphabetically
|
||||
return a.first->getName() < b.first->getName();
|
||||
});
|
||||
|
||||
mods.mods.clear();
|
||||
// Pick out only the mods in the page and page size specified in the query
|
||||
for (
|
||||
size_t i = query.page * query.pageSize;
|
||||
i < filtered.size() && i < (query.page + 1) * query.pageSize;
|
||||
i += 1
|
||||
) {
|
||||
mods.mods.push_back(filtered.at(i).first);
|
||||
}
|
||||
|
||||
mods.totalModCount = filtered.size();
|
||||
}
|
||||
|
||||
InvalidateCacheEvent::InvalidateCacheEvent(ModListSource* src) : source(src) {}
|
||||
|
||||
ListenerResult InvalidateCacheFilter::handle(MiniFunction<Callback> fn, InvalidateCacheEvent* event) {
|
||||
|
@ -154,232 +72,16 @@ void ModListSource::search(std::string const& query) {
|
|||
this->clearCache();
|
||||
}
|
||||
|
||||
ModListSource::ModListSource() {}
|
||||
|
||||
InstalledModListSource::InstalledModListSource(bool onlyUpdates)
|
||||
: m_onlyUpdates(onlyUpdates)
|
||||
{
|
||||
this->resetQuery();
|
||||
ModListSource::ModListSource() {
|
||||
ALL_EXTANT_SOURCES.push_back(this);
|
||||
}
|
||||
|
||||
InstalledModListSource* InstalledModListSource::get(bool onlyUpdates) {
|
||||
if (onlyUpdates) {
|
||||
static auto inst = new InstalledModListSource(true);
|
||||
return inst;
|
||||
}
|
||||
else {
|
||||
static auto inst = new InstalledModListSource(false);
|
||||
return inst;
|
||||
void ModListSource::clearAllCaches() {
|
||||
for (auto src : ALL_EXTANT_SOURCES) {
|
||||
src->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledModListSource::resetQuery() {
|
||||
m_query = InstalledModsQuery {
|
||||
.onlyUpdates = m_onlyUpdates,
|
||||
};
|
||||
}
|
||||
|
||||
InstalledModListSource::ProviderTask InstalledModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
m_query.page = page;
|
||||
m_query.pageSize = pageSize;
|
||||
|
||||
auto content = ModListSource::ProvidedMods();
|
||||
for (auto& mod : Loader::get()->getAllMods()) {
|
||||
content.mods.push_back(ModSource(mod));
|
||||
}
|
||||
// If we're only checking mods that have updates, we first have to run
|
||||
// update checks every mod...
|
||||
if (m_query.onlyUpdates && content.mods.size()) {
|
||||
// return ProviderTask::immediate(Ok(content));
|
||||
return ProviderTask::runWithCallback([content, query = m_query](auto finish, auto progress, auto hasBeenCancelled) {
|
||||
struct Waiting final {
|
||||
ModListSource::ProvidedMods content;
|
||||
std::atomic_size_t waitingFor;
|
||||
std::vector<Task<std::monostate>> waitingTasks;
|
||||
};
|
||||
auto waiting = std::make_shared<Waiting>();
|
||||
waiting->waitingFor = content.mods.size();
|
||||
waiting->content = std::move(content);
|
||||
waiting->waitingTasks.reserve(content.mods.size());
|
||||
for (auto& src : waiting->content.mods) {
|
||||
// Need to store the created task so it doesn't get destroyed
|
||||
waiting->waitingTasks.emplace_back(src.checkUpdates().map(
|
||||
[waiting, finish, query](auto* result) {
|
||||
waiting->waitingFor -= 1;
|
||||
if (waiting->waitingFor == 0) {
|
||||
// Make sure to clear our waiting tasks vector so
|
||||
// we don't have a funky lil circular ref and a
|
||||
// memory leak!
|
||||
waiting->waitingTasks.clear();
|
||||
|
||||
// Filter the results based on the current search
|
||||
// query and return them
|
||||
filterModsWithQuery(waiting->content, query);
|
||||
finish(Ok(std::move(waiting->content)));
|
||||
}
|
||||
return std::monostate();
|
||||
},
|
||||
[](auto*) { return std::monostate(); }
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
// Otherwise simply construct the result right away
|
||||
else {
|
||||
filterModsWithQuery(content, m_query);
|
||||
return ProviderTask::immediate(Ok(content));
|
||||
}
|
||||
}
|
||||
|
||||
void InstalledModListSource::setSearchQuery(std::string const& query) {
|
||||
m_query.query = query.size() ? std::optional(query) : std::nullopt;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> InstalledModListSource::getModTags() const {
|
||||
return m_query.tags;
|
||||
}
|
||||
void InstalledModListSource::setModTags(std::unordered_set<std::string> const& tags) {
|
||||
m_query.tags = tags;
|
||||
this->clearCache();
|
||||
}
|
||||
|
||||
InstalledModsQuery const& InstalledModListSource::getQuery() const {
|
||||
return m_query;
|
||||
}
|
||||
|
||||
InvalidateQueryAfter<InstalledModsQuery> InstalledModListSource::getQueryMut() {
|
||||
return InvalidateQueryAfter(m_query, this);
|
||||
}
|
||||
|
||||
void ServerModListSource::resetQuery() {
|
||||
switch (m_type) {
|
||||
case ServerModListType::Download: {
|
||||
m_query = server::ModsQuery {};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Featured: {
|
||||
m_query = server::ModsQuery {
|
||||
.featured = true,
|
||||
};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Trending: {
|
||||
m_query = server::ModsQuery {
|
||||
.sorting = server::ModsSort::RecentlyUpdated,
|
||||
};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Recent: {
|
||||
m_query = server::ModsQuery {
|
||||
.sorting = server::ModsSort::RecentlyPublished,
|
||||
};
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
ServerModListSource::ProviderTask ServerModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
m_query.page = page;
|
||||
m_query.pageSize = pageSize;
|
||||
return server::getMods(m_query, !forceUpdate).map(
|
||||
[](Result<server::ServerModsList, server::ServerError>* result) -> ProviderTask::Value {
|
||||
if (result->isOk()) {
|
||||
auto list = result->unwrap();
|
||||
auto content = ModListSource::ProvidedMods();
|
||||
for (auto&& mod : std::move(list.mods)) {
|
||||
content.mods.push_back(ModSource(std::move(mod)));
|
||||
}
|
||||
content.totalModCount = list.totalModCount;
|
||||
return Ok(content);
|
||||
}
|
||||
return Err(LoadPageError("Error loading mods", result->unwrapErr().details));
|
||||
},
|
||||
[](auto* prog) {
|
||||
return prog->percentage;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ServerModListSource::ServerModListSource(ServerModListType type)
|
||||
: m_type(type)
|
||||
{
|
||||
this->resetQuery();
|
||||
}
|
||||
|
||||
ServerModListSource* ServerModListSource::get(ServerModListType type) {
|
||||
switch (type) {
|
||||
default:
|
||||
case ServerModListType::Download: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Download);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Featured: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Featured);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Trending: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Trending);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Recent: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Recent);
|
||||
return inst;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerModListSource::setSearchQuery(std::string const& query) {
|
||||
m_query.query = query.size() ? std::optional(query) : std::nullopt;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> ServerModListSource::getModTags() const {
|
||||
return m_query.tags;
|
||||
}
|
||||
void ServerModListSource::setModTags(std::unordered_set<std::string> const& tags) {
|
||||
m_query.tags = tags;
|
||||
this->clearCache();
|
||||
}
|
||||
|
||||
server::ModsQuery const& ServerModListSource::getQuery() const {
|
||||
return m_query;
|
||||
}
|
||||
InvalidateQueryAfter<server::ModsQuery> ServerModListSource::getQueryMut() {
|
||||
return InvalidateQueryAfter(m_query, this);
|
||||
}
|
||||
|
||||
void ModPackListSource::resetQuery() {}
|
||||
ModPackListSource::ProviderTask ModPackListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
return ProviderTask::immediate(Err(LoadPageError("Coming soon ;)")));
|
||||
}
|
||||
|
||||
ModPackListSource::ModPackListSource() {}
|
||||
|
||||
ModPackListSource* ModPackListSource::get() {
|
||||
static auto inst = new ModPackListSource();
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ModPackListSource::setSearchQuery(std::string const& query) {}
|
||||
|
||||
std::unordered_set<std::string> ModPackListSource::getModTags() const {
|
||||
return {};
|
||||
}
|
||||
void ModPackListSource::setModTags(std::unordered_set<std::string> const& set) {}
|
||||
|
||||
void clearAllModListSourceCaches() {
|
||||
InstalledModListSource::get(false)->clearCache();
|
||||
InstalledModListSource::get(true)->clearCache();
|
||||
|
||||
ServerModListSource::get(ServerModListType::Download)->clearCache();
|
||||
ServerModListSource::get(ServerModListType::Featured)->clearCache();
|
||||
ServerModListSource::get(ServerModListType::Trending)->clearCache();
|
||||
ServerModListSource::get(ServerModListType::Recent)->clearCache();
|
||||
|
||||
ModPackListSource::get()->clearCache();
|
||||
}
|
||||
bool isRestartRequired() {
|
||||
bool ModListSource::isRestartRequired() {
|
||||
for (auto mod : Loader::get()->getAllMods()) {
|
||||
if (mod->getRequestedAction() != ModRequestedAction::None) {
|
||||
return true;
|
||||
|
@ -390,3 +92,30 @@ bool isRestartRequired() {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out) {
|
||||
int score;
|
||||
if (fts::fuzzy_match(kw.c_str(), str.c_str(), score)) {
|
||||
out = std::max(out, score * weight);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool modFuzzyMatch(ModMetadata const& metadata, std::string const& kw, double& weighted) {
|
||||
bool addToList = false;
|
||||
addToList |= weightedFuzzyMatch(metadata.getName(), kw, 1, weighted);
|
||||
addToList |= weightedFuzzyMatch(metadata.getID(), kw, 0.5, weighted);
|
||||
for (auto& dev : metadata.getDevelopers()) {
|
||||
addToList |= weightedFuzzyMatch(dev, kw, 0.25, weighted);
|
||||
}
|
||||
if (auto details = metadata.getDetails()) {
|
||||
addToList |= weightedFuzzyMatch(*details, kw, 0.005, weighted);
|
||||
}
|
||||
if (auto desc = metadata.getDescription()) {
|
||||
addToList |= weightedFuzzyMatch(*desc, kw, 0.02, weighted);
|
||||
}
|
||||
if (weighted < 2) {
|
||||
addToList = false;
|
||||
}
|
||||
return addToList;
|
||||
}
|
||||
|
|
|
@ -26,14 +26,6 @@ public:
|
|||
InvalidateCacheFilter(ModListSource* src);
|
||||
};
|
||||
|
||||
struct InstalledModsQuery final {
|
||||
std::optional<std::string> query;
|
||||
bool onlyUpdates = false;
|
||||
std::unordered_set<std::string> tags = {};
|
||||
size_t page = 0;
|
||||
size_t pageSize = 10;
|
||||
};
|
||||
|
||||
// Handles loading the entries for the mods list
|
||||
class ModListSource {
|
||||
public:
|
||||
|
@ -81,6 +73,9 @@ public:
|
|||
PageLoadTask loadPage(size_t page, bool forceUpdate = false);
|
||||
std::optional<size_t> getPageCount() const;
|
||||
std::optional<size_t> getItemCount() const;
|
||||
|
||||
static void clearAllCaches();
|
||||
static bool isRestartRequired();
|
||||
};
|
||||
|
||||
template <class T>
|
||||
|
@ -99,19 +94,37 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
struct LocalModsQueryBase {
|
||||
std::optional<std::string> query;
|
||||
std::unordered_set<std::string> tags = {};
|
||||
size_t page = 0;
|
||||
size_t pageSize = 10;
|
||||
};
|
||||
|
||||
enum class InstalledModListType {
|
||||
All,
|
||||
OnlyUpdates,
|
||||
OnlyErrors,
|
||||
};
|
||||
struct InstalledModsQuery final : public LocalModsQueryBase {
|
||||
InstalledModListType type = InstalledModListType::All;
|
||||
bool preCheck(ModSource const& src) const;
|
||||
bool queryCheck(ModSource const& src, double& weighted) const;
|
||||
};
|
||||
|
||||
class InstalledModListSource : public ModListSource {
|
||||
protected:
|
||||
bool m_onlyUpdates;
|
||||
InstalledModListType m_type;
|
||||
InstalledModsQuery m_query;
|
||||
|
||||
void resetQuery() override;
|
||||
ProviderTask fetchPage(size_t page, size_t pageSize, bool forceUpdate) override;
|
||||
void setSearchQuery(std::string const& query) override;
|
||||
|
||||
InstalledModListSource(bool onlyUpdates);
|
||||
InstalledModListSource(InstalledModListType type);
|
||||
|
||||
public:
|
||||
static InstalledModListSource* get(bool onlyUpdates);
|
||||
static InstalledModListSource* get(InstalledModListType type);
|
||||
|
||||
std::unordered_set<std::string> getModTags() const override;
|
||||
void setModTags(std::unordered_set<std::string> const& tags) override;
|
||||
|
@ -120,6 +133,28 @@ public:
|
|||
InvalidateQueryAfter<InstalledModsQuery> getQueryMut();
|
||||
};
|
||||
|
||||
struct SuggestedModsQuery final : public LocalModsQueryBase {
|
||||
bool preCheck(ModSource const& src) const;
|
||||
bool queryCheck(ModSource const& src, double& weighted) const;
|
||||
};
|
||||
|
||||
class SuggestedModListSource : public ModListSource {
|
||||
protected:
|
||||
SuggestedModsQuery m_query;
|
||||
|
||||
void resetQuery() override;
|
||||
ProviderTask fetchPage(size_t page, size_t pageSize, bool forceUpdate) override;
|
||||
void setSearchQuery(std::string const& query) override;
|
||||
|
||||
SuggestedModListSource();
|
||||
|
||||
public:
|
||||
static SuggestedModListSource* get();
|
||||
|
||||
std::unordered_set<std::string> getModTags() const override;
|
||||
void setModTags(std::unordered_set<std::string> const& tags) override;
|
||||
};
|
||||
|
||||
enum class ServerModListType {
|
||||
Download,
|
||||
Featured,
|
||||
|
@ -163,5 +198,58 @@ public:
|
|||
void setModTags(std::unordered_set<std::string> const& tags) override;
|
||||
};
|
||||
|
||||
void clearAllModListSourceCaches();
|
||||
bool isRestartRequired();
|
||||
bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out);
|
||||
bool modFuzzyMatch(ModMetadata const& metadata, std::string const& kw, double& out);
|
||||
|
||||
template <std::derived_from<LocalModsQueryBase> Query>
|
||||
void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& query) {
|
||||
std::vector<std::pair<ModSource, double>> filtered;
|
||||
|
||||
// Filter installed mods based on query
|
||||
for (auto& src : mods.mods) {
|
||||
double weighted = 0;
|
||||
bool addToList = true;
|
||||
// Do any checks additional this query has to start off with
|
||||
if (!query.preCheck(src)) {
|
||||
addToList = false;
|
||||
}
|
||||
// If some tags are provided, only return mods that match
|
||||
if (addToList && query.tags.size()) {
|
||||
auto compare = src.getMetadata().getTags();
|
||||
for (auto& tag : query.tags) {
|
||||
if (!compare.contains(tag)) {
|
||||
addToList = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't bother with unnecessary fuzzy match calculations if this mod isn't going to be added anyway
|
||||
if (addToList && query.query) {
|
||||
addToList = query.queryCheck(src, weighted);
|
||||
}
|
||||
if (addToList) {
|
||||
filtered.push_back({ src, weighted });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort list based on score
|
||||
std::sort(filtered.begin(), filtered.end(), [](auto a, auto b) {
|
||||
// Sort primarily by score
|
||||
if (a.second != b.second) {
|
||||
return a.second > b.second;
|
||||
}
|
||||
// Sort secondarily alphabetically
|
||||
return a.first.getMetadata().getName() < b.first.getMetadata().getName();
|
||||
});
|
||||
|
||||
mods.mods.clear();
|
||||
// Pick out only the mods in the page and page size specified in the query
|
||||
for (
|
||||
size_t i = query.page * query.pageSize;
|
||||
i < filtered.size() && i < (query.page + 1) * query.pageSize;
|
||||
i += 1
|
||||
) {
|
||||
mods.mods.push_back(filtered.at(i).first);
|
||||
}
|
||||
|
||||
mods.totalModCount = filtered.size();
|
||||
}
|
||||
|
|
20
loader/src/ui/mods/sources/ModPackListSource.cpp
Normal file
20
loader/src/ui/mods/sources/ModPackListSource.cpp
Normal file
|
@ -0,0 +1,20 @@
|
|||
#include "ModListSource.hpp"
|
||||
|
||||
void ModPackListSource::resetQuery() {}
|
||||
ModPackListSource::ProviderTask ModPackListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
return ProviderTask::immediate(Err(LoadPageError("Coming soon ;)")));
|
||||
}
|
||||
|
||||
ModPackListSource::ModPackListSource() {}
|
||||
|
||||
ModPackListSource* ModPackListSource::get() {
|
||||
static auto inst = new ModPackListSource();
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ModPackListSource::setSearchQuery(std::string const& query) {}
|
||||
|
||||
std::unordered_set<std::string> ModPackListSource::getModTags() const {
|
||||
return {};
|
||||
}
|
||||
void ModPackListSource::setModTags(std::unordered_set<std::string> const& set) {}
|
|
@ -1,9 +1,39 @@
|
|||
#include "ModSource.hpp"
|
||||
#include <Geode/ui/GeodeUI.hpp>
|
||||
#include <server/DownloadManager.hpp>
|
||||
#include <Geode/binding/GameObject.hpp>
|
||||
|
||||
LoadModSuggestionTask loadModSuggestion(LoadProblem const& problem) {
|
||||
// Recommended / suggested are essentially the same thing for the purposes of this
|
||||
if (problem.type == LoadProblem::Type::Recommendation || problem.type == LoadProblem::Type::Suggestion) {
|
||||
auto suggestionID = problem.message.substr(0, problem.message.find(' '));
|
||||
auto suggestionVersionStr = problem.message.substr(problem.message.find(' ') + 1);
|
||||
|
||||
if (auto suggestionVersionRes = ComparableVersionInfo::parse(suggestionVersionStr)) {
|
||||
// todo: should this just always default to installing the latest available version?
|
||||
auto suggestionVersion = suggestionVersionRes->getUnderlyingVersion();
|
||||
|
||||
if (auto mod = std::get_if<Mod*>(&problem.cause)) {
|
||||
return server::getModVersion(suggestionID, suggestionVersion).map(
|
||||
[mod = *mod](auto* result) -> LoadModSuggestionTask::Value {
|
||||
if (result->isOk()) {
|
||||
return ModSuggestion {
|
||||
.suggestion = result->unwrap().metadata,
|
||||
.forMod = mod,
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return LoadModSuggestionTask::immediate(std::nullopt);
|
||||
}
|
||||
|
||||
ModSource::ModSource(Mod* mod) : m_value(mod) {}
|
||||
ModSource::ModSource(server::ServerModMetadata&& metadata) : m_value(metadata) {}
|
||||
ModSource::ModSource(ModSuggestion&& suggestion) : m_value(suggestion) {}
|
||||
|
||||
std::string ModSource::getID() const {
|
||||
return std::visit(makeVisitor {
|
||||
|
@ -11,9 +41,11 @@ std::string ModSource::getID() const {
|
|||
return mod->getID();
|
||||
},
|
||||
[](server::ServerModMetadata const& metadata) {
|
||||
// Versions should be guaranteed to have at least one item
|
||||
return metadata.id;
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
return suggestion.suggestion.getID();
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
ModMetadata ModSource::getMetadata() const {
|
||||
|
@ -24,7 +56,10 @@ ModMetadata ModSource::getMetadata() const {
|
|||
[](server::ServerModMetadata const& metadata) {
|
||||
// Versions should be guaranteed to have at least one item
|
||||
return metadata.versions.front().metadata;
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
return suggestion.suggestion;
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
CCNode* ModSource::createModLogo() const {
|
||||
|
@ -34,7 +69,10 @@ CCNode* ModSource::createModLogo() const {
|
|||
},
|
||||
[](server::ServerModMetadata const& metadata) {
|
||||
return createServerModLogo(metadata.id);
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
return createServerModLogo(suggestion.suggestion.getID());
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
bool ModSource::wantsRestart() const {
|
||||
|
@ -49,14 +87,17 @@ bool ModSource::wantsRestart() const {
|
|||
},
|
||||
[](server::ServerModMetadata const& metdata) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
return false;
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
std::optional<server::ServerModUpdate> ModSource::hasUpdates() const {
|
||||
return m_availableUpdate;
|
||||
}
|
||||
|
||||
ModSource ModSource::tryConvertToMod() const {
|
||||
ModSource ModSource::convertForPopup() const {
|
||||
return std::visit(makeVisitor {
|
||||
[](Mod* mod) {
|
||||
return ModSource(mod);
|
||||
|
@ -66,7 +107,10 @@ ModSource ModSource::tryConvertToMod() const {
|
|||
return ModSource(mod);
|
||||
}
|
||||
return ModSource(server::ServerModMetadata(metadata));
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
return ModSource(ModSuggestion(suggestion));
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
|
||||
|
@ -77,40 +121,35 @@ Mod* ModSource::asMod() const {
|
|||
server::ServerModMetadata const* ModSource::asServer() const {
|
||||
return std::get_if<server::ServerModMetadata>(&m_value);
|
||||
}
|
||||
ModSuggestion const* ModSource::asSuggestion() const {
|
||||
return std::get_if<ModSuggestion>(&m_value);
|
||||
}
|
||||
|
||||
server::ServerRequest<std::optional<std::string>> ModSource::fetchAbout() const {
|
||||
return std::visit(makeVisitor {
|
||||
[](Mod* mod) {
|
||||
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getDetails()));
|
||||
},
|
||||
[](server::ServerModMetadata const& metadata) {
|
||||
return server::getMod(metadata.id).map(
|
||||
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
|
||||
if (result->isOk()) {
|
||||
return Ok(result->unwrap().about);
|
||||
}
|
||||
return Err(result->unwrapErr());
|
||||
}
|
||||
);
|
||||
if (auto mod = this->asMod()) {
|
||||
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getDetails()));
|
||||
}
|
||||
return server::getMod(this->getID()).map(
|
||||
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
|
||||
if (result->isOk()) {
|
||||
return Ok(result->unwrap().about);
|
||||
}
|
||||
return Err(result->unwrapErr());
|
||||
}
|
||||
}, m_value);
|
||||
);
|
||||
}
|
||||
server::ServerRequest<std::optional<std::string>> ModSource::fetchChangelog() const {
|
||||
return std::visit(makeVisitor {
|
||||
[](Mod* mod) {
|
||||
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getChangelog()));
|
||||
},
|
||||
[](server::ServerModMetadata const& metadata) {
|
||||
return server::getMod(metadata.id).map(
|
||||
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
|
||||
if (result->isOk()) {
|
||||
return Ok(result->unwrap().changelog);
|
||||
}
|
||||
return Err(result->unwrapErr());
|
||||
}
|
||||
);
|
||||
if (auto mod = this->asMod()) {
|
||||
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getChangelog()));
|
||||
}
|
||||
return server::getMod(this->getID()).map(
|
||||
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
|
||||
if (result->isOk()) {
|
||||
return Ok(result->unwrap().changelog);
|
||||
}
|
||||
return Err(result->unwrapErr());
|
||||
}
|
||||
}, m_value);
|
||||
);
|
||||
}
|
||||
server::ServerRequest<server::ServerModMetadata> ModSource::fetchServerInfo() const {
|
||||
// Request the info even if this is already a server mod because this might
|
||||
|
@ -144,7 +183,11 @@ server::ServerRequest<std::unordered_set<std::string>> ModSource::fetchValidTags
|
|||
[](server::ServerModMetadata const& metadata) {
|
||||
// Server info tags are always certain to be valid since the server has already validated them
|
||||
return server::ServerRequest<std::unordered_set<std::string>>::immediate(Ok(metadata.tags));
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
// Suggestions are also guaranteed to be valid since they come from the server
|
||||
return server::ServerRequest<std::unordered_set<std::string>>::immediate(Ok(suggestion.suggestion.getTags()));
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
server::ServerRequest<std::optional<server::ServerModUpdate>> ModSource::checkUpdates() {
|
||||
|
@ -164,6 +207,10 @@ server::ServerRequest<std::optional<server::ServerModUpdate>> ModSource::checkUp
|
|||
[](server::ServerModMetadata const& metadata) {
|
||||
// Server mods aren't installed so you can't install updates for them
|
||||
return server::ServerRequest<std::optional<server::ServerModUpdate>>::immediate(Ok(std::nullopt));
|
||||
}
|
||||
},
|
||||
[](ModSuggestion const& suggestion) {
|
||||
// Suggestions also aren't installed so you can't install updates for them
|
||||
return server::ServerRequest<std::optional<server::ServerModUpdate>>::immediate(Ok(std::nullopt));
|
||||
},
|
||||
}, m_value);
|
||||
}
|
||||
|
|
|
@ -5,15 +5,25 @@
|
|||
|
||||
using namespace geode::prelude;
|
||||
|
||||
struct ModSuggestion final {
|
||||
ModMetadata suggestion;
|
||||
Mod* forMod;
|
||||
};
|
||||
|
||||
// you can't put these in ModSuggestion itself because of the concepts in Task :sob:
|
||||
using LoadModSuggestionTask = Task<std::optional<ModSuggestion>, server::ServerProgress>;
|
||||
LoadModSuggestionTask loadModSuggestion(LoadProblem const& problem);
|
||||
|
||||
class ModSource final {
|
||||
private:
|
||||
std::variant<Mod*, server::ServerModMetadata> m_value;
|
||||
std::variant<Mod*, server::ServerModMetadata, ModSuggestion> m_value;
|
||||
std::optional<server::ServerModUpdate> m_availableUpdate;
|
||||
|
||||
public:
|
||||
ModSource() = default;
|
||||
ModSource(Mod* mod);
|
||||
ModSource(server::ServerModMetadata&& metadata);
|
||||
ModSource(ModSuggestion&& suggestion);
|
||||
|
||||
std::string getID() const;
|
||||
ModMetadata getMetadata() const;
|
||||
|
@ -28,10 +38,11 @@ public:
|
|||
|
||||
// Returns a new ModSource that is either a copy of the current source or
|
||||
// an installed version of a server mod
|
||||
ModSource tryConvertToMod() const;
|
||||
ModSource convertForPopup() const;
|
||||
|
||||
Mod* asMod() const;
|
||||
server::ServerModMetadata const* asServer() const;
|
||||
ModSuggestion const* asSuggestion() const;
|
||||
|
||||
server::ServerRequest<server::ServerModMetadata> fetchServerInfo() const;
|
||||
server::ServerRequest<std::optional<std::string>> fetchAbout() const;
|
||||
|
|
99
loader/src/ui/mods/sources/ServerModListSource.cpp
Normal file
99
loader/src/ui/mods/sources/ServerModListSource.cpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
#include "ModListSource.hpp"
|
||||
|
||||
void ServerModListSource::resetQuery() {
|
||||
switch (m_type) {
|
||||
case ServerModListType::Download: {
|
||||
m_query = server::ModsQuery {};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Featured: {
|
||||
m_query = server::ModsQuery {
|
||||
.featured = true,
|
||||
};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Trending: {
|
||||
m_query = server::ModsQuery {
|
||||
.sorting = server::ModsSort::RecentlyUpdated,
|
||||
};
|
||||
} break;
|
||||
|
||||
case ServerModListType::Recent: {
|
||||
m_query = server::ModsQuery {
|
||||
.sorting = server::ModsSort::RecentlyPublished,
|
||||
};
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
ServerModListSource::ProviderTask ServerModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
m_query.page = page;
|
||||
m_query.pageSize = pageSize;
|
||||
return server::getMods(m_query, !forceUpdate).map(
|
||||
[](Result<server::ServerModsList, server::ServerError>* result) -> ProviderTask::Value {
|
||||
if (result->isOk()) {
|
||||
auto list = result->unwrap();
|
||||
auto content = ModListSource::ProvidedMods();
|
||||
for (auto&& mod : std::move(list.mods)) {
|
||||
content.mods.push_back(ModSource(std::move(mod)));
|
||||
}
|
||||
content.totalModCount = list.totalModCount;
|
||||
return Ok(content);
|
||||
}
|
||||
return Err(LoadPageError("Error loading mods", result->unwrapErr().details));
|
||||
},
|
||||
[](auto* prog) {
|
||||
return prog->percentage;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ServerModListSource::ServerModListSource(ServerModListType type)
|
||||
: m_type(type)
|
||||
{
|
||||
this->resetQuery();
|
||||
}
|
||||
|
||||
ServerModListSource* ServerModListSource::get(ServerModListType type) {
|
||||
switch (type) {
|
||||
default:
|
||||
case ServerModListType::Download: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Download);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Featured: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Featured);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Trending: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Trending);
|
||||
return inst;
|
||||
} break;
|
||||
|
||||
case ServerModListType::Recent: {
|
||||
static auto inst = new ServerModListSource(ServerModListType::Recent);
|
||||
return inst;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerModListSource::setSearchQuery(std::string const& query) {
|
||||
m_query.query = query.size() ? std::optional(query) : std::nullopt;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> ServerModListSource::getModTags() const {
|
||||
return m_query.tags;
|
||||
}
|
||||
void ServerModListSource::setModTags(std::unordered_set<std::string> const& tags) {
|
||||
m_query.tags = tags;
|
||||
this->clearCache();
|
||||
}
|
||||
|
||||
server::ModsQuery const& ServerModListSource::getQuery() const {
|
||||
return m_query;
|
||||
}
|
||||
InvalidateQueryAfter<server::ModsQuery> ServerModListSource::getQueryMut() {
|
||||
return InvalidateQueryAfter(m_query, this);
|
||||
}
|
57
loader/src/ui/mods/sources/SuggestedModListSource.cpp
Normal file
57
loader/src/ui/mods/sources/SuggestedModListSource.cpp
Normal file
|
@ -0,0 +1,57 @@
|
|||
#include "ModListSource.hpp"
|
||||
|
||||
bool SuggestedModsQuery::preCheck(ModSource const& src) const {
|
||||
// There are no extra fields in SuggestedModsQuery
|
||||
return true;
|
||||
}
|
||||
bool SuggestedModsQuery::queryCheck(ModSource const& src, double& weighted) const {
|
||||
if (!modFuzzyMatch(src.asSuggestion()->suggestion, *query, weighted)) {
|
||||
return false;
|
||||
}
|
||||
return modFuzzyMatch(src.asSuggestion()->forMod->getMetadata(), *query, weighted);
|
||||
}
|
||||
|
||||
void SuggestedModListSource::resetQuery() {
|
||||
m_query = SuggestedModsQuery();
|
||||
}
|
||||
SuggestedModListSource::ProviderTask SuggestedModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
|
||||
m_query.page = page;
|
||||
m_query.pageSize = pageSize;
|
||||
std::vector<LoadModSuggestionTask> tasks;
|
||||
for (auto problem : Loader::get()->getRecommendations()) {
|
||||
tasks.push_back(loadModSuggestion(problem));
|
||||
}
|
||||
return LoadModSuggestionTask::all(std::move(tasks)).map(
|
||||
[query = m_query](auto* results) -> ProviderTask::Value {
|
||||
auto content = ProvidedMods();
|
||||
for (auto& result : *results) {
|
||||
if (result && *result) {
|
||||
content.mods.push_back(ModSource(ModSuggestion(**result)));
|
||||
}
|
||||
}
|
||||
// Filter the results based on the current search
|
||||
// query and return them
|
||||
filterModsWithLocalQuery(content, query);
|
||||
return Ok(std::move(content));
|
||||
},
|
||||
[](auto*) -> ProviderTask::Progress { return std::nullopt; }
|
||||
);
|
||||
}
|
||||
|
||||
SuggestedModListSource::SuggestedModListSource() {}
|
||||
|
||||
SuggestedModListSource* SuggestedModListSource::get() {
|
||||
static auto inst = new SuggestedModListSource();
|
||||
return inst;
|
||||
}
|
||||
|
||||
void SuggestedModListSource::setSearchQuery(std::string const& query) {
|
||||
m_query.query = query.size() ? std::optional(query) : std::nullopt;
|
||||
}
|
||||
std::unordered_set<std::string> SuggestedModListSource::getModTags() const {
|
||||
return m_query.tags;
|
||||
}
|
||||
void SuggestedModListSource::setModTags(std::unordered_set<std::string> const& tags) {
|
||||
m_query.tags = tags;
|
||||
this->clearCache();
|
||||
}
|
28
loader/src/ui/other/FixIssuesPopup.cpp
Normal file
28
loader/src/ui/other/FixIssuesPopup.cpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#include "FixIssuesPopup.hpp"
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
|
||||
bool FixIssuesPopup::setup() {
|
||||
m_noElasticity = true;
|
||||
|
||||
this->setTitle("Problems Loading Mods");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FixIssuesPopup* FixIssuesPopup::create() {
|
||||
auto ret = new FixIssuesPopup();
|
||||
if (ret && ret->init(350, 280)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void checkLoadingIssues(CCNode* targetScene) {
|
||||
if (Loader::get()->getProblems().size()) {
|
||||
auto popup = FixIssuesPopup::create();
|
||||
popup->m_scene = targetScene;
|
||||
popup->show();
|
||||
}
|
||||
}
|
19
loader/src/ui/other/FixIssuesPopup.hpp
Normal file
19
loader/src/ui/other/FixIssuesPopup.hpp
Normal file
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <Geode/ui/Popup.hpp>
|
||||
#include "../mods/GeodeStyle.hpp"
|
||||
#include "../mods/list/ModProblemItemList.hpp"
|
||||
|
||||
using namespace geode::prelude;
|
||||
|
||||
class FixIssuesPopup : public GeodePopup<> {
|
||||
protected:
|
||||
ModProblemItemList* m_list;
|
||||
|
||||
bool setup() override;
|
||||
|
||||
public:
|
||||
static FixIssuesPopup* create();
|
||||
};
|
||||
|
||||
void checkLoadingIssues(CCNode* targetScene = nullptr);
|
|
@ -114,14 +114,20 @@ Result<VersionInfo> VersionInfo::parse(std::string const& string) {
|
|||
}
|
||||
|
||||
std::string VersionInfo::toString(bool includeTag) const {
|
||||
return this->toVString();
|
||||
}
|
||||
std::string VersionInfo::toVString(bool includeTag) const {
|
||||
return fmt::format("v{}", this->toNonVString(includeTag));
|
||||
}
|
||||
std::string VersionInfo::toNonVString(bool includeTag) const {
|
||||
if (includeTag && m_tag) {
|
||||
return fmt::format(
|
||||
"v{}.{}.{}{}",
|
||||
"{}.{}.{}{}",
|
||||
m_major, m_minor, m_patch,
|
||||
m_tag.value().toSuffixString()
|
||||
);
|
||||
}
|
||||
return fmt::format("v{}.{}.{}", m_major, m_minor, m_patch);
|
||||
return fmt::format("{}.{}.{}", m_major, m_minor, m_patch);
|
||||
}
|
||||
|
||||
std::string geode::format_as(VersionInfo const& version) {
|
||||
|
|
Loading…
Reference in a new issue