diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1221ec8..a237e2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: push: branches: - "main" + - "v3-nong-data" jobs: build: diff --git a/CMakeLists.txt b/CMakeLists.txt index 889d1a4..48748f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ file(GLOB SOURCES src/hooks/*.cpp src/events/*.cpp src/api/*.cpp + src/utils/*.cpp src/*.cpp ) diff --git a/include/index.hpp b/include/index.hpp new file mode 100644 index 0000000..3943bf1 --- /dev/null +++ b/include/index.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +#include "platform.hpp" + +namespace jukebox { + +struct JUKEBOX_DLL IndexSource final { + std::string m_url; + bool m_userAdded; + bool m_enabled; + + bool operator==(IndexSource const& other) const { + return m_url == other.m_url && m_userAdded == other.m_userAdded && m_enabled == other.m_enabled; + } +}; + +struct IndexMetadata final { + struct Links final { + std::optional m_discord = std::nullopt; + }; + + struct Features { + struct RequestParams final { + std::string m_url; + bool m_params; + }; + + struct RequestLink final { + std::string m_url; + }; + + enum class SupportedSongType { + local, + youtube, + hosted, + }; + + struct Submit final { + std::optional m_requestParams = std::nullopt; + std::optional m_preSubmitMessage; + std::unordered_map m_supportedSongTypes = { + {SupportedSongType::local, false}, + {SupportedSongType::youtube, false}, + {SupportedSongType::hosted, false}, + }; + }; + + struct Report final { + std::optional m_requestParams = std::nullopt; + }; + + std::optional m_submit = std::nullopt; + std::optional m_report = std::nullopt; + }; + + int m_manifest; + std::string m_url; + std::string m_id; + std::string m_name; + std::optional m_description; + std::optional m_lastUpdate; + Links m_links; + Features m_features; +}; + +} diff --git a/include/index_serialize.hpp b/include/index_serialize.hpp new file mode 100644 index 0000000..8ee75ef --- /dev/null +++ b/include/index_serialize.hpp @@ -0,0 +1,191 @@ +#pragma once + +#include "index.hpp" + +template<> +struct matjson::Serialize { + static geode::Result from_json(matjson::Value const& value, int manifest) { + if (manifest != 1) return geode::Err("Using unsupported manifest version: " + std::to_string(manifest)); + + if (!value.is_object()) { + return geode::Err("Expected object in requestParams"); + } + if (!value.contains("url") || !value["url"].is_string()) { + return geode::Err("Expected url in requestParams"); + } + if (!value.contains("params") || !value["params"].is_bool()) { + return geode::Err("Expected params in requestParams"); + } + + return geode::Ok(jukebox::IndexMetadata::Features::RequestParams { + .m_url = value["url"].as_string(), + .m_params = value["params"].as_bool() + }); + } +}; + +template<> +struct matjson::Serialize { +protected: + static geode::Result getSupportedSongTypeFromString(const std::string& str) { + if (str == "local") return geode::Ok(jukebox::IndexMetadata::Features::SupportedSongType::local); + if (str == "youtube") return geode::Ok(jukebox::IndexMetadata::Features::SupportedSongType::youtube); + if (str == "hosted") return geode::Ok(jukebox::IndexMetadata::Features::SupportedSongType::hosted); + return geode::Err("Invalid supported song type: " + str); + } +public: + static geode::Result from_json(matjson::Value const& value, int manifest) { + if (manifest != 1) return geode::Err("Using unsupported manifest version: " + std::to_string(manifest)); + + if (!value.is_object()) { + return geode::Err("Expected object in features"); + } + + jukebox::IndexMetadata::Features features; + + const auto& featuresObj = value; + + if (featuresObj.contains("submit")) { + if (!featuresObj["submit"].is_object()) { + return geode::Err("Expected submit to be an object in features"); + } + const auto& submitObj = featuresObj["submit"]; + + features.m_submit = jukebox::IndexMetadata::Features::Submit { }; + + if (submitObj.contains("preSubmitMessage")) { + if (!submitObj["preSubmitMessage"].is_string()) { + return geode::Err("Expected preSubmitMessage to be a string in submit"); + } + features.m_submit.value().m_preSubmitMessage = submitObj["preSubmitMessage"].as_string(); + } + + if (submitObj.contains("supportedSongTypes")) { + if (!submitObj["supportedSongTypes"].is_array()) { + return geode::Err("Expected supportedSongTypes to be an array in submit"); + } + for (const auto& songType : submitObj["supportedSongTypes"].as_array()) { + if (!songType.is_string()) { + return geode::Err("Expected supportedSongTypes to be an array of strings in submit"); + } + auto supportedSongType = getSupportedSongTypeFromString(songType.as_string()); + if (supportedSongType.isErr()) { + return geode::Err(supportedSongType.error()); + } + features.m_submit.value().m_supportedSongTypes[supportedSongType.value()] = true; + } + } + + if (submitObj.contains("requestParams")) { + auto requestParams = matjson::Serialize::from_json(submitObj["requestParams"], manifest); + if (requestParams.isErr()) { + return geode::Err(requestParams.error()); + } + features.m_submit.value().m_requestParams = requestParams.value(); + } + } + + if (featuresObj.contains("report")) { + if (!featuresObj["report"].is_object()) { + return geode::Err("Expected search to be an object in features"); + } + const auto& searchObj = featuresObj["report"]; + + features.m_report = jukebox::IndexMetadata::Features::Report { }; + + if (searchObj.contains("requestParams")) { + auto requestParams = matjson::Serialize::from_json(searchObj["requestParams"], manifest); + if (requestParams.isErr()) { + return geode::Err(requestParams.error()); + } + features.m_report.value().m_requestParams = requestParams.value(); + } + } + + return geode::Ok(features); + }; +}; + +template<> +struct matjson::Serialize { + static geode::Result from_json(matjson::Value const& value) { + if (!value.is_object()) { + return geode::Err("Expected object"); + } + + if (!value.contains("manifest") || !value["manifest"].is_number()) { + return geode::Err("Expected manifest version"); + } + const int manifestVersion = value["manifest"].as_int(); + + if (manifestVersion == 1) { + // Links + jukebox::IndexMetadata::Links links; + if (value.contains("links") && value["links"].is_object()) { + const auto& linksObj = value["links"]; + if (linksObj.contains("discord")) { + if (!linksObj["discord"].is_string()) { + return geode::Err("Expected discord to be a string in links"); + } + } + links.m_discord = linksObj["discord"].as_string(); + } + + // Features + const auto featuresResult = value.contains("features") + ? matjson::Serialize::from_json(value["features"], manifestVersion) + : geode::Ok(jukebox::IndexMetadata::Features()); + if (featuresResult.has_error()) { + return geode::Err(featuresResult.error()); + } + + // Validate fields + if (!value.contains("name") || !value["name"].is_string()) { + return geode::Err("Expected name"); + } + if (!value.contains("id") || !value["id"].is_string()) { + return geode::Err("Expected id"); + } + if (value.contains("description") && !value["description"].is_string()) { + return geode::Err("Description must be a string"); + } + + return geode::Ok(jukebox::IndexMetadata { + .m_manifest = manifestVersion, + .m_url = value["url"].as_string(), + .m_id = value["id"].as_string(), + .m_name = value["name"].as_string(), + .m_description = value.contains("description") + && value["description"].is_string() + ? std::optional(value["description"].as_string()) + : std::nullopt, + .m_lastUpdate = value.contains("lastUpdate") && value["lastUpdate"].is_number() + ? std::optional(value["lastUpdate"].as_int()) + : std::nullopt, + .m_links = links, + .m_features = featuresResult.value() + }); + } + + return geode::Err("Using unsupported manifest version: " + std::to_string(manifestVersion)); + } +}; + +template<> +struct matjson::Serialize { + static jukebox::IndexSource from_json(matjson::Value const& value) { + return jukebox::IndexSource { + .m_url = value["url"].as_string(), + .m_userAdded = value["userAdded"].as_bool(), + .m_enabled = value["enabled"].as_bool() + }; + } + + static matjson::Value to_json(jukebox::IndexSource const& value) { + auto indexObject = matjson::Object(); + indexObject["url"] = value.m_url; + indexObject["userAdded"] = value.m_userAdded; + indexObject["enabled"] = value.m_enabled; + return indexObject; + } +}; diff --git a/include/jukebox.hpp b/include/jukebox.hpp index 3b7ea15..0a6e0c2 100644 --- a/include/jukebox.hpp +++ b/include/jukebox.hpp @@ -1,56 +1,48 @@ #pragma once -#include "../src/types/song_info.hpp" +#include "../include/nong.hpp" +#include "platform.hpp" #include -#ifdef GEODE_IS_WINDOWS - #ifdef FLEYM_JUKEBOX_EXPORTING - #define JUKEBOX_DLL __declspec(dllexport) - #else - #define JUKEBOX_DLL __declspec(dllimport) - #endif -#else - #define JUKEBOX_DLL __attribute__((visibility("default"))) -#endif namespace jukebox { - /** - * Adds a NONG to the JSON of a songID - * - * @param song the song to add - * @param songID the id of the song - */ - JUKEBOX_DLL void addNong(SongInfo const& song, int songID); + // /** + // * Adds a NONG to the JSON of a songID + // * + // * @param song the song to add + // * @param songID the id of the song + // */ + // JUKEBOX_DLL void addNong(SongInfo const& song, int songID); - /** - * Sets the active song of a songID - * - * @param song the song to set as active - * @param songID the id of the song - * @param customSongWidget optional custom song widget to update - */ - JUKEBOX_DLL void setActiveNong(SongInfo const& song, int songID, const std::optional>& customSongWidget); + // /** + // * Sets the active song of a songID + // * + // * @param song the song to set as active + // * @param songID the id of the song + // * @param customSongWidget optional custom song widget to update + // */ + // JUKEBOX_DLL void setActiveNong(SongInfo const& song, int songID, const std::optional>& customSongWidget); - /** - * Gets the active song of a songID - * - * @param songID the id of the song - */ - JUKEBOX_DLL std::optional getActiveNong(int songID); + // /** + // * Gets the active song of a songID + // * + // * @param songID the id of the song + // */ + // JUKEBOX_DLL std::optional getActiveNong(int songID); - /** - * Deletes a NONG from the JSON of a songID - * - * @param song the song to delete - * @param songID the id of the replaced song - * @param deleteFile whether to delete the corresponding audio file created by Jukebox - */ - JUKEBOX_DLL void deleteNong(SongInfo const& song, int songID, bool deleteFile = true); + // /** + // * Deletes a NONG from the JSON of a songID + // * + // * @param song the song to delete + // * @param songID the id of the replaced song + // * @param deleteFile whether to delete the corresponding audio file created by Jukebox + // */ + // JUKEBOX_DLL void deleteNong(SongInfo const& song, int songID, bool deleteFile = true); - /** - * Sets the song of the songID as the default song provided by GD - * - * @param songID the id of the song - */ - JUKEBOX_DLL std::optional getDefaultNong(int songID); + // /** + // * Sets the song of the songID as the default song provided by GD + // * + // * @param songID the id of the song + // */ + // JUKEBOX_DLL std::optional getDefaultNong(int songID); } diff --git a/include/nong.hpp b/include/nong.hpp new file mode 100644 index 0000000..6eee9d6 --- /dev/null +++ b/include/nong.hpp @@ -0,0 +1,240 @@ +#pragma once + + +#include +#include +#include +#include +#include +#include +#include + +#include "Geode/binding/SongInfoObject.hpp" +#include "Geode/utils/Result.hpp" + +#include "platform.hpp" + +namespace jukebox { + +struct JUKEBOX_DLL SongMetadata final { + int m_gdID; + std::string m_uniqueID; + std::string m_name; + std::string m_artist; + std::optional m_level; + int m_startOffset; + + SongMetadata( + int gdID, + std::string uniqueID, + std::string name, + std::string artist, + std::optional level = std::nullopt, + int offset = 0 + ) : m_gdID(gdID), + m_uniqueID(uniqueID), + m_name(name), + m_artist(artist), + m_level(level), + m_startOffset(offset) + {} + + bool operator==(const SongMetadata& other) const { + return m_gdID == other.m_gdID && + m_uniqueID == other.m_uniqueID && + m_name == other.m_name && + m_artist == other.m_artist && + m_level == other.m_level && + m_startOffset == other.m_startOffset; + } +}; + +class JUKEBOX_DLL LocalSong final { +private: + class Impl; + + std::unique_ptr m_impl; +public: + LocalSong( + SongMetadata&& metadata, + const std::filesystem::path& path + ); + + LocalSong(const LocalSong& other); + + LocalSong& operator=(const LocalSong& other); + + LocalSong(LocalSong&& other); + LocalSong& operator=(LocalSong&& other); + + ~LocalSong(); + + SongMetadata* metadata() const; + std::filesystem::path path() const; + + // Might not be used + static LocalSong createUnknown(int songID); + static LocalSong fromSongObject(SongInfoObject* obj); +}; + +class JUKEBOX_DLL YTSong final { +private: + class Impl; + + std::unique_ptr m_impl; + +public: + YTSong( + SongMetadata&& metadata, + std::string youtubeID, + std::optional m_indexID, + std::optional path = std::nullopt + ); + YTSong(const YTSong& other); + YTSong& operator=(const YTSong& other); + + YTSong(YTSong&& other); + YTSong& operator=(YTSong&& other); + + ~YTSong(); + + SongMetadata* metadata() const; + std::string youtubeID() const; + std::optional indexID() const; + std::optional path() const; +}; + + +class JUKEBOX_DLL HostedSong final { +private: + class Impl; + + std::unique_ptr m_impl; + +public: + HostedSong( + SongMetadata&& metadata, + std::string url, + std::optional m_indexID, + std::optional path = std::nullopt + ); + HostedSong(const HostedSong& other); + HostedSong& operator=(const HostedSong& other); + + HostedSong(HostedSong&& other); + HostedSong& operator=(HostedSong&& other); + + ~HostedSong(); + + SongMetadata* metadata() const; + std::string url() const; + std::optional indexID() const; + std::optional path() const; +}; + +class Nong; + +class JUKEBOX_DLL Nongs final { +private: + class Impl; + + std::unique_ptr m_impl; +public: + Nongs(int songID, LocalSong&& defaultSong); + Nongs(int songID); + + // No copies for this one + Nongs(const Nongs&) = delete; + Nongs& operator=(const Nongs&) = delete; + + Nongs(Nongs&&); + Nongs& operator=(Nongs&&); + + ~Nongs(); + + int songID() const; + LocalSong* defaultSong() const; + std::string active() const; + Nong activeNong() const; + + bool isDefaultActive() const; + + /** + * Returns Err if there is no NONG with the given path for the song ID + * Otherwise, returns ok + */ + geode::Result<> setActive(const std::string& uniqueID); + geode::Result<> merge(Nongs&&); + // Remove all custom nongs and set the default song as active + geode::Result<> deleteAllSongs(); + geode::Result<> deleteSong(const std::string& uniqueID, bool audio = true); + geode::Result<> deleteSongAudio(const std::string& uniqueID); + std::optional getNongFromID(const std::string& uniqueID) const; + geode::Result<> replaceSong(std::string prevUniqueID, Nong&& song); + + std::vector>& locals(); + std::vector>& youtube(); + std::vector>& hosted(); + + geode::Result add(LocalSong song); + geode::Result add(YTSong song); + geode::Result add(HostedSong song); + geode::Result<> add(Nong&& song); +}; + +class JUKEBOX_DLL Manifest { + friend class NongManager; +private: + int m_version = s_latestVersion; + std::unordered_map> m_nongs = {}; +public: + constexpr static inline int s_latestVersion = 4; + + Manifest() = default; + Manifest(const Manifest&) = delete; + Manifest& operator=(const Manifest&) = delete; + + int version() const { + return m_version; + } +}; + +class JUKEBOX_DLL Nong final { +private: + class Impl; + + std::unique_ptr m_impl; +public: + Nong(const LocalSong& local); + Nong(const YTSong& yt); + Nong(const HostedSong& hosted); + + Nong(const Nong&); + Nong& operator=(const Nong&); + + Nong(Nong&&); + Nong& operator=(Nong&&); + + ~Nong(); + + enum class Type { + Local, + YT, + Hosted, + }; + + SongMetadata* metadata() const; + std::optional path() const; + std::optional indexID() const; + geode::Result toNongs() const; + Type type() const; + + template + ReturnType visit( + std::function local, + std::function yt, + std::function hosted + ) const; +}; + +} diff --git a/include/nong_serialize.hpp b/include/nong_serialize.hpp new file mode 100644 index 0000000..41f0328 --- /dev/null +++ b/include/nong_serialize.hpp @@ -0,0 +1,368 @@ +#pragma once + +#include +#include +#include +#include + +#include "Geode/loader/Log.hpp" +#include "Geode/utils/Result.hpp" +#include "Geode/utils/string.hpp" +#include "nong.hpp" + +template<> +struct matjson::Serialize { + static geode::Result from_json( + const matjson::Value& value, + int songID + ) { + if (!(value.contains("name") || !value["name"].is_string())) { + return geode::Err("Invalid JSON key name"); + } + if (!value.contains("artist") || !value["artist"].is_string()) { + return geode::Err("Invalid JSON key artist"); + } + if (!value.contains("unique_id") || !value["unique_id"].is_string()) { + return geode::Err("Invalid JSON key artist"); + } + + return geode::Ok(jukebox::SongMetadata { + songID, + value["unique_id"].as_string(), + value["name"].as_string(), + value["artist"].as_string(), + value.contains("level") + && value["level"].is_string() + ? std::optional(value["level"].as_string()) + : std::nullopt, + value.contains("offset") + && value["offset"].is_number() + ? value["offset"].as_int() + : 0 + }); + } +}; + +template<> +struct matjson::Serialize { + static geode::Result from_json( + const matjson::Value& value, + int songID + ) { + auto metadata = matjson::Serialize + ::from_json(value, songID); + + if (metadata.isErr()) { + return geode::Err( + fmt::format( + "Local Song {} is invalid. Reason: {}", + value.dump(matjson::NO_INDENTATION), + metadata.unwrapErr() + ) + ); + } + + if (!value.contains("path") || !value["path"].is_string()) { + return geode::Err( + "Local Song {} is invalid. Reason: invalid path", + value.dump(matjson::NO_INDENTATION) + ); + } + + return geode::Ok(jukebox::LocalSong { + std::move(metadata.unwrap()), + value["path"].as_string() + }); + } + + static geode::Result to_json(const jukebox::LocalSong& value) { + matjson::Object ret = {}; + ret["name"] = value.metadata()->m_name; + ret["unique_id"] = value.metadata()->m_uniqueID; + ret["artist"] = value.metadata()->m_artist; + #ifdef GEODE_IS_WINDOWS + ret["path"] = geode::utils::string::wideToUtf8(value.path().c_str()); + #else + ret["path"] = value.path().string(); + #endif + ret["offset"] = value.metadata()->m_startOffset; + if (value.metadata()->m_level.has_value()) { + ret["level"] = value.metadata()->m_level.value(); + } + return geode::Ok(ret); + } +}; + +template<> +struct matjson::Serialize { + static geode::Result from_json( + const matjson::Value& value, + int songID + ) { + auto metadata = matjson::Serialize + ::from_json(value, songID); + + if (metadata.isErr()) { + return geode::Err( + fmt::format( + "Local Song {} is invalid. Reason: {}", + value.dump(matjson::NO_INDENTATION), + metadata.unwrapErr() + ) + ); + } + + if (value.contains("path") && !value["path"].is_string()) { + return geode::Err( + "YT Song {} is invalid. Reason: invalid path", + value.dump(matjson::NO_INDENTATION) + ); + } + + if (!value.contains("youtube_id") || !value["youtube_id"].is_string()) { + return geode::Err( + "YT Song {} is invalid. Reason: invalid youtube ID", + value.dump(matjson::NO_INDENTATION) + ); + } + + return geode::Ok(jukebox::YTSong { + std::move(metadata.unwrap()), + value["youtube_id"].as_string(), + value.contains("index_id") && value["index_id"].is_string() ? std::optional(value["index_id"].as_string()) : std::nullopt, + value.contains("path") ? std::optional(value["path"].as_string()) : std::nullopt, + }); + } + + static geode::Result to_json(const jukebox::YTSong& value) { + matjson::Object ret = {}; + ret["name"] = value.metadata()->m_name; + ret["unique_id"] = value.metadata()->m_uniqueID; + ret["artist"] = value.metadata()->m_artist; + ret["offset"] = value.metadata()->m_startOffset; + ret["youtube_id"] = value.youtubeID(); + if (value.indexID().has_value()) { + ret["index_id"] = value.indexID().value(); + } + + if (value.path().has_value()) { + #ifdef GEODE_IS_WINDOWS + ret["path"] = geode::utils::string::wideToUtf8(value.path().value().c_str()); + #else + ret["path"] = value.path().value().string(); + #endif + } + + if (value.metadata()->m_level.has_value()) { + ret["level"] = value.metadata()->m_level.value(); + } + return geode::Ok(ret); + } +}; + +template<> +struct matjson::Serialize { + static geode::Result from_json( + const matjson::Value& value, + int songID + ) { + auto metadata = matjson::Serialize + ::from_json(value, songID); + + if (metadata.isErr()) { + return geode::Err( + fmt::format( + "Local Song {} is invalid. Reason: {}", + value.dump(matjson::NO_INDENTATION), + metadata.unwrapErr() + ) + ); + } + + if (value.contains("path") && !value["path"].is_string()) { + return geode::Err( + "Hosted Song {} is invalid. Reason: invalid path", + value.dump(matjson::NO_INDENTATION) + ); + } + + if (!value.contains("url") || !value["url"].is_string()) { + return geode::Err( + "Hosted Song {} is invalid. Reason: invalid url", + value.dump(matjson::NO_INDENTATION) + ); + } + + return geode::Ok(jukebox::HostedSong{ + std::move(metadata.unwrap()), + value["url"].as_string(), + value.contains("index_id") && value["index_id"].is_string() ? std::optional(value["index_id"].as_string()) : std::nullopt, + value.contains("path") ? std::optional(value["path"].as_string()) : std::nullopt, + }); + } + + static geode::Result to_json(const jukebox::HostedSong& value) { + matjson::Object ret = {}; + ret["name"] = value.metadata()->m_name; + ret["unique_id"] = value.metadata()->m_uniqueID; + ret["artist"] = value.metadata()->m_artist; + ret["offset"] = value.metadata()->m_startOffset; + ret["url"] = value.url(); + if (value.indexID().has_value()) { + ret["index_id"] = value.indexID(); + } + + if (value.path().has_value()) { + #ifdef GEODE_IS_WINDOWS + ret["path"] = geode::utils::string::wideToUtf8(value.path().value().c_str()); + #else + ret["path"] = value.path().value().string(); + #endif + } + + if (value.metadata()->m_level.has_value()) { + ret["level"] = value.metadata()->m_level.value(); + } + return geode::Ok(ret); + } +}; + +template<> +struct matjson::Serialize { + static geode::Result to_json( + jukebox::Nongs& value + ) { + matjson::Object ret = {}; + + auto resDefault = matjson::Serialize::to_json(*value.defaultSong()); + if (resDefault.isErr()) { + return geode::Err(resDefault.error()); + } + ret["default"] = resDefault.ok(); + + ret["active"] = value.active(); + + matjson::Array locals = {}; + for (auto& local : value.locals()) { + auto res = matjson::Serialize::to_json(*local); + if (res.isErr()) { + return geode::Err(res.error()); + } + locals.push_back(res.ok()); + } + ret["locals"] = locals; + + matjson::Array youtubes = {}; + for (auto& youtube : value.youtube()) { + auto res = matjson::Serialize::to_json(*youtube); + if (res.isErr()) { + return geode::Err(res.error()); + } + youtubes.push_back(res.ok()); + } + ret["youtube"] = youtubes; + + matjson::Array hosteds = {}; + for (auto& hosted : value.hosted()) { + auto res = matjson::Serialize::to_json(*hosted); + if (res.isErr()) { + return geode::Err(res.error()); + } + hosteds.push_back(res.ok()); + } + ret["hosted"] = hosteds; + + return geode::Ok(ret); + } + + static geode::Result from_json( + const matjson::Value& value, + int songID + ) { + if (!value.contains("default") || !value["default"].is_object()) { + return geode::Err("Invalid nongs object for id {}", songID); + } + + auto defaultSong = matjson::Serialize + ::from_json(value["default"].as_object(), songID); + + if (defaultSong.isErr()) { + // TODO think about something + // Try recwovery if the song ID has some nongs + return geode::Err("Failed to parse default song for id {}", songID); + } + + jukebox::Nongs nongs = { + songID, + std::move(defaultSong.unwrap()) + }; + + if (value.contains("locals") && value["locals"].is_array()) { + for (auto& local : value["locals"].as_array()) { + auto res = matjson::Serialize + ::from_json(local, songID); + + if (res.isErr()) { + geode::log::error("{}", res.unwrapErr()); + continue; + } + + auto song = res.unwrap(); + + nongs.locals() + .push_back( + std::make_unique(song) + ); + } + } + + if (value.contains("youtube") && value["youtube"].is_array()) { + for (auto& yt : value["youtube"].as_array()) { + auto res = matjson::Serialize + ::from_json(yt, songID); + + if (res.isErr()) { + geode::log::error("{}", res.unwrapErr()); + continue; + } + + auto song = res.unwrap(); + + nongs.youtube() + .push_back( + std::make_unique(song) + ); + } + } + + if (value.contains("hosted") && value["hosted"].is_array()) { + for (auto& hosted : value["hosted"].as_array()) { + auto res = matjson::Serialize + ::from_json(hosted, songID); + + if (res.isErr()) { + geode::log::error("{}", res.unwrapErr()); + } + + auto song = res.unwrap(); + + nongs.hosted() + .push_back( + std::make_unique(song) + ); + } + } + + if (!value.contains("active") || !value["active"].is_string()) { + // Can't fail... + auto _ = nongs.setActive(defaultSong.unwrap().metadata()->m_uniqueID); + } else { + if (auto res = nongs.setActive(value["active"].as_string()); res.isErr()) { + // Can't fail... + auto _ = nongs.setActive(defaultSong.unwrap().metadata()->m_uniqueID); + } + } + + return geode::Ok(std::move(nongs)); + } +}; diff --git a/include/platform.hpp b/include/platform.hpp new file mode 100644 index 0000000..1b74bc3 --- /dev/null +++ b/include/platform.hpp @@ -0,0 +1,11 @@ +#pragma once + +#ifdef GEODE_IS_WINDOWS + #ifdef FLEYM_JUKEBOX_EXPORTING + #define JUKEBOX_DLL __declspec(dllexport) + #else + #define JUKEBOX_DLL __declspec(dllimport) + #endif +#else + #define JUKEBOX_DLL __attribute__((visibility("default"))) +#endif \ No newline at end of file diff --git a/mod.json b/mod.json index 1f76cd4..ff4bbea 100644 --- a/mod.json +++ b/mod.json @@ -1,20 +1,28 @@ { "geode": "3.8.1", - "version": "2.11.0", + "version": "3.0.0-alpha.1", "id": "fleym.nongd", "name": "Jukebox", "gd": { "win": "2.206", "android": "2.206" }, - "developer": "Fleym", + "developers": [ "Fleym", "Flafy" ], "description": "A simple song manager for Geometry Dash", "repository": "https://github.com/Fleeym/jukebox", + "links": { + "community": "https://discord.gg/SFE7qxYFyU", + "source": "https://github.com/Fleeym/jukebox" + }, "issues": { - "info": "For any issues regarding this mod, send me a message on my discord: 'fleeym'. If you can, please give the level or song ID you are having problems with." + "info": "For any issues regarding this mod, send me a message on my discord: 'fleeym'. If you can, please give the level or song ID you are having problems with.", + "url": "https://github.com/Fleeym/jukebox/issues" }, "tags": [ - "music" + "music", + "online", + "utility", + "interface" ], "settings": { "fix-empty-size": { @@ -28,12 +36,20 @@ "type": "bool", "description": "Try to autocomplete song info from metadata when adding. Causes a tiny big of lag after picking a song file.", "default": true + }, + "indexes": { + "name": "Indexes", + "description": "Jukebox Indexes.\nFrom indexes the mod knows where to fetch the songs from.", + "type": "custom", + "default": [ + {"url": "https://raw.githubusercontent.com/FlafyDev/auto-nong-indexes/v2/auto-nong-index.min.json", "enabled": true, "userAdded": false}, + {"url": "https://raw.githubusercontent.com/FlafyDev/auto-nong-indexes/v2/sfh-index.min.json", "enabled": true, "userAdded": false} + ] } }, "api": { "include": [ - "include/*.hpp", - "src/types/song_info.hpp" + "include/*.hpp" ] }, "resources": { diff --git a/resources/JB_Edit.png b/resources/JB_Edit.png new file mode 100644 index 0000000..f301b75 Binary files /dev/null and b/resources/JB_Edit.png differ diff --git a/src/api/jukebox.cpp b/src/api/jukebox.cpp index 07b9fae..ef9422f 100644 --- a/src/api/jukebox.cpp +++ b/src/api/jukebox.cpp @@ -1,45 +1,45 @@ -#include "../../include/jukebox.hpp" +#include "../../../include/jukebox.hpp" #include "../managers/nong_manager.hpp" namespace fs = std::filesystem; -namespace jukebox { - JUKEBOX_DLL void addNong(SongInfo const& song, int songID) { - NongManager::get()->addNong(song, songID); - } +// namespace jukebox { +// JUKEBOX_DLL void addNong(SongInfo const& song, int songID) { +// NongManager::get()->addNong(song, songID); +// } - JUKEBOX_DLL void setActiveNong(SongInfo const& song, int songID, const std::optional>& customSongWidget) { - NongData data = NongManager::get()->getNongs(songID).value(); +// JUKEBOX_DLL void setActiveNong(SongInfo const& song, int songID, const std::optional>& customSongWidget) { +// Nongs data = NongManager::get()->getNongs(songID).value(); - if (!fs::exists(song.path)) { - return; - } +// if (!fs::exists(song.path)) { +// return; +// } - data.active = song.path; +// data.active = song.path; - NongManager::get()->saveNongs(data, songID); +// NongManager::get()->saveNongs(data, songID); - if (customSongWidget.has_value()) { - const Ref widget = customSongWidget.value(); +// if (customSongWidget.has_value()) { +// const Ref widget = customSongWidget.value(); - widget->m_songInfoObject->m_artistName = song.authorName; - widget->m_songInfoObject->m_songName = song.songName; - if (song.songUrl != "local") { - widget->m_songInfoObject->m_songUrl = song.songUrl; - } - widget->updateSongInfo(); - } - } +// widget->m_songInfoObject->m_artistName = song.authorName; +// widget->m_songInfoObject->m_songName = song.songName; +// if (song.songUrl != "local") { +// widget->m_songInfoObject->m_songUrl = song.songUrl; +// } +// widget->updateSongInfo(); +// } +// } - JUKEBOX_DLL std::optional getActiveNong(int songID) { - return NongManager::get()->getActiveNong(songID); - } +// JUKEBOX_DLL std::optional getActiveNong(int songID) { +// return NongManager::get()->getActiveNong(songID); +// } - JUKEBOX_DLL void deleteNong(SongInfo const& song, int songID, bool deleteFile) { - NongManager::get()->deleteNong(song, songID, deleteFile); - } +// JUKEBOX_DLL void deleteNong(SongInfo const& song, int songID, bool deleteFile) { +// NongManager::get()->deleteNong(song, songID, deleteFile); +// } - JUKEBOX_DLL std::optional getDefaultNong(int songID) { - return NongManager::get()->getDefaultNong(songID); - } -} +// JUKEBOX_DLL std::optional getDefaultNong(int songID) { +// return NongManager::get()->getDefaultNong(songID); +// } +// } diff --git a/src/events/get_song_info_event.cpp b/src/events/get_song_info_event.cpp deleted file mode 100644 index e742e68..0000000 --- a/src/events/get_song_info_event.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - -#include "get_song_info_event.hpp" - -ListenerResult GetSongInfoEventFilter::handle(MiniFunction fn, GetSongInfoEvent* event) { - if (event->getObject() == nullptr) { - return ListenerResult::Propagate; - } - - if (event->getObject()->m_songID == m_songID) { - fn(event->getObject()); - return ListenerResult::Propagate; - } - return ListenerResult::Propagate; -} \ No newline at end of file diff --git a/src/events/get_song_info_event.hpp b/src/events/get_song_info_event.hpp index f2438d4..ca0eaa0 100644 --- a/src/events/get_song_info_event.hpp +++ b/src/events/get_song_info_event.hpp @@ -1,31 +1,32 @@ #pragma once -#include "Geode/binding/SongInfoObject.hpp" -#include "Geode/cocos/base_nodes/CCNode.h" #include "Geode/loader/Event.hpp" -#include "Geode/utils/MiniFunction.hpp" +#include "../managers/nong_manager.hpp" +#include "../managers/index_manager.hpp" +#include "../hooks/music_download_manager.hpp" using namespace geode::prelude; +namespace jukebox { + class GetSongInfoEvent : public Event { +private: + std::string m_songName; + std::string m_artistName; + int m_gdSongID; + protected: - SongInfoObject* m_songInfo; - int m_songID; + friend class ::JBMusicDownloadManager; + + GetSongInfoEvent(std::string songName, std::string artistName, int gdSongID) + : m_songName(songName), m_artistName(artistName), m_gdSongID(gdSongID) {} + public: - GetSongInfoEvent(SongInfoObject* object, int songID) - : m_songInfo(object), m_songID(songID) {} - SongInfoObject* getObject() { return m_songInfo; } - int getID() { return m_songID; } + std::string songName() { return m_songName; } + std::string artistName() { return m_artistName; } + int gdSongID() { return m_gdSongID; } }; -class GetSongInfoEventFilter : public EventFilter { -protected: - int m_songID; - CCNode* m_target; -public: - int getSongID() { return m_songID; } - using Callback = ListenerResult(SongInfoObject* obj); - ListenerResult handle(MiniFunction fn, GetSongInfoEvent* event); - GetSongInfoEventFilter(CCNode* target, int songID) - : m_songID(songID), m_target(target) {} -}; \ No newline at end of file +using GetSongInfoFilter = EventFilter; + +} diff --git a/src/events/song_download_progress_event.hpp b/src/events/song_download_progress_event.hpp new file mode 100644 index 0000000..bc858c6 --- /dev/null +++ b/src/events/song_download_progress_event.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "Geode/loader/Event.hpp" +#include "../managers/nong_manager.hpp" +#include "../managers/index_manager.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class SongDownloadProgressEvent final : public Event { +private: + int m_gdSongID; + std::string m_uniqueID; + float m_progress; + +protected: + friend class NongManager; + friend class IndexManager; + + SongDownloadProgressEvent( + int gdSongID, + std::string m_uniqueID, + float progress + ) : m_gdSongID(gdSongID), + m_uniqueID(m_uniqueID), + m_progress(progress) + {}; + +public: + int gdSongID() { return m_gdSongID; } + std::string uniqueID() { return m_uniqueID; } + float progress() { return m_progress; } +}; + +using SongDownloadProgressFilter = EventFilter; + +} diff --git a/src/events/song_error_event.hpp b/src/events/song_error_event.hpp new file mode 100644 index 0000000..6f3fa17 --- /dev/null +++ b/src/events/song_error_event.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "Geode/loader/Event.hpp" +#include "../managers/nong_manager.hpp" +#include "../managers/index_manager.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class SongErrorEvent final : public Event { +private: + bool m_notifyUser; + std::string m_error; + +protected: + friend class NongManager; + friend class IndexManager; + + template + SongErrorEvent( + bool notifyUser, + fmt::format_string format, + Args&&... args + ) : m_error(fmt::format(format, std::forward(args)...)), + m_notifyUser(notifyUser) + {}; + +public: + std::string error() { return m_error; } + bool notifyUser() { return m_notifyUser; } +}; + +using SongErrorFilter = EventFilter; + +} diff --git a/src/events/song_state_changed_event.hpp b/src/events/song_state_changed_event.hpp new file mode 100644 index 0000000..2d9683a --- /dev/null +++ b/src/events/song_state_changed_event.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "Geode/loader/Event.hpp" +#include "../managers/nong_manager.hpp" +#include "../managers/index_manager.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class SongStateChangedEvent final : public Event { +private: + int m_gdSongID; + +protected: + friend class NongManager; + friend class IndexManager; + + SongStateChangedEvent( + int gdSongID + ) : m_gdSongID(gdSongID) + {}; + +public: + int gdSongID() const { + return m_gdSongID; + }; +}; + +using SongStateChangedFilter = EventFilter; + +} diff --git a/src/hooks/custom_song_widget.cpp b/src/hooks/custom_song_widget.cpp index c5318e9..077e91a 100644 --- a/src/hooks/custom_song_widget.cpp +++ b/src/hooks/custom_song_widget.cpp @@ -12,7 +12,6 @@ #include #include -#include "../types/song_info.hpp" #include "../managers/nong_manager.hpp" #include "../ui/nong_dropdown_layer.hpp" #include "Geode/cocos/base_nodes/Layout.hpp" @@ -23,17 +22,17 @@ using namespace jukebox; class $modify(JBSongWidget, CustomSongWidget) { struct Fields { - NongData nongs; + Nongs* nongs; CCMenu* menu; CCMenuItemSpriteExtra* songNameLabel; CCLabelBMFont* sizeIdLabel; std::string songIds = ""; std::string sfxIds = ""; - bool fetchedAssetInfo = false; bool firstRun = true; bool searching = false; - std::unordered_map assetNongData; + std::unordered_map assetNongData; EventListener m_multiAssetListener; + std::unique_ptr>> m_songStateListener; }; bool init( @@ -48,14 +47,14 @@ class $modify(JBSongWidget, CustomSongWidget) { int unkInt ) { if (!CustomSongWidget::init( - songInfo, - songDelegate, - showSongSelect, - showPlayMusic, - showDownload, - isRobtopSong, - unk, - isMusicLibrary, + songInfo, + songDelegate, + showSongSelect, + showPlayMusic, + showDownload, + isRobtopSong, + unk, + isMusicLibrary, unkInt )) { return false; @@ -66,6 +65,25 @@ class $modify(JBSongWidget, CustomSongWidget) { m_songLabel->setVisible(false); this->setupJBSW(); m_fields->firstRun = false; + + m_fields->m_songStateListener = std::make_unique>>([this](SongStateChangedEvent* event){ + if (!m_songInfoObject) return ListenerResult::Propagate; + if (event->gdSongID() != m_songInfoObject->m_songID) return ListenerResult::Propagate; + + auto nongs = NongManager::get()->getNongs(event->gdSongID()); + + if (!nongs.has_value()) return ListenerResult::Propagate; + + auto active = nongs.value()->activeNong(); + + m_songInfoObject->m_songName = active.metadata()->m_name; + m_songInfoObject->m_artistName = active.metadata()->m_artist; + m_songInfoObject->m_songUrl = "local"; + updateSongInfo(); + + return ListenerResult::Propagate; + }); + return true; } @@ -73,9 +91,7 @@ class $modify(JBSongWidget, CustomSongWidget) { CustomSongWidget::updateWithMultiAssets(p1, p2, p3); m_fields->songIds = std::string(p1); m_fields->sfxIds = std::string(p2); - if (m_fields->fetchedAssetInfo) { - this->fixMultiAssetSize(); - } + this->fixMultiAssetSize(); if (m_isRobtopSong) { return; } @@ -84,9 +100,7 @@ class $modify(JBSongWidget, CustomSongWidget) { void updateMultiAssetInfo(bool p) { CustomSongWidget::updateMultiAssetInfo(p); - if (m_fields->fetchedAssetInfo) { - this->fixMultiAssetSize(); - } + this->fixMultiAssetSize(); } void fixMultiAssetSize() { @@ -124,7 +138,7 @@ class $modify(JBSongWidget, CustomSongWidget) { void setupJBSW() { SongInfoObject* obj = m_songInfoObject; if (obj == nullptr) return; - if (!m_fields->fetchedAssetInfo && m_songs.size() != 0) { + if (m_songs.size() != 0) { this->getMultiAssetSongInfo(); } int id = obj->m_songID; @@ -141,54 +155,14 @@ class $modify(JBSongWidget, CustomSongWidget) { return; } m_songLabel->setVisible(false); - if (obj->m_songUrl.empty() && obj->m_artistName.empty()) { - // we have an invalid songID - if (!NongManager::get()->getNongs(id).has_value()) { - NongManager::get()->createUnknownDefault(id); - } - } - std::optional result = NongManager::get()->getNongs(id); - if (!result.has_value()) { - NongManager::get()->createDefault(m_songInfoObject, id, m_isRobtopSong); - result = NongManager::get()->getNongs(id); - if (!result.has_value()) { - return; - } - } + NongManager::get()->initSongID(obj, obj->m_songID, m_isRobtopSong); + std::optional result = NongManager::get()->getNongs(NongManager::get()->adjustSongID(id, m_isRobtopSong)); - NongData nongData = result.value(); + auto nongs = result.value(); - SongInfo active = NongManager::get()->getActiveNong(id).value(); - if ( - !m_isRobtopSong - && !nongData.defaultValid - && active.path == nongData.defaultPath - && !NongManager::get()->isFixingDefault(id) - ) { - NongManager::get()->prepareCorrectDefault(id); - this->template addEventListener( - [this, id](SongInfoObject* obj) { - if (!NongManager::get()->isFixingDefault(id)) { - return ListenerResult::Propagate; - } - // log::info("update song object"); - // log::info("{}, {}, {}, {}, {}", obj->m_songName, obj->m_artistName, obj->m_songUrl, obj->m_isUnkownSong, id); - NongManager::get()->fixDefault(obj); - m_sliderGroove->setVisible(false); - m_fields->nongs = NongManager::get()->getNongs(id).value(); - this->createSongLabels(); - return ListenerResult::Propagate; - }, - id - ); - auto result = NongManager::get()->getNongs(id); - if (result) { - nongData = result.value(); - } - } - m_fields->nongs = nongData; + m_fields->nongs = nongs; this->createSongLabels(); - if (active.path != nongData.defaultPath) { + if (nongs->isDefaultActive()) { m_deleteBtn->setVisible(false); } } @@ -203,24 +177,12 @@ class $modify(JBSongWidget, CustomSongWidget) { } void getMultiAssetSongInfo() { - bool allDownloaded = true; for (auto const& kv : m_songs) { + NongManager::get()->initSongID(nullptr, kv.first, false); auto result = NongManager::get()->getNongs(kv.first); - if (!result.has_value()) { - NongManager::get()->createDefault(nullptr, kv.first, false); - result = NongManager::get()->getNongs(kv.first); - if (!result.has_value()) { - // its downloading - allDownloaded = false; - continue; - } - } auto value = result.value(); m_fields->assetNongData[kv.first] = value; } - if (allDownloaded) { - m_fields->fetchedAssetInfo = true; - } } void createSongLabels() { @@ -229,23 +191,21 @@ class $modify(JBSongWidget, CustomSongWidget) { songID++; songID = -songID; } - // log::info("createsonglabel"); - auto res = NongManager::get()->getActiveNong(songID); + auto res = NongManager::get()->getNongs(songID); if (!res) { return; } - SongInfo active = res.value(); + auto nongs = res.value(); + auto active = nongs->activeNong(); if (m_fields->menu != nullptr) { m_fields->menu->removeFromParent(); } auto menu = CCMenu::create(); menu->setID("song-name-menu"); - auto label = CCLabelBMFont::create(active.songName.c_str(), "bigFont.fnt"); - // if (!m_isMusicLibrary) { - // label->limitLabelWidth(220.f, 0.8f, 0.1f); - // } else { - // label->limitLabelWidth(130.f, 0.4f, 0.1f); - // } + auto label = CCLabelBMFont::create( + active.metadata()->m_name.c_str(), + "bigFont.fnt" + ); auto songNameMenuLabel = CCMenuItemSpriteExtra::create( label, this, @@ -266,29 +226,33 @@ class $modify(JBSongWidget, CustomSongWidget) { menu->setPosition(m_songLabel->getPosition()); menu->updateLayout(); m_fields->menu = menu; - this->addChild(menu); + this->addChild(menu); if (m_songs.size() == 0 && m_sfx.size() == 0 && !m_isMusicLibrary) { if (m_fields->sizeIdLabel != nullptr) { m_fields->sizeIdLabel->removeFromParent(); } auto data = NongManager::get()->getNongs(songID).value(); - if (!fs::exists(active.path) && active.path == data.defaultPath) { + // TODO this might be fuckery + if ( + !std::filesystem::exists(active.path().value()) + && nongs->isDefaultActive() + ) { m_songIDLabel->setVisible(true); - return; geode::cocos::handleTouchPriority(this); - } else if (m_songIDLabel) { - m_songIDLabel->setVisible(false); - } + return; + } else if (m_songIDLabel) { + m_songIDLabel->setVisible(false); + } - std::string sizeText; - if (fs::exists(active.path)) { - sizeText = NongManager::get()->getFormattedSize(active); - } else { - sizeText = "NA"; - } - std::string labelText; - if (active.path == data.defaultPath) { + std::string sizeText; + if (std::filesystem::exists(active.path().value())) { + sizeText = NongManager::get()->getFormattedSize(active.path().value()); + } else { + sizeText = "NA"; + } + std::string labelText; + if (nongs->isDefaultActive()) { std::stringstream ss; int displayId = songID; if (displayId < 0) { @@ -296,16 +260,19 @@ class $modify(JBSongWidget, CustomSongWidget) { ss << "(R) "; } ss << displayId; - labelText = "SongID: " + ss.str() + " Size: " + sizeText; - } else { + labelText = "SongID: " + ss.str() + " Size: " + sizeText; + } else { std::string display = "NONG"; if (songID < 0) { display = "(R) NONG"; } - labelText = "SongID: " + display + " Size: " + sizeText; - } + labelText = "SongID: " + display + " Size: " + sizeText; + } - auto label = CCLabelBMFont::create(labelText.c_str(), "bigFont.fnt"); + auto label = CCLabelBMFont::create( + labelText.c_str(), + "bigFont.fnt" + ); label->setID("nongd-id-and-size-label"); label->setPosition(ccp(-139.f, -31.f)); label->setAnchorPoint({0, 0.5f}); @@ -323,11 +290,8 @@ class $modify(JBSongWidget, CustomSongWidget) { } void addNongLayer(CCObject* target) { - if (m_songs.size() > 1 && !m_fields->fetchedAssetInfo) { + if (m_songs.size() != 0) { this->getMultiAssetSongInfo(); - if (!m_fields->fetchedAssetInfo) { - return; - } } auto scene = CCDirector::sharedDirector()->getRunningScene(); std::vector ids; diff --git a/src/hooks/fmod_audio_engine.cpp b/src/hooks/fmod_audio_engine.cpp index 731b5a0..23cc528 100644 --- a/src/hooks/fmod_audio_engine.cpp +++ b/src/hooks/fmod_audio_engine.cpp @@ -6,22 +6,43 @@ using namespace jukebox; class $modify(FMODAudioEngine) { - void queueStartMusic(gd::string audioFilename, float p1, float p2, float p3, bool p4, - int ms, int p6, int p7, int p8, int p9, bool p10, - int p11, bool p12) { + void queueStartMusic( + gd::string audioFilename, + float p1, + float p2, + float p3, + bool p4, + int ms, + int p6, + int p7, + int p8, + int p9, + bool p10, + int p11, + bool p12 + ) { if (NongManager::get()->m_currentlyPreparingNong) { - int additionalOffset = NongManager::get()->m_currentlyPreparingNong.value().startOffset; - FMODAudioEngine::queueStartMusic(audioFilename, p1, p2, p3, p4, ms+additionalOffset, p6, p7, p8, p9, p10, p11, p12); + int additionalOffset = NongManager::get() + ->m_currentlyPreparingNong.value() + ->activeNong() + .metadata() + ->m_startOffset; + FMODAudioEngine::queueStartMusic(audioFilename, p1, p2, p3, p4, ms+additionalOffset, p6, p7, p8, p9, p10, p11, p12); } else { - FMODAudioEngine::queueStartMusic(audioFilename, p1, p2, p3, p4, ms, p6, p7, p8, p9, p10, p11, p12); + FMODAudioEngine::queueStartMusic(audioFilename, p1, p2, p3, p4, ms, p6, p7, p8, p9, p10, p11, p12); } } + void setMusicTimeMS(unsigned int ms, bool p1, int channel) { if (NongManager::get()->m_currentlyPreparingNong) { - int additionalOffset = NongManager::get()->m_currentlyPreparingNong.value().startOffset; + int additionalOffset = NongManager::get() + ->m_currentlyPreparingNong.value() + ->activeNong() + .metadata() + ->m_startOffset; FMODAudioEngine::setMusicTimeMS(ms+additionalOffset, p1, channel); } else { FMODAudioEngine::setMusicTimeMS(ms, p1, channel); } } -}; \ No newline at end of file +}; diff --git a/src/hooks/gj_game_level.cpp b/src/hooks/gj_game_level.cpp index bfad1e2..80f69be 100644 --- a/src/hooks/gj_game_level.cpp +++ b/src/hooks/gj_game_level.cpp @@ -6,7 +6,6 @@ #include #include "../managers/nong_manager.hpp" -#include "../types/song_info.hpp" using namespace geode::prelude; @@ -17,19 +16,19 @@ class $modify(GJGameLevel) { return GJGameLevel::getAudioFileName(); } int id = (-m_audioTrack) - 1; - auto active = jukebox::NongManager::get()->getActiveNong(id); - if (!active.has_value()) { + auto res = jukebox::NongManager::get()->getNongs(id); + if (!res.has_value()) { return GJGameLevel::getAudioFileName(); } - jukebox::SongInfo value = active.value(); - if (!std::filesystem::exists(value.path)) { + auto active = res.value()->activeNong(); + if (!std::filesystem::exists(active.path().value())) { return GJGameLevel::getAudioFileName(); } - jukebox::NongManager::get()->m_currentlyPreparingNong = value; + jukebox::NongManager::get()->m_currentlyPreparingNong = res.value(); #ifdef GEODE_IS_WINDOWS - return geode::utils::string::wideToUtf8(value.path.c_str()); + return geode::utils::string::wideToUtf8(active.path().value().c_str()); #else - return value.path.string(); + return active.path().value().string(); #endif } -}; \ No newline at end of file +}; diff --git a/src/hooks/level_tools.cpp b/src/hooks/level_tools.cpp index 9369c73..d4ae5e3 100644 --- a/src/hooks/level_tools.cpp +++ b/src/hooks/level_tools.cpp @@ -38,9 +38,9 @@ class $modify(LevelTools) { return LevelTools::getAudioTitle(id); } int searchID = -id - 1; - auto active = jukebox::NongManager::get()->getActiveNong(searchID); - if (active.has_value()) { - return active.value().songName; + auto res = jukebox::NongManager::get()->getNongs(searchID); + if (res.has_value()) { + return res.value()->activeNong().metadata()->m_name; } return LevelTools::getAudioTitle(id); } diff --git a/src/hooks/music_download_manager.cpp b/src/hooks/music_download_manager.cpp index 7fdcfe7..9b9055f 100644 --- a/src/hooks/music_download_manager.cpp +++ b/src/hooks/music_download_manager.cpp @@ -1,51 +1,67 @@ -#include -#include #include +#include "music_download_manager.hpp" #include "../managers/nong_manager.hpp" -#include "../types/song_info.hpp" +#include "../../include/nong.hpp" #include "../events/get_song_info_event.hpp" +#include "Geode/binding/GameLevelManager.hpp" +#include "Geode/binding/MusicDownloadManager.hpp" +#include "Geode/binding/SongInfoObject.hpp" +#include "Geode/cocos/CCDirector.h" using namespace jukebox; -class $modify(MusicDownloadManager) { - gd::string pathForSong(int id) { - NongManager::get()->m_currentlyPreparingNong = std::nullopt; - auto active = NongManager::get()->getActiveNong(id); - if (!active.has_value()) { - return MusicDownloadManager::pathForSong(id); - } - auto value = active.value(); - if (!fs::exists(value.path)) { - return MusicDownloadManager::pathForSong(id); - } - NongManager::get()->m_currentlyPreparingNong = active; - #ifdef GEODE_IS_WINDOWS - return geode::utils::string::wideToUtf8(value.path.c_str()); - #else - return value.path.string(); - #endif - } - void onGetSongInfoCompleted(gd::string p1, gd::string p2) { - MusicDownloadManager::onGetSongInfoCompleted(p1, p2); - auto songID = std::stoi(p2); - GetSongInfoEvent(this->getSongInfoObject(songID), songID).post(); +gd::string JBMusicDownloadManager::pathForSong(int id) { + NongManager::get()->m_currentlyPreparingNong = std::nullopt; + auto nongs = NongManager::get()->getNongs(id); + if (!nongs.has_value()) { + return MusicDownloadManager::pathForSong(id); } + auto value = nongs.value(); + auto active = value->activeNong(); + if (!std::filesystem::exists(active.path().value())) { + return MusicDownloadManager::pathForSong(id); + } + NongManager::get()->m_currentlyPreparingNong = value; + #ifdef GEODE_IS_WINDOWS + return geode::utils::string::wideToUtf8(active.path().value().c_str()); + #else + return active.path().value().string(); + #endif +} - SongInfoObject* getSongInfoObject(int id) { - auto og = MusicDownloadManager::getSongInfoObject(id); - if (og == nullptr) { - return og; - } - if (NongManager::get()->hasActions(id)) { - return og; - } - auto active = NongManager::get()->getActiveNong(id); - if (active.has_value()) { - auto value = active.value(); - og->m_songName = value.songName; - og->m_artistName = value.authorName; - } +void JBMusicDownloadManager::onGetSongInfoCompleted(gd::string p1, gd::string p2) { + MusicDownloadManager::onGetSongInfoCompleted(p1, p2); + auto songID = std::stoi(p2); + + // CCArray* keys = GameLevelManager::sharedState()->responseToDict(p1, true)->allKeys(); + // // Example of p1: 1~|~945695~|~2~|~Tennobyte - Fly Away~|~3~|~51164~|~4~|~Tennobyte~|~5~|~6.47~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F945000%2F945695_Tennobyte---Fly-Away.mp3%3Ff1593523523~|~7~|~~|~11~|~0~|~12~|~~|~13~|~~|~14~|~0 + // // Values are the keys in the Dictionary. For some reason.. + // // It also seems like only sometimes empty values are ommited. + CCDictionary* dict = GameLevelManager::sharedState()->responseToDict(p1, true); + + // for (int i = 0; i < keys->count(); i++) { + // log::info("{}", keys->objectAtIndex(i)); + // log::info("{}", static_cast(keys->objectAtIndex(i))->getCString()); + // } + // GetSongInfoEvent(this->getSongInfoObject(songID), songID).post(); + GetSongInfoEvent( + static_cast(dict->allKeys()->objectAtIndex(3))->getCString(), + static_cast(dict->allKeys()->objectAtIndex(7))->getCString(), + songID + ).post(); +} + +SongInfoObject* JBMusicDownloadManager::getSongInfoObject(int id) { + auto og = MusicDownloadManager::getSongInfoObject(id); + if (og == nullptr) { return og; } -}; \ No newline at end of file + auto res = NongManager::get()->getNongs(id); + if (res.has_value()) { + auto active = res.value()->activeNong(); + og->m_songName = active.metadata()->m_name; + og->m_artistName = active.metadata()->m_artist; + } + return og; +} diff --git a/src/hooks/music_download_manager.hpp b/src/hooks/music_download_manager.hpp new file mode 100644 index 0000000..ca481ae --- /dev/null +++ b/src/hooks/music_download_manager.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +struct JBMusicDownloadManager : geode::Modify { + gd::string pathForSong(int id); + void onGetSongInfoCompleted(gd::string p1, gd::string p2); + SongInfoObject* getSongInfoObject(int id); +}; diff --git a/src/main.cpp b/src/main.cpp index 3fdc075..61de6aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,8 @@ #include #include "managers/nong_manager.hpp" +#include "managers/index_manager.hpp" $on_mod(Loaded) { - jukebox::NongManager::get()->loadSongs(); + jukebox::IndexManager::get(); }; - -$on_mod(DataSaved) { - jukebox::NongManager::get()->writeJson(); -} \ No newline at end of file diff --git a/src/managers/index_manager.cpp b/src/managers/index_manager.cpp new file mode 100644 index 0000000..1deaa5a --- /dev/null +++ b/src/managers/index_manager.cpp @@ -0,0 +1,581 @@ +#include "Geode/loader/Event.hpp" +#include "Geode/utils/general.hpp" +#include "nong_manager.hpp" +#include "index_manager.hpp" + +#include +#include +#include + +#include "../../include/nong.hpp" +#include "../../include/index_serialize.hpp" +#include "../ui/indexes_setting.hpp" +#include "../events/song_error_event.hpp" + +namespace jukebox { + +bool IndexManager::init() { + if (m_initialized) { + return true; + } + + Mod::get()->addCustomSetting( + "indexes", Mod::get() + ->getSettingDefinition("indexes") + ->get() + ->json->get>("default")); + + auto path = this->baseIndexesPath(); + if (!std::filesystem::exists(path)) { + std::filesystem::create_directory(path); + return true; + } + + if (auto err = fetchIndexes().error(); !err.empty()) { + SongErrorEvent(false, "Failed to fetch indexes: {}", err).post(); + return false; + } + + m_initialized = true; + return true; +} + +Result> IndexManager::getIndexes() { + auto setting = Mod::get()->getSettingValue("indexes"); + log::info("Indexes: {}", setting.m_indexes.size()); + for (const auto index : setting.m_indexes) { + log::info("Index({}): {}", index.m_enabled, index.m_url); + } + return Ok(setting.m_indexes); +} + +std::filesystem::path IndexManager::baseIndexesPath() { + static std::filesystem::path path = Mod::get()->getSaveDir() / "indexes-cache"; + return path; +} + +Result<> IndexManager::loadIndex(std::filesystem::path path) { + if (!std::filesystem::exists(path)) { + return Err("Index file does not exist"); + } + + std::ifstream input(path); + if (!input.is_open()) { + return Err(fmt::format("Couldn't open file: {}", path.filename().string())); + } + + std::string contents; + input.seekg(0, std::ios::end); + contents.resize(input.tellg()); + input.seekg(0, std::ios::beg); + input.read(&contents[0], contents.size()); + input.close(); + + std::string error; + std::optional jsonObj = matjson::parse(contents, error); + + if (!jsonObj.has_value()) { + return Err(error); + } + + const auto indexRes = matjson::Serialize::from_json(jsonObj.value()); + + if (indexRes.isErr()) { + return Err(indexRes.error()); + } + + const auto&& index = std::move(indexRes.value()); + + cacheIndexName(index.m_id, index.m_name); + + for (const auto& [key, ytNong] : jsonObj.value()["nongs"]["youtube"].as_object()) { + const auto& gdSongIDs = ytNong["songs"].as_array(); + for (const auto& gdSongIDValue : gdSongIDs) { + int gdSongID = gdSongIDValue.as_int(); + if (!m_indexNongs.contains(gdSongID)) { + m_indexNongs.emplace(gdSongID, Nongs(gdSongID)); + } + if (auto err = m_indexNongs.at(gdSongID).add( + YTSong( + SongMetadata( + gdSongID, + key, + ytNong["name"].as_string(), + ytNong["artist"].as_string(), + std::nullopt, + ytNong.contains("startOffset") ? ytNong["startOffset"].as_int() : 0 + ), + ytNong["ytID"].as_string(), + index.m_id, + std::nullopt + )); err.isErr()) { + SongErrorEvent(false, "Failed to add YT song from index: {}", err.error()).post(); + } + } + } + + for (const auto& [key, hostedNong] : jsonObj.value()["nongs"]["hosted"].as_object()) { + const auto& gdSongIDs = hostedNong["songs"].as_array(); + for (const auto& gdSongIDValue : gdSongIDs) { + int gdSongID = gdSongIDValue.as_int(); + if (!m_indexNongs.contains(gdSongID)) { + m_indexNongs.emplace(gdSongID, Nongs(gdSongID)); + } + if (auto err = m_indexNongs.at(gdSongID).add( + HostedSong( + SongMetadata( + gdSongID, + key, + hostedNong["name"].as_string(), + hostedNong["artist"].as_string(), + std::nullopt, + hostedNong.contains("startOffset") ? hostedNong["startOffset"].as_int() : 0 + ), + hostedNong["url"].as_string(), + index.m_id, + std::nullopt + )); err.isErr()) { + SongErrorEvent(false, "Failed to add Hosted song from index: {}", err.error()).post(); + } + } + } + + m_loadedIndexes.emplace( + index.m_id, + std::make_unique(index) + ); + + log::info("Index \"{}\" ({}) Loaded. There are currently {} index Nongs objects.", index.m_name, index.m_id, m_indexNongs.size()); + + return Ok(); +} + +Result<> IndexManager::fetchIndexes() { + m_indexListeners.clear(); + m_indexNongs.clear(); + m_downloadSongListeners.clear(); + + const auto indexesRes = getIndexes(); + if (indexesRes.isErr()) { + return Err(indexesRes.error()); + } + const auto indexes = indexesRes.value(); + + for (const auto index : indexes) { + log::info("Fetching index {}", index.m_url); + if (!index.m_enabled || index.m_url.size() < 3) continue; + + // Hash url to use as a filename per index + std::hash hasher; + std::size_t hashValue = hasher(index.m_url); + std::stringstream hashStream; + hashStream << std::hex << hashValue; + + auto filepath = baseIndexesPath() / fmt::format("{}.json", hashStream.str()); + + FetchIndexTask task = web::WebRequest().timeout(std::chrono::seconds(30)).get(index.m_url).map( + [this, filepath, index](web::WebResponse *response) -> FetchIndexTask::Value { + if (response->ok() && response->string().isOk()) { + std::string error; + std::optional jsonObj = matjson::parse(response->string().value(), error); + + if (!jsonObj.has_value()) { + return Err(error); + } + + if (!jsonObj.value().is_object()) { + return Err("Index supposed to be an object"); + } + jsonObj.value().set("url", index.m_url); + const auto indexRes = matjson::Serialize::from_json(jsonObj.value()); + + if (indexRes.isErr()) { + return Err(indexRes.error()); + } + + std::ofstream output(filepath); + if (!output.is_open()) { + return Err(fmt::format("Couldn't open file: {}", filepath)); + } + output << jsonObj.value().dump(matjson::NO_INDENTATION); + output.close(); + + return Ok(); + } + return Err("Web request failed"); + }, + [](web::WebProgress *progress) -> FetchIndexTask::Progress { + return progress->downloadProgress().value_or(0) / 100.f; + } + ); + + auto listener = EventListener(); + listener.bind([this, index, filepath](FetchIndexTask::Event* event) { + if (float *progress = event->getProgress()) { + return; + } + + m_indexListeners.erase(index.m_url); + + if (FetchIndexTask::Value *result = event->getValue()) { + if (result->isErr()) { + SongErrorEvent(false, "Failed to fetch index: {}", result->error()).post(); + } else { + log::info("Index fetched and cached: {}", index.m_url); + } + } else if (event->isCancelled()) {} + + if (auto err = loadIndex(filepath).error(); !err.empty()) { + SongErrorEvent(false, "Failed to load index: {}", err).post(); + } + }); + listener.setFilter(task); + m_indexListeners.emplace(index.m_url, std::move(listener)); + } + + return Ok(); +} + +std::optional IndexManager::getSongDownloadProgress(const std::string& uniqueID) { + if (m_downloadSongListeners.contains(uniqueID)) { + return m_downloadProgress.at(uniqueID); + } + return std::nullopt; +} + +std::optional IndexManager::getIndexName(const std::string& indexID) { + auto jsonObj = Mod::get()->getSavedValue("cached-index-names"); + if (!jsonObj.contains(indexID)) return std::nullopt; + return jsonObj[indexID].as_string(); +} + +void IndexManager::cacheIndexName(const std::string& indexId, const std::string& indexName) { + auto jsonObj = Mod::get()->getSavedValue("cached-index-names", {}); + jsonObj.set(indexId, indexName); + Mod::get()->setSavedValue("cached-index-names", jsonObj); +} + +Result> IndexManager::getNongs(int gdSongID) { + auto nongs = std::vector(); + auto localNongs = NongManager::get()->getNongs(gdSongID); + if (!localNongs.has_value()) { + return Err("Failed to get nongs"); + } + std::optional indexNongs = m_indexNongs.contains(gdSongID) ? std::optional(&m_indexNongs.at(gdSongID)) : std::nullopt; + + nongs.push_back(Nong(*localNongs.value()->defaultSong())); + + for (std::unique_ptr& song : localNongs.value()->locals()) { + nongs.push_back(Nong(*song)); + } + + std::vector addedIndexSongs; + for (std::unique_ptr& song : localNongs.value()->youtube()) { + // Check if song is from an index + if (indexNongs.has_value() && song->indexID().has_value()) { + for (std::unique_ptr& indexSong : indexNongs.value()->youtube()) { + if (song->metadata()->m_uniqueID == indexSong->metadata()->m_uniqueID) { + addedIndexSongs.push_back(song->metadata()->m_uniqueID); + } + } + } + nongs.push_back(Nong(*song)); + } + + for (std::unique_ptr& song : localNongs.value()->hosted()) { + // Check if song is from an index + if (indexNongs.has_value() && song->indexID().has_value()) { + for (std::unique_ptr& indexSong : indexNongs.value()->hosted()) { + if (song->metadata()->m_uniqueID == indexSong->metadata()->m_uniqueID) { + addedIndexSongs.push_back(song->metadata()->m_uniqueID); + } + } + } + nongs.push_back(Nong(*song)); + } + + if (indexNongs.has_value()) { + for (std::unique_ptr& song : indexNongs.value()->youtube()) { + // Check if song is not already added + if (std::find(addedIndexSongs.begin(), addedIndexSongs.end(), song->metadata()->m_uniqueID) == addedIndexSongs.end()) { + nongs.push_back(Nong(*song)); + } + } + + for (std::unique_ptr& song : indexNongs.value()->hosted()) { + // Check if song is not already added + if (std::find(addedIndexSongs.begin(), addedIndexSongs.end(), song->metadata()->m_uniqueID) == addedIndexSongs.end()) { + nongs.push_back(Nong(*song)); + } + } + } + + std::unordered_map sortedNongType = { + { Nong::Type::Local, 1 }, + { Nong::Type::Hosted, 2 }, + { Nong::Type::YT, 3 } + }; + + std::sort(nongs.begin(), nongs.end(), [&sortedNongType, defaultUniqueID = localNongs.value()->defaultSong()->metadata()->m_uniqueID](const Nong& a, const Nong& b) { + // Place the object with isDefault == true at the front + if (a.metadata()->m_uniqueID == defaultUniqueID) return true; + if (b.metadata()->m_uniqueID == defaultUniqueID) return false; + + // Next, those without an index + if (!a.indexID().has_value() && b.indexID().has_value()) return true; + if (a.indexID().has_value() && !b.indexID().has_value()) return false; + + // Next, compare whether path exists or not + if (a.path().has_value() && std::filesystem::exists(a.path().value())) { + if (!b.path().has_value() || !std::filesystem::exists(b.path().value())) { + return true; + } + } else if (b.path().has_value() && std::filesystem::exists(b.path().value())) { + return false; + } + + // Next, compare by type + if (a.type() != b.type()) { + return sortedNongType.at(a.type()) < sortedNongType.at(b.type()); + } + + // Next, compare whether indexID exists or not (std::nullopt should be first) + if (a.indexID().has_value() != b.indexID().has_value()) { + return !a.indexID().has_value() && b.indexID().has_value(); + } + + // Finally, compare by name + return a.metadata()->m_name < b.metadata()->m_name; + }); + + return Ok(std::move(nongs)); +} + +Result<> IndexManager::downloadSong(int gdSongID, const std::string& uniqueID) { + auto nongs = IndexManager::get()->getNongs(gdSongID); + if (!nongs.has_value()) { + return Err("GD song {} not initialized in manifest", gdSongID); + } + for (Nong& nong : nongs.value()) { + if (nong.metadata()->m_uniqueID == uniqueID) { + return IndexManager::get()->downloadSong(nong); + } + } + + return Err("Song {} not found in manifest", uniqueID); +} + +Result<> IndexManager::downloadSong(Nong nong) { + const std::string id = nong.metadata()->m_uniqueID; + auto gdSongID = nong.metadata()->m_gdID; + + if (m_downloadSongListeners.contains(id)) { + m_downloadSongListeners.at(id).getFilter().cancel(); + } + DownloadSongTask task; + + nong.visit([this, &task](LocalSong* local){ + task = DownloadSongTask::immediate(Err("Can't download local song")); + }, [this, &task](YTSong* yt) { + EventListener* cobaltMetadataListener = new EventListener(); + EventListener* cobaltSongListener = new EventListener(); + + task = DownloadSongTask::runWithCallback([ + this, + yt = *yt, + cobaltMetadataListener, + cobaltSongListener + ] ( + utils::MiniFunction finish, + utils::MiniFunction progress, + utils::MiniFunction hasBeenCancelled + ) { + if (yt.youtubeID().length() != 11) { + return finish(Err("Invalid YouTube ID")); + } + + std::function finishErr = [finish, cobaltMetadataListener, cobaltSongListener](std::string err){ + delete cobaltSongListener; + delete cobaltMetadataListener; + finish(Err(err)); + }; + + cobaltMetadataListener->bind([this, hasBeenCancelled, cobaltMetadataListener, cobaltSongListener, yt, finishErr, finish](web::WebTask::Event* event) { + if (hasBeenCancelled() || event->isCancelled()) { + return finishErr("Cancelled while fetching song metadata from Cobalt"); + } + + if (event->getProgress() != nullptr) { + float progress = event->getProgress()->downloadProgress().value_or(0) / 1000.f; + m_downloadProgress[yt.metadata()->m_uniqueID] = progress; + SongDownloadProgressEvent(yt.metadata()->m_gdID, yt.metadata()->m_uniqueID, progress).post(); + return; + } + + if (event->getValue() == nullptr) return; + + if (!event->getValue()->ok() || !event->getValue()->json().isOk()) { + return finishErr("Unable to get/parse Cobalt metadata response"); + } + + matjson::Value jsonObj = event->getValue()->json().unwrap(); + + if (!jsonObj.contains("status") || jsonObj["status"] != "stream") { + return finishErr("Cobalt metadata response is not a stream"); + } + + if (!jsonObj.contains("url") || !jsonObj["url"].is_string()) { + return finishErr("Cobalt metadata bad response"); + } + + std::string audio_url = jsonObj["url"].as_string(); + log::info("Cobalt metadata response: {}", audio_url); + + cobaltSongListener->bind([this, hasBeenCancelled, cobaltMetadataListener, cobaltSongListener, finishErr, yt, finish](web::WebTask::Event* event) { + if (hasBeenCancelled() || event->isCancelled()) { + return finishErr("Cancelled while fetching song data from Cobalt"); + } + + if (event->getProgress() != nullptr) { + float progress = event->getProgress()->downloadProgress().value_or(0) / 100.f * 0.9f + 0.1f; + m_downloadProgress[yt.metadata()->m_uniqueID] = progress; + SongDownloadProgressEvent(yt.metadata()->m_gdID, yt.metadata()->m_uniqueID, progress).post(); + return; + } + + if (event->getValue() == nullptr) return; + + if (!event->getValue()->ok()) { + return finishErr("Unable to get Cobalt song response"); + } + + ByteVector data = event->getValue()->data(); + + auto destination = NongManager::get()->generateSongFilePath("mp3"); + std::ofstream file(destination, std::ios::out | std::ios::binary); + file.write(reinterpret_cast(data.data()), data.size()); + file.close(); + + delete cobaltSongListener; + delete cobaltMetadataListener; + finish(Ok(destination)); + }); + + cobaltSongListener->setFilter( + web::WebRequest() + .timeout(std::chrono::seconds(30)) + .get(audio_url) + ); + }); + + cobaltMetadataListener->setFilter( + web::WebRequest() + .timeout(std::chrono::seconds(30)) + .bodyJSON(matjson::Object { + {"url", fmt::format("https://www.youtube.com/watch?v={}", yt.youtubeID())}, + {"aFormat", "mp3"}, + {"isAudioOnly", "true"} + }) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .post("https://api.cobalt.tools/api/json") + ); + }, "Download a YouTube song from Cobalt"); + }, [this, &task](HostedSong* hosted) { + task = web::WebRequest().timeout(std::chrono::seconds(30)).get(hosted->url()).map( + [this](web::WebResponse *response) -> DownloadSongTask::Value { + if (response->ok()) { + + auto destination = NongManager::get()->generateSongFilePath("mp3"); + std::ofstream file(destination, std::ios::out | std::ios::binary); + file.write(reinterpret_cast(response->data().data()), response->data().size()); + file.close(); + + return Ok(destination); + } + return Err("Web request failed"); + }, + [](web::WebProgress *progress) -> DownloadSongTask::Progress { + return progress->downloadProgress().value_or(0) / 100.f; + } + ); + }); + + auto listener = EventListener(); + + listener.bind([this, gdSongID, id, nong](DownloadSongTask::Event* event) { + if (float *progress = event->getProgress()) { + m_downloadProgress[id] = *progress; + SongDownloadProgressEvent(gdSongID, id, *event->getProgress()).post(); + return; + } + m_downloadProgress.erase(id); + m_downloadSongListeners.erase(id); + if (event->isCancelled()) { + SongErrorEvent(false, "Failed to fetch song: cancelled").post(); + SongStateChangedEvent(gdSongID).post(); + return; + } + DownloadSongTask::Value *result = event->getValue(); + if (result->isErr()) { + SongErrorEvent(true, "Failed to fetch song: {}", result->error()).post(); + SongStateChangedEvent(gdSongID).post(); + return; + } + + if (result->value().string().size() == 0) { + SongStateChangedEvent(gdSongID).post(); + return; + } + + if (!nong.indexID().has_value()) { + auto _ = NongManager::get()->getNongs(gdSongID).value()->deleteSong(id); + } + + Nong* newNongPtr; + nong.visit( + [](LocalSong* _) {}, + [&newNongPtr, result](YTSong* yt) { + newNongPtr = new Nong { + YTSong { + SongMetadata(*yt->metadata()), + yt->youtubeID(), + yt->indexID(), + result->ok().value(), + }, + }; + }, + [&newNongPtr, result](HostedSong* hosted) { + newNongPtr = new Nong { + HostedSong { + SongMetadata(*hosted->metadata()), + hosted->url(), + hosted->indexID(), + result->ok().value(), + }, + }; + } + ); + Nong newNong = std::move(*newNongPtr); + + if (auto res = NongManager::get()->addNongs(std::move(newNong.toNongs().unwrap())); res.isErr()) { + SongErrorEvent(true, "Failed to add song: {}", res.error()).post(); + SongStateChangedEvent(gdSongID).post(); + return; + } + if (auto res = NongManager::get()->setActiveSong(gdSongID, id); res.isErr()) { + SongErrorEvent(true, "Failed to set song as active: {}", res.error()).post(); + SongStateChangedEvent(gdSongID).post(); + return; + } + + SongStateChangedEvent(gdSongID).post(); + }); + listener.setFilter(task); + m_downloadSongListeners.emplace(id, std::move(listener)); + m_downloadProgress[id] = 0.f; + SongDownloadProgressEvent(gdSongID, id, 0.f).post(); + return Ok(); +} + +}; diff --git a/src/managers/index_manager.hpp b/src/managers/index_manager.hpp new file mode 100644 index 0000000..da661a9 --- /dev/null +++ b/src/managers/index_manager.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include + +#include "Geode/utils/Task.hpp" +#include "Geode/binding/SongInfoObject.hpp" +#include "Geode/loader/Event.hpp" +#include "Geode/loader/Mod.hpp" + +#include "../../include/nong.hpp" +#include "../../include/index.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class IndexManager : public CCObject { + friend class NongManager; +protected: + inline static IndexManager* m_instance = nullptr; + bool m_initialized = false; + + using FetchIndexTask = Task, float>; + using DownloadSongTask = Task, float>; + + bool init(); + // index url -> task listener + std::unordered_map> m_indexListeners; + + std::unordered_map m_indexNongs; + // song id -> download song task + std::unordered_map> m_downloadSongListeners; + // song id -> current download progress (used when opening NongDropdownLayer while a song is being downloaded) + std::unordered_map m_downloadProgress; + +public: + // index id -> index metadata + std::unordered_map> m_loadedIndexes; + + bool initialized() const { + return m_initialized; + } + + Result<> fetchIndexes(); + + Result<> loadIndex(std::filesystem::path path); + + Result> getIndexes(); + + std::optional getSongDownloadProgress(const std::string& uniqueID); + std::optional getIndexName(const std::string& indexID); + void cacheIndexName(const std::string& indexId, const std::string& indexName); + + std::filesystem::path baseIndexesPath(); + + Result> getNongs(int gdSongID); + + std::function createDownloadSongBind(int gdSongID, Nong nong); + Result<> downloadSong(int gdSongID, const std::string& uniqueID); + Result<> downloadSong(Nong hosted); + + static IndexManager* get() { + if (m_instance == nullptr) { + m_instance = new IndexManager(); + m_instance->retain(); + m_instance->init(); + } + + return m_instance; + } +}; + +} diff --git a/src/managers/nong_manager.cpp b/src/managers/nong_manager.cpp index 67ab3dc..5038969 100644 --- a/src/managers/nong_manager.cpp +++ b/src/managers/nong_manager.cpp @@ -1,273 +1,206 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "nong_manager.hpp" + +#include "Geode/binding/LevelTools.hpp" +#include "Geode/binding/MusicDownloadManager.hpp" +#include "Geode/binding/SongInfoObject.hpp" +#include "Geode/loader/Log.hpp" + #include #include #include +#include +#include #include +#include #include -#include #include -#include "nong_manager.hpp" +#include "../../include/nong.hpp" +#include "../../include/nong_serialize.hpp" +#include "../utils/random_string.hpp" +#include "index_manager.hpp" -namespace jukebox { -std::optional NongManager::getNongs(int songID) { - if (!m_state.m_nongs.contains(songID)) { - return std::nullopt; - } - - return m_state.m_nongs[songID]; -} +namespace jukebox { -std::optional NongManager::getActiveNong(int songID) { - auto nongs_res = this->getNongs(songID); - if (!nongs_res.has_value()) { +std::optional NongManager::getNongs(int songID) { + if (!m_manifest.m_nongs.contains(songID)) { return std::nullopt; } - auto nongs = nongs_res.value(); - for (auto &song : nongs.songs) { - if (song.path == nongs.active) { - return song; - } - } - - nongs.active = nongs.defaultPath; - - for (auto &song : nongs.songs) { - if (song.path == nongs.active) { - return song; + // Try to update saved songs from the index in case it changed + auto localNongs = m_manifest.m_nongs[songID].get(); + + if (!IndexManager::get()->m_indexNongs.contains(songID)) return localNongs; + + Nongs* indexNongs = &IndexManager::get()->m_indexNongs.at(songID); + + bool changed = false; + + for (std::unique_ptr& song : localNongs->youtube()) { + // Check if song is from an index + if (!song->indexID().has_value()) continue; + for (std::unique_ptr& indexSong : indexNongs->youtube()) { + if (song->indexID() != indexSong->indexID()) continue; + auto metadata = song->metadata(); + auto indexMetadata = indexSong->metadata(); + if (metadata->m_uniqueID != indexMetadata->m_uniqueID) continue; + if ( + metadata->m_gdID == indexMetadata->m_gdID && + metadata->m_name == indexMetadata->m_name && + metadata->m_artist == indexMetadata->m_artist && + metadata->m_level == indexMetadata->m_level && + metadata->m_startOffset == indexMetadata->m_startOffset && + song->youtubeID() == indexSong->youtubeID() + ) continue; + + bool deleteAudio = song->youtubeID() != indexSong->youtubeID(); + + if (auto res = localNongs->replaceSong(song->metadata()->m_uniqueID, Nong { + YTSong { + SongMetadata(*indexSong->metadata()), + indexSong->youtubeID(), + indexSong->indexID(), + deleteAudio ? std::nullopt : song->path(), + }, + }); res.isErr()) { + SongErrorEvent(false, "Failed to replace song while updating saved songs from index: {}", res.error()).post(); + continue; + } + changed = true; } } - return std::nullopt; -} - -bool NongManager::hasActions(int songID) { - return m_getSongInfoActions.contains(songID); -} - -bool NongManager::isFixingDefault(int songID) { - if (!m_getSongInfoActions.contains(songID)) { - return false; - } - - for (auto action : m_getSongInfoActions[songID]) { - if (action == SongInfoGetAction::FixDefault) { - return true; + for (std::unique_ptr& song : localNongs->hosted()) { + // Check if song is from an index + if (!song->indexID().has_value()) continue; + for (std::unique_ptr& indexSong : indexNongs->hosted()) { + if (song->indexID() != indexSong->indexID()) continue; + auto metadata = song->metadata(); + auto indexMetadata = indexSong->metadata(); + if (metadata->m_uniqueID != indexMetadata->m_uniqueID) continue; + if ( + metadata->m_gdID == indexMetadata->m_gdID && + metadata->m_name == indexMetadata->m_name && + metadata->m_artist == indexMetadata->m_artist && + metadata->m_level == indexMetadata->m_level && + metadata->m_startOffset == indexMetadata->m_startOffset && + song->url() == indexSong->url() + ) continue; + + bool deleteAudio = song->url() != indexSong->url(); + + if (auto res = localNongs->replaceSong(song->metadata()->m_uniqueID, Nong { + HostedSong { + SongMetadata(*indexSong->metadata()), + indexSong->url(), + indexSong->indexID(), + deleteAudio ? std::nullopt : song->path(), + }, + }); res.isErr()) { + SongErrorEvent(false, "Failed to replace song while updating saved songs from index: {}", res.error()).post(); + continue; + } + changed = true; } } - return false; -} - -std::optional NongManager::getDefaultNong(int songID) { - auto nongs_res = this->getNongs(songID); - if (!nongs_res.has_value()) { - return std::nullopt; + if (changed) { + saveNongs(songID); } - auto nongs = nongs_res.value(); - for (auto &song : nongs.songs) { - if (song.path == nongs.defaultPath) { - return song; - } - } - - return std::nullopt; + return m_manifest.m_nongs[songID].get(); } -void NongManager::resolveSongInfoCallback(int id) { - if (m_getSongInfoCallbacks.contains(id)) { - m_getSongInfoCallbacks[id](id); - m_getSongInfoCallbacks.erase(id); - } -} - -std::vector NongManager::validateNongs(int songID) { - auto result = this->getNongs(songID); - // Validate nong paths and delete those that don't exist anymore - std::vector invalidSongs; - std::vector validSongs; - if (!result.has_value()) { - return invalidSongs; - } - auto currentData = result.value(); - - for (auto &song : currentData.songs) { - if (!fs::exists(song.path) && currentData.defaultPath != song.path && song.songUrl == "local") { - invalidSongs.push_back(song); - if (song.path == currentData.active) { - currentData.active = currentData.defaultPath; - } - } else { - validSongs.push_back(song); - } - } - - if (invalidSongs.size() > 0) { - NongData newData = { - .active = currentData.active, - .defaultPath = currentData.defaultPath, - .songs = validSongs, - }; - - this->saveNongs(newData, songID); - } - - return invalidSongs; +Result NongManager::getNongFromManifest(int gdSongID, std::string uniqueID) { + auto nongs = getNongs(gdSongID); + if (nongs.has_value()) { + return Err("Song not initialized in manifest"); + } + auto nong = nongs.value()->getNongFromID(uniqueID); + if (!nong.has_value()) { + return Err("Nong {} not found in song {}", uniqueID, gdSongID); + } + return Ok(std::move(nong.value())); } int NongManager::getCurrentManifestVersion() { - return m_state.m_manifestVersion; + return m_manifest.m_version; } int NongManager::getStoredIDCount() { - return m_state.m_nongs.size(); + return m_manifest.m_nongs.size(); } -void NongManager::saveNongs(NongData const& data, int songID) { - m_state.m_nongs[songID] = data; - this->writeJson(); +int NongManager::adjustSongID(int id, bool robtop) { + return robtop ? (id < 0 ? id : -id - 1) : id; } -void NongManager::writeJson() { - auto json = matjson::Serialize::to_json(m_state); - auto path = this->getJsonPath(); - std::ofstream output(path.c_str()); - output << json.dump(matjson::NO_INDENTATION); - output.close(); -} - -void NongManager::addNong(SongInfo const& song, int songID) { - auto result = this->getNongs(songID); - if (!result.has_value()) { +void NongManager::initSongID(SongInfoObject* obj, int id, bool robtop) { + if (m_manifest.m_nongs.contains(id)) { return; } - auto existingData = result.value(); - for (auto const& savedSong : existingData.songs) { - if (song.path.string() == savedSong.path.string()) { - return; - } - } - existingData.songs.push_back(song); - this->saveNongs(existingData, songID); -} - -void NongManager::deleteAll(int songID) { - std::vector newSongs; - auto result = this->getNongs(songID); - if (!result.has_value()) { + if (!obj && robtop) { + log::error("Critical. No song object for RobTop song"); return; } - auto existingData = result.value(); - - for (auto savedSong : existingData.songs) { - if (savedSong.path != existingData.defaultPath) { - if (fs::exists(savedSong.path)) { - std::error_code ec; - fs::remove(savedSong.path, ec); - if (ec) { - log::error("Couldn't delete nong. Category: {}, message: {}", ec.category().name(), ec.category().message(ec.value())); - return; - } - } - continue; - } - newSongs.push_back(savedSong); - } - - NongData newData = { - .active = existingData.defaultPath, - .defaultPath = existingData.defaultPath, - .songs = newSongs, - }; - this->saveNongs(newData, songID); -} -void NongManager::deleteNong(SongInfo const& song, int songID, bool deleteFile) { - std::vector newSongs; - auto result = this->getNongs(songID); - if(!result.has_value()) { - return; - } - auto existingData = result.value(); - for (auto savedSong : existingData.songs) { - if (savedSong.path == song.path) { - if (song.path == existingData.active) { - existingData.active = existingData.defaultPath; - } - if (deleteFile && existingData.defaultPath != song.path && fs::exists(song.path)) { - std::error_code ec; - fs::remove(song.path, ec); - if (ec) { - log::error("Couldn't delete nong. Category: {}, message: {}", ec.category().name(), ec.category().message(ec.value())); - return; - } + if (obj && robtop) { + int adjusted = adjustSongID(id, robtop); + std::string filename = LevelTools::getAudioFileName(adjusted); + std::filesystem::path gdDir = std::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str()); + m_manifest.m_nongs.insert({adjusted, std::make_unique(Nongs { + adjusted, + LocalSong { + SongMetadata { + adjusted, + jukebox::random_string(16), + obj->m_songName, + obj->m_artistName + }, + gdDir / "Resources" / filename } - continue; - } - newSongs.push_back(savedSong); - } - NongData newData = { - .active = existingData.active, - .defaultPath = existingData.defaultPath, - .songs = newSongs, - }; - this->saveNongs(newData, songID); -} + })}); + saveNongs(adjusted); -void NongManager::createDefault(SongInfoObject* object, int songID, bool robtop) { - if (m_state.m_nongs.contains(songID)) { - return; - } - if (!robtop) { - object = MusicDownloadManager::sharedState()->getSongInfoObject(songID); - } - if (!robtop && object == nullptr && !m_getSongInfoCallbacks.contains(songID)) { - MusicDownloadManager::sharedState()->getSongInfo(songID, true); - this->addSongIDAction(songID, SongInfoGetAction::CreateDefault); - return; - } - if (object == nullptr && !robtop) { return; } - this->createDefaultCallback(object, songID); -} + if (!obj) { + // Try and maybe fetch it + obj = MusicDownloadManager::sharedState()->getSongInfoObject(id); + } -void NongManager::createUnknownDefault(int songID) { - if (this->getNongs(songID).has_value()) { + if (!obj) { + // Try fetch song info from servers + MusicDownloadManager::sharedState()->getSongInfo(id, true); + m_manifest.m_nongs.insert({id, std::make_unique(Nongs { + id, + LocalSong::createUnknown(id) + })}); + saveNongs(id); return; } - fs::path songPath = fs::path(std::string(MusicDownloadManager::sharedState()->pathForSong(songID))); - NongData data; - SongInfo defaultSong; - defaultSong.authorName = ""; - defaultSong.songName = "Unknown"; - defaultSong.path = songPath; - defaultSong.songUrl = ""; - defaultSong.startOffset = 0; - data.active = songPath; - data.defaultPath = songPath; - data.songs.push_back(defaultSong); - data.defaultValid = false; - m_state.m_nongs[songID] = data; - this->writeJson(); + + m_manifest.m_nongs.insert({id, std::make_unique(Nongs { + id, + LocalSong { + SongMetadata { + id, + jukebox::random_string(16), + obj->m_songName, + obj->m_artistName + }, + std::filesystem::path(MusicDownloadManager::sharedState()->pathForSong(id).c_str()) + } + })}); + saveNongs(id); } -std::string NongManager::getFormattedSize(SongInfo const& song) { +std::string NongManager::getFormattedSize(const std::filesystem::path& path) { std::error_code code; - auto size = fs::file_size(song.path, code); + auto size = std::filesystem::file_size(path, code); if (code) { return "N/A"; } @@ -278,8 +211,9 @@ std::string NongManager::getFormattedSize(SongInfo const& song) { } NongManager::MultiAssetSizeTask NongManager::getMultiAssetSizes(std::string songs, std::string sfx) { - fs::path resources = fs::path(CCFileUtils::get()->getWritablePath2().c_str()) / "Resources"; - fs::path songDir = fs::path(CCFileUtils::get()->getWritablePath().c_str()); + auto resources = std::filesystem::path(CCFileUtils::get()->getWritablePath2()) + / "Resources"; + auto songDir = std::filesystem::path(CCFileUtils::get()->getWritablePath()); return MultiAssetSizeTask::run([this, songs, sfx, resources, songDir](auto progress, auto hasBeenCanceled) -> MultiAssetSizeTask::Result { float sum = 0.f; @@ -287,16 +221,17 @@ NongManager::MultiAssetSizeTask NongManager::getMultiAssetSizes(std::string song std::string s; while (std::getline(stream, s, ',')) { int id = std::stoi(s); - auto result = this->getActiveNong(id); + auto result = this->getNongs(id); if (!result.has_value()) { continue; } - auto path = result->path; + auto nongs = result.value(); + auto path = nongs->getNongFromID(nongs->active()).value().path().value(); if (path.string().starts_with("songs/")) { path = resources / path; } - if (fs::exists(path)) { - sum += fs::file_size(path); + if (std::filesystem::exists(path)) { + sum += std::filesystem::file_size(path); } } stream = std::istringstream(sfx); @@ -306,13 +241,13 @@ NongManager::MultiAssetSizeTask NongManager::getMultiAssetSizes(std::string song std::string filename = ss.str(); auto localPath = resources / "sfx" / filename; std::error_code _ec; - if (fs::exists(localPath, _ec)) { - sum += fs::file_size(localPath); + if (std::filesystem::exists(localPath, _ec)) { + sum += std::filesystem::file_size(localPath); continue; } auto path = songDir / filename; - if (fs::exists(path, _ec)) { - sum += fs::file_size(path); + if (std::filesystem::exists(path, _ec)) { + sum += std::filesystem::file_size(path); } } @@ -320,216 +255,219 @@ NongManager::MultiAssetSizeTask NongManager::getMultiAssetSizes(std::string song std::stringstream ss; ss << std::setprecision(3) << toMegabytes << "MB"; return ss.str(); - }, fmt::format("Multiasset: {}|{}", songs, sfx)); + }, "Multiasset calculation"); } -fs::path NongManager::getJsonPath() { - auto savedir = Mod::get()->getSaveDir(); - return savedir / "nong_data.json"; -} - -void NongManager::loadSongs() { +bool NongManager::init() { if (m_initialized) { - return; + return true; } - auto path = this->getJsonPath(); - if (!fs::exists(path)) { - this->setDefaultState(); - return; - } - std::ifstream input(path.string()); - std::stringstream buffer; - buffer << input.rdbuf(); - input.close(); - std::string string = buffer.str(); - if (string.empty()) { - this->setDefaultState(); - return; + m_songErrorListener.bind([this](SongErrorEvent* event){ + log::error("{}", event->error()); + return ListenerResult::Propagate; + }); + + m_songInfoListener.bind([this](GetSongInfoEvent* event){ + log::info("Song info event for {}", event->gdSongID()); + log::info("info: {} - {}", event->songName(), event->artistName()); + + auto nongs = getNongs(event->gdSongID()); + if (!nongs.has_value()) return ListenerResult::Stop; + SongMetadata* defaultSongMetadata = nongs.value()->defaultSong()->metadata(); + if (event->songName() == defaultSongMetadata->m_name && event->artistName() == defaultSongMetadata->m_artist) return ListenerResult::Stop; + + defaultSongMetadata->m_name = event->songName(); + defaultSongMetadata->m_artist = event->artistName(); + + log::info("got here?"); + + saveNongs(event->gdSongID()); + + return ListenerResult::Propagate; + }); + + auto path = this->baseManifestPath(); + if (!std::filesystem::exists(path)) { + std::filesystem::create_directory(path); + return true; } - std::string fixed; - fixed.reserve(string.size()); - for (size_t i = 0; i < string.size(); i++) { - if (string[i] != '\0') { - fixed += string[i]; + + for (const auto& entry : std::filesystem::directory_iterator(path)) { + if (entry.path().extension() != ".json") { + continue; } - } + log::info("Loading nong from {}", entry.path().string()); - std::string error; - auto json = matjson::parse(std::string_view(fixed), error); - if (!json.has_value()) { - this->backupCurrentJSON(); - this->setDefaultState(); - Mod::get()->setSavedValue("failed-load", true); - return; + auto res = this->loadNongsFromPath(entry.path()); + if (res.isErr()) { + log::error("{}", res.unwrapErr()); + std::filesystem::rename(entry.path(), path / fmt::format("{}.bak", entry.path().filename().string())); + continue; + } + + m_manifest.m_nongs.insert({ res.unwrap()->songID(), std::move(res.unwrap()) }); } - m_state = matjson::Serialize::from_json(json.value()); + m_initialized = true; + return true; } -void NongManager::backupCurrentJSON() { - auto savedir = Mod::get()->getSaveDir(); - auto backups = savedir / "backups"; - if (!fs::exists(backups)) { - std::error_code ec; - bool result = fs::create_directory(backups, ec); - if (ec) { - log::error("Couldn't create backups directory, error category: {}, message: {}", ec.category().name(), ec.category().message(ec.value())); - return; +Result<> NongManager::saveNongs(std::optional saveID) { + auto path = this->baseManifestPath(); + + if (!std::filesystem::exists(path)) { + std::filesystem::create_directory(path); + } + + for (const auto& entry : m_manifest.m_nongs) { + auto id = entry.first; + + if (saveID.has_value() && saveID.value() != id) continue; + + auto& nong = entry.second; + + auto json = matjson::Serialize::to_json(*nong); + if (json.isErr()) { + return Err(json.error()); } - if (!result) { - log::error("Couldn't create backups directory"); - return; + + auto filepath = fmt::format("{}/{}.json", path.string(), id); + std::ofstream output(filepath); + if (!output.is_open()) { + return Err(fmt::format("Couldn't open file: {}", filepath)); } + output << json.unwrap().dump(matjson::NO_INDENTATION); + output.close(); } - auto now = std::chrono::system_clock::now(); - std::string formatted = fmt::format("backup-{:%d-%m-%Y %H-%M-%OS}.json", now); - fs::path backupPath = backups / formatted; - std::error_code ec; - fs::path currentJson = this->getJsonPath(); - bool result = fs::copy_file(currentJson, backupPath, ec); - if (ec) { - log::error("Couldn't create backup for nong_data.json, error category: {}, message: {}", ec.category().name(), ec.category().message(ec.value())); + if (saveID.has_value()) { + SongStateChangedEvent(saveID.value()).post(); } -} -void NongManager::setDefaultState() { - m_state.m_manifestVersion = jukebox::getManifestVersion(); + return Ok(); } -void NongManager::prepareCorrectDefault(int songID) { - auto res = this->getNongs(songID); - if (!res.has_value()) { - return; + +Result> NongManager::loadNongsFromPath(const std::filesystem::path& path) { + auto stem = path.stem().string(); + // Watch someone edit a json name and have the game crash + int id = std::stoi(stem); + if (id == 0) { + return Err(fmt::format("Invalid filename {}", path.filename().string())); } - auto nongs = res.value(); - if (nongs.defaultValid) { - return; + std::ifstream input(path); + if (!input.is_open()) { + return Err(fmt::format("Couldn't open file: {}", path.filename().string())); } - nongs.defaultValid = false; - this->saveNongs(nongs, songID); - MusicDownloadManager::sharedState()->clearSong(songID); - MusicDownloadManager::sharedState()->getSongInfo(songID, true); - this->addSongIDAction(songID, SongInfoGetAction::FixDefault); -} -void NongManager::markAsInvalidDefault(int songID) { - auto res = this->getNongs(songID); + // Did some brief research, this seems to be the most efficient method + // https://insanecoding.blogspot.com/2011/11/how-to-read-in-file-in-c.html + + std::string contents; + input.seekg(0, std::ios::end); + contents.resize(input.tellg()); + input.seekg(0, std::ios::beg); + input.read(&contents[0], contents.size()); + input.close(); + + std::string err; + auto res = matjson::parse(std::string_view(contents), err); if (!res.has_value()) { - return; + return Err(fmt::format( + "{}: Couldn't parse JSON from file: {}", id, err + )); } - auto nongs = res.value(); - nongs.defaultValid = false; + auto json = res.value(); + auto nongs = matjson::Serialize::from_json(json, id); + if (nongs.isErr()) { + return Err(fmt::format( + "{}: Failed to parse JSON: {}", + id, + nongs.unwrapErr() + )); + } - this->saveNongs(nongs, songID); + return Ok(std::make_unique(std::move(nongs.unwrap()))); } -void NongManager::fixDefault(SongInfoObject* obj) { - int songID = obj->m_songID; - auto nongs = this->getNongs(songID).value(); - auto defaultNong = this->getDefaultNong(songID).value(); - if (obj->m_songUrl.empty() && obj->m_artistName.empty()) { - nongs.defaultValid = false; - this->saveNongs(nongs, obj->m_songID); - return; +void NongManager::refetchDefault(int songID) { + MusicDownloadManager::sharedState()->clearSong(songID); + MusicDownloadManager::sharedState()->getSongInfo(songID, true); +} + +Result<> NongManager::addNongs(Nongs&& nongs) { + if (!m_manifest.m_nongs.contains(nongs.songID())) { + return Err("Song not initialized in manifest"); } - for (auto& song : nongs.songs) { - if (song.path != defaultNong.path) { - continue; - } - song.songName = obj->m_songName; - song.authorName = obj->m_artistName; - song.songUrl = obj->m_songUrl; + auto& manifestNongs = m_manifest.m_nongs.at(nongs.songID()); + if (auto err = manifestNongs->merge(std::move(nongs)); err.isErr()) { + return err; } - nongs.defaultValid = true; - this->saveNongs(nongs, obj->m_songID); + return saveNongs(manifestNongs->songID()); } -void NongManager::createDefaultCallback(SongInfoObject* obj, int songID) { - int id = obj->m_songID; - if (songID != 0) { - id = songID; - } - if (auto nongs = NongManager::get()->getNongs(songID)) { - return; +Result<> NongManager::setActiveSong(int gdSongID, std::string uniqueID) { + auto nongs = getNongs(gdSongID); + if (!nongs.has_value()) { + return Err("Song not initialized in manifest"); } - - fs::path songPath; - if (id < 0) { - gd::string filename = LevelTools::getAudioFileName((-id) - 1); - fs::path gdDir = fs::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str()); - songPath = gdDir / "Resources" / filename.c_str(); - } else { - songPath = fs::path(MusicDownloadManager::sharedState()->pathForSong(obj->m_songID).c_str()); + if (auto err = nongs.value()->setActive(uniqueID); err.isErr()) { + return err; } - - NongData data; - SongInfo defaultSong; - defaultSong.authorName = obj->m_artistName; - defaultSong.songName = obj->m_songName; - defaultSong.path = songPath; - defaultSong.songUrl = obj->m_songUrl; - data.active = songPath; - data.defaultPath = songPath; - data.songs.push_back(defaultSong); - data.defaultValid = true; - m_state.m_nongs[id] = data; + return saveNongs(gdSongID); } -ListenerResult NongManager::onSongInfoFetched(GetSongInfoEvent* event) { - SongInfoObject* obj = event->getObject(); - int id = event->getID(); - auto res = this->getSongIDActions(id); - if (!res.has_value()) { - return ListenerResult::Propagate; - } - - auto actions = res.value(); - if (actions.contains(SongInfoGetAction::CreateDefault)) { - if (obj == nullptr) { - this->createUnknownDefault(id); - } else { - this->createDefaultCallback(obj); - } - } - if (actions.contains(SongInfoGetAction::FixDefault)) { - if (obj != nullptr) { - this->fixDefault(obj); - } - } - m_getSongInfoActions.erase(id); - return ListenerResult::Propagate; +Result<> NongManager::deleteAllSongs(int gdSongID) { + auto nongs = getNongs(gdSongID); + if (!nongs.has_value()) { + return Err("Song not initialized in manifest"); + } + if (auto err = m_manifest.m_nongs.at(gdSongID)->deleteAllSongs(); err.isErr()) { + return err; + } + return saveNongs(gdSongID); } -std::optional> NongManager::getSongIDActions(int songID) { - if (!m_getSongInfoActions.contains(songID)) { - return std::nullopt; +Result<> NongManager::deleteSongAudio(int gdSongID, std::string uniqueID) { + auto nongs = getNongs(gdSongID); + if (!nongs.has_value()) { + return Err("Song not initialized in manifest"); } - if (m_getSongInfoActions[songID].empty()) { - m_getSongInfoActions.erase(songID); - return std::nullopt; + if (auto err = nongs.value()->deleteSongAudio(uniqueID); err.isErr()) { + return Err("Couldn't delete Nong: {}", err.error()); } - return m_getSongInfoActions[songID]; + return saveNongs(gdSongID); } -void NongManager::addSongIDAction(int songID, SongInfoGetAction action) { - if (!m_getSongInfoActions.contains(songID)) { - m_getSongInfoActions[songID] = { action }; - return; +Result<> NongManager::deleteSong(int gdSongID, std::string uniqueID) { + auto nongs = getNongs(gdSongID); + if (!nongs.has_value()) { + return Err("Song not initialized in manifest"); } - if (m_getSongInfoActions[songID].contains(action)) { - return; + if (auto err = nongs.value()->deleteSong(uniqueID); err.isErr()) { + return Err("Couldn't delete Nong: {}", err.error()); } - m_getSongInfoActions[songID].insert(action); + return saveNongs(gdSongID); } +std::filesystem::path NongManager::generateSongFilePath(const std::string& extension, std::optional filename) { + auto unique = filename.value_or(jukebox::random_string(16)); + auto destination = Mod::get()->getSaveDir() / "nongs"; + if (!std::filesystem::exists(destination)) { + std::filesystem::create_directory(destination); + } + unique += extension; + destination = destination / unique; + return destination; } + +}; diff --git a/src/managers/nong_manager.hpp b/src/managers/nong_manager.hpp index 1074f6f..0c4acc2 100644 --- a/src/managers/nong_manager.hpp +++ b/src/managers/nong_manager.hpp @@ -1,50 +1,61 @@ #pragma once -#include -#include +#include #include -#include -#include -#include -#include -#include "../types/song_info.hpp" -#include "../types/nong_state.hpp" +#include "Geode/utils/Task.hpp" +#include "Geode/binding/SongInfoObject.hpp" +#include "Geode/loader/Event.hpp" +#include "Geode/loader/Mod.hpp" + +#include "../../include/nong.hpp" + +#include "../events/song_state_changed_event.hpp" +#include "../events/song_error_event.hpp" #include "../events/get_song_info_event.hpp" +#include "../events/song_download_progress_event.hpp" using namespace geode::prelude; namespace jukebox { -enum class SongInfoGetAction { - CreateDefault, - FixDefault -}; - class NongManager : public CCObject { protected: inline static NongManager* m_instance = nullptr; - NongState m_state; - std::unordered_map> m_getSongInfoCallbacks; - std::unordered_map> m_getSongInfoActions; - EventListener> m_songInfoListener = { this, &NongManager::onSongInfoFetched }; + Manifest m_manifest; bool m_initialized = false; - std::optional> getSongIDActions(int songID); - void addSongIDAction(int songID, SongInfoGetAction action); - void createDefaultCallback(SongInfoObject* obj, int songID = 0); - void setDefaultState(); - void backupCurrentJSON(); + void setupManifestPath() { + auto path = this->baseManifestPath(); + if (!std::filesystem::exists(path)) { + std::filesystem::create_directory(path); + } + } + + std::filesystem::path baseManifestPath() { + static std::filesystem::path path = Mod::get()->getSaveDir() / "manifest"; + return path; + } + + bool init(); + Result<> saveNongs(std::optional saveId = std::nullopt); + EventListener m_songErrorListener; + EventListener m_songInfoListener; + Result> loadNongsFromPath(const std::filesystem::path& path); public: using MultiAssetSizeTask = Task; - std::optional m_currentlyPreparingNong; + std::optional m_currentlyPreparingNong; + + bool initialized() const { + return m_initialized; + } - bool initialized() { return m_initialized; } + void initSongID(SongInfoObject* obj, int id, bool robtop); /** - * Only used once, on game launch. Reads the json and loads it into memory. + * Adjusts a song ID with respect to Robtop songs */ - void loadSongs(); + int adjustSongID(int id, bool robtop); /** * Execute callbacks stored for getSongInfo for a songID, if they exist @@ -52,7 +63,7 @@ class NongManager : public CCObject { void resolveSongInfoCallback(int id); /** - * Gets the current manifest version stored in state + * Gets the current manifest version stored in state */ int getCurrentManifestVersion(); @@ -62,151 +73,87 @@ class NongManager : public CCObject { int getStoredIDCount(); /** - * Adds a NONG to the JSON of a songID - * - * @param song the song to add - * @param songID the id of the song - */ - void addNong(SongInfo const& song, int songID); - - /** - * Removes a NONG from the JSON of a songID - * - * @param song the song to remove - * @param songID the id of the song - * @param deleteFile whether to delete the corresponding audio file created by Jukebox - */ - void deleteNong(SongInfo const& song, int songID, bool deleteFile = true); + * Get Nong from manifest + * @param gdSongID the id of the song in GD + * @param uniqueID the unique id of the song in Jukebox + */ + Result getNongFromManifest(int gdSongID, std::string uniqueID); /** * Fetches all NONG data for a certain songID - * + * * @param songID the id of the song * @return the data from the JSON or nullopt if it wasn't created yet */ - std::optional getNongs(int songID); - - /** - * Fetches the active song from the songID JSON - * - * @param songID the id of the song - * @return the song data or nullopt in case of an error - */ - std::optional getActiveNong(int songID); - - /** - * Fetches the default song from the songID JSON - * - * @param songID the id of the song - * @return the song data or nullopt in case of an error - */ - std::optional getDefaultNong(int songID); - - /** - * Validates any local nongs that have an invalid path - * - * @param songID the id of the song - * - * @return an array of songs that were deleted as result of the validation - */ - std::vector validateNongs(int songID); - - /** - * Saves NONGS to the songID JSON - * - * @param data the data to save - * @param songID the id of the song - */ - void saveNongs(NongData const& data, int songID); - - /** - * Writes song data to the JSON - */ - void writeJson(); - - /** - * Removes all NONG data for a song ID - * - * @param songID the id of the song - */ - void deleteAll(int songID); + std::optional getNongs(int songID); /** * Formats a size in bytes to a x.xxMB string - * - * @param song the song - * + * + * @param path the path to calculate the filesize of + * * @return the formatted size, with the format x.xxMB */ - std::string getFormattedSize(SongInfo const& song); + std::string getFormattedSize(const std::filesystem::path& path); /** * Calculates the total size of multiple assets, then writes it to a string. * Runs on a separate thread. Returns a task that will resolve to the total size. - * + * * @param songs string of song ids, separated by commas * @param sfx string of sfx ids, separated by commas */ MultiAssetSizeTask getMultiAssetSizes(std::string songs, std::string sfx); /** - * Fetches song info for an id and creates the default entry in the json. - * - * @param songID the id of the song + * Add actions needed to fix a broken song default + * @param songID id of the song */ - void createDefault(SongInfoObject* object, int songID, bool robtop); + void refetchDefault(int songID); /** - * Creates a default with name unknown and artist unknown. Used for invalid song ids. - * - * @param songID the id of the song + * Add NONGs + * @param nong NONG to add */ - void createUnknownDefault(int songID); + Result<> addNongs(Nongs&& nong); /** - * Checks if a song ID has actions associated to it - * - * @param songID the id of the song + * Set active song + * @param gdSongID the id of the song in GD + * @param uniqueID the unique id of the song in Jukebox */ - bool hasActions(int songID); + Result<> setActiveSong(int gdSongID, std::string uniqueID); /** - * Checks if the default song is being fixed for a song ID - * - * @param songID the id of the song + * Delete a song + * @param gdSongID the id of the song in GD + * @param uniqueID the unique id of the song in Jukebox */ - bool isFixingDefault(int songID); + Result<> deleteSong(int gdSongID, std::string uniqueID); /** - * Returns the savefile path - * - * @return the path of the JSON + * Delete a song's audio file + * @param gdSongID the id of the song in GD + * @param uniqueID the unique id of the song in Jukebox */ - fs::path getJsonPath(); + Result<> deleteSongAudio(int gdSongID, std::string uniqueID); /** - * Add actions needed to fix a broken song default - * @param songID id of the song + * Delete all NONGs for a song ID + * @param gdSongID id of the song */ - void prepareCorrectDefault(int songID); - /** - * Callback that runs when a fix song default action runs - * @param songID id of the song - */ - void fixDefault(SongInfoObject* obj); + Result<> deleteAllSongs(int gdSongID); /** - * Marks a song ID as having an invalid default - * @param songID id of the song + * Get a path to a song file */ - void markAsInvalidDefault(int songID); - - ListenerResult onSongInfoFetched(GetSongInfoEvent* event); + std::filesystem::path generateSongFilePath(const std::string& extension, std::optional filename = std::nullopt); static NongManager* get() { if (m_instance == nullptr) { - m_instance = new NongManager; + m_instance = new NongManager(); m_instance->retain(); + m_instance->init(); } return m_instance; diff --git a/src/manifest.cpp b/src/manifest.cpp deleted file mode 100644 index 8a8cd75..0000000 --- a/src/manifest.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "manifest.hpp" - -namespace jukebox { - int getManifestVersion() { - return 3; - } -} \ No newline at end of file diff --git a/src/manifest.hpp b/src/manifest.hpp deleted file mode 100644 index f2dcfdc..0000000 --- a/src/manifest.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace jukebox { - int getManifestVersion(); -} \ No newline at end of file diff --git a/src/nong.cpp b/src/nong.cpp new file mode 100644 index 0000000..968f0ef --- /dev/null +++ b/src/nong.cpp @@ -0,0 +1,844 @@ +#include "../include/nong.hpp" + +#include "Geode/binding/MusicDownloadManager.hpp" +#include "./utils/random_string.hpp" + +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +namespace jukebox { + +class LocalSong::Impl { +private: + friend class LocalSong; + + std::unique_ptr m_metadata; + std::filesystem::path m_path; +public: + Impl( + SongMetadata&& metadata, + const std::filesystem::path& path + ) : m_metadata(std::make_unique(metadata)), + m_path(path) + {} + + Impl(const Impl& other) + : m_path(other.m_path), + m_metadata(std::make_unique(*other.m_metadata)) + {} + Impl& operator=(const Impl&) = delete; + + ~Impl() = default; + + Impl(Impl&&) = default; + Impl& operator=(Impl&&) = default; + + SongMetadata* metadata() const { + return m_metadata.get(); + } + std::filesystem::path path() const { + return m_path; + } +}; + +LocalSong::LocalSong( + SongMetadata&& metadata, + const std::filesystem::path& path +) : m_impl(std::make_unique(std::move(metadata), path)) +{} + +LocalSong::LocalSong(const LocalSong& other) + : m_impl(std::make_unique(*other.m_impl)) +{} + +LocalSong& LocalSong::operator=(const LocalSong& other) { + m_impl->m_path = other.path(); + m_impl->m_metadata = std::make_unique(*other.m_impl->m_metadata); + + return *this; +} + +LocalSong::LocalSong(LocalSong&&) = default; +LocalSong& LocalSong::operator=(LocalSong&&) = default; +LocalSong::~LocalSong() = default; + +SongMetadata* LocalSong::metadata() const { + return m_impl->metadata(); +} +std::filesystem::path LocalSong::path() const { + return m_impl->path(); +} + +LocalSong LocalSong::createUnknown(int songID) { + return LocalSong { + SongMetadata { + songID, + jukebox::random_string(16), + "Unknown", + "" + }, + std::filesystem::path( + MusicDownloadManager::sharedState()->pathForSong(songID) + ) + }; +} + +LocalSong LocalSong::fromSongObject(SongInfoObject* obj) { + return LocalSong { + SongMetadata { + obj->m_songID, + jukebox::random_string(16), + obj->m_songName, + obj->m_artistName + }, + std::filesystem::path( + MusicDownloadManager::sharedState()->pathForSong(obj->m_songID) + ) + }; +} + +class YTSong::Impl { +private: + friend class YTSong; + + std::unique_ptr m_metadata; + std::string m_youtubeID; + std::optional m_indexID; + std::optional m_path; +public: + Impl( + SongMetadata&& metadata, + std::string youtubeID, + std::optional indexID, + std::optional path = std::nullopt + ) : m_metadata(std::make_unique(metadata)), + m_youtubeID(youtubeID), + m_indexID(indexID), + m_path(path) + {} + + Impl(const Impl& other) + : m_metadata(std::make_unique(*other.m_metadata)), + m_path(other.m_path), + m_indexID(other.m_indexID), + m_youtubeID(other.m_youtubeID) + {} + Impl& operator=(const Impl&) = delete; + + ~Impl() = default; + + Impl(Impl&&) = default; + Impl& operator=(Impl&&) = default; + + SongMetadata* metadata() const { + return m_metadata.get(); + } + std::optional path() const { + return m_path; + } + std::string youtubeID() const { + return m_youtubeID; + } + std::optional indexID() const { + return m_indexID; + } +}; + +YTSong::YTSong( + SongMetadata&& metadata, + std::string youtubeID, + std::optional indexID, + std::optional path +) : m_impl(std::make_unique( + std::move(metadata), + youtubeID, + indexID, + path +)) {} + +YTSong::YTSong(const YTSong& other) + : m_impl(std::make_unique(*other.m_impl)) +{} + +YTSong& YTSong::operator=(const YTSong& other) { + m_impl->m_youtubeID = other.m_impl->m_youtubeID; + m_impl->m_path = other.m_impl->m_path; + m_impl->m_indexID = other.m_impl->m_indexID; + m_impl->m_metadata = std::make_unique(*other.m_impl->m_metadata); + + return *this; +} + +YTSong::YTSong(YTSong&& other) = default; +YTSong& YTSong::operator=(YTSong&& other) = default; +YTSong::~YTSong() = default; + +SongMetadata* YTSong::metadata() const { + return m_impl->metadata(); +} + +std::string YTSong::youtubeID() const { + return m_impl->youtubeID(); +} + +std::optional YTSong::indexID() const { + return m_impl->indexID(); +} + +std::optional YTSong::path() const { + return m_impl->path(); +} + +class HostedSong::Impl { +private: + friend class HostedSong; + + std::unique_ptr m_metadata; + std::string m_url; + std::optional m_indexID; + std::optional m_path; +public: + Impl( + SongMetadata&& metadata, + std::string url, + std::optional indexID, + std::optional path = std::nullopt + ) : m_metadata(std::make_unique(metadata)), + m_url(url), + m_indexID(indexID), + m_path(path) + {} + + Impl(const Impl& other) + : m_metadata(std::make_unique(*other.m_metadata)), + m_path(other.m_path), + m_indexID(other.m_indexID), + m_url(other.m_url) + {} + Impl& operator=(const Impl& other) = delete; + + Impl(Impl&&) = default; + Impl& operator=(Impl&&) = default; + + ~Impl() = default; + + SongMetadata* metadata() const { + return m_metadata.get(); + } + std::string url() const { + return m_url; + } + std::optional indexID() const { + return m_indexID; + } + std::optional path() const { + return m_path; + } +}; + +HostedSong::HostedSong( + SongMetadata&& metadata, + std::string url, + std::optional indexID, + std::optional path +) : m_impl(std::make_unique( + std::move(metadata), + url, + indexID, + path +)) {} + +HostedSong::HostedSong(const HostedSong& other) + : m_impl(std::make_unique(*other.m_impl)) +{} + +HostedSong& HostedSong::operator=(const HostedSong& other) { + m_impl->m_metadata = std::make_unique(*other.m_impl->m_metadata); + m_impl->m_path = other.m_impl->m_path; + m_impl->m_indexID = other.m_impl->m_indexID; + m_impl->m_url = other.m_impl->m_url; + + return *this; +} +SongMetadata* HostedSong::metadata() const { + return m_impl->metadata(); +} +std::string HostedSong::url() const { + return m_impl->url(); +} +std::optional HostedSong::indexID() const { + return m_impl->indexID(); +} +std::optional HostedSong::path() const { + return m_impl->path(); +} + +HostedSong::HostedSong(HostedSong&& other) = default; +HostedSong& HostedSong::operator=(HostedSong&& other) = default; +HostedSong::~HostedSong() = default; + +class Nongs::Impl { +private: + friend class Nongs; + + int m_songID; + std::string m_active; + std::unique_ptr m_default; + std::vector> m_locals {}; + std::vector> m_youtube {}; + std::vector> m_hosted {}; + + void deletePath(std::optional path) { + std::error_code ec; + if (path.has_value() && std::filesystem::exists(path.value())) { + std::filesystem::remove(path.value(), ec); + if (ec) { + log::error( + "Couldn't delete nong. Category: {}, message: {}", + ec.category().name(), + ec.category().message(ec.value()) + ); + ec = {}; + } + } + } +public: + Impl(int songID, LocalSong&& defaultSong) + : m_songID(songID), + m_default(std::make_unique(defaultSong)), + // Initialize default song as active + m_active(defaultSong.metadata()->m_uniqueID) + {} + + Impl(int songID) + : Impl(songID, LocalSong::createUnknown(songID)) + {} + + geode::Result<> setActive(const std::string& uniqueID) { + auto nong = getNongFromID(uniqueID); + + if (!nong.has_value()) { + return Err("No song found with given path for song ID"); + } + + if (m_default->metadata()->m_uniqueID != uniqueID && (!nong->path().has_value() || !std::filesystem::exists(nong->path().value()))) { + return Err("Song path doesn't exist"); + } + + m_active = uniqueID; + + return Ok(); + } + + geode::Result<> merge(Nongs&& other) { + if (other.songID() != m_songID) return Err("Merging with NONGs of a different song ID"); + + for (const auto& i : other.locals()) { + if (i->path() == other.defaultSong()->path()) continue; + m_locals.emplace_back(std::make_unique(*i)); + } + + for (const auto& i : other.youtube()) { + m_youtube.emplace_back(std::make_unique(*i)); + } + + for (const auto& i : other.hosted()) { + m_hosted.emplace_back(std::make_unique(*i)); + } + + return Ok(); + } + + geode::Result<> deleteAllSongs() { + for (auto& local : m_locals) { + this->deletePath(local->path()); + } + m_locals.clear(); + + for (auto& youtube : m_youtube) { + this->deletePath(youtube->path()); + } + m_youtube.clear(); + + for (auto& hosted : m_hosted) { + this->deletePath(hosted->path()); + } + m_hosted.clear(); + + m_active = m_default->metadata()->m_uniqueID; + + return Ok(); + } + + geode::Result<> deleteSong(const std::string& uniqueID, bool audio) { + if (m_default->metadata()->m_uniqueID == uniqueID) { + return Err("Cannot delete default song"); + } + + if (m_active == uniqueID) { + m_active = m_default->metadata()->m_uniqueID; + } + + for (auto i = m_locals.begin(); i != m_locals.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + if (audio) { + this->deletePath((*i)->path()); + } + m_locals.erase(i); + return Ok(); + } + } + + for (auto i = m_youtube.begin(); i != m_youtube.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + if (audio) { + this->deletePath((*i)->path()); + } + m_youtube.erase(i); + return Ok(); + } + } + + for (auto i = m_hosted.begin(); i != m_hosted.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + if (audio) { + this->deletePath((*i)->path()); + } + m_hosted.erase(i); + return Ok(); + } + } + + return Err("No song found with given path for song ID"); + } + + geode::Result<> deleteSongAudio(const std::string& uniqueID) { + if (m_default->metadata()->m_uniqueID == uniqueID) { + return Err("Cannot delete audio of the default song"); + } + + if (m_active == uniqueID) { + m_active = m_default->metadata()->m_uniqueID; + } + + for (auto i = m_locals.begin(); i != m_locals.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + return Err("Cannot delete audio of local songs"); + return Ok(); + } + } + + for (auto i = m_youtube.begin(); i != m_youtube.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + this->deletePath((*i)->path()); + return Ok(); + } + } + + for (auto i = m_hosted.begin(); i != m_hosted.end(); ++i) { + if ((*i)->metadata()->m_uniqueID == uniqueID) { + this->deletePath((*i)->path()); + return Ok(); + } + } + + return Err("No song found with given path for song ID"); + } + + + std::optional getNongFromID(const std::string& uniqueID) const { + if (m_default->metadata()->m_uniqueID == uniqueID) { + return Nong(*m_default); + } + + for (const auto& i : m_locals) { + if (i->metadata()->m_uniqueID == uniqueID) { + return Nong(*i); + } + } + + for (const auto& i : m_youtube) { + if (i->metadata()->m_uniqueID == uniqueID) { + return Nong(*i); + } + } + + for (const auto& i : m_hosted) { + if (i->metadata()->m_uniqueID == uniqueID) { + return Nong(*i); + } + } + + return std::nullopt; + } + + geode::Result<> replaceSong(std::string prevUniqueID, Nong&& song) { + auto isActive = m_active == prevUniqueID; + + auto prevNong = getNongFromID(prevUniqueID); + bool deleteAudio = prevNong.has_value() && prevNong->path() != song.path(); + + // Not required to succeed + auto _ = deleteSong(prevUniqueID, deleteAudio); + if (auto res = add(std::move(song)); res.isErr()) return res; + if (isActive) { + // Not required to succeed + auto _ = setActive(song.metadata()->m_uniqueID); + } + return Ok(); + } + + Result add(LocalSong song) { + for (const auto& i : m_locals) { + if (i->path() == song.path()) { + std::string err = fmt::format("Attempted to add a duplicate song for id {}", song.metadata()->m_gdID); + // log::error(err); + return Err(err); + } + } + + auto s = std::make_unique(song); + auto ret = s.get(); + m_locals.push_back(std::move(s)); + return Ok(ret); + } + + Result add(YTSong song) { + auto s = std::make_unique(song); + auto ret = s.get(); + m_youtube.push_back(std::move(s)); + return Ok(ret); + } + + Result add(HostedSong song) { + auto s = std::make_unique(song); + auto ret = s.get(); + m_hosted.push_back(std::move(s)); + return Ok(ret); + } + + Result<> add(Nong&& song) { + std::optional err = std::nullopt; + song.visit( + [this, &err](LocalSong* local){ + if (auto addRes = this->add(*local); addRes.isErr()) err = addRes.err(); + }, [this, &err](YTSong* yt){ + if (auto addRes = this->add(*yt); addRes.isErr()) err = addRes.err(); + }, [this, &err](HostedSong* hosted){ + if (auto addRes = this->add(*hosted); addRes.isErr()) err = addRes.err(); + } + ); + if (err.has_value()) return Err(err.value()); + return Ok(); + } + + bool isDefaultActive() const { + return m_active == m_default->metadata()->m_uniqueID; + } + int songID() const { + return m_songID; + } + LocalSong* defaultSong() const { + return m_default.get(); + } + Nong activeNong() const { + return getNongFromID(m_active).value(); + } + std::string active() const { + return m_active; + } + std::vector>& locals() { + return m_locals; + } + std::vector>& youtube() { + return m_youtube; + } + std::vector>& hosted() { + return m_hosted; + } +}; + +Nongs::Nongs(int songID, LocalSong&& defaultSong) + : m_impl(std::make_unique( + songID, + std::move(defaultSong) + )) +{} + +Nongs::Nongs(int songID) + : m_impl(std::make_unique(songID)) +{} + +std::optional Nongs::getNongFromID(const std::string& uniqueID) const { + return m_impl->getNongFromID(uniqueID); +} + +geode::Result<> Nongs::replaceSong(std::string prevUniqueID, Nong&& song) { + return m_impl->replaceSong(prevUniqueID, std::move(song)); +} + +bool Nongs::isDefaultActive() const { + return m_impl->isDefaultActive(); +} + +int Nongs::songID() const { + return m_impl->songID(); +} + +LocalSong* Nongs::defaultSong() const { + return m_impl->defaultSong(); +} + +std::string Nongs::active() const { + return m_impl->active(); +} + +Nong Nongs::activeNong() const { + return m_impl->activeNong(); +}; + +Result<> Nongs::setActive(const std::string& uniqueID) { + return m_impl->setActive(uniqueID); +} + +Result<> Nongs::merge(Nongs&& other) { + return m_impl->merge(std::move(other)); +} + +Result<> Nongs::deleteAllSongs() { + return m_impl->deleteAllSongs(); +} + +Result<> Nongs::deleteSong(const std::string& uniqueID, bool audio) { + return m_impl->deleteSong(uniqueID, audio); +} + +Result<> Nongs::deleteSongAudio(const std::string& uniqueID) { + return m_impl->deleteSongAudio(uniqueID); +} + +std::vector>& Nongs::locals() { + return m_impl->locals(); +} + +std::vector>& Nongs::youtube() { + return m_impl->youtube(); +} + +std::vector>& Nongs::hosted() { + return m_impl->hosted(); +} + +Result Nongs::add(LocalSong song) { + return m_impl->add(song); +} + +Result Nongs::add(YTSong song) { + return m_impl->add(song); +} + +Result Nongs::add(HostedSong song) { + return m_impl->add(song); +} + +Result<> Nongs::add(Nong&& song) { + return m_impl->add(std::move(song)); +} + +// Nongs::Nongs(const Nongs&) = delete; +// Nongs& Nongs::operator=(const Nongs&) = delete; +Nongs::Nongs(Nongs&&) = default; +Nongs& Nongs::operator=(Nongs&&) = default; +Nongs::~Nongs() = default; + +class Nong::Impl { +private: + friend class Nong; + + Type m_type; + std::unique_ptr m_localSong = nullptr; + std::unique_ptr m_ytSong = nullptr; + std::unique_ptr m_hostedSong = nullptr; + +public: + Impl(const LocalSong local) : m_type(Type::Local), m_localSong(std::make_unique(local)) {} + Impl(const YTSong yt) : m_type(Type::YT), m_ytSong(std::make_unique(yt)) {} + Impl(const HostedSong hosted) : m_type(Type::Hosted), m_hostedSong(std::make_unique(hosted)) {} + + Impl(const Impl& other) + : m_type(other.m_type), + m_localSong(other.m_type == Type::Local ? std::make_unique(*other.m_localSong) : nullptr), + m_ytSong(other.m_type == Type::YT ? std::make_unique(*other.m_ytSong) : nullptr), + m_hostedSong(other.m_type == Type::Hosted ? std::make_unique(*other.m_hostedSong) : nullptr) {} + + Impl& operator=(const Impl&) = delete; + + template + ReturnType visit( + std::function local, + std::function yt, + std::function hosted + ) const { + switch (m_type) { + case Type::Local: + return local(m_localSong.get()); + case Type::YT: + return yt(m_ytSong.get()); + case Type::Hosted: + return hosted(m_hostedSong.get()); + } + } + + SongMetadata* metadata() const { + switch (m_type) { + case Type::Local: + return m_localSong->metadata(); + case Type::YT: + return m_ytSong->metadata(); + case Type::Hosted: + return m_hostedSong->metadata(); + } + } + + std::optional path() const { + switch (m_type) { + case Type::Local: + return m_localSong->path(); + case Type::YT: + return m_ytSong->path(); + case Type::Hosted: + return m_hostedSong->path(); + } + } + + std::optional indexID() const { + switch (m_type) { + case Type::Local: + return std::nullopt; + case Type::YT: + return m_ytSong->indexID(); + case Type::Hosted: + return m_hostedSong->indexID(); + } + } + + Result toNongs() const { + int songId = metadata()->m_gdID; + switch (m_type) { + case Type::Local: { + auto nongs = Nongs(songId); + if (auto err = nongs.add(std::move(*m_localSong)); err.isErr()) { + return Err(err.error()); + } + return Ok(std::move(nongs)); + } + case Type::YT: { + auto nongs = Nongs(songId); + if (auto err = nongs.add(std::move(*m_ytSong)); err.isErr()) { + return Err(err.error()); + } + return Ok(std::move(nongs)); + } + case Type::Hosted: { + auto nongs = Nongs(songId); + if (auto err = nongs.add(std::move(*m_hostedSong)); err.isErr()) { + return Err(err.error()); + } + return Ok(std::move(nongs)); + } + } + } + + Type type() const { + return m_type; + } +}; + +// Explicit template instantiation +template Result<> Nong::visit>( + std::function(LocalSong*)> local, + std::function(YTSong*)> yt, + std::function(HostedSong*)> hosted +) const; + +template void Nong::visit( + std::function local, + std::function yt, + std::function hosted +) const; + +template bool Nong::visit( + std::function local, + std::function yt, + std::function hosted +) const; + + +Nong::Nong(const LocalSong& local) + : m_impl(std::make_unique(local)) +{}; + +Nong::Nong(const YTSong& yt) + : m_impl(std::make_unique(yt)) +{}; + +Nong::Nong(const HostedSong& hosted) + : m_impl(std::make_unique(hosted)) +{}; + +Nong::Nong(const Nong& other) + : m_impl(std::make_unique(*other.m_impl)) +{}; + +Nong& Nong::operator=(const Nong& other) { + m_impl->m_type = other.m_impl->m_type; + if (other.m_impl->m_type == Type::Local) { + m_impl->m_localSong = std::make_unique(*other.m_impl->m_localSong); + } else if (other.m_impl->m_type == Type::YT) { + m_impl->m_ytSong = std::make_unique(*other.m_impl->m_ytSong); + } else if (other.m_impl->m_type == Type::Hosted) { + m_impl->m_hostedSong = std::make_unique(*other.m_impl->m_hostedSong); + } + return *this; +} + +SongMetadata* Nong::metadata() const { + return m_impl->metadata(); +}; + +Result Nong::toNongs() const { + return m_impl->toNongs(); +}; + +std::optional Nong::path() const { + return m_impl->path(); +}; + +std::optional Nong::indexID() const { + return m_impl->indexID(); +} + +Nong::Type Nong::type() const { + return m_impl->type(); +} + +template +ReturnType Nong::visit( + std::function local, + std::function yt, + std::function hosted +) const { + return m_impl->visit(local, yt, hosted); +} + +Nong::Nong(Nong&&) = default; +Nong& Nong::operator=(Nong&&) = default; +Nong::~Nong() = default; + + +} diff --git a/src/types/nong_list_type.hpp b/src/types/nong_list_type.hpp deleted file mode 100644 index e8503c7..0000000 --- a/src/types/nong_list_type.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace jukebox { - - -} diff --git a/src/types/nong_state.hpp b/src/types/nong_state.hpp deleted file mode 100644 index c7fd458..0000000 --- a/src/types/nong_state.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include - -#include "song_info.hpp" -#include "../manifest.hpp" - -namespace jukebox { - -struct NongState { - int m_manifestVersion; - std::map m_nongs; -}; - -} - -template<> -struct matjson::Serialize { - static jukebox::NongState from_json(matjson::Value const& value) { - jukebox::NongState ret; - ret.m_manifestVersion = value["version"].as_int(); - auto nongs = value["nongs"].as_object(); - for (auto const& kv : nongs) { - int id = stoi(kv.first); - jukebox::NongData data = matjson::Serialize::from_json(kv.second, id); - ret.m_nongs[id] = data; - } - - return ret; - } - - static matjson::Value to_json(jukebox::NongState const& value) { - auto ret = matjson::Object(); - auto nongs = matjson::Object(); - ret["version"] = jukebox::getManifestVersion(); - for (auto const& kv : value.m_nongs) { - nongs[std::to_string(kv.first)] = matjson::Serialize::to_json(kv.second); - } - ret["nongs"] = nongs; - return ret; - } -}; \ No newline at end of file diff --git a/src/types/song_info.hpp b/src/types/song_info.hpp deleted file mode 100644 index d14f71a..0000000 --- a/src/types/song_info.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include "Geode/loader/Log.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -namespace fs = std::filesystem; - -namespace jukebox { - -struct SongInfo { - fs::path path; - std::string songName; - std::string authorName; - std::string songUrl; - std::string levelName; - int startOffset = 0; -}; - -struct NongData { - fs::path active; - fs::path defaultPath; - std::vector songs; - bool defaultValid; -}; - -} - -template<> -struct matjson::Serialize { - static jukebox::NongData from_json(matjson::Value const& value, int songID) { - bool mainSong = songID < 0; - std::vector songs; - auto jsonSongs = value["songs"].as_array(); - bool valid = true; - if (value.contains("defaultValid")) { - valid = value["defaultValid"].as_bool(); - } - - std::string defaultFilename = ""; - if (mainSong) { - geode::log::info("{}", songID); - defaultFilename = LevelTools::getAudioFileName(-songID - 1); - } else { - if (songID < 10000000) { - // newgrounds songs - defaultFilename = fmt::format("{}.mp3", songID); - } else { - // music library songs - defaultFilename = fmt::format("{}.ogg", songID); - } - } - - std::string activeFilename = ""; - if (!value.contains("activeFilename") && value.contains("active")) { - auto path = fs::path(value["active"].as_string()); - activeFilename = path.filename().string(); - } else if (value.contains("activeFilename")) { - activeFilename = value["activeFilename"].as_string(); - } else { - activeFilename = "nongd:invalid"; - } - - static const fs::path nongDir = geode::Mod::get()->getSaveDir(); - static const fs::path customSongPath = fs::path( - cocos2d::CCFileUtils::get()->getWritablePath().c_str() - ); - static const fs::path gdDir = fs::path( - cocos2d::CCFileUtils::get()->getWritablePath2().c_str() - ); - - fs::path active; - fs::path defaultPath; - if (mainSong) { - defaultPath = gdDir / "Resources" / defaultFilename; - } else { - defaultPath = customSongPath / defaultFilename; - } - - if (activeFilename == defaultFilename) { - active = defaultPath; - } else { - active = nongDir / "nongs" / activeFilename; - } - - bool activeFound = false; - - for (auto jsonSong : jsonSongs) { - std::string levelName = ""; - if (jsonSong.contains("levelName")) { - levelName = jsonSong["levelName"].as_string(); - } - std::string filename = ""; - if (!jsonSong.contains("filename") && jsonSong.contains("path")) { - auto path = fs::path(jsonSong["path"].as_string()); - filename = path.filename().string(); - } else if (jsonSong.contains("filename")) { - filename = jsonSong["filename"].as_string(); - } else { - filename = "nongd:invalid"; - } - - fs::path path; - if (filename == defaultFilename) { - if (mainSong) { - path = gdDir / "Resources" / filename; - } else { - path = customSongPath / filename; - } - } else { - path = nongDir / "nongs" / filename; - } - - if (filename != "nongd:invalid" && filename == activeFilename) { - activeFound = true; - std::error_code ec; - if (!fs::exists(path, ec) || ec) { - activeFound = false; - } - } - - jukebox::SongInfo song = { - .path = path, - .songName = jsonSong["songName"].as_string(), - .authorName = jsonSong["authorName"].as_string(), - .songUrl = jsonSong["songUrl"].as_string(), - .levelName = levelName, - .startOffset = jsonSong.try_get("startOffset").value_or(0) - }; - songs.push_back(song); - } - - if (!activeFound) { - active = defaultPath; - } - - return jukebox::NongData { - .active = active, - .defaultPath = defaultPath, - .songs = songs, - .defaultValid = valid - }; - } - - static matjson::Value to_json(jukebox::NongData const& value) { - auto ret = matjson::Object(); - auto array = matjson::Array(); - ret["active"] = value.active.string(); - ret["defaultPath"] = value.defaultPath.string(); - ret["defaultValid"] = value.defaultValid; - ret["activeFilename"] = value.active.filename().string(); - for (auto song : value.songs) { - auto obj = matjson::Object(); - obj["path"] = song.path.string(); - obj["filename"] = song.path.filename().string(); - obj["songName"] = song.songName; - obj["authorName"] = song.authorName; - obj["songUrl"] = song.songUrl; - obj["levelName"] = song.levelName; - obj["startOffset"] = song.startOffset; - - array.push_back(obj); - } - - ret["songs"] = array; - return ret; - } -}; \ No newline at end of file diff --git a/src/ui/index_choose_popup.cpp b/src/ui/index_choose_popup.cpp new file mode 100644 index 0000000..d00f1c9 --- /dev/null +++ b/src/ui/index_choose_popup.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "index_choose_popup.hpp" +#include "list/index_cell.hpp" +#include "../managers/index_manager.hpp" + +namespace jukebox { + +bool IndexChoosePopup::setup( + std::vector indexIDs, + std::function chooseIndex +) { + m_indexIDs = indexIDs; + m_chooseIndex = chooseIndex; + + auto menu = CCMenu::create(); + menu->setContentWidth(m_mainLayer->getContentWidth() - 10.f); + menu->setContentHeight(m_mainLayer->getContentHeight() - 20.f); + + auto switchMenu = CCMenu::create(); + switchMenu->setLayout(RowLayout::create()); + switchMenu->setContentSize({menu->getContentSize().width, 40.f}); + + auto spriteLeft = CCSprite::createWithSpriteFrameName("edit_leftBtn_001.png"); + spriteLeft->setScale(1.5); + auto btnLeft = CCMenuItemSpriteExtra::create( + spriteLeft, + this, + menu_selector(IndexChoosePopup::onLeft) + ); + + auto spriteRight = CCSprite::createWithSpriteFrameName("edit_rightBtn_001.png"); + spriteRight->setScale(1.5); + auto btnRight = CCMenuItemSpriteExtra::create( + spriteRight, + this, + menu_selector(IndexChoosePopup::onRight) + ); + + auto label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label = label; + label->limitLabelWidth(switchMenu->getContentWidth() - 10.f, 0.8f, 0.1f); + label->setID("index-name"); + auto labelMenu = CCMenu::create(); + labelMenu->addChild(label); + labelMenu->setContentWidth(switchMenu->getContentWidth()); + labelMenu->addChildAtPosition(label, Anchor::Center); + + switchMenu->addChild(btnLeft); + switchMenu->addChild(labelMenu); + switchMenu->addChild(btnRight); + switchMenu->updateLayout(); + + auto addSongButton = CCMenuItemSpriteExtra::create( + ButtonSprite::create("OK"), + this, + menu_selector(IndexChoosePopup::onOK) + ); + auto addSongMenu = CCMenu::create(); + addSongMenu->setID("add-song-menu"); + addSongButton->setID("add-song-button"); + addSongMenu->setAnchorPoint({0.5f, 0.5f}); + addSongMenu->setContentSize(addSongButton->getContentSize()); + addSongMenu->addChildAtPosition(addSongButton, Anchor::Center); + + menu->addChildAtPosition(switchMenu, Anchor::Center); + + m_mainLayer->addChildAtPosition(menu, Anchor::Center); + m_mainLayer->addChildAtPosition(addSongMenu, Anchor::Bottom, { 0, 3 }); + + label->setPositionY(label->getPositionY() + 1.f); + + + // // auto indexes = + // m_ + + // m_mainLayer->addChild(addSongButton); + // auto menu = CCMenu::create(); + // menu->addChild(addBtn); + // menu->setZOrder(1); + // m_mainLayer->addChildAtPosition(menu, Anchor::BottomRight, { -5.f - addBtn->getContentWidth()/2.f, 8.f + addBtn->getContentHeight()/2.f }); + + updateLabel(); + + return true; +} + +void IndexChoosePopup::updateLabel() { + m_label->setString(IndexManager::get()->getIndexName(m_indexIDs.at(m_currentIndex))->c_str()); +} + +void IndexChoosePopup::onLeft(CCObject*) { + m_currentIndex = (m_currentIndex - 1 + m_indexIDs.size()) % m_indexIDs.size(); + updateLabel(); +} + +void IndexChoosePopup::onRight(CCObject*) { + m_currentIndex = (m_currentIndex + 1) % m_indexIDs.size(); + updateLabel(); +} + +void IndexChoosePopup::onOK(CCObject*) { + m_chooseIndex(m_indexIDs.at(m_currentIndex)); + m_closeBtn->activate(); +} + +IndexChoosePopup* IndexChoosePopup::create( + std::vector indexIDs, + std::function setIndexesCallback +) { + auto ret = new IndexChoosePopup(); + if (ret && ret->initAnchored(280, 80, indexIDs, setIndexesCallback)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +} diff --git a/src/ui/index_choose_popup.hpp b/src/ui/index_choose_popup.hpp new file mode 100644 index 0000000..fc77aa2 --- /dev/null +++ b/src/ui/index_choose_popup.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Geode/loader/Event.hpp" +#include "Geode/utils/Result.hpp" +#include "Geode/utils/Task.hpp" +#include "nong_dropdown_layer.hpp" +#include "../../include/index.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class IndexChoosePopup : public Popup, std::function> { +protected: + std::vector m_indexIDs; + std::function m_chooseIndex; + CCLabelBMFont* m_label = nullptr; + int m_currentIndex = 0; + + bool setup(std::vector indexIDs, std::function chooseIndex) override; + void updateLabel(); + void onRight(CCObject*); + void onLeft(CCObject*); + void onOK(CCObject*); +public: + static IndexChoosePopup* create(std::vector indexIDs, std::function chooseIndex); +}; + +} diff --git a/src/ui/indexes_popup.cpp b/src/ui/indexes_popup.cpp new file mode 100644 index 0000000..f256a37 --- /dev/null +++ b/src/ui/indexes_popup.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "indexes_popup.hpp" +#include "list/index_cell.hpp" + +namespace jukebox { + +bool IndexesPopup::setup(std::vector indexes, std::function)> setIndexesCallback) { + m_indexes = indexes; + m_setIndexesCallback = setIndexesCallback; + + this->createList(); + + auto spr = CCSprite::createWithSpriteFrameName("GJ_plusBtn_001.png"); + spr->setScale(0.7f); + auto addBtn = CCMenuItemSpriteExtra::create( + spr, + this, + menu_selector(IndexesPopup::onAdd) + ); + addBtn->setAnchorPoint({ 0.5f, 0.5f }); + auto menu = CCMenu::create(); + menu->addChild(addBtn); + menu->setZOrder(1); + m_mainLayer->addChildAtPosition(menu, Anchor::BottomRight, { -5.f - addBtn->getContentWidth()/2.f, 8.f + addBtn->getContentHeight()/2.f }); + + return true; +} + +void IndexesPopup::onClose(CCObject* sender) { + log::info("IndexesPopup::onClose"); + for (auto& index : m_indexes) { + log::info("Index: {}", index.m_enabled); + } + m_setIndexesCallback(m_indexes); + this->Popup::onClose(sender); +} + +void IndexesPopup::onAdd(CCObject*) { + m_indexes.push_back({ .m_url="", .m_userAdded = true, .m_enabled = true, }); + this->createList(); +} + +IndexesPopup* IndexesPopup::create(std::vector indexes, std::function)> setIndexesCallback) { + auto ret = new IndexesPopup(); + auto size = ret->getPopupSize(); + if (ret && ret->initAnchored(size.width, size.height, indexes, setIndexesCallback)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCSize IndexesPopup::getPopupSize() { + return { 320.f, 240.f }; +} + +void IndexesPopup::createList() { + auto size = this->m_mainLayer->getContentSize(); + + static const float HORIZONTAL_PADDING = 5.f; + + if (m_list) { + m_list->removeFromParent(); + } + + m_list = ScrollLayer::create({ size.width - HORIZONTAL_PADDING * 2, size.height-HORIZONTAL_PADDING*2 - 4.f}); + m_list->m_contentLayer->setLayout( + ColumnLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ->setAutoGrowAxis(size.height - HORIZONTAL_PADDING*2 - 4.f) + ->setGap(HORIZONTAL_PADDING/2) + ); + m_list->setPosition({ HORIZONTAL_PADDING, HORIZONTAL_PADDING + 2.f }); + + for (int i = 0; i < m_indexes.size(); i++) { + auto cell = IndexCell::create( + this, + &m_indexes[i], + [this, i] { + m_indexes.erase(m_indexes.begin() + i); + this->createList(); + }, + CCSize { this->getPopupSize().width-HORIZONTAL_PADDING*2, 35.f } + ); + cell->setAnchorPoint({ 0.f, 0.f }); + m_list->m_contentLayer->addChild(cell); + } + auto menu = CCMenu::create(); + menu->setContentSize({ 0.f, 36.f }); + m_list->m_contentLayer->addChild(menu); + + m_list->m_contentLayer->updateLayout(); + this->m_mainLayer->addChild(m_list); + handleTouchPriority(this); +} + +} diff --git a/src/ui/indexes_popup.hpp b/src/ui/indexes_popup.hpp new file mode 100644 index 0000000..fe9d908 --- /dev/null +++ b/src/ui/indexes_popup.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Geode/loader/Event.hpp" +#include "Geode/utils/Result.hpp" +#include "Geode/utils/Task.hpp" +#include "nong_dropdown_layer.hpp" +#include "../../include/index.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class IndexesPopup : public Popup, std::function)>> { +protected: + std::vector m_indexes; + std::function)> m_setIndexesCallback; + geode::ScrollLayer* m_list; + + bool setup(std::vector, std::function)> setIndexesCallback) override; + void createList(); + CCSize getPopupSize(); + void onClose(CCObject*) override; + void onAdd(CCObject*); +public: + static IndexesPopup* create(std::vector, std::function)> setIndexesCallback); +}; + +} diff --git a/src/ui/indexes_setting.cpp b/src/ui/indexes_setting.cpp new file mode 100644 index 0000000..238af5e --- /dev/null +++ b/src/ui/indexes_setting.cpp @@ -0,0 +1,137 @@ +#include "indexes_setting.hpp" +#include "indexes_popup.hpp" +#include +#include + +using namespace geode::prelude; + +SettingNode *IndexesSettingValue::createNode(float width) { + return IndexesSettingNode::create(this, width); +} + +bool IndexesSettingNode::init(IndexesSettingValue *value, float width) { + if (!SettingNode::init(value)) + return false; + this->m_value = value; + for (IndexSource &index : value->getIndexes()) { + m_localValue.push_back(index); + } + + float height = 40.f; + this->setContentSize({width, height}); + + auto menu = CCMenu::create(); + menu->setPosition(0, 0); + menu->setID("inputs-menu"); + + // No way to get the JSON without hardcoding the setting ID... + auto settingJson = + Mod::get()->getSettingDefinition("indexes")->get()->json; + m_defaultValue = settingJson->get>("default"); + m_name = settingJson->get("name"); + m_description = settingJson->get("description"); + + m_label = CCLabelBMFont::create(m_name.c_str(), "bigFont.fnt"); + m_label->setAnchorPoint({0.f, 0.5f}); + m_label->setPosition(20.f, height / 2); + m_label->setScale(0.5f); + menu->addChild(m_label); + + auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + infoSpr->setScale(.6f); + auto infoBtn = CCMenuItemSpriteExtra::create( + infoSpr, this, menu_selector(IndexesSettingNode::onDesc)); + infoBtn->setPosition(m_label->getScaledContentSize().width + 40.f, + height / 2); + menu->addChild(infoBtn); + + auto resetSpr = + CCSprite::createWithSpriteFrameName("geode.loader/reset-gold.png"); + resetSpr->setScale(.5f); + m_resetBtn = CCMenuItemSpriteExtra::create( + resetSpr, this, menu_selector(IndexesSettingNode::onReset)); + m_resetBtn->setPosition(m_label->getScaledContentSize().width + 40.f + 20.f, + height / 2); + menu->addChild(m_resetBtn); + + auto viewSpr = ButtonSprite::create("View"); + viewSpr->setScale(0.72f); + auto viewBtn = CCMenuItemSpriteExtra::create( + viewSpr, this, menu_selector(IndexesSettingNode::onView)); + viewBtn->setPosition(width - 40.f, height - 20.f); + menu->addChild(viewBtn); + + this->addChild(menu); + handleTouchPriority(this); + + updateVisuals(); + + return true; +} + +void IndexesSettingNode::onView(CCObject *) { + auto popup = + IndexesPopup::create(m_localValue, [this](std::vector newIndexes) { + m_localValue = newIndexes; + updateVisuals(); + }); + popup->m_noElasticity = true; + popup->show(); +} + +void IndexesSettingNode::onReset(CCObject *) { resetToDefault(); } + +void IndexesSettingNode::onDesc(CCObject *) { + FLAlertLayer::create(m_name.c_str(), m_description.c_str(), "OK")->show(); +} + +void IndexesSettingNode::updateVisuals() { + m_resetBtn->setVisible(hasNonDefaultValue()); + m_label->setColor(hasUncommittedChanges() ? cc3x(0x1d0) : cc3x(0xfff)); + this->dispatchChanged(); +} + +void IndexesSettingNode::commit() { + this->m_value->setIndexes(m_localValue); + updateVisuals(); + this->dispatchCommitted(); +} + +bool IndexesSettingNode::hasUncommittedChanges() { + if (m_localValue.size() != m_value->getIndexes().size()) + return true; + for (int i = 0; i < m_localValue.size(); i++) { + if (m_localValue[i] != m_value->getIndexes()[i]) + return true; + } + return false; +} + +bool IndexesSettingNode::hasNonDefaultValue() { + if (m_localValue.size() != m_defaultValue.size()) + return true; + for (int i = 0; i < m_localValue.size(); i++) { + if (m_localValue[i] != m_defaultValue[i]) + return true; + } + return false; +} + +void IndexesSettingNode::resetToDefault() { + m_localValue.clear(); + for (IndexSource &index : m_defaultValue) { + m_localValue.push_back(index); + } + updateVisuals(); +} + +IndexesSettingNode *IndexesSettingNode::create(IndexesSettingValue *value, + float width) { + auto ret = new IndexesSettingNode(); + if (ret && ret->init(value, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/src/ui/indexes_setting.hpp b/src/ui/indexes_setting.hpp new file mode 100644 index 0000000..6adcdd3 --- /dev/null +++ b/src/ui/indexes_setting.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "../../include/index.hpp" +#include "../../include/index_serialize.hpp" +#include "indexes_popup.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace geode::prelude; +using namespace jukebox; + +namespace jukebox { + +struct IndexesSettingStruct { + std::vector m_indexes; +}; + +class IndexesSettingValue; + +class IndexesSettingValue : public SettingValue { +protected: + std::vector m_indexes; + +public: + IndexesSettingValue(std::string const &key, std::string const &modID, + std::vector indexes) + : SettingValue(key, modID), m_indexes(indexes) {} + + bool load(matjson::Value const &json) override { + m_indexes.clear(); + auto array = json.as_array(); + for (auto const &elem : array) { + m_indexes.push_back(matjson::Serialize::from_json(elem)); + } + return true; + } + + bool save(matjson::Value &json) const override { + auto array = matjson::Array(); + for (auto const &index : m_indexes) { + array.push_back(matjson::Serialize::to_json(index)); + } + json = array; + return true; + } + + SettingNode *createNode(float width) override; + + void setIndexes(std::vector indexes) { + this->m_indexes = indexes; + this->valueChanged(); + } + + std::vector getIndexes() const { return this->m_indexes; } +}; + +class IndexesSettingNode : public SettingNode { +protected: + IndexesSettingValue *m_value; + CCMenuItemSpriteExtra *m_resetBtn; + CCLabelBMFont *m_label; + std::vector m_localValue; + std::string m_name; + std::string m_description; + std::vector m_defaultValue; + + bool init(IndexesSettingValue *value, float width); + +public: + void updateVisuals(); + void onView(CCObject *); + void onReset(CCObject *); + void onDesc(CCObject *); + void commit() override; + bool hasUncommittedChanges() override; + bool hasNonDefaultValue() override; + void resetToDefault() override; + static IndexesSettingNode *create(IndexesSettingValue *value, float width); +}; + +} + +template <> struct SettingValueSetter { + static IndexesSettingStruct get(SettingValue *setting) { + return IndexesSettingStruct{ + static_cast(setting)->getIndexes()}; + }; + static void set(IndexesSettingValue *setting, + IndexesSettingStruct const &value) { + setting->setIndexes(value.m_indexes); + }; +}; diff --git a/src/ui/list/index_cell.cpp b/src/ui/list/index_cell.cpp new file mode 100644 index 0000000..9133ac5 --- /dev/null +++ b/src/ui/list/index_cell.cpp @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "index_cell.hpp" + +namespace jukebox { + +bool IndexCell::init( + IndexesPopup* parentPopup, + IndexSource* index, + std::function onDelete, + CCSize const& size +) { + if (!CCNode::init()) { + return false; + } + + static const float HORIZONTAL_PADDING = 2.5f; + + m_parentPopup = parentPopup; + m_index = index; + m_onDelete = onDelete; + + this->setContentSize(size); + this->setAnchorPoint(CCPoint { 0.5f, 0.5f }); + + auto bg = CCScale9Sprite::create("square02b_001.png"); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(75); + bg->setScale(0.3f); + bg->setContentSize(size / bg->getScale()); + this->addChildAtPosition(bg, Anchor::Center); + + // m_enableButton = CCMenuItemSpriteExtra::create( + // CCSprite::createWithSpriteFrameName("GJ_checkOn_001.png"), + // this, + // nullptr + // ); + // + // m_enableButton->setAnchorPoint(ccp(0.5f, 0.5f)); + // m_enableButton->setID("set-button"); + + float m_buttonsSize = 0.f; + + m_toggleButton = CCMenuItemToggler::createWithStandardSprites( + this, + menu_selector(IndexCell::onToggle), + .6f + ); + m_toggleButton->setAnchorPoint(ccp(0.5f, 0.5f)); + + m_buttonsSize += m_toggleButton->getContentSize().width; + + auto buttonsMenu = CCMenu::create(); + buttonsMenu->addChild(m_toggleButton); + buttonsMenu->setAnchorPoint(CCPoint { 1.0f, 0.5f }); + buttonsMenu->setContentSize(CCSize { 50.f, 30.f }); + + if (m_index->m_userAdded) { + auto sprite = CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"); + sprite->setScale(0.7f); + auto deleteButton = CCMenuItemSpriteExtra::create( + sprite, + this, + menu_selector(IndexCell::onDelete) + ); + deleteButton->setID("delete-button"); + buttonsMenu->addChild(deleteButton); + m_buttonsSize += deleteButton->getContentSize().width; + } + + buttonsMenu->setLayout( + RowLayout::create() + ->setGap(5.f) + ->setAxisAlignment(AxisAlignment::Even) + ); + buttonsMenu->updateLayout(); + buttonsMenu->setID("button-menu"); + + auto inputNode = TextInput::create(size.width - HORIZONTAL_PADDING*2 - m_buttonsSize, "Index url", "chatFont.fnt"); + inputNode->setScale(1.f); + // inputNode->setPosition(size.width / 2 - 15.f, size.height / 2); + inputNode->setCommonFilter(CommonFilter::Any); + inputNode->setMaxCharCount(300); + inputNode->setString(m_index->m_url, false); + inputNode->setTextAlign(TextInputAlign::Left); + inputNode->setCallback([this](std::string const &str) { + log::info("Index new URL: {}", str); + log::info("Index URL: {}", m_index->m_url); + m_index->m_url = str; + }); + + auto menu = CCMenu::create(); + menu->addChild(inputNode); + menu->addChild(buttonsMenu); + + menu->setLayout( + RowLayout::create() + ->setGap(5.f) + ->setAxisAlignment(AxisAlignment::Between) + ); + menu->setID("menu"); + menu->setAnchorPoint(CCPoint { 0.5f, 0.5f }); + menu->setContentSize(CCSize { size.width - HORIZONTAL_PADDING*2, size.height }); + menu->updateLayout(); + + this->addChildAtPosition(menu, Anchor::Center, CCPoint { 0.0f, 0.0f }); + this->updateUI(); + return true; +} + +void IndexCell::updateUI() { + m_toggleButton->toggle(m_index->m_enabled); +} + +void IndexCell::onToggle(CCObject*) { + m_index->m_enabled = !m_index->m_enabled; + log::info("Index enabled: {}", m_index->m_enabled); + this->updateUI(); + // Cancel the toggling of the button by cocos that happens after the callback. + m_toggleButton->toggle(!m_toggleButton->isToggled()); +} + +void IndexCell::onDelete(CCObject*) { + m_onDelete(); +} + +IndexCell* IndexCell::create( + IndexesPopup* parentPopup, + IndexSource* index, + std::function onDelete, + CCSize const& size +) { + auto ret = new IndexCell(); + if (ret && ret->init(parentPopup, index, onDelete, size)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +} diff --git a/src/ui/list/index_cell.hpp b/src/ui/list/index_cell.hpp new file mode 100644 index 0000000..5db42b2 --- /dev/null +++ b/src/ui/list/index_cell.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include "../../../include/index.hpp" + +using namespace geode::prelude; + +namespace jukebox { + +class IndexesPopup; + +class IndexCell : public CCNode { +protected: + IndexesPopup* m_parentPopup; + IndexSource* m_index; + std::function m_onDelete; + + CCMenuItemToggler* m_toggleButton; + + bool init( + IndexesPopup* parentPopup, + IndexSource* index, + std::function onDelete, + CCSize const& size + ); +public: + static IndexCell* create( + IndexesPopup* parentPopup, + IndexSource* index, + std::function onDelete, + CCSize const& size + ); + void updateUI(); + void onToggle(CCObject*); + void onDelete(CCObject*); +}; + +} diff --git a/src/ui/list/nong_cell.cpp b/src/ui/list/nong_cell.cpp index 3d14394..aea42ec 100644 --- a/src/ui/list/nong_cell.cpp +++ b/src/ui/list/nong_cell.cpp @@ -9,32 +9,44 @@ #include #include #include +#include +#include "../../managers/index_manager.hpp" #include "nong_cell.hpp" namespace jukebox { bool NongCell::init( int songID, - SongInfo info, + Nong info, bool isDefault, - bool selected, + bool selected, CCSize const& size, std::function onSelect, std::function onFixDefault, - std::function onDelete + std::function onDelete, + std::function onDownload, + std::function onEdit ) { if (!CCNode::init()) { return false; } m_songID = songID; - m_songInfo = info; + m_songInfo = std::move(info); m_isDefault = isDefault; m_isActive = selected; + m_isDownloaded = m_songInfo.path().has_value() && std::filesystem::exists(m_songInfo.path().value()); + m_isDownloadable = m_songInfo.visit( + [](auto _){return false;}, + [](auto _){return true;}, + [](auto _){return true;} + ); m_onSelect = onSelect; m_onFixDefault = onFixDefault; m_onDelete = onDelete; + m_onDownload = onDownload; + m_onEdit = onEdit; this->setContentSize(size); this->setAnchorPoint(CCPoint { 0.5f, 0.5f }); @@ -69,25 +81,36 @@ bool NongCell::init( button->setID("set-button"); auto menu = CCMenu::create(); - menu->addChild(button); + if (m_isDownloaded) { + menu->addChild(button); + } menu->setAnchorPoint(CCPoint { 1.0f, 0.5f }); - menu->setContentSize(CCSize { 50.f, 30.f }); + menu->setContentSize(CCSize { this->getContentSize().width, 200.f }); - if (!isDefault) { - auto sprite = CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"); - sprite->setScale(0.7f); - auto deleteButton = CCMenuItemSpriteExtra::create( - sprite, + menu->setLayout( + RowLayout::create() + ->setGap(5.f) + ->setAxisAlignment(AxisAlignment::End) + ); + + if (!m_isDefault && !m_songInfo.indexID().has_value()) { + auto spr = CCSprite::create("JB_Edit.png"_spr); + spr->setScale(0.7f); + auto editButton = CCMenuItemSpriteExtra::create( + spr, this, - menu_selector(NongCell::deleteSong) + menu_selector(NongCell::onEdit) ); - deleteButton->setID("delete-button"); - menu->addChild(deleteButton); - } else { + editButton->setID("edit-button"); + editButton->setAnchorPoint(ccp(0.5f, 0.5f)); + menu->addChild(editButton); + } + + if (isDefault) { auto sprite = CCSprite::createWithSpriteFrameName("GJ_downloadsIcon_001.png"); sprite->setScale(0.8f); if (!selected) { - sprite->setColor(cc3x(0x808080)); + sprite->setColor({0x80, 0x80, 0x80}); } auto fixButton = CCMenuItemSpriteExtra::create( sprite, @@ -96,55 +119,128 @@ bool NongCell::init( ); fixButton->setID("fix-button"); menu->addChild(fixButton); + } else if (m_isDownloadable && !m_isDownloaded) { + auto sprite = CCSprite::createWithSpriteFrameName("GJ_downloadBtn_001.png"); + sprite->setScale(0.7f); + m_downloadButton = CCMenuItemSpriteExtra::create( + sprite, + this, + menu_selector(NongCell::onDownload) + ); + m_downloadButton->setID("download-button"); + menu->addChild(m_downloadButton); + + auto progressBarBack = CCSprite::createWithSpriteFrameName("d_circle_01_001.png"); + progressBarBack->setColor(ccc3(50, 50, 50)); + progressBarBack->setScale(0.62f); + + auto spr = CCSprite::createWithSpriteFrameName("d_circle_01_001.png"); + spr->setColor(ccc3(0, 255, 0)); + + m_downloadProgress = CCProgressTimer::create(spr); + m_downloadProgress->setType(CCProgressTimerType::kCCProgressTimerTypeRadial); + m_downloadProgress->setPercentage(50.f); + m_downloadProgress->setID("progress-bar"); + m_downloadProgress->setScale(0.66f); + + m_downloadProgressContainer = CCMenu::create(); + + m_downloadProgressContainer->addChildAtPosition(m_downloadProgress, Anchor::Center); + m_downloadProgressContainer->addChildAtPosition(progressBarBack, Anchor::Center); + + m_downloadProgressContainer->setZOrder(-1); + m_downloadProgressContainer->setVisible(false); + + m_downloadButton->addChildAtPosition(m_downloadProgressContainer, Anchor::Center); + } + + if (!m_isDefault && !(m_songInfo.indexID().has_value() && !m_isDownloaded)) { + bool trashSprite = m_isDownloadable && m_isDownloaded; + auto sprite = CCSprite::createWithSpriteFrameName( + trashSprite ? "GJ_trashBtn_001.png" : "GJ_deleteIcon_001.png" + ); + sprite->setScale(trashSprite ? 0.6475 : 0.7f); + auto deleteButton = CCMenuItemSpriteExtra::create( + sprite, + this, + menu_selector(NongCell::onDelete) + ); + deleteButton->setID("delete-button"); + menu->addChild(deleteButton); } - menu->setLayout( - RowLayout::create() - ->setGap(5.f) - ->setAxisAlignment(AxisAlignment::Even) - ); menu->updateLayout(); menu->setID("button-menu"); this->addChildAtPosition(menu, Anchor::Right, CCPoint { -5.0f, 0.0f }); m_songInfoLayer = CCLayer::create(); + auto songMetadata = m_songInfo.metadata(); - if (!m_songInfo.levelName.empty()) { - m_levelNameLabel = CCLabelBMFont::create(m_songInfo.levelName.c_str(), "bigFont.fnt"); - m_levelNameLabel->limitLabelWidth(220.f, 0.4f, 0.1f); - m_levelNameLabel->setColor(cc3x(0x00c9ff)); - m_levelNameLabel->setID("level-name"); + std::vector metadataList = {}; + + m_songInfo.visit([&metadataList](auto _){ + }, [&metadataList](YTSong* yt){ + metadataList.push_back("youtube"); + }, [&metadataList](HostedSong* hosted){ + metadataList.push_back("hosted"); + }); + + if (m_songInfo.indexID().has_value()) { + auto indexID = m_songInfo.indexID().value(); + auto indexName = IndexManager::get()->getIndexName(indexID); + metadataList.push_back(indexName.has_value() ? indexName.value() : indexID); } - - m_songNameLabel = CCLabelBMFont::create(m_songInfo.songName.c_str(), "bigFont.fnt"); + + if (m_songInfo.metadata()->m_level.has_value()) { + metadataList.push_back(m_songInfo.metadata()->m_level.value()); + } + + m_metadataLabel = CCLabelBMFont::create( + metadataList.size() >= 1 + ? std::accumulate( + std::next(metadataList.begin()), + metadataList.end(), + metadataList[0], + [](const std::string& a, const std::string& b) { + return a + " : " + b; + } + ).c_str() + : "", + "bigFont.fnt" + ); + m_metadataLabel->limitLabelWidth(220.f, 0.4f, 0.1f); + m_metadataLabel->setColor({0x00, 0xc9, 0xff}); + m_metadataLabel->setID("metadata"); + + m_songNameLabel = CCLabelBMFont::create(songMetadata->m_name.c_str(), "bigFont.fnt"); m_songNameLabel->limitLabelWidth(220.f, 0.7f, 0.1f); if (selected) { m_songNameLabel->setColor(ccc3(188, 254, 206)); } - m_authorNameLabel = CCLabelBMFont::create(m_songInfo.authorName.c_str(), "goldFont.fnt"); + m_authorNameLabel = CCLabelBMFont::create(songMetadata->m_artist.c_str(), "goldFont.fnt"); m_authorNameLabel->limitLabelWidth(220.f, 0.7f, 0.1f); m_authorNameLabel->setID("author-name"); m_songNameLabel->setID("song-name"); - if (m_levelNameLabel != nullptr) { - m_songInfoLayer->addChild(m_levelNameLabel); + if (m_metadataLabel != nullptr) { + m_songInfoLayer->addChild(m_metadataLabel); } m_songInfoLayer->addChild(m_authorNameLabel); m_songInfoLayer->addChild(m_songNameLabel); m_songInfoLayer->setID("song-info"); auto layout = ColumnLayout::create(); layout->setAutoScale(false); - if (m_levelNameLabel != nullptr) { + if (m_metadataLabel != nullptr) { layout->setAxisAlignment(AxisAlignment::Even); } else { layout->setAxisAlignment(AxisAlignment::Center); } layout->setCrossAxisLineAlignment(AxisAlignment::Start); - m_songInfoLayer->setContentSize(ccp(240.f, this->getContentSize().height - 6.f)); + m_songInfoLayer->setContentSize(ccp(240.f, this->getContentSize().height)); m_songInfoLayer->setAnchorPoint(ccp(0.f, 0.f)); - m_songInfoLayer->setPosition(ccp(12.f, 1.5f)); + m_songInfoLayer->setPosition(ccp(12.f, 0.f)); m_songInfoLayer->setLayout(layout); this->addChild(m_songInfoLayer); @@ -169,36 +265,45 @@ void NongCell::onFixDefault(CCObject* target) { ); } +void NongCell::setDownloadProgress(float progress) { + m_downloadProgressContainer->setVisible(true); + auto sprite = CCSprite::createWithSpriteFrameName("GJ_cancelDownloadBtn_001.png"); + sprite->setScale(0.7f); + m_downloadButton->setSprite(sprite); + m_downloadButton->setColor(ccc3(105, 105, 105)); + m_downloadProgress->setPercentage(progress*100.f); +} + void NongCell::onSet(CCObject* target) { m_onSelect(); } -void NongCell::deleteSong(CCObject* target) { - createQuickPopup( - "Are you sure?", - fmt::format("Are you sure you want to delete {} from your NONGs?", m_songInfo.songName), - "No", - "Yes", - [this] (FLAlertLayer* self, bool btn2) { - if (btn2) { - m_onDelete(); - } - } - ); +void NongCell::onDownload(CCObject* target) { + m_onDownload(); +} + +void NongCell::onEdit(CCObject* target) { + m_onEdit(); +} + +void NongCell::onDelete(CCObject* target) { + m_onDelete(); } NongCell* NongCell::create( int songID, - SongInfo info, + Nong info, bool isDefault, - bool selected, + bool selected, CCSize const& size, std::function onSelect, std::function onFixDefault, - std::function onDelete + std::function onDelete, + std::function onDownload, + std::function onEdit ) { auto ret = new NongCell(); - if (ret && ret->init(songID, info, isDefault, selected, size, onSelect, onFixDefault, onDelete)) { + if (ret && ret->init(songID, std::move(info), isDefault, selected, size, onSelect, onFixDefault, onDelete, onDownload, onEdit)) { return ret; } CC_SAFE_DELETE(ret); diff --git a/src/ui/list/nong_cell.hpp b/src/ui/list/nong_cell.hpp index d9a54a2..93720cf 100644 --- a/src/ui/list/nong_cell.hpp +++ b/src/ui/list/nong_cell.hpp @@ -5,7 +5,7 @@ #include #include -#include "../../types/song_info.hpp" +#include "../../../include/nong.hpp" using namespace geode::prelude; @@ -16,43 +16,58 @@ class NongDropdownLayer; class NongCell : public CCNode { protected: int m_songID; - SongInfo m_songInfo; CCLabelBMFont* m_songNameLabel = nullptr; CCLabelBMFont* m_authorNameLabel = nullptr; - CCLabelBMFont* m_levelNameLabel = nullptr; + CCLabelBMFont* m_metadataLabel = nullptr; CCLayer* m_songInfoLayer; std::function m_onSelect; std::function m_onFixDefault; std::function m_onDelete; + std::function m_onDownload; + std::function m_onEdit; bool m_isDefault; bool m_isActive; + bool m_isDownloaded; + bool m_isDownloadable; + + CCMenuItemSpriteExtra* m_downloadButton; + CCMenu* m_downloadProgressContainer; + CCProgressTimer* m_downloadProgress; bool init( int songID, - SongInfo info, + Nong info, bool isDefault, - bool selected, + bool selected, CCSize const& size, std::function onSelect, std::function onFixDefault, - std::function onDelete + std::function onDelete, + std::function onDownload, + std::function onEdit ); public: static NongCell* create( int songID, - SongInfo info, + Nong info, bool isDefault, - bool selected, + bool selected, CCSize const& size, std::function onSelect, std::function onFixDefault, - std::function onDelete + std::function onDelete, + std::function onDownload, + std::function onEdit ); + Nong m_songInfo = Nong(LocalSong::createUnknown(0)); void onSet(CCObject*); - void deleteSong(CCObject*); + void onDelete(CCObject*); void onFixDefault(CCObject*); + void onDownload(CCObject*); + void onEdit(CCObject*); + void setDownloadProgress(float progress); }; } diff --git a/src/ui/list/nong_list.cpp b/src/ui/list/nong_list.cpp index 7010b14..ce3828e 100644 --- a/src/ui/list/nong_list.cpp +++ b/src/ui/list/nong_list.cpp @@ -7,11 +7,12 @@ #include #include #include +#include #include #include #include "../../managers/nong_manager.hpp" -#include "nong_cell.hpp" +#include "../../managers/index_manager.hpp" #include "song_cell.hpp" using namespace geode::prelude; @@ -19,25 +20,29 @@ using namespace geode::prelude; namespace jukebox { bool NongList::init( - std::unordered_map& data, + std::vector& songIds, const cocos2d::CCSize& size, - std::function onSetActive, + std::function onSetActive, std::function onFixDefault, - std::function onDelete, - std::function onListTypeChange + std::function onDelete, + std::function onDownload, + std::function onEdit, + std::function)> onListTypeChange ) { if (!CCNode::init()) { return false; } - m_data = data; + m_songIds = songIds; m_onSetActive = onSetActive; m_onFixDefault = onFixDefault; m_onDelete = onDelete; + m_onDownload = onDownload; + m_onEdit = onEdit; m_onListTypeChange = onListTypeChange; - if (m_data.size() == 1) { - m_currentSong = m_data.begin()->first; + if (m_songIds.size() == 1) { + m_currentSong = m_songIds.front(); } this->setContentSize(size); @@ -80,16 +85,28 @@ bool NongList::init( -m_list->getScaledContentSize() / 2 ); + m_onListTypeChange(m_currentSong); + this->build(); return true; } +void NongList::setDownloadProgress(std::string uniqueID, float progress) { + for (auto& cell : listedNongCells) { + if (cell->m_songInfo.metadata()->m_uniqueID == uniqueID) { + cell->setDownloadProgress(progress); + break; + } + } +} + void NongList::build() { + listedNongCells.clear(); if (m_list->m_contentLayer->getChildrenCount() > 0) { m_list->m_contentLayer->removeAllChildren(); } - if (m_data.size() == 0) { + if (m_songIds.size() == 0) { return; } @@ -98,23 +115,24 @@ void NongList::build() { s_itemSize }; + if (m_onListTypeChange) { + m_onListTypeChange(m_currentSong); + } + if (!m_currentSong) { - if (m_onListTypeChange) { - m_onListTypeChange(true); - } - for (const auto& kv : m_data) { - SongInfo display; - auto active = NongManager::get()->getActiveNong(kv.first); + for (const auto& id : m_songIds) { + auto nongs = NongManager::get()->getNongs(id); - if (!active) { + if (!nongs) { continue; } - int id = kv.first; + auto active = nongs.value()->activeNong(); + m_list->m_contentLayer->addChild( jukebox::SongCell::create( id, - active.value(), + active.metadata(), itemSize, [this, id] () { this->onSelectSong(id); @@ -123,66 +141,85 @@ void NongList::build() { ); } } else { - if (m_onListTypeChange) { - m_onListTypeChange(false); - } // Single item int id = m_currentSong.value(); - if (!m_data.contains(id)) { - return; - } - NongData data = m_data.at(id); - auto active = NongManager::get()->getActiveNong(id); - auto defaultRes = NongManager::get()->getDefaultNong(id); - if (!defaultRes) { + + auto localNongs = NongManager::get()->getNongs(id); + if (!localNongs) { return; } - if (!active) { + + auto defaultSong = localNongs.value()->defaultSong(); + auto active = localNongs.value()->active(); + + auto nongs = IndexManager::get()->getNongs(id); + if (nongs.isErr()) { return; } - auto defaultSong = defaultRes.value(); - - m_list->m_contentLayer->addChild( - jukebox::NongCell::create( - id, defaultSong, - true, - defaultSong.path == data.active, - itemSize, - [this, id, defaultSong] () { - m_onSetActive(id, defaultSong); - }, - [this, id] () { - m_onFixDefault(id); - }, - [this, id, defaultSong] () { - m_onDelete(id, defaultSong); - } - ) - ); - - for (const SongInfo& song : data.songs) { - if (song.path == data.defaultPath) { - continue; - } - m_list->m_contentLayer->addChild( - jukebox::NongCell::create( - id, song, - song.path == data.defaultPath, - song.path == data.active, - itemSize, - [this, id, song] () { - m_onSetActive(id, song); - }, - [this, id] () { - m_onFixDefault(id); - }, - [this, id, song] () { - m_onDelete(id, song); - } - ) - ); + for (auto& nong : nongs.value()) { + auto metadata = *nong.metadata(); + auto path = nong.path(); + auto uniqueID = metadata.m_uniqueID; + + bool isFromIndex = nong.indexID().has_value(); + bool isDownloaded = path.has_value() && std::filesystem::exists(path.value()); + bool deleteOnlyAudio = !isFromIndex && isDownloaded; + bool deletePopup = !isFromIndex && !isDownloaded; + + auto cell = jukebox::NongCell::create( + id, + std::move(nong), + uniqueID == defaultSong->metadata()->m_uniqueID, + uniqueID == active, + itemSize, + [this, id, uniqueID] () { + m_onSetActive(id, uniqueID); + }, + [this, id] () { + m_onFixDefault(id); + }, + [this, id, uniqueID, deleteOnlyAudio, deletePopup]() { + m_onDelete(id, uniqueID, deleteOnlyAudio, deletePopup); + }, + [this, id, uniqueID] () { + m_onDownload(id, uniqueID); + }, + [this, id, uniqueID] () { + m_onEdit(id, uniqueID); + } + ); + + if (auto progress = IndexManager::get()->getSongDownloadProgress(metadata.m_uniqueID); progress.has_value()) { + cell->setDownloadProgress(progress.value()); + }; + + listedNongCells.push_back(cell); + m_list->m_contentLayer->addChild(cell); } + + // for (std::unique_ptr& song : nongs.value()->locals()) { + // m_list->m_contentLayer->addChild( + // jukebox::NongCell::create( + // id, Nong(*song), + // false, + // song->path() == active->m_path, + // itemSize, + // [this, id, &song] () { + // m_onSetActive(id, SongMetadataPathed( + // *song->metadata(), + // song->path() + // )); + // }, + // [this, id] () { + // m_onFixDefault(id); + // }, + // [this, id, &song] () { + // m_onDelete(id, SongMetadataPathed{*song}); + // } + // ) + // ); + // } } m_list->m_contentLayer->updateLayout(); this->scrollToTop(); @@ -194,12 +231,8 @@ void NongList::scrollToTop() { ); } -void NongList::setData(std::unordered_map& data) { - m_data = data; -} - void NongList::onBack(cocos2d::CCObject* target) { - if (!m_currentSong.has_value() || m_data.size() < 2) { + if (!m_currentSong.has_value() || m_songIds.size() < 2) { return; } @@ -210,13 +243,13 @@ void NongList::onBack(cocos2d::CCObject* target) { } void NongList::setCurrentSong(int songId) { - if (m_data.contains(songId)) { + if (std::find(m_songIds.begin(), m_songIds.end(), songId) != m_songIds.end()) { m_currentSong = songId; } } void NongList::onSelectSong(int songId) { - if (m_currentSong.has_value() || !m_data.contains(songId)) { + if (m_currentSong.has_value() || std::find(m_songIds.begin(), m_songIds.end(), songId) == m_songIds.end()) { return; } @@ -227,15 +260,17 @@ void NongList::onSelectSong(int songId) { } NongList* NongList::create( - std::unordered_map& data, + std::vector& songIds, const cocos2d::CCSize& size, - std::function onSetActive, + std::function onSetActive, std::function onFixDefault, - std::function onDelete, - std::function onListTypeChange + std::function onDelete, + std::function onDownload, + std::function onEdit, + std::function)> onListTypeChange ) { auto ret = new NongList(); - if (!ret->init(data, size, onSetActive, onFixDefault, onDelete, onListTypeChange)) { + if (!ret->init(songIds, size, onSetActive, onFixDefault, onDelete, onDownload, onEdit, onListTypeChange)) { CC_SAFE_DELETE(ret); return nullptr; } @@ -244,4 +279,4 @@ NongList* NongList::create( return ret; } -} \ No newline at end of file +} diff --git a/src/ui/list/nong_list.hpp b/src/ui/list/nong_list.hpp index 72da003..d339294 100644 --- a/src/ui/list/nong_list.hpp +++ b/src/ui/list/nong_list.hpp @@ -7,9 +7,9 @@ #include #include #include -#include -#include "../../types/song_info.hpp" +#include "nong_cell.hpp" +#include "../../../include/nong.hpp" namespace jukebox { @@ -21,44 +21,52 @@ class NongList : public cocos2d::CCNode { Multiple = 1 }; protected: - std::unordered_map m_data; + std::vector m_songIds; geode::ScrollLayer* m_list; cocos2d::extension::CCScale9Sprite* m_bg; std::optional m_currentSong = std::nullopt; CCMenuItemSpriteExtra* m_backBtn = nullptr; - std::function m_onSetActive; + std::function m_onSetActive; std::function m_onFixDefault; - std::function m_onDelete; - std::function m_onListTypeChange; + std::function m_onDelete; + std::function m_onDownload; + std::function m_onEdit; + std::function)> m_onListTypeChange; + + std::vector listedNongCells; static constexpr float s_padding = 10.0f; static constexpr float s_itemSize = 60.f; public: void scrollToTop(); - void setData(std::unordered_map& data); void setCurrentSong(int songId); void build(); void onBack(cocos2d::CCObject*); void onSelectSong(int songId); + void setDownloadProgress(std::string uniqueID, float progress); static NongList* create( - std::unordered_map& data, + std::vector& songIds, const cocos2d::CCSize& size, - std::function onSetActive, + std::function onSetActive, std::function onFixDefault, - std::function onDelete, - std::function onListTypeChange = {} + std::function onDelete, + std::function onDownload, + std::function onEdit, + std::function)> onListTypeChange = {} ); protected: bool init( - std::unordered_map& data, + std::vector& songIds, const cocos2d::CCSize& size, - std::function onSetActive, + std::function onSetActive, std::function onFixDefault, - std::function onDelete, - std::function onListTypeChange = {} + std::function onDelete, + std::function onDownload, + std::function onEdit, + std::function)> onListTypeChange = {} ); }; diff --git a/src/ui/list/song_cell.cpp b/src/ui/list/song_cell.cpp index ff32a81..eb313a1 100644 --- a/src/ui/list/song_cell.cpp +++ b/src/ui/list/song_cell.cpp @@ -11,7 +11,7 @@ namespace jukebox { bool SongCell::init( int id, - const SongInfo& songInfo, + SongMetadata* songInfo, const CCSize& size, std::function selectCallback ) { @@ -32,13 +32,13 @@ bool SongCell::init( bg->setContentSize(size / bg->getScale()); this->addChildAtPosition(bg, Anchor::Center); - auto label = CCLabelBMFont::create(m_active.songName.c_str(), "bigFont.fnt"); + auto label = CCLabelBMFont::create(songInfo->m_name.c_str(), "bigFont.fnt"); label->setAnchorPoint(ccp(0, 0.5f)); label->limitLabelWidth(240.f, 0.8f, 0.1f); label->setPosition(ccp(12.f, 40.f)); this->addChild(label); m_songNameLabel = label; - auto author = CCLabelBMFont::create(m_active.authorName.c_str(), "goldFont.fnt"); + auto author = CCLabelBMFont::create(songInfo->m_artist.c_str(), "goldFont.fnt"); author->setAnchorPoint(ccp(0, 0.5f)); author->limitLabelWidth(260.f, 0.6f, 0.1f); author->setPosition(ccp(12.f, 15.f)); @@ -60,7 +60,7 @@ bool SongCell::init( spr->setScale(0.8f); auto btn = CCMenuItemSpriteExtra::create( spr, - this, + this, menu_selector(SongCell::onSelectSong) ); menu->addChild(btn); @@ -73,4 +73,4 @@ void SongCell::onSelectSong(CCObject*) { m_callback(); } -} \ No newline at end of file +} diff --git a/src/ui/list/song_cell.hpp b/src/ui/list/song_cell.hpp index e09c0c1..153caeb 100644 --- a/src/ui/list/song_cell.hpp +++ b/src/ui/list/song_cell.hpp @@ -5,7 +5,7 @@ #include #include -#include "../../types/song_info.hpp" +#include "../../../include/nong.hpp" using namespace geode::prelude; @@ -15,7 +15,7 @@ class NongDropdownLayer; class SongCell : public CCNode { protected: - SongInfo m_active; + SongMetadata* m_active; CCLabelBMFont* m_songNameLabel; CCLabelBMFont* m_authorNameLabel; CCLabelBMFont* m_songIDLabel; @@ -25,14 +25,14 @@ class SongCell : public CCNode { bool init( int id, - const SongInfo& songInfo, + SongMetadata* songInfo, const CCSize& size, std::function selectCallback ); public: static SongCell* create( int id, - const SongInfo& songInfo, + SongMetadata* songInfo, const CCSize& size, std::function selectCallback ) { diff --git a/src/ui/nong_add_popup.cpp b/src/ui/nong_add_popup.cpp index 02a3ef4..85adeb3 100644 --- a/src/ui/nong_add_popup.cpp +++ b/src/ui/nong_add_popup.cpp @@ -26,9 +26,13 @@ #include #include #include +#include #include "nong_add_popup.hpp" -#include "../random_string.hpp" +#include "../utils/random_string.hpp" +#include "../managers/index_manager.hpp" +#include "Geode/binding/FLAlertLayerProtocol.hpp" +#include "index_choose_popup.hpp" std::optional parseFromFMODTag(const FMOD_TAG& tag) { std::string ret = ""; @@ -49,30 +53,45 @@ std::optional parseFromFMODTag(const FMOD_TAG& tag) { ); } +class IndexDisclaimerPopup : public FLAlertLayer, public FLAlertLayerProtocol { +protected: + MiniFunction m_selected; + + void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override { + m_selected(layer, btn2); + } +public: + static IndexDisclaimerPopup* create( + char const* title, std::string const& content, char const* btn1, char const* btn2, + float width, MiniFunction selected + ) { + auto inst = new IndexDisclaimerPopup; + inst->m_selected = selected; + if (inst->init(inst, title, content, btn1, btn2, width, true, .0f, 1.0f)) { + inst->autorelease(); + return inst; + } + + delete inst; + return nullptr; + } +}; + namespace jukebox { -bool NongAddPopup::setup(NongDropdownLayer* parent) { +bool NongAddPopup::setup(NongDropdownLayer* parent, int songID, std::optional replacedNong) { this->setTitle("Add Song"); m_parentPopup = parent; + m_songID = songID; + m_replacedNong = std::move(replacedNong); auto spr = CCSprite::createWithSpriteFrameName("accountBtn_myLevels_001.png"); spr->setScale(0.7f); - m_selectSongButton = CCMenuItemSpriteExtra::create( - spr, - this, - menu_selector(NongAddPopup::openFile) - ); - m_selectSongMenu = CCMenu::create(); - m_selectSongMenu->setID("select-file-menu"); - m_selectSongButton->setID("select-file-button"); - m_selectSongMenu->addChild(this->m_selectSongButton); - m_selectSongMenu->setContentSize(m_selectSongButton->getScaledContentSize()); - m_selectSongMenu->setAnchorPoint({1.0f, 0.0f}); - m_selectSongMenu->setPosition(m_mainLayer->getContentSize().width - 10.f, 10.f); - m_selectSongMenu->setLayout(ColumnLayout::create()); + + auto buttonsContainer = CCMenu::create(); m_addSongButton = CCMenuItemSpriteExtra::create( - ButtonSprite::create("Add"), + ButtonSprite::create(m_replacedNong.has_value() ? "Edit" : "Add"), this, menu_selector(NongAddPopup::addSong) ); @@ -80,39 +99,104 @@ bool NongAddPopup::setup(NongDropdownLayer* parent) { m_addSongMenu->setID("add-song-menu"); m_addSongButton->setID("add-song-button"); m_addSongMenu->addChild(this->m_addSongButton); - m_addSongMenu->setPosition(m_mainLayer->getContentSize().width / 2, 10.f); m_addSongMenu->setAnchorPoint({0.5f, 0.0f}); m_addSongMenu->setContentSize(m_addSongButton->getContentSize()); + m_addSongMenu->setContentHeight(20.f); m_addSongMenu->setLayout(ColumnLayout::create()); - auto selectedContainer = CCNode::create(); - selectedContainer->setID("selected-container"); - m_selectedContainer = selectedContainer; - auto selectedLabel = CCLabelBMFont::create("Selected file:", "goldFont.fnt"); - selectedLabel->setScale(0.75f); - selectedLabel->setID("selected-label"); - selectedContainer->addChild(selectedLabel); - auto layout = ColumnLayout::create(); - layout->setAutoScale(false); - layout->setAxisReverse(true); - layout->setAxisAlignment(AxisAlignment::End); - selectedContainer->setContentSize({ 250.f, 50.f }); - selectedContainer->setPosition(m_mainLayer->getContentSize().width / 2, m_mainLayer->getContentSize().height / 2 - 25.f); - selectedContainer->setAnchorPoint({0.5f, 1.0f}); - selectedContainer->setLayout(layout); - m_mainLayer->addChild(selectedContainer); - - m_mainLayer->addChild(this->m_selectSongMenu); - m_mainLayer->addChild(this->m_addSongMenu); + buttonsContainer->addChild(m_addSongMenu); + + if (m_replacedNong.has_value()) { + { + std::vector indexIDs; + + for (const auto& pair : IndexManager::get()->m_loadedIndexes) { + if (!pair.second->m_features.m_submit.has_value()) continue; + IndexMetadata::Features::Submit& submit = pair.second->m_features.m_submit.value(); + if (!m_replacedNong.value().visit( + [submit](auto _){ + return submit.m_supportedSongTypes.at(IndexMetadata::Features::SupportedSongType::local); + }, + [submit](auto _){ + return submit.m_supportedSongTypes.at(IndexMetadata::Features::SupportedSongType::youtube); + }, + [submit](auto _){ + return submit.m_supportedSongTypes.at(IndexMetadata::Features::SupportedSongType::hosted); + } + )) { + continue; + } + + indexIDs.push_back(pair.first); + } + + std::stable_sort(indexIDs.begin(), indexIDs.end(), [](const std::string& a, const std::string& b) { + // Ensure "song-file-hub-index" comes first + if (a == "song-file-hub-index") return true; + if (b == "song-file-hub-index") return false; + return false; // maintain relative order for other strings + }); + + m_publishableIndexes = std::move(indexIDs); + } + + if (!m_publishableIndexes.empty()) { + auto publishSongButton = CCMenuItemSpriteExtra::create( + ButtonSprite::create("Publish"), + this, + menu_selector(NongAddPopup::onPublish) + ); + auto publishSongMenu = CCMenu::create(); + publishSongMenu->setID("publish-song-menu"); + publishSongButton->setID("publish-song-button"); + publishSongMenu->addChild(publishSongButton); + publishSongMenu->setAnchorPoint({0.5f, 0.0f}); + publishSongMenu->setContentSize(publishSongButton->getContentSize()); + publishSongMenu->setContentHeight(20.f); + publishSongMenu->setLayout(ColumnLayout::create()); + + buttonsContainer->addChild(publishSongMenu); + } + } + + buttonsContainer->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::Center)); + buttonsContainer->setContentWidth(m_mainLayer->getContentWidth()); + buttonsContainer->updateLayout(); + m_mainLayer->addChildAtPosition(buttonsContainer, Anchor::Bottom, {0.f, 25.f}); + this->createInputs(); + if (m_replacedNong.has_value()) { + m_replacedNong->visit( + [this](LocalSong* local) { + setSongType(NongAddPopupSongType::local); + m_localLinkInput->setString(local->path().string()); + }, + [this](YTSong* yt) { + setSongType(NongAddPopupSongType::yt); + m_ytLinkInput->setString(yt->youtubeID()); + }, + [this](HostedSong* hosted) { + setSongType(NongAddPopupSongType::hosted); + m_hostedLinkInput->setString(hosted->url()); + } + ); + + m_songNameInput->setString(m_replacedNong->metadata()->m_name); + m_artistNameInput->setString(m_replacedNong->metadata()->m_artist); + if (m_replacedNong->metadata()->m_level.has_value()) { + m_levelNameInput->setString(m_replacedNong->metadata()->m_level.value()); + } + m_startOffsetInput->setString(std::to_string(m_replacedNong->metadata()->m_startOffset)); + } + return true; } -NongAddPopup* NongAddPopup::create(NongDropdownLayer* parent) { +NongAddPopup* NongAddPopup::create(NongDropdownLayer* parent, int songID, std::optional replacedNong) { auto ret = new NongAddPopup(); auto size = ret->getPopupSize(); - if (ret && ret->initAnchored(size.width, size.height, parent)) { + if (ret && ret->initAnchored(size.width, size.height, parent, songID, std::move(replacedNong))) { ret->autorelease(); return ret; } @@ -124,40 +208,6 @@ CCSize NongAddPopup::getPopupSize() { return { 320.f, 240.f }; } -void NongAddPopup::addPathLabel(std::string const& path) { - if (m_songPathContainer != nullptr) { - m_songPathContainer->removeFromParent(); - m_songPathContainer = nullptr; - m_songPathLabel = nullptr; - } - auto container = CCNode::create(); - container->setID("song-path-container"); - auto label = CCLabelBMFont::create(path.c_str(), "bigFont.fnt"); - label->limitLabelWidth(240.f, 0.6f, 0.1f); - label->setID("song-path-label"); - m_songPathLabel = label; - m_songPathContainer = container; - - auto bgSprite = CCScale9Sprite::create( - "square02b_001.png", - { 0.0f, 0.0f, 80.0f, 80.0f } - ); - bgSprite->setID("song-path-bg"); - bgSprite->setColor({ 0, 0, 0 }); - bgSprite->setOpacity(75); - bgSprite->setScale(0.4f); - bgSprite->setContentSize(CCPoint { 250.f, 25.f } / bgSprite->getScale()); - container->setContentSize(bgSprite->getScaledContentSize()); - bgSprite->setPosition(container->getScaledContentSize() / 2); - label->setPosition(container->getScaledContentSize() / 2); - container->addChild(bgSprite); - container->addChild(label); - container->setAnchorPoint({0.5f, 0.5f}); - container->setPosition(m_mainLayer->getContentSize() / 2); - m_selectedContainer->addChild(container); - m_selectedContainer->updateLayout(); -} - void NongAddPopup::openFile(CCObject* target) { #ifdef GEODE_IS_WINDOWS file::FilePickOptions::Filter filter = { @@ -267,15 +317,45 @@ void NongAddPopup::onFileOpen(Task>::Event* event) } } - this->addPathLabel(strPath); - m_songPath = path; + m_localLinkInput->setString(strPath); } } +void NongAddPopup::setSongType(NongAddPopupSongType type) { + m_songType = type; + + m_switchLocalButtonSprite->updateBGImage(type == NongAddPopupSongType::local ? "GJ_button_01.png" : "GJ_button_04.png"); + m_switchYTButtonSprite->updateBGImage(type == NongAddPopupSongType::yt ? "GJ_button_01.png" : "GJ_button_04.png"); + m_switchHostedButtonSprite->updateBGImage(type == NongAddPopupSongType::hosted ? "GJ_button_01.png" : "GJ_button_04.png"); + + m_specificInputsMenu->removeAllChildren(); + if (type == NongAddPopupSongType::local) { + m_specificInputsMenu->addChild(m_localMenu); + } else if (type == NongAddPopupSongType::yt) { + m_specificInputsMenu->addChild(m_ytLinkInput); + } else if (type == NongAddPopupSongType::hosted) { + m_specificInputsMenu->addChild(m_hostedLinkInput); + } + m_specificInputsMenu->updateLayout(); +} + +void NongAddPopup::onSwitchToLocal(CCObject*) { + this->setSongType(NongAddPopupSongType::local); +} + +void NongAddPopup::onSwitchToYT(CCObject*) { + this->setSongType(NongAddPopupSongType::yt); +} + +void NongAddPopup::onSwitchToHosted(CCObject*) { + this->setSongType(NongAddPopupSongType::hosted); +} + + void NongAddPopup::createInputs() { auto inputParent = CCNode::create(); inputParent->setID("input-parent"); - auto songInput = TextInput::create(250.f, "Song name*", "bigFont.fnt"); + auto songInput = TextInput::create(350.f, "Song name*", "bigFont.fnt"); songInput->setID("song-name-input"); songInput->setCommonFilter(CommonFilter::Any); songInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); @@ -283,7 +363,7 @@ void NongAddPopup::createInputs() { songInput->getInputNode()->setLabelPlaceholderScale(0.7f); m_songNameInput = songInput; - auto artistInput = TextInput::create(250.f, "Artist name*", "bigFont.fnt"); + auto artistInput = TextInput::create(350.f, "Artist name*", "bigFont.fnt"); artistInput->setID("artist-name-input"); artistInput->setCommonFilter(CommonFilter::Any); artistInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); @@ -291,7 +371,7 @@ void NongAddPopup::createInputs() { artistInput->getInputNode()->setLabelPlaceholderScale(0.7f); m_artistNameInput = artistInput; - auto levelNameInput = TextInput::create(250.f, "Level name", "bigFont.fnt"); + auto levelNameInput = TextInput::create(350.f, "Level name", "bigFont.fnt"); levelNameInput->setID("artist-name-input"); levelNameInput->setCommonFilter(CommonFilter::Any); levelNameInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); @@ -299,7 +379,7 @@ void NongAddPopup::createInputs() { levelNameInput->getInputNode()->setLabelPlaceholderScale(0.7f); m_levelNameInput = levelNameInput; - auto startOffsetInput = TextInput::create(250.f, "Start offset (ms)", "bigFont.fnt"); + auto startOffsetInput = TextInput::create(350.f, "Start offset (ms)", "bigFont.fnt"); startOffsetInput->setID("start-offset-input"); startOffsetInput->setCommonFilter(CommonFilter::Int); startOffsetInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); @@ -307,13 +387,109 @@ void NongAddPopup::createInputs() { startOffsetInput->getInputNode()->setLabelPlaceholderScale(0.7f); m_startOffsetInput = startOffsetInput; + + m_switchLocalButtonSprite = ButtonSprite::create("Local", "bigFont.fnt", "GJ_button_01.png"); + m_switchYTButtonSprite = ButtonSprite::create("YouTube", "bigFont.fnt", "GJ_button_01.png"); + m_switchHostedButtonSprite = ButtonSprite::create("Hosted", "bigFont.fnt", "GJ_button_01.png"); + m_switchLocalButton = CCMenuItemSpriteExtra::create( + m_switchLocalButtonSprite, + this, + menu_selector(NongAddPopup::onSwitchToLocal) + ); + m_switchYTButton = CCMenuItemSpriteExtra::create( + m_switchYTButtonSprite, + this, + menu_selector(NongAddPopup::onSwitchToYT) + ); + m_switchHostedButton = CCMenuItemSpriteExtra::create( + m_switchHostedButtonSprite, + this, + menu_selector(NongAddPopup::onSwitchToHosted) + ); + m_switchLocalMenu = CCMenu::create(); + m_switchYTMenu = CCMenu::create(); + m_switchHostedMenu = CCMenu::create(); + m_switchLocalMenu->setContentSize({ m_switchLocalButton->getContentWidth(), 30.f }); + m_switchLocalMenu->addChildAtPosition(m_switchLocalButton, Anchor::Center); + m_switchYTMenu->setContentSize({ m_switchYTButton->getContentWidth(), 30.f }); + m_switchYTMenu->addChildAtPosition(m_switchYTButton, Anchor::Center); + m_switchHostedMenu->setContentSize({ m_switchHostedButton->getContentWidth(), 30.f }); + m_switchHostedMenu->addChildAtPosition(m_switchHostedButton, Anchor::Center); + + m_switchButtonsMenu = CCMenu::create(); + m_switchButtonsMenu->addChild(m_switchLocalMenu); + m_switchButtonsMenu->addChild(m_switchYTMenu); + m_switchButtonsMenu->addChild(m_switchHostedMenu); + m_switchButtonsMenu->setContentSize({ 280.f, 50.f }); + m_switchButtonsMenu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::Center) + ); + + m_specificInputsMenu = CCMenu::create(); + m_specificInputsMenu->setContentSize({ 350.f, 70.f }); + m_specificInputsMenu->setLayout( + ColumnLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ); + + m_localMenu = CCMenu::create(); + m_localMenu->setContentSize(m_specificInputsMenu->getContentSize()); + m_localMenu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ); + auto spr = CCSprite::createWithSpriteFrameName("accountBtn_myLevels_001.png"); + spr->setScale(0.7f); + m_localSongButton = CCMenuItemSpriteExtra::create( + spr, + this, + menu_selector(NongAddPopup::openFile) + ); + m_localSongButton->setID("select-file-button"); + m_localSongButtonMenu = CCMenu::create(); + m_localSongButtonMenu->setID("select-file-menu"); + m_localSongButtonMenu->addChild(this->m_localSongButton); + m_localSongButtonMenu->setContentSize(m_localSongButton->getScaledContentSize()); + m_localSongButtonMenu->setAnchorPoint({1.0f, 0.0f}); + m_localSongButtonMenu->setPosition(m_mainLayer->getContentSize().width - 10.f, 10.f); + m_localSongButtonMenu->setLayout(ColumnLayout::create()); + m_localLinkInput = TextInput::create(350.f, "Local Path", "bigFont.fnt"); + m_localLinkInput->setID("local-link-input"); + m_localLinkInput->setCommonFilter(CommonFilter::Any); + m_localLinkInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); + m_localLinkInput->getInputNode()->setMaxLabelScale(0.7f); + m_localLinkInput->getInputNode()->setLabelPlaceholderScale(0.7f); + m_localMenu->addChild(m_localLinkInput); + m_localMenu->addChild(m_localSongButtonMenu); + m_localMenu->updateLayout(); + m_localMenu->retain(); + + m_ytLinkInput = TextInput::create(350.f, "YouTube Link", "bigFont.fnt"); + m_ytLinkInput->setID("yt-link-input"); + m_ytLinkInput->setCommonFilter(CommonFilter::Any); + m_ytLinkInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); + m_ytLinkInput->getInputNode()->setMaxLabelScale(0.7f); + m_ytLinkInput->getInputNode()->setLabelPlaceholderScale(0.7f); + m_ytLinkInput->retain(); + + m_hostedLinkInput = TextInput::create(350.f, "Hosted Link", "bigFont.fnt"); + m_hostedLinkInput->setID("hosted-link-input"); + m_hostedLinkInput->setCommonFilter(CommonFilter::Any); + m_hostedLinkInput->getInputNode()->setLabelPlaceholderColor(ccColor3B {108, 153, 216}); + m_hostedLinkInput->getInputNode()->setMaxLabelScale(0.7f); + m_hostedLinkInput->getInputNode()->setLabelPlaceholderScale(0.7f); + m_hostedLinkInput->retain(); + float inputHeight = songInput->getContentSize().height; inputParent->addChild(songInput); inputParent->addChild(artistInput); inputParent->addChild(levelNameInput); inputParent->addChild(startOffsetInput); - inputParent->setContentSize({ 250.f, 100.f }); + inputParent->addChild(m_switchButtonsMenu); + inputParent->addChild(m_specificInputsMenu); + inputParent->setContentSize({ 250.f, 150.f }); inputParent->setAnchorPoint({ 0.5f, 1.0f }); inputParent->setLayout( ColumnLayout::create() @@ -324,95 +500,226 @@ void NongAddPopup::createInputs() { Anchor::Top, { 0.0f, -40.f } ); + + this->setSongType(NongAddPopupSongType::local); +} + +void NongAddPopup::onPublish(CCObject* target) { + IndexChoosePopup::create(m_publishableIndexes, [this](std::string id){ + auto index = IndexManager::get()->m_loadedIndexes.at(id).get(); + auto name = IndexManager::get()->getIndexName(id).value(); + auto submit = index->m_features.m_submit.value(); + + auto submitFunc = [this, index, submit](FLAlertLayer* _, bool confirmed){ + if (submit.m_requestParams.has_value()) { + if (!submit.m_requestParams.value().m_params) { + CCApplication::get()->openURL(submit.m_requestParams.value().m_url.c_str()); + return; + } + + std::string songSpecificParams = ""; + m_replacedNong.value().visit([&songSpecificParams](LocalSong* local) { + songSpecificParams = fmt::format("&path={}&source=local", local->path()); + }, [&songSpecificParams](YTSong* yt) { + songSpecificParams = fmt::format("&yt-id={}&source=youtube", yt->youtubeID()); + }, [&songSpecificParams](HostedSong* hosted) { + songSpecificParams = fmt::format("&url={}&source=youtube", hosted->url()); + }); + + auto metadata = m_replacedNong.value().metadata(); + CCApplication::get()->openURL( + fmt::format( + "{}song-name={}&artist-name={}&start-offset={}&song-id={}&level-id={}&level-name={}{}", + submit.m_requestParams.value().m_url.c_str(), metadata->m_name, metadata->m_artist, + metadata->m_startOffset, metadata->m_gdID, 0, metadata->m_level.value_or(""), songSpecificParams + ).c_str() + ); + } + }; + + if (submit.m_preSubmitMessage.has_value()) { + auto popup = IndexDisclaimerPopup::create(fmt::format("{} Disclaimer", name).c_str(), submit.m_preSubmitMessage.value(), "Back", "Continue", 420.f, submitFunc); + popup->m_scene = this; + popup->show(); + } else { + submitFunc(nullptr, true); + } + })->show(); } void NongAddPopup::addSong(CCObject* target) { auto artistName = std::string(m_artistNameInput->getString()); auto songName = std::string(m_songNameInput->getString()); - std::string levelName = m_levelNameInput->getString(); + std::optional levelName = m_levelNameInput->getString().size() > 0 ? std::optional(m_levelNameInput->getString()) : std::nullopt; auto startOffsetStr = m_startOffsetInput->getString(); - #ifdef GEODE_IS_WINDOWS - if (wcslen(m_songPath.c_str()) == 0) { - #else - if (strlen(m_songPath.c_str()) == 0) { - #endif - FLAlertLayer::create("Error", "No file selected.", "Ok")->show(); - return; - } - if (!fs::exists(m_songPath)) { - std::stringstream ss; - ss << "The selected file (" << m_songPath.string() << ") does not exist."; - FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); - return; - } - if (fs::is_directory(m_songPath)) { - FLAlertLayer::create("Error", "You selected a directory.", "Ok")->show(); - return; + int startOffset = 0; + + if (startOffsetStr != "") { + startOffset = std::stoi(startOffsetStr); } - std::string extension = m_songPath.extension().string(); + Nong* nong = nullptr; - if ( - extension != ".mp3" - && extension != ".ogg" - && extension != ".wav" - && extension != ".flac" - ) { - FLAlertLayer::create("Error", "The selected file must be one of the following: mp3, wav, flac, ogg.", "Ok")->show(); - return; - } + if (m_songType == NongAddPopupSongType::local) { + fs::path songPath = m_localLinkInput->getString(); + #ifdef GEODE_IS_WINDOWS + if (wcslen(songPath.c_str()) == 0) { + #else + if (strlen(songPath.c_str()) == 0) { + #endif + FLAlertLayer::create("Error", "No file selected.", "Ok")->show(); + return; + } + if (!fs::exists(songPath)) { + std::stringstream ss; + ss << "The selected file (" << songPath.string() << ") does not exist."; + FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); + return; + } - if (songName == "") { - FLAlertLayer::create("Error", "Song name is empty.", "Ok")->show(); - return; - } + if (fs::is_directory(songPath)) { + FLAlertLayer::create("Error", "You selected a directory.", "Ok")->show(); + return; + } - if (artistName == "") { - FLAlertLayer::create("Error", "Artist name is empty.", "Ok")->show(); - return; - } + std::string extension = songPath.extension().string(); - int startOffset = 0; + if ( + extension != ".mp3" + && extension != ".ogg" + && extension != ".wav" + && extension != ".flac" + ) { + FLAlertLayer::create("Error", "The selected file must be one of the following: mp3, wav, flac, ogg.", "Ok")->show(); + return; + } - if (startOffsetStr != "") { - startOffset = std::stoi(startOffsetStr); - } + if (songName == "") { + FLAlertLayer::create("Error", "Song name is empty.", "Ok")->show(); + return; + } - auto unique = jukebox::random_string(16); - auto destination = Mod::get()->getSaveDir() / "nongs"; - if (!fs::exists(destination)) { - fs::create_directory(destination); - } - unique += m_songPath.extension().string(); - destination = destination / unique; - bool result; - std::error_code error_code; - result = fs::copy_file(m_songPath, destination, error_code); - if (error_code) { - std::stringstream ss; - ss << "Failed to save song. Please try again! Error category: " << error_code.category().name() << ", message: " << error_code.category().message(error_code.value()); - FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); - return; - } - if (!result) { - FLAlertLayer::create("Error", "Failed to save song. Please try again!", "Ok")->show(); - return; - } + if (artistName == "") { + FLAlertLayer::create("Error", "Artist name is empty.", "Ok")->show(); + return; + } - SongInfo song = { - .path = destination, - .songName = songName, - .authorName = artistName, - .songUrl = "local", - .levelName = levelName, - .startOffset = startOffset, - }; + auto unique = jukebox::random_string(16); + auto destination = Mod::get()->getSaveDir() / "nongs"; + if (!fs::exists(destination)) { + fs::create_directory(destination); + } + unique += songPath.extension().string(); + destination = destination / unique; + bool result; + std::error_code error_code; + result = fs::copy_file(songPath, destination, error_code); + if (error_code) { + std::stringstream ss; + ss << "Failed to save song. Please try again! Error category: " << error_code.category().name() << ", message: " << error_code.category().message(error_code.value()); + FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); + return; + } + if (!result) { + FLAlertLayer::create("Error", "Failed to save song. Please try again!", "Ok")->show(); + return; + } + + nong = new Nong { + LocalSong { + SongMetadata { + m_songID, + jukebox::random_string(16), + songName, + artistName, + levelName, + startOffset, + }, + destination, + }, + }; + } else if (m_songType == NongAddPopupSongType::yt) { + std::string ytLink = m_ytLinkInput->getString(); + if (strlen(ytLink.c_str()) == 0) { + FLAlertLayer::create("Error", "No YouTube video specified", "Ok")->show(); + return; + } + std::string ytID; + + if (ytLink.size() == 11) { + ytID = ytLink; + } + + const std::regex youtube_regex("(?:youtube\\.com\\/.*[?&]v=|youtu\\.be\\/)([a-zA-Z0-9_-]{11})"); + std::smatch match; + + if (std::regex_search(ytLink, match, youtube_regex) && match.size() > 1) { + ytID = match.str(1); + } - m_parentPopup->addSong(song); + if (ytID.size() != 11) { + FLAlertLayer::create("Error", "Invalid YouTube video ID", "Ok")->show(); + return; + } + + nong = new Nong { + YTSong { + SongMetadata { + m_songID, + jukebox::random_string(16), + songName, + artistName, + levelName, + startOffset, + }, + ytID, + std::nullopt, + }, + }; + } else if (m_songType == NongAddPopupSongType::hosted) { + std::string hostedLink = m_hostedLinkInput->getString(); + if (strlen(hostedLink.c_str()) == 0) { + FLAlertLayer::create("Error", "No URL specified", "Ok")->show(); + return; + } + + nong = new Nong { + HostedSong { + SongMetadata { + m_songID, + jukebox::random_string(16), + songName, + artistName, + levelName, + startOffset, + }, + m_hostedLinkInput->getString(), + std::nullopt, + }, + }; + } + + if (m_replacedNong.has_value()) { + m_parentPopup->deleteSong( + m_replacedNong->metadata()->m_gdID, + m_replacedNong->metadata()->m_uniqueID, + false, + false + ); + } + m_parentPopup->addSong(std::move(nong->toNongs().unwrap()), !m_replacedNong.has_value()); + delete nong; this->onClose(this); } +void NongAddPopup::onClose(CCObject* target) { + m_localMenu->release(); + m_ytLinkInput->release(); + m_hostedLinkInput->release(); + this->Popup::onClose(target); +} + std::optional NongAddPopup::tryParseMetadata( std::filesystem::path path ) { diff --git a/src/ui/nong_add_popup.hpp b/src/ui/nong_add_popup.hpp index c16fd56..6e16c9f 100644 --- a/src/ui/nong_add_popup.hpp +++ b/src/ui/nong_add_popup.hpp @@ -6,52 +6,91 @@ #include #include +#include "Geode/cocos/cocoa/CCObject.h" #include "Geode/loader/Event.hpp" #include "Geode/utils/Result.hpp" #include "Geode/utils/Task.hpp" #include "nong_dropdown_layer.hpp" using namespace geode::prelude; +namespace fs = std::filesystem; namespace jukebox { class NongDropdownLayer; -class NongAddPopup : public Popup { +class NongAddPopup : public Popup> { protected: + enum class NongAddPopupSongType { + local, + yt, + hosted, + }; + struct ParsedMetadata { std::optional name; std::optional artist; }; NongDropdownLayer* m_parentPopup; - CCMenuItemSpriteExtra* m_selectSongButton; + + int m_songID; + + std::vector m_publishableIndexes; + CCMenuItemSpriteExtra* m_addSongButton; - CCMenu* m_selectSongMenu; + CCMenuItemSpriteExtra* m_switchLocalButton; + CCMenuItemSpriteExtra* m_switchYTButton; + CCMenuItemSpriteExtra* m_switchHostedButton; + // CCMenu* m_selectSongMenu; CCMenu* m_addSongMenu; + CCMenu* m_switchLocalMenu; + CCMenu* m_switchYTMenu; + CCMenu* m_switchHostedMenu; + ButtonSprite* m_switchLocalButtonSprite; + ButtonSprite* m_switchYTButtonSprite; + ButtonSprite* m_switchHostedButtonSprite; + + CCMenu* m_switchButtonsMenu; + CCMenu* m_specificInputsMenu; + + CCMenu* m_localMenu; + CCMenu* m_localSongButtonMenu; + CCMenuItemSpriteExtra* m_localSongButton; + TextInput* m_localLinkInput; + + TextInput* m_ytLinkInput; + + TextInput* m_hostedLinkInput; + + NongAddPopupSongType m_songType; TextInput* m_songNameInput; TextInput* m_artistNameInput; TextInput* m_levelNameInput; TextInput* m_startOffsetInput; - fs::path m_songPath; - CCNode* m_selectedContainer; - CCNode* m_songPathContainer; - CCLabelBMFont* m_songPathLabel; EventListener>> m_pickListener; - bool setup(NongDropdownLayer* parent) override; + std::optional m_replacedNong; + + bool setup(NongDropdownLayer* parent, int songID, std::optional replacedNong) override; void createInputs(); void addPathLabel(std::string const& path); void onFileOpen(Task>::Event* event); + void setSongType(NongAddPopupSongType type); + void onSwitchToLocal(CCObject*); + void onSwitchToYT(CCObject*); + void onSwitchToHosted(CCObject*); CCSize getPopupSize(); void openFile(CCObject*); void addSong(CCObject*); + void onPublish(CCObject*); std::optional tryParseMetadata(std::filesystem::path path); + void onClose(CCObject*) override; public: - static NongAddPopup* create(NongDropdownLayer* parent); + static NongAddPopup* create(NongDropdownLayer* parent, int songID, std::optional nong = std::nullopt); }; } diff --git a/src/ui/nong_dropdown_layer.cpp b/src/ui/nong_dropdown_layer.cpp index 5890e17..9bd129d 100644 --- a/src/ui/nong_dropdown_layer.cpp +++ b/src/ui/nong_dropdown_layer.cpp @@ -3,19 +3,16 @@ #include #include #include -#include #include #include "nong_dropdown_layer.hpp" -#include "../managers/nong_manager.hpp" #include "Geode/binding/CCMenuItemSpriteExtra.hpp" #include "Geode/cocos/cocoa/CCGeometry.h" #include "Geode/cocos/cocoa/CCObject.h" #include "Geode/cocos/sprite_nodes/CCSprite.h" #include "ccTypes.h" #include "list/nong_list.hpp" - -namespace fs = std::filesystem; +#include "../managers/index_manager.hpp" namespace jukebox { @@ -23,15 +20,16 @@ bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, in m_songIDS = ids; m_parentWidget = parent; m_defaultSongID = defaultSongID; - for (auto const& id : m_songIDS) { - auto result = NongManager::get()->getNongs(id); - auto value = result.value(); - m_data[id] = value; - } + // for (auto const& id : m_songIDS) { + // auto result = NongManager::get()->getNongs(id); + // auto value = result.value(); + // m_data[id] = value; + // } bool isMultiple = ids.size() > 1; if (ids.size() == 1) { m_currentSongID = ids[0]; } + auto contentSize = m_mainLayer->getContentSize(); int manifest = NongManager::get()->getCurrentManifestVersion(); @@ -46,32 +44,9 @@ bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, in manifestLabel->setID("manifest-label"); m_mainLayer->addChild(manifestLabel); - auto bigMenu = CCMenu::create(); - bigMenu->setID("big-menu"); - bigMenu->ignoreAnchorPointForPosition(false); - auto discordSpr = CCSprite::createWithSpriteFrameName("gj_discordIcon_001.png"); - auto discordBtn = CCMenuItemSpriteExtra::create( - discordSpr, - this, - menu_selector(NongDropdownLayer::onDiscord) - ); - discordBtn->setID("discord-button"); - CCPoint position = discordBtn->getScaledContentSize() / 2; - position += { 5.f, 5.f }; - bigMenu->addChildAtPosition(discordBtn, Anchor::BottomLeft, position); - this->addChild(bigMenu); - - auto spr = CCSprite::createWithSpriteFrameName("GJ_downloadBtn_001.png"); - spr->setScale(0.7f); auto menu = CCMenu::create(); menu->setID("bottom-right-menu"); - auto downloadBtn = CCMenuItemSpriteExtra::create( - spr, - this, - menu_selector(NongDropdownLayer::fetchSongFileHub) - ); - m_downloadBtn = downloadBtn; - spr = CCSprite::createWithSpriteFrameName("GJ_plusBtn_001.png"); + auto spr = CCSprite::createWithSpriteFrameName("GJ_plusBtn_001.png"); spr->setScale(0.7f); auto addBtn = CCMenuItemSpriteExtra::create( spr, @@ -79,7 +54,14 @@ bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, in menu_selector(NongDropdownLayer::openAddPopup) ); m_addBtn = addBtn; - spr = CCSprite::createWithSpriteFrameName("GJ_trashBtn_001.png"); + spr = CCSprite::createWithSpriteFrameName("gj_discordIcon_001.png"); + auto discordBtn = CCMenuItemSpriteExtra::create( + spr, + this, + menu_selector(NongDropdownLayer::onDiscord) + ); + discordBtn->setID("discord-button"); + spr = CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"); spr->setScale(0.7f); auto removeBtn = CCMenuItemSpriteExtra::create( spr, @@ -90,14 +72,12 @@ bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, in if (isMultiple) { m_addBtn->setVisible(false); m_deleteBtn->setVisible(false); - m_downloadBtn->setVisible(false); } else { m_addBtn->setVisible(true); m_deleteBtn->setVisible(true); - m_downloadBtn->setVisible(true); } menu->addChild(addBtn); - menu->addChild(downloadBtn); + menu->addChild(discordBtn); menu->addChild(removeBtn); auto layout = ColumnLayout::create(); layout->setAxisAlignment(AxisAlignment::Start); @@ -154,6 +134,35 @@ bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, in title->setScale(0.75f); m_mainLayer->addChild(title); handleTouchPriority(this); + + m_songErrorListener.bind([](SongErrorEvent* event){ + if (event->notifyUser()) { + FLAlertLayer::create( + "Error", + event->error(), + "OK" + )->show(); + } + return ListenerResult::Propagate; + }); + + m_songStateListener.bind([this](SongStateChangedEvent* event){ + if (!m_list || m_currentSongID != event->gdSongID()) return ListenerResult::Propagate; + + log::info("song state changed"); + createList(); + + return ListenerResult::Propagate; + }); + + m_downloadListener.bind([this](SongDownloadProgressEvent* event){ + if (!m_list || m_currentSongID != event->gdSongID()) return ListenerResult::Propagate; + + m_list->setDownloadProgress(event->uniqueID(), event->progress()); + + return ListenerResult::Propagate; + }); + return true; } @@ -163,52 +172,69 @@ void NongDropdownLayer::onSettings(CCObject* sender) { void NongDropdownLayer::onSelectSong(int songID) { m_currentSongID = songID; - this->createList(); } void NongDropdownLayer::openAddPopup(CCObject* target) { - NongAddPopup::create(this)->show(); + if (!m_currentSongID.has_value()) return; + NongAddPopup::create(this, m_currentSongID.value())->show(); } void NongDropdownLayer::createList() { if (!m_list) { m_list = NongList::create( - m_data, + m_songIDS, CCSize { this->getCellSize().width, 220.f }, - [this](int id, const SongInfo& song) { - m_currentSongID = id; - this->setActiveSong(song); + [this](int gdSongID, const std::string& uniqueID) { + this->setActiveSong(gdSongID, uniqueID); }, - [this](int id) { - if (!NongManager::get()->isFixingDefault(id)) { - this->template addEventListener( - [this](auto song) { - this->refreshList(); - FLAlertLayer::create( - "Success", - "Default song data was refetched successfully!", - "Ok" - )->show(); - return ListenerResult::Propagate; - }, id - ); - NongManager::get()->markAsInvalidDefault(id); - NongManager::get()->prepareCorrectDefault(id); - } + [this](int gdSongID) { + // TODO + // if (!NongManager::get()->isFixingDefault(id)) { + // this->template addEventListener( + // [this](auto song) { + // this->refreshList(); + // FLAlertLayer::create( + // "Success", + // "Default song data was refetched successfully!", + // "Ok" + // )->show(); + // return ListenerResult::Propagate; + // }, id + // ); + // NongManager::get()->markAsInvalidDefault(id); + // NongManager::get()->prepareCorrectDefault(id); + // } + MusicDownloadManager::sharedState()->clearSong(gdSongID); + MusicDownloadManager::sharedState()->getSongInfo(gdSongID, true); + }, + [this](int gdSongID, const std::string& uniqueID, bool onlyAudio, bool confirm) { + this->deleteSong(gdSongID, uniqueID, onlyAudio, confirm); }, - [this](int id, const SongInfo& song) { - m_currentSongID = id; - this->deleteSong(song); + [this](int gdSongID, const std::string& uniqueID) { + this->downloadSong(gdSongID, uniqueID); }, - [this](bool multiple) { + [this](int gdSongID, const std::string& uniqueID) { + auto nongs = NongManager::get()->getNongs(gdSongID); + if (!nongs.has_value()) { + FLAlertLayer::create( + "Error", + "Song is not initialized", + "Ok" + )->show(); + return; + } + auto nong = nongs.value()->getNongFromID(uniqueID); + NongAddPopup::create(this, gdSongID, std::move(nong))->show(); + }, + [this](std::optional currentSongID) { + m_currentSongID = currentSongID; + bool multiple = !currentSongID.has_value(); if (multiple) { m_addBtn->setVisible(false); m_deleteBtn->setVisible(false); - m_downloadBtn->setVisible(false); } else { m_addBtn->setVisible(true); m_deleteBtn->setVisible(true); - m_downloadBtn->setVisible(true); } } ); @@ -216,7 +242,6 @@ void NongDropdownLayer::createList() { return; } - m_list->setData(m_data); // TODO FIX if (m_currentSongID.has_value()) { m_list->setCurrentSong(m_currentSongID.value()); @@ -225,17 +250,6 @@ void NongDropdownLayer::createList() { handleTouchPriority(this); } -SongInfo NongDropdownLayer::getActiveSong() { - int id = m_currentSongID.value(); - auto active = NongManager::get()->getActiveNong(id); - if (!active.has_value()) { - m_data[id].active = m_data[id].defaultPath; - NongManager::get()->saveNongs(m_data[id], id); - return NongManager::get()->getActiveNong(id).value(); - } - return active.value(); -} - CCSize NongDropdownLayer::getCellSize() const { return { 320.f, @@ -243,34 +257,11 @@ CCSize NongDropdownLayer::getCellSize() const { }; } -void NongDropdownLayer::setActiveSong(SongInfo const& song) { - if (!m_currentSongID) { +void NongDropdownLayer::setActiveSong(int gdSongID, const std::string& uniqueID) { + if (auto err = NongManager::get()->setActiveSong(gdSongID, uniqueID); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to set song: {}", err.error()), "Ok")->show(); return; } - int id = m_currentSongID.value(); - auto songs = m_data[id]; - auto defaultNong = NongManager::get()->getDefaultNong(id); - auto isDefault = defaultNong.has_value() && defaultNong->path == song.path; - if (!isDefault && !fs::exists(song.path)) { - FLAlertLayer::create("Failed", "Failed to set song: file not found", "Ok")->show(); - return; - } - - m_data[id].active = song.path; - - NongManager::get()->saveNongs(m_data[id], id); - - this->updateParentWidget(song); - - this->createList(); -} - -void NongDropdownLayer::refreshList() { - if (m_currentSongID.has_value()) { - int id = m_currentSongID.value(); - m_data[id] = NongManager::get()->getNongs(id).value(); - } - this->createList(); } void NongDropdownLayer::onDiscord(CCObject* target) { @@ -287,78 +278,70 @@ void NongDropdownLayer::onDiscord(CCObject* target) { ); } -void NongDropdownLayer::updateParentWidget(SongInfo const& song) { - m_parentWidget->m_songInfoObject->m_artistName = song.authorName; - m_parentWidget->m_songInfoObject->m_songName = song.songName; - if (song.songUrl != "local") { - m_parentWidget->m_songInfoObject->m_songUrl = song.songUrl; - } - m_parentWidget->updateSongInfo(); -} +void NongDropdownLayer::deleteSong(int gdSongID, const std::string& uniqueID, bool onlyAudio, bool confirm) { + auto func = [gdSongID, uniqueID, onlyAudio, confirm](){ + if (onlyAudio) { + if (auto err = NongManager::get()->deleteSongAudio(gdSongID, uniqueID); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to delete song: {}", err.error()), "Ok")->show(); + return; + } + } else { + if (auto err = NongManager::get()->deleteSong(gdSongID, uniqueID); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to delete song: {}", err.error()), "Ok")->show(); + return; + } + } -void NongDropdownLayer::deleteSong(SongInfo const& song) { - if (!m_currentSongID) { + if (confirm) { + FLAlertLayer::create("Success", "The song was deleted!", "Ok")->show(); + } + }; + + if (!confirm) { + func(); return; } - int id = m_currentSongID.value(); - NongManager::get()->deleteNong(song, id); - auto active = NongManager::get()->getActiveNong(id).value(); - this->updateParentWidget(active); - FLAlertLayer::create("Success", "The song was deleted!", "Ok")->show(); - m_data[id] = NongManager::get()->getNongs(id).value(); - this->createList(); + + createQuickPopup( + "Are you sure?", + fmt::format("Are you sure you want to delete the song from your NONGs?"), + "No", + "Yes", + [this, func] (FLAlertLayer* self, bool btn2) { + if (!btn2) return; + func(); + } + ); } -void NongDropdownLayer::addSong(SongInfo const& song) { - if (!m_currentSongID) { +void NongDropdownLayer::downloadSong(int gdSongID, const std::string& uniqueID) { + if (auto err = IndexManager::get()->downloadSong(gdSongID, uniqueID); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to start/stop downloading song: {}", err.error()), "Ok")->show(); return; } - int id = m_currentSongID.value(); - auto data = m_data[id]; - for (auto savedSong : data.songs) { - if (song.path.string() == savedSong.path.string()) { - FLAlertLayer::create("Error", "This NONG already exists! (" + savedSong.songName + ")", "Ok")->show(); - return; - } - } - NongManager::get()->addNong(song, id); - FLAlertLayer::create("Success", "The song was added!", "Ok")->show(); - m_data[id] = NongManager::get()->getNongs(id).value(); - this->createList(); } -void NongDropdownLayer::fetchSongFileHub(CCObject*) { - // TODO FIX +void NongDropdownLayer::addSong(Nongs&& song, bool popup) { if (!m_currentSongID) { return; } int id = m_currentSongID.value(); - std::stringstream ss; - ss << "Do you want to open Song File Hub? The song ID (" << id << ") will be copied to your clipboard."; - createQuickPopup( - "Song File Hub", - ss.str(), - "No", - "Yes", - [this, id](FLAlertLayer* alert, bool btn2) { - if (!btn2) { - return; - } - std::stringstream ss; - ss << id; - geode::utils::clipboard::write(ss.str()); - geode::utils::web::openLinkInBrowser("https://songfilehub.com"); - } - ); + if (auto err = NongManager::get()->addNongs(std::move(song)); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to add song: {}", err.error()), "Ok")->show(); + return; + } + if (popup) { + FLAlertLayer::create("Success", "The song was added!", "Ok")->show(); + } } void NongDropdownLayer::deleteAllNongs(CCObject*) { if (!m_currentSongID) { return; } - createQuickPopup("Delete all nongs", - "Are you sure you want to delete all nongs for this song?", - "No", + createQuickPopup("Delete all nongs", + "Are you sure you want to delete all nongs for this song?", + "No", "Yes", [this](auto, bool btn2) { if (!btn2) { @@ -370,12 +353,11 @@ void NongDropdownLayer::deleteAllNongs(CCObject*) { } int id = m_currentSongID.value(); - NongManager::get()->deleteAll(id); + if (auto err = NongManager::get()->deleteAllSongs(id); err.isErr()) { + FLAlertLayer::create("Failed", fmt::format("Failed to delete nongs: {}", err.error()), "Ok")->show(); + return; + } auto data = NongManager::get()->getNongs(id).value(); - auto active = NongManager::get()->getActiveNong(id).value(); - m_data[id] = data; - this->updateParentWidget(active); - this->createList(); FLAlertLayer::create("Success", "All nongs were deleted successfully!", "Ok")->show(); } ); diff --git a/src/ui/nong_dropdown_layer.hpp b/src/ui/nong_dropdown_layer.hpp index 4440614..03e2bbb 100644 --- a/src/ui/nong_dropdown_layer.hpp +++ b/src/ui/nong_dropdown_layer.hpp @@ -6,11 +6,12 @@ #include #include -#include "../types/song_info.hpp" +#include "../../../include/nong.hpp" #include "list/nong_list.hpp" #include "nong_add_popup.hpp" #include "list/nong_cell.hpp" #include "list/song_cell.hpp" +#include "../managers/nong_manager.hpp" using namespace geode::prelude; @@ -18,22 +19,23 @@ namespace jukebox { class NongDropdownLayer : public Popup, CustomSongWidget*, int> { protected: - std::unordered_map m_data; std::vector m_songIDS; std::optional m_currentSongID = std::nullopt; int m_defaultSongID; Ref m_parentWidget; NongList* m_list = nullptr; - CCMenuItemSpriteExtra* m_downloadBtn = nullptr; CCMenuItemSpriteExtra* m_addBtn = nullptr; CCMenuItemSpriteExtra* m_deleteBtn = nullptr; + EventListener m_songErrorListener; + EventListener m_downloadListener; + EventListener m_songStateListener; + bool m_fetching = false; bool setup(std::vector ids, CustomSongWidget* parent, int defaultSongID) override; void createList(); - SongInfo getActiveSong(); CCSize getCellSize() const; void deleteAllNongs(CCObject*); void fetchSongFileHub(CCObject*); @@ -42,11 +44,11 @@ class NongDropdownLayer : public Popup, CustomSongWidget*, int> public: void onSelectSong(int songID); void onDiscord(CCObject*); - void setActiveSong(SongInfo const& song); - void deleteSong(SongInfo const& song); - void addSong(SongInfo const& song); - void updateParentWidget(SongInfo const& song); - void refreshList(); + void setActiveSong(int gdSongID, const std::string& uniqueID); + void deleteSong(int gdSongID, const std::string& uniqueID, bool onlyAudio, bool confirm); + void downloadSong(int gdSongID, const std::string& uniqueID); + void addSong(Nongs&& song, bool popup = true); + void updateParentWidget(SongMetadata const& song); static NongDropdownLayer* create(std::vector ids, CustomSongWidget* parent, int defaultSongID) { auto ret = new NongDropdownLayer; @@ -60,4 +62,4 @@ class NongDropdownLayer : public Popup, CustomSongWidget*, int> } }; -} \ No newline at end of file +} diff --git a/src/random_string.cpp b/src/utils/random_string.cpp similarity index 100% rename from src/random_string.cpp rename to src/utils/random_string.cpp diff --git a/src/random_string.hpp b/src/utils/random_string.hpp similarity index 100% rename from src/random_string.hpp rename to src/utils/random_string.hpp diff --git a/src/trim.cpp b/src/utils/trim.cpp similarity index 100% rename from src/trim.cpp rename to src/utils/trim.cpp diff --git a/src/trim.hpp b/src/utils/trim.hpp similarity index 100% rename from src/trim.hpp rename to src/utils/trim.hpp