├── .github └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── README.md ├── about.md ├── changelog.md ├── logo.png ├── mod.json ├── resources └── JB_ListLogo.png └── src ├── filesystem.hpp ├── hooks ├── custom_song_widget.cpp └── music_download_manager.cpp ├── main.cpp ├── managers ├── nong_manager.cpp └── nong_manager.hpp ├── manifest.cpp ├── manifest.hpp ├── random_string.cpp ├── random_string.hpp ├── trim.cpp ├── trim.hpp ├── types ├── fetch_status.hpp ├── nong_list_type.hpp ├── nong_state.hpp ├── sfh_item.hpp └── song_info.hpp └── ui ├── list_cell.cpp ├── list_cell.hpp ├── nong_add_popup.cpp ├── nong_add_popup.hpp ├── nong_cell.cpp ├── nong_cell.hpp ├── nong_dropdown_layer.cpp ├── nong_dropdown_layer.hpp ├── song_cell.cpp └── song_cell.hpp /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Geode Mod 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | config: 15 | - name: Windows 16 | os: windows-latest 17 | 18 | # - name: macOS 19 | # os: macos-latest 20 | - name: Android32 21 | os: ubuntu-latest 22 | target: Android32 23 | 24 | - name: Android64 25 | os: ubuntu-latest 26 | target: Android64 27 | name: ${{matrix.config.name}} 28 | runs-on: ${{matrix.config.os}} 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - name: Build the mod 34 | uses: geode-sdk/build-geode-mod@main 35 | with: 36 | bindings: fleeym/sapphire-bindings 37 | bindings-ref: nongd 38 | combine: true 39 | target: ${{matrix.config.target}} 40 | package: 41 | name: Package builds 42 | runs-on: ubuntu-latest 43 | needs: ['build'] 44 | 45 | steps: 46 | - uses: geode-sdk/build-geode-mod@combine 47 | id: build 48 | 49 | - uses: actions/upload-artifact@v3 50 | with: 51 | name: Build Output 52 | path: ${{steps.build.outputs.build-output}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Macos be like 35 | **/.DS_Store 36 | 37 | # Cache files for Sublime Text 38 | *.tmlanguage.cache 39 | *.tmPreferences.cache 40 | *.stTheme.cache 41 | 42 | # Workspace files are user-specific 43 | *.sublime-workspace 44 | 45 | # I need to find a way to automatically remove this 46 | Source/Geode/pkg/uber-apk-signer.jar 47 | 48 | # Ignore build folders 49 | **/build 50 | 51 | # ILY vscode 52 | **/.vscode 53 | 54 | build-android.bat 55 | 56 | # clangd cache 57 | .cache/clangd -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.0) 2 | set(CMAKE_CXX_STANDARD 20) 3 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 4 | 5 | project(jukebox VERSION 2.0.0) 6 | 7 | file(GLOB SOURCES 8 | src/ui/*.cpp 9 | src/managers/*.cpp 10 | src/hooks/*.cpp 11 | src/*.cpp 12 | ) 13 | 14 | add_library(${PROJECT_NAME} SHARED ${SOURCES}) 15 | 16 | if (NOT DEFINED ENV{GEODE_SDK}) 17 | message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") 18 | else() 19 | message(STATUS "Found Geode: $ENV{GEODE_SDK}") 20 | endif() 21 | 22 | add_subdirectory($ENV{GEODE_SDK} $ENV{GEODE_SDK}/build) 23 | 24 | target_link_libraries(${PROJECT_NAME} geode-sdk) 25 | create_geode_file(${PROJECT_NAME}) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jukebox 2 | 3 | NONGD logo 4 | 5 | The Jukebox Mod is a song manager for Geometry Dash. The primary goal is simplifying the process of swapping Newgrounds songs with any NONGs. 6 | 7 | ## What is a NONG, anyway? 8 | 9 | NONG stands for **Not On NewGrounds**. Basically, it means any song that is not on Newgrounds that was replaced manually through the game files. 10 | 11 | NONGs have always been a hassle to manage, because some level creators use popular Newgrounds song IDs and replace them with a NONG. So you have to swap those song files around quite a bit if you play a level with the Newgrounds song and a level with a NONG song. 12 | 13 | ## Start your jukebox! 14 | 15 | The Jukebox Mod makes the process of managing your songs a breeze. You have 2 choices for adding a song to the game. 16 | 17 | 1. Fetch from Song File Hub 18 | 2. Download MP3 manually and add it ingame 19 | 20 | You can download your NONGs using your method of choice. A recommandation of mine is [yt-dlp](https://github.com/yt-dlp/yt-dlp), a CLI application. After getting your MP3 file, you can enter a song and author name, for easier management. 21 | 22 | > Note that Jukebox copies imported MP3 files in the storage location designated by Geode. You can open this folder from ingame. 23 | 24 | Alternatively, you can download song data from **Song File Hub**, which is all tightly integrated inside the mod! Huge thanks to their team for helping out with the integration. 25 | 26 | ## So, how do I begin? 27 | 28 | You can open up the Jukebox menu form any Level page. Just click on the song name, and either a song list (if the level has multiple songs), or the song management screen (if the level only uses 1 song) will open. From here, you can add, remove, swap and fetch songs. 29 | 30 | ## Credits 31 | 32 | - The Geode team, for creating such an amazing toolkit 33 | - Song File Hub, for creating the best song archive in the community (and also letting me interact with their API) -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | # Jukebox 2 | 3 | The Jukebox Mod is a song manager for Geometry Dash. The primary goal is simplifying the process of swapping Newgrounds songs with any NONGs. 4 | 5 | ## What is a NONG, anyway? 6 | 7 | NONG stands for **Not On NewGrounds**. Basically, it means any song that is not on Newgrounds that was replaced manually through the game files. 8 | 9 | NONGs have always been a hassle to manage, because some level creators use popular Newgrounds song IDs and replace them with a NONG. So you have to swap those song files around quite a bit if you play a level with the Newgrounds song and a level with a NONG song. 10 | 11 | ## Start your jukebox! 12 | 13 | The Jukebox Mod makes the process of managing your songs a breeze. You have 2 choices for adding a song to the game. 14 | 15 | 1. Fetch from Song File Hub 16 | 2. Download MP3 manually and add it ingame 17 | 18 | You can download your NONGs using your method of choice. A recommandation of mine is [yt-dlp](https://github.com/yt-dlp/yt-dlp), a CLI application. After getting your MP3 file, you can enter a song and author name, for easier management. 19 | 20 | > Note that Jukebox copies imported MP3 files in the storage location designated by Geode. You can open this folder from ingame. 21 | 22 | Alternatively, you can download song data from **Song File Hub**, which is all tightly integrated inside the mod! Huge thanks to their team for helping out with the integration. 23 | 24 | ## So, how do I begin? 25 | 26 | You can open up the Jukebox menu form any Level page. Just click on the song name, and either a song list (if the level has multiple songs), or the song management screen (if the level only uses 1 song) will open. From here, you can add, remove, swap and fetch songs. 27 | 28 | ## Credits 29 | 30 | - The Geode team, for creating such an amazing toolkit 31 | - Song File Hub, for creating the best song archive in the community (and also letting me interact with their API) -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.1.5 4 | 5 | * Reenable manual song add on Android 6 | 7 | ## v2.1.4 8 | 9 | * Temporarily disable manual song add on Android 10 | * Fix touch priority issues in the song list 11 | 12 | ## v2.1.3 13 | 14 | * Fix buttons not working in the song list 15 | 16 | ## v2.1.2 17 | 18 | * Fix crashes on Android 19 | * Fix manual song add on Android 20 | 21 | ## v2.1.1 22 | 23 | * Fix the Robtop Music Library being... a little weird 24 | * Fix the random Error text that would appear in the song widget sometimes 25 | * 0.0B fix now accounts for songs that are included with the game (GD/Resources/songs folder) 26 | * Fix some editor song select issues 27 | * Android support (experimental) 28 | 29 | ## v2.1.0 30 | 31 | * Correctly disable nongs for levels that have robtop levels 32 | * Fix a crash that happened when entering a level with an invalid song id 33 | * Store level name separate from song name (and display it in the song list) 34 | * Store song data as minified json 35 | * Fix 0.0B on multi asset levels (experimental) 36 | 37 | ## v2.0.1 38 | 39 | * Fix a crash that happens when entering a level with song info data not fetched 40 | 41 | ## v2.0.0 42 | 43 | * Add 2.2 support 44 | * Add support for levels with multiple songs (experimental) 45 | * Optimizations and bug fixes 46 | * Rebranding! 47 | 48 | This release is only available on Windows, next one should be available on Android too. (Sorry android fellas) 49 | 50 | ## v1.2.3 51 | 52 | * Fix crash when adding a NONG manually for the first time 53 | 54 | ## v1.2.2 55 | 56 | * Fix crashes on Android 57 | 58 | ## v1.2.1 59 | 60 | * Increased the Z Layer for the NONG popup 61 | 62 | ## v1.2.0 63 | 64 | * Replace old popup with a layer that fits the game more 65 | * Recompile for Android NDK r26b 66 | 67 | ## v1.1.1 68 | 69 | * Add experimental Android support 70 | 71 | ## v1.1.0 72 | 73 | * Changed the song size label to show N/A instead of 0.00MB for songs that are missing their file 74 | * Added a setting that prevents mashup downloading from Song File Hub 75 | * Added a "Remove All" button to the NONG list 76 | * Fixed aspect ratio issues in the popups 77 | * Copy locally added nongs to the mod storage instead of using the file provided by the user 78 | * Created a manifest system to track JSON structure updates 79 | * Added a button that opens the settings page in the nong popup 80 | 81 | ## v1.0.6 82 | 83 | * Fixed a bug that prevented saving songs to disk if the Song File Hub name contained Unicode characters 84 | 85 | ## v1.0.4 86 | 87 | * Switched to using the new API for Song File Hub 88 | * Gave the add song popup some elasticity 89 | 90 | ## v1.0.3 91 | 92 | * Removed all filters for the song file picker so that MacOS can actually use it. 93 | 94 | ## v1.0.2 95 | 96 | * Fixed a crash that occured by pressing ESC while downloading a NONG 97 | * Disable NONGd for levels that use Robtop level songs 98 | * Update json impl to match the new json library version API 99 | * Add a small indicator to the song label to hint that you can click it 100 | * Fixed text inputs for Geode v1.0.0-beta.14 101 | 102 | ## v1.0.1 103 | 104 | * Mod can now build on MacOS 105 | 106 | ## v1.0.0 107 | 108 | * Implement async file downloads 109 | * Fix some crashes 110 | * Only download one song at a time instead of downloading all of them 111 | * Fix size label showing 0.00mb for undownloaded newgrounds songs 112 | * Reduce calls to updateSongObject 113 | 114 | ## v1.0.0-beta.3 115 | 116 | * Fix the invalid SFH download popup being positioned weirdly 117 | * Update the Custom Song Widget on every nong update 118 | * Use layouts for list cells 119 | * Try to fix unicode not being parsed correctly 120 | * Fix nongd folder not being created properly on some occasions 121 | 122 | ## v1.0.0-beta.1 123 | 124 | * Initial version -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fleeym/nongd/7824b010d52387af7477d001769287e07323e95e/logo.png -------------------------------------------------------------------------------- /mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "geode": "2.0.0", 3 | "version": "2.1.5", 4 | "id": "fleym.nongd", 5 | "name": "Jukebox", 6 | "gd": { 7 | "win": "2.204", 8 | "android": "2.205" 9 | }, 10 | "developer": "Fleym", 11 | "description": "A simple song manager for Geometry Dash", 12 | "repository": "https://github.com/Fleeym/nongd", 13 | "issues": { 14 | "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." 15 | }, 16 | "tags": [ 17 | "Music" 18 | ], 19 | "settings": { 20 | "store-mashups": { 21 | "name": "Store Mashups", 22 | "type": "bool", 23 | "description": "Allows mashups to be downloaded from Song File Hub", 24 | "default": true 25 | }, 26 | "fix-empty-size": { 27 | "name": "Fix 0.0B", 28 | "type": "bool", 29 | "description": "Fixes multi asset levels showing 0.0B size (experimental, might cause lag when downloading assets)", 30 | "default": false 31 | } 32 | }, 33 | "resources": { 34 | "sprites": [ 35 | "resources/*.png" 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /resources/JB_ListLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fleeym/nongd/7824b010d52387af7477d001769287e07323e95e/resources/JB_ListLogo.png -------------------------------------------------------------------------------- /src/filesystem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef GEODE_IS_MACOS 6 | #include 7 | namespace fs = std::filesystem; 8 | #else 9 | namespace fs = ghc::filesystem; 10 | #endif -------------------------------------------------------------------------------- /src/hooks/custom_song_widget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../types/song_info.hpp" 7 | #include "../managers/nong_manager.hpp" 8 | #include "../ui/nong_dropdown_layer.hpp" 9 | 10 | using namespace geode::prelude; 11 | 12 | class $modify(JBSongWidget, CustomSongWidget) { 13 | NongData nongs; 14 | CCMenu* menu; 15 | CCMenuItemSpriteExtra* songNameLabel; 16 | CCLabelBMFont* sizeIdLabel; 17 | std::string songIds = ""; 18 | std::string sfxIds = ""; 19 | bool fetchedAssetInfo = false; 20 | bool firstRun = true; 21 | bool searching = false; 22 | std::unordered_map assetNongData; 23 | 24 | bool init( 25 | SongInfoObject* songInfo, 26 | CustomSongDelegate* songDelegate, 27 | bool showSongSelect, 28 | bool showPlayMusic, 29 | bool showDownload, 30 | bool isRobtopSong, 31 | bool unk, 32 | bool isMusicLibrary 33 | ) { 34 | if (!CustomSongWidget::init(songInfo, songDelegate, showSongSelect, showPlayMusic, showDownload, isRobtopSong, unk, isMusicLibrary)) { 35 | return false; 36 | } 37 | if (isRobtopSong) { 38 | return true; 39 | } 40 | // log::info("songselect {}, playmusic {}, download {}, robtop {}, unk {}, musiclib {}", m_showSelectSongBtn, m_showPlayMusicBtn, m_showDownloadBtn, m_isRobtopSong, m_unkBool1, m_isMusicLibrary); 41 | 42 | 43 | m_songLabel->setVisible(false); 44 | return true; 45 | } 46 | 47 | void updateWithMultiAssets(gd::string p1, gd::string p2, int p3) { 48 | CustomSongWidget::updateWithMultiAssets(p1, p2, p3); 49 | m_fields->songIds = std::string(p1); 50 | m_fields->sfxIds = std::string(p2); 51 | if (m_fields->fetchedAssetInfo) { 52 | this->fixMultiAssetSize(); 53 | } 54 | if (m_isRobtopSong) { 55 | return; 56 | } 57 | this->createSongLabels(); 58 | } 59 | 60 | void updateMultiAssetInfo(bool p) { 61 | CustomSongWidget::updateMultiAssetInfo(p); 62 | if (m_fields->fetchedAssetInfo) { 63 | this->fixMultiAssetSize(); 64 | } 65 | } 66 | 67 | void fixMultiAssetSize() { 68 | auto flag = Mod::get()->getSettingValue("fix-empty-size"); 69 | if ((m_fields->songIds.empty() && m_fields->sfxIds.empty()) || !flag) { 70 | return; 71 | } 72 | NongManager::get()->getMultiAssetSizes(m_fields->songIds, m_fields->sfxIds, [this](std::string result) { 73 | std::stringstream ss; 74 | ss << "Songs: " << m_songs.size() << " SFX: " << m_sfx.size() << " Size: " << result; 75 | m_songIDLabel->setString(ss.str().c_str()); 76 | }); 77 | } 78 | 79 | void restoreUI() { 80 | m_songLabel->setVisible(true); 81 | if (m_fields->menu != nullptr) { 82 | m_fields->songNameLabel->removeFromParent(); 83 | m_fields->songNameLabel = nullptr; 84 | m_fields->menu->removeFromParent(); 85 | m_fields->menu = nullptr; 86 | } 87 | if (m_fields->sizeIdLabel != nullptr) { 88 | m_songIDLabel->setVisible(true); 89 | m_fields->sizeIdLabel->removeFromParent(); 90 | m_fields->sizeIdLabel = nullptr; 91 | } 92 | } 93 | 94 | void updateSongObject(SongInfoObject* obj) { 95 | // log::info("{}, {}, {}, {}, {}", obj->m_songName, obj->m_artistName, obj->m_songUrl, obj->m_isUnkownSong, obj->m_songID); 96 | // log::info("songselect {}, playmusic {}, download {}, robtop {}, unk {}, musiclib {}", m_showSelectSongBtn, m_showPlayMusicBtn, m_showDownloadBtn, m_isRobtopSong, m_unkBool1, m_isMusicLibrary); 97 | CustomSongWidget::updateSongObject(obj); 98 | if (obj->m_songID == 0) { 99 | this->restoreUI(); 100 | return; 101 | } 102 | if (m_showSelectSongBtn && obj->m_artistName.empty() && obj->m_songUrl.empty()) { 103 | // log::info("returning"); 104 | this->restoreUI(); 105 | return; 106 | } 107 | if (m_isRobtopSong) { 108 | return; 109 | } 110 | // log::info("not returning?"); 111 | m_songLabel->setVisible(false); 112 | if (obj->m_artistName.empty() && obj->m_songUrl.empty()) { 113 | // we have an invalid songID 114 | auto res = NongManager::get()->getActiveNong(obj->m_songID); 115 | if (res.has_value()) { 116 | auto value = res.value(); 117 | obj->m_artistName = value.authorName; 118 | } else { 119 | NongManager::get()->createUnknownDefault(obj->m_songID); 120 | obj->m_artistName = "Unknown"; 121 | } 122 | } 123 | auto result = NongManager::get()->getNongs(obj->m_songID); 124 | if (!result.has_value()) { 125 | NongManager::get()->createDefault(obj->m_songID); 126 | result = NongManager::get()->getNongs(obj->m_songID); 127 | if (!result.has_value()) { 128 | return; 129 | } 130 | } 131 | m_fields->nongs = result.value(); 132 | this->createSongLabels(); 133 | auto active = NongManager::get()->getActiveNong(obj->m_songID).value(); 134 | auto data = NongManager::get()->getNongs(obj->m_songID).value(); 135 | if (active.path != data.defaultPath) { 136 | m_deleteBtn->setVisible(false); 137 | } 138 | } 139 | 140 | void updateSongInfo() { 141 | CustomSongWidget::updateSongInfo(); 142 | if (m_isRobtopSong || m_songInfoObject == nullptr || m_songInfoObject->m_songID == 0) { 143 | return; 144 | } 145 | if (!m_fields->fetchedAssetInfo && m_songs.size() != 0) { 146 | this->getMultiAssetSongInfo(); 147 | } 148 | } 149 | 150 | void getMultiAssetSongInfo() { 151 | bool allDownloaded = true; 152 | for (auto const& kv : m_songs) { 153 | auto result = NongManager::get()->getNongs(kv.first); 154 | if (!result.has_value()) { 155 | NongManager::get()->createDefault(kv.first); 156 | result = NongManager::get()->getNongs(kv.first); 157 | if (!result.has_value()) { 158 | // its downloading 159 | allDownloaded = false; 160 | continue; 161 | } 162 | } 163 | auto value = result.value(); 164 | m_fields->assetNongData[kv.first] = value; 165 | } 166 | if (allDownloaded) { 167 | m_fields->fetchedAssetInfo = true; 168 | } 169 | } 170 | 171 | void createSongLabels() { 172 | int songID = m_songInfoObject->m_songID; 173 | auto active = NongManager::get()->getActiveNong(songID).value(); 174 | if (m_fields->menu != nullptr) { 175 | m_fields->menu->removeFromParent(); 176 | } 177 | auto menu = CCMenu::create(); 178 | menu->setID("song-name-menu"); 179 | auto label = CCLabelBMFont::create(active.songName.c_str(), "bigFont.fnt"); 180 | if (!m_isMusicLibrary) { 181 | label->limitLabelWidth(220.f, 0.8f, 0.1f); 182 | } else { 183 | label->limitLabelWidth(130.f, 0.4f, 0.1f); 184 | } 185 | auto songNameMenuLabel = CCMenuItemSpriteExtra::create( 186 | label, 187 | this, 188 | menu_selector(JBSongWidget::addNongLayer) 189 | ); 190 | songNameMenuLabel->setTag(songID); 191 | // // I am not even gonna try and understand why this works, but this places the label perfectly in the menu 192 | auto labelScale = label->getScale(); 193 | songNameMenuLabel->setID("song-name-label"); 194 | songNameMenuLabel->setPosition(ccp(0.f, 0.f)); 195 | songNameMenuLabel->setAnchorPoint(ccp(0.f, 0.5f)); 196 | m_fields->songNameLabel = songNameMenuLabel; 197 | menu->addChild(songNameMenuLabel); 198 | menu->setContentSize(ccp(label->getContentSize().width * labelScale, 25.f)); 199 | if (!m_isMusicLibrary) { 200 | menu->setPosition(ccp(-140.f, 27.5f)); 201 | } else { 202 | menu->setPosition(ccp(-150.f, 9.f)); 203 | } 204 | songNameMenuLabel->setContentSize({ label->getContentSize().width * labelScale, labelScale * 30 }); 205 | m_fields->menu = menu; 206 | this->addChild(menu); 207 | if (m_songs.size() == 0 && m_sfx.size() == 0 && !m_isMusicLibrary) { 208 | if (m_fields->sizeIdLabel != nullptr) { 209 | m_fields->sizeIdLabel->removeFromParent(); 210 | } 211 | auto data = NongManager::get()->getNongs(songID).value(); 212 | 213 | if (!fs::exists(active.path) && active.path == data.defaultPath) { 214 | m_songIDLabel->setVisible(true); 215 | return; 216 | } else if (m_songIDLabel) { 217 | m_songIDLabel->setVisible(false); 218 | } 219 | 220 | std::string sizeText; 221 | if (fs::exists(active.path)) { 222 | sizeText = NongManager::get()->getFormattedSize(active); 223 | } else { 224 | sizeText = "NA"; 225 | } 226 | std::string labelText; 227 | if (active.path == data.defaultPath) { 228 | labelText = "SongID: " + std::to_string(songID) + " Size: " + sizeText; 229 | } else { 230 | labelText = "SongID: NONG Size: " + sizeText; 231 | } 232 | 233 | auto label = CCLabelBMFont::create(labelText.c_str(), "bigFont.fnt"); 234 | label->setID("nongd-id-and-size-label"); 235 | label->setPosition(ccp(-139.f, -31.f)); 236 | label->setAnchorPoint({0, 0.5f}); 237 | label->setScale(0.4f); 238 | this->addChild(label); 239 | m_fields->sizeIdLabel = label; 240 | } else { 241 | if (m_fields->sizeIdLabel) { 242 | m_fields->sizeIdLabel->setVisible(false); 243 | } 244 | m_songIDLabel->setVisible(true); 245 | } 246 | } 247 | 248 | void addNongLayer(CCObject* target) { 249 | if (m_songs.size() > 1 && !m_fields->fetchedAssetInfo) { 250 | this->getMultiAssetSongInfo(); 251 | if (!m_fields->fetchedAssetInfo) { 252 | return; 253 | } 254 | } 255 | auto scene = CCDirector::sharedDirector()->getRunningScene(); 256 | std::vector ids; 257 | if (m_songs.size() > 1) { 258 | for (auto const& kv : m_songs) { 259 | if (!NongManager::get()->getNongs(kv.first).has_value()) { 260 | return; 261 | } 262 | ids.push_back(kv.first); 263 | } 264 | } else { 265 | ids.push_back(m_songInfoObject->m_songID); 266 | } 267 | auto layer = NongDropdownLayer::create(ids, this, m_songInfoObject->m_songID); 268 | layer->m_noElasticity = true; 269 | // based robtroll 270 | layer->setZOrder(106); 271 | layer->show(); 272 | } 273 | }; 274 | 275 | class $modify(JBLevelInfoLayer, LevelInfoLayer) { 276 | bool init(GJGameLevel* level, bool p1) { 277 | if (!LevelInfoLayer::init(level, p1)) { 278 | return false; 279 | } 280 | if (Mod::get()->getSavedValue("show-tutorial", true) && GameManager::get()->m_levelEditorLayer == nullptr) { 281 | auto popup = FLAlertLayer::create("Jukebox", "Thank you for using Jukebox! To begin swapping songs, click on the song name!", "Ok"); 282 | Mod::get()->setSavedValue("show-tutorial", false); 283 | popup->m_scene = this; 284 | popup->show(); 285 | } 286 | return true; 287 | } 288 | }; 289 | 290 | // class $modify(NongSongWidget, CustomSongWidget) { 291 | // NongData nongData; 292 | // int nongdSong; 293 | 294 | // bool hasDefaultSong = false; 295 | // bool firstRun = true; 296 | 297 | // bool init(SongInfoObject* songInfo, LevelSettingsObject* levelSettings, bool p2, bool p3, bool p4, bool hasDefaultSong, bool hideBackground) { 298 | // if (!CustomSongWidget::init(songInfo, levelSettings, p2, p3, p4, hasDefaultSong, hideBackground)) return false; 299 | 300 | // if (!songInfo) { 301 | // return true; 302 | // } 303 | 304 | // m_fields->firstRun = false; 305 | 306 | // if (hasDefaultSong) { 307 | // m_fields->hasDefaultSong = true; 308 | // this->updateSongObject(m_songInfo); 309 | // return true; 310 | // } 311 | 312 | // auto songNameLabel = typeinfo_cast(this->getChildByID("song-name-label")); 313 | // songNameLabel->setVisible(false); 314 | 315 | // auto idAndSizeLabel = typeinfo_cast(this->getChildByID("id-and-size-label")); 316 | // idAndSizeLabel->setVisible(false); 317 | // auto newLabel = CCLabelBMFont::create("new", "bigFont.fnt"); 318 | // newLabel->setID("nongd-id-and-size-label"); 319 | // newLabel->setPosition(ccp(0.f, -32.f)); 320 | // newLabel->setScale(0.4f); 321 | // this->addChild(newLabel); 322 | 323 | // m_fields->nongdSong = songInfo->m_songID; 324 | 325 | // if (!NongManager::get()->checkIfNongsExist(songInfo->m_songID)) { 326 | // auto strPath = std::string(MusicDownloadManager::sharedState()->pathForSong(songInfo->m_songID)); 327 | 328 | // SongInfo defaultSong = { 329 | // .path = ghc::filesystem::path(strPath), 330 | // .songName = songInfo->m_songName, 331 | // .authorName = songInfo->m_artistName, 332 | // .songUrl = songInfo->m_songURL, 333 | // }; 334 | 335 | // NongManager::get()->createDefaultSongIfNull(defaultSong, songInfo->m_songID); 336 | // } 337 | 338 | // auto invalidSongs = NongManager::get()->validateNongs(songInfo->m_songID); 339 | 340 | // if (invalidSongs.size() > 0) { 341 | // std::string invalidSongList = ""; 342 | // for (auto &song : invalidSongs) { 343 | // invalidSongList += song.songName + ", "; 344 | // } 345 | 346 | // invalidSongList = invalidSongList.substr(0, invalidSongList.size() - 2); 347 | // // If anyone asks this was mat's idea 348 | // Loader::get()->queueInMainThread([this, invalidSongList]() { 349 | // auto alert = FLAlertLayer::create("Invalid NONGs", "The NONGs [" + invalidSongList + "] have been deleted, because their paths were invalid.", "Ok"); 350 | // alert->m_scene = this->getParent(); 351 | // alert->show(); 352 | // }); 353 | // } 354 | 355 | // m_fields->nongData = NongManager::get()->getNongs(m_songInfo->m_songID); 356 | // SongInfo nong; 357 | // for (auto song : m_fields->nongData.songs) { 358 | // if (song.path == m_fields->nongData.active) { 359 | // nong = song; 360 | // } 361 | // } 362 | 363 | // m_songInfo->m_artistName = nong.authorName; 364 | // m_songInfo->m_songName = nong.songName; 365 | // this->updateSongObject(m_songInfo); 366 | // if (auto found = this->getChildByID("song-name-menu")) { 367 | // this->updateSongNameLabel(m_songInfo->m_songName, m_songInfo->m_songID); 368 | // } else { 369 | // this->addMenuItemLabel(m_songInfo->m_songName, m_songInfo->m_songID); 370 | // } 371 | // if (nong.path == m_fields->nongData.defaultPath) { 372 | // this->updateIDAndSizeLabel(nong, m_songInfo->m_songID); 373 | // } else { 374 | // this->updateIDAndSizeLabel(nong); 375 | // } 376 | 377 | // return true; 378 | // } 379 | 380 | // void addMenuItemLabel(std::string const& text, int songID) { 381 | // auto menu = CCMenu::create(); 382 | // menu->setID("song-name-menu"); 383 | 384 | // auto label = CCLabelBMFont::create(text.c_str(), "bigFont.fnt"); 385 | // label->limitLabelWidth(220.f, 0.8f, 0.1f); 386 | // auto info = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); 387 | // info->setScale(0.5f); 388 | // auto songNameMenuLabel = CCMenuItemSpriteExtra::create( 389 | // label, 390 | // this, 391 | // menu_selector(NongSongWidget::addNongLayer) 392 | // ); 393 | // songNameMenuLabel->addChild(info); 394 | // songNameMenuLabel->setTag(songID); 395 | // // I am not even gonna try and understand why this works, but this places the label perfectly in the menu 396 | // auto labelScale = label->getScale(); 397 | // songNameMenuLabel->setID("song-name-label"); 398 | // songNameMenuLabel->setPosition(ccp(0.f, 0.f)); 399 | // songNameMenuLabel->setAnchorPoint(ccp(0.f, 0.5f)); 400 | // menu->addChild(songNameMenuLabel); 401 | // menu->setContentSize(ccp(220.f, 25.f)); 402 | // menu->setPosition(ccp(-140.f, 27.5f)); 403 | // auto layout = RowLayout::create(); 404 | // layout->setAxisAlignment(AxisAlignment::Start); 405 | // layout->setAutoScale(false); 406 | // songNameMenuLabel->setLayout(layout); 407 | // songNameMenuLabel->setContentSize({ 220.f, labelScale * 30 }); 408 | 409 | // this->addChild(menu); 410 | // } 411 | 412 | // void updateSongNameLabel(std::string const& text, int songID) { 413 | // auto menu = this->getChildByID("song-name-menu"); 414 | // auto labelMenuItem = typeinfo_cast(menu->getChildByID("song-name-label")); 415 | // labelMenuItem->setTag(songID); 416 | // auto child = typeinfo_cast(labelMenuItem->getChildren()->objectAtIndex(0)); 417 | // child->setString(text.c_str()); 418 | // child->limitLabelWidth(220.f, 0.8f, 0.1f); 419 | // auto labelScale = child->getScale(); 420 | // labelMenuItem->setContentSize({ 220.f, labelScale * 30 }); 421 | // labelMenuItem->updateLayout(); 422 | // } 423 | 424 | // void updateIDAndSizeLabel(SongInfo const& song, int songID = 0) { 425 | // auto label = typeinfo_cast(this->getChildByID("nongd-id-and-size-label")); 426 | // auto normalLabel = typeinfo_cast(this->getChildByID("id-and-size-label")); 427 | // auto defaultPath = m_fields->nongData.defaultPath; 428 | 429 | // if (!ghc::filesystem::exists(song.path) && song.path == defaultPath) { 430 | // label->setVisible(false); 431 | // this->getChildByID("id-and-size-label")->setVisible(true); 432 | // return; 433 | // } else if (normalLabel && normalLabel->isVisible()) { 434 | // normalLabel->setVisible(false); 435 | // label->setVisible(true); 436 | // } 437 | 438 | // std::string sizeText; 439 | // if (ghc::filesystem::exists(song.path)) { 440 | // sizeText = NongManager::get()->getFormattedSize(song); 441 | // } else { 442 | // sizeText = "NA"; 443 | // } 444 | // std::string labelText; 445 | // if (songID != 0) { 446 | // labelText = "SongID: " + std::to_string(songID) + " Size: " + sizeText; 447 | // } else { 448 | // labelText = "SongID: NONG Size: " + sizeText; 449 | // } 450 | 451 | // if (label) { 452 | // label->setString(labelText.c_str()); 453 | // } 454 | // } 455 | 456 | // void updateSongObject(SongInfoObject* song) { 457 | // if (m_fields->firstRun) { 458 | // CustomSongWidget::updateSongObject(song); 459 | // return; 460 | // } 461 | 462 | // if (m_fields->hasDefaultSong) { 463 | // CustomSongWidget::updateSongObject(song); 464 | // if (auto found = this->getChildByID("song-name-menu")) { 465 | // found->setVisible(false); 466 | // this->getChildByID("nongd-id-and-size-label")->setVisible(false); 467 | // } 468 | // this->getChildByID("id-and-size-label")->setVisible(true); 469 | // return; 470 | // } 471 | 472 | // m_fields->nongdSong = song->m_songID; 473 | // if (!NongManager::get()->checkIfNongsExist(song->m_songID)) { 474 | // auto strPath = std::string(MusicDownloadManager::sharedState()->pathForSong(song->m_songID)); 475 | 476 | // SongInfo defaultSong = { 477 | // .path = ghc::filesystem::path(strPath), 478 | // .songName = song->m_songName, 479 | // .authorName = song->m_artistName, 480 | // .songUrl = song->m_songURL, 481 | // }; 482 | 483 | // NongManager::get()->createDefaultSongIfNull(defaultSong, song->m_songID); 484 | // } 485 | // SongInfo active; 486 | // auto nongData = NongManager::get()->getNongs(song->m_songID); 487 | // for (auto nong : nongData.songs) { 488 | // if (nong.path == nongData.active) { 489 | // active = nong; 490 | // song->m_songName = nong.songName; 491 | // song->m_artistName = nong.authorName; 492 | // if (nong.songUrl != "local") { 493 | // song->m_songURL = nong.songUrl; 494 | // } 495 | // } 496 | // } 497 | // CustomSongWidget::updateSongObject(song); 498 | // if (auto found = this->getChildByID("song-name-menu")) { 499 | // this->updateSongNameLabel(song->m_songName, song->m_songID); 500 | // } else { 501 | // this->addMenuItemLabel(song->m_songName, song->m_songID); 502 | // } 503 | // if (active.path == nongData.defaultPath) { 504 | // this->updateIDAndSizeLabel(active, song->m_songID); 505 | // } else { 506 | // this->updateIDAndSizeLabel(active); 507 | // } 508 | // } 509 | 510 | // void addNongLayer(CCObject* target) { 511 | // auto scene = CCDirector::sharedDirector()->getRunningScene(); 512 | // auto layer = NongDropdownLayer::create(m_fields->nongdSong, this); 513 | // // based robtroll 514 | // layer->setZOrder(106); 515 | // scene->addChild(layer); 516 | // layer->showLayer(false); 517 | // } 518 | 519 | // }; -------------------------------------------------------------------------------- /src/hooks/music_download_manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../managers/nong_manager.hpp" 5 | #include "../types/song_info.hpp" 6 | 7 | class $modify(MusicDownloadManager) { 8 | gd::string pathForSong(int id) { 9 | auto active = NongManager::get()->getActiveNong(id); 10 | if (!active.has_value()) { 11 | return MusicDownloadManager::pathForSong(id); 12 | } 13 | auto value = active.value(); 14 | if (!fs::exists(value.path)) { 15 | return MusicDownloadManager::pathForSong(id); 16 | } 17 | return value.path.string(); 18 | } 19 | void onGetSongInfoCompleted(gd::string p1, gd::string p2) { 20 | MusicDownloadManager::onGetSongInfoCompleted(p1, p2); 21 | auto songID = std::stoi(p2); 22 | NongManager::get()->resolveSongInfoCallback(songID); 23 | } 24 | 25 | SongInfoObject* getSongInfoObject(int id) { 26 | auto og = MusicDownloadManager::getSongInfoObject(id); 27 | if (og == nullptr) { 28 | return og; 29 | } 30 | auto active = NongManager::get()->getActiveNong(id); 31 | if (active.has_value()) { 32 | auto value = active.value(); 33 | og->m_songName = value.songName; 34 | og->m_artistName = value.authorName; 35 | } 36 | return og; 37 | } 38 | }; -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // hello :) 2 | 3 | #include "managers/nong_manager.hpp" 4 | 5 | $execute { 6 | NongManager::get()->loadSongs(); 7 | }; -------------------------------------------------------------------------------- /src/managers/nong_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "nong_manager.hpp" 2 | 3 | std::optional NongManager::getNongs(int songID) { 4 | if (!m_state.m_nongs.contains(songID)) { 5 | return std::nullopt; 6 | } 7 | 8 | return m_state.m_nongs[songID]; 9 | } 10 | 11 | std::optional NongManager::getActiveNong(int songID) { 12 | auto nongs_res = this->getNongs(songID); 13 | if (!nongs_res.has_value()) { 14 | return std::nullopt; 15 | } 16 | auto nongs = nongs_res.value(); 17 | 18 | for (auto &song : nongs.songs) { 19 | if (song.path == nongs.active) { 20 | return song; 21 | } 22 | } 23 | 24 | nongs.active = nongs.defaultPath; 25 | 26 | for (auto &song : nongs.songs) { 27 | if (song.path == nongs.active) { 28 | return song; 29 | } 30 | } 31 | 32 | return std::nullopt; 33 | } 34 | 35 | std::optional NongManager::getDefaultNong(int songID) { 36 | auto nongs_res = this->getNongs(songID); 37 | if (!nongs_res.has_value()) { 38 | return std::nullopt; 39 | } 40 | auto nongs = nongs_res.value(); 41 | 42 | for (auto &song : nongs.songs) { 43 | if (song.path == nongs.defaultPath) { 44 | return song; 45 | } 46 | } 47 | 48 | return std::nullopt; 49 | } 50 | 51 | void NongManager::resolveSongInfoCallback(int id) { 52 | if (m_getSongInfoCallbacks.contains(id)) { 53 | m_getSongInfoCallbacks[id](id); 54 | m_getSongInfoCallbacks.erase(id); 55 | } 56 | } 57 | 58 | std::vector NongManager::validateNongs(int songID) { 59 | auto result = this->getNongs(songID); 60 | // Validate nong paths and delete those that don't exist anymore 61 | std::vector invalidSongs; 62 | std::vector validSongs; 63 | if (!result.has_value()) { 64 | return invalidSongs; 65 | } 66 | auto currentData = result.value(); 67 | 68 | for (auto &song : currentData.songs) { 69 | if (!fs::exists(song.path) && currentData.defaultPath != song.path && song.songUrl == "local") { 70 | invalidSongs.push_back(song); 71 | if (song.path == currentData.active) { 72 | currentData.active = currentData.defaultPath; 73 | } 74 | } else { 75 | validSongs.push_back(song); 76 | } 77 | } 78 | 79 | if (invalidSongs.size() > 0) { 80 | NongData newData = { 81 | .active = currentData.active, 82 | .defaultPath = currentData.defaultPath, 83 | .songs = validSongs, 84 | }; 85 | 86 | this->saveNongs(newData, songID); 87 | } 88 | 89 | return invalidSongs; 90 | } 91 | 92 | int NongManager::getCurrentManifestVersion() { 93 | return m_state.m_manifestVersion; 94 | } 95 | 96 | int NongManager::getStoredIDCount() { 97 | return m_state.m_nongs.size(); 98 | } 99 | 100 | void NongManager::saveNongs(NongData const& data, int songID) { 101 | m_state.m_nongs[songID] = data; 102 | this->writeJson(); 103 | } 104 | 105 | void NongManager::writeJson() { 106 | auto json = matjson::Serialize::to_json(m_state); 107 | auto path = this->getJsonPath(); 108 | std::ofstream output(path.string()); 109 | output << json.dump(matjson::NO_INDENTATION); 110 | output.close(); 111 | } 112 | 113 | void NongManager::addNong(SongInfo const& song, int songID) { 114 | auto result = this->getNongs(songID); 115 | if (!result.has_value()) { 116 | return; 117 | } 118 | auto existingData = result.value(); 119 | 120 | for (auto const& savedSong : existingData.songs) { 121 | if (song.path.string() == savedSong.path.string()) { 122 | return; 123 | } 124 | } 125 | existingData.songs.push_back(song); 126 | this->saveNongs(existingData, songID); 127 | } 128 | 129 | void NongManager::deleteAll(int songID) { 130 | std::vector newSongs; 131 | auto result = this->getNongs(songID); 132 | if (!result.has_value()) { 133 | return; 134 | } 135 | auto existingData = result.value(); 136 | 137 | for (auto savedSong : existingData.songs) { 138 | if (savedSong.path != existingData.defaultPath) { 139 | if (fs::exists(savedSong.path)) { 140 | fs::remove(savedSong.path); 141 | } 142 | continue; 143 | } 144 | newSongs.push_back(savedSong); 145 | } 146 | 147 | NongData newData = { 148 | .active = existingData.defaultPath, 149 | .defaultPath = existingData.defaultPath, 150 | .songs = newSongs, 151 | }; 152 | this->saveNongs(newData, songID); 153 | } 154 | 155 | void NongManager::deleteNong(SongInfo const& song, int songID) { 156 | std::vector newSongs; 157 | auto result = this->getNongs(songID); 158 | if(!result.has_value()) { 159 | return; 160 | } 161 | auto existingData = result.value(); 162 | for (auto savedSong : existingData.songs) { 163 | if (savedSong.path == song.path) { 164 | if (song.path == existingData.active) { 165 | existingData.active = existingData.defaultPath; 166 | } 167 | if (song.songUrl != "local" && existingData.defaultPath != song.path && fs::exists(song.path)) { 168 | fs::remove(song.path); 169 | } 170 | continue; 171 | } 172 | newSongs.push_back(savedSong); 173 | } 174 | NongData newData = { 175 | .active = existingData.active, 176 | .defaultPath = existingData.defaultPath, 177 | .songs = newSongs, 178 | }; 179 | this->saveNongs(newData, songID); 180 | } 181 | 182 | void NongManager::createDefault(int songID, bool fromCallback) { 183 | if (m_state.m_nongs.contains(songID)) { 184 | return; 185 | } 186 | SongInfoObject* songInfo = MusicDownloadManager::sharedState()->getSongInfoObject(songID); 187 | if (songInfo == nullptr && !m_getSongInfoCallbacks.contains(songID) && !fromCallback) { 188 | MusicDownloadManager::sharedState()->getSongInfo(songID, true); 189 | m_getSongInfoCallbacks[songID] = [this](int songID) { 190 | this->createDefault(songID, true); 191 | }; 192 | return; 193 | } 194 | if (songInfo == nullptr) { 195 | return; 196 | } 197 | fs::path songPath = fs::path(std::string(MusicDownloadManager::sharedState()->pathForSong(songID))); 198 | NongData data; 199 | SongInfo defaultSong; 200 | defaultSong.authorName = songInfo->m_artistName; 201 | defaultSong.songName = songInfo->m_songName; 202 | defaultSong.path = songPath; 203 | defaultSong.songUrl = songInfo->m_songUrl; 204 | data.active = songPath; 205 | data.defaultPath = songPath; 206 | data.songs.push_back(defaultSong); 207 | m_state.m_nongs[songID] = data; 208 | } 209 | 210 | void NongManager::createUnknownDefault(int songID) { 211 | if (this->getNongs(songID).has_value()) { 212 | return; 213 | } 214 | fs::path songPath = fs::path(std::string(MusicDownloadManager::sharedState()->pathForSong(songID))); 215 | NongData data; 216 | SongInfo defaultSong; 217 | defaultSong.authorName = "Unknown"; 218 | defaultSong.songName = "Unknown"; 219 | defaultSong.path = songPath; 220 | defaultSong.songUrl = ""; 221 | data.active = songPath; 222 | data.defaultPath = songPath; 223 | data.songs.push_back(defaultSong); 224 | data.defaultValid = false; 225 | m_state.m_nongs[songID] = data; 226 | this->writeJson(); 227 | } 228 | 229 | std::string NongManager::getFormattedSize(SongInfo const& song) { 230 | try { 231 | auto size = fs::file_size(song.path); 232 | double toMegabytes = size / 1024.f / 1024.f; 233 | std::stringstream ss; 234 | ss << std::setprecision(3) << toMegabytes << "MB"; 235 | return ss.str(); 236 | } catch (fs::filesystem_error) { 237 | return "N/A"; 238 | } 239 | } 240 | 241 | void NongManager::getMultiAssetSizes(std::string songs, std::string sfx, std::function callback) { 242 | fs::path resources = fs::path(CCFileUtils::get()->getWritablePath2().c_str()) / "Resources"; 243 | fs::path songDir = fs::path(CCFileUtils::get()->getWritablePath().c_str()); 244 | std::thread([this, songs, sfx, callback, resources, songDir]() { 245 | float sum = 0.f; 246 | std::istringstream stream(songs); 247 | std::string s; 248 | while (std::getline(stream, s, ',')) { 249 | int id = std::stoi(s); 250 | auto result = this->getActiveNong(id); 251 | if (!result.has_value()) { 252 | continue; 253 | } 254 | auto path = result.value().path; 255 | if (path.string().starts_with("songs/")) { 256 | path = resources / path; 257 | } 258 | if (fs::exists(path)) { 259 | sum += fs::file_size(path); 260 | } 261 | } 262 | stream = std::istringstream(sfx); 263 | while (std::getline(stream, s, ',')) { 264 | std::stringstream ss; 265 | ss << "s" << s << ".ogg"; 266 | std::string filename = ss.str(); 267 | auto localPath = resources / "sfx" / filename; 268 | if (fs::exists(localPath)) { 269 | sum += fs::file_size(localPath); 270 | continue; 271 | } 272 | auto path = songDir / filename; 273 | if (fs::exists(path)) { 274 | sum += fs::file_size(path); 275 | } 276 | } 277 | 278 | double toMegabytes = sum / 1024.f / 1024.f; 279 | std::stringstream ss; 280 | ss << std::setprecision(3) << toMegabytes << "MB"; 281 | callback(ss.str()); 282 | }).detach(); 283 | } 284 | 285 | fs::path NongManager::getJsonPath() { 286 | auto savedir = fs::path(Mod::get()->getSaveDir().string()); 287 | return savedir / "nong_data.json"; 288 | } 289 | 290 | void NongManager::fetchSFH(int songID, std::function callback) { 291 | std::string url = "https://api.songfilehub.com/songs?songID=" + std::to_string(songID); 292 | web::AsyncWebRequest() 293 | .fetch(url) 294 | .text() 295 | .then([this, callback, songID](std::string const& text) { 296 | auto copy = text; 297 | copy.erase(std::remove_if(copy.begin(), copy.end(), [](char x) { 298 | return static_cast(x) < 0; 299 | }), copy.end()); 300 | matjson::Value data; 301 | data = matjson::parse(copy); 302 | std::vector ret; 303 | if (!data.is_array()) { 304 | callback(nongd::FetchStatus::FAILED); 305 | return; 306 | } 307 | auto songs = data.as_array(); 308 | bool getMashups = Mod::get()->getSettingValue("store-mashups"); 309 | for (auto const& song : songs) { 310 | bool isMashup = song["state"].as_string() == "mashup"; 311 | if (isMashup && !getMashups) { 312 | continue; 313 | } 314 | SFHItem item = { 315 | .songName = song["songName"].as_string(), 316 | .downloadUrl = song["downloadUrl"].as_string(), 317 | .levelName = song.contains("name") ? song["name"].as_string() : "", 318 | .songURL = song.contains("songURL") ? song["songURL"].as_string() : "" 319 | }; 320 | ret.push_back(item); 321 | } 322 | if (ret.size() == 0) { 323 | callback(nongd::FetchStatus::NOTHING_FOUND); 324 | return; 325 | } 326 | if (!this->addNongsFromSFH(ret, songID)) { 327 | callback(nongd::FetchStatus::FAILED); 328 | } else { 329 | callback(nongd::FetchStatus::SUCCESS); 330 | } 331 | }) 332 | .expect([callback](std::string const& error) { 333 | log::error("{}", error); 334 | callback(nongd::FetchStatus::FAILED); 335 | }) 336 | .cancelled([callback](auto r) { 337 | callback(nongd::FetchStatus::FAILED); 338 | }); 339 | } 340 | 341 | void NongManager::downloadSong(SongInfo const& song, std::function progress, std::function failed) { 342 | if (fs::exists(song.path)) { 343 | return; 344 | } 345 | 346 | web::AsyncWebRequest() 347 | .fetch(song.songUrl) 348 | .into(ghc::filesystem::path(song.path.string())) 349 | .then([progress](std::monostate state){ 350 | progress(100.f); 351 | }) 352 | .expect([song, failed](std::string const& error) { 353 | failed(song, error); 354 | }) 355 | .cancelled([song, failed](auto request) { 356 | failed(song, "The download was cancelled"); 357 | }); 358 | } 359 | 360 | bool NongManager::addNongsFromSFH(std::vector const& songs, int songID) { 361 | auto savedir = fs::path(Mod::get()->getSaveDir().string()); 362 | auto nongsPath = savedir / "nongs"; 363 | if (!fs::exists(nongsPath)) { 364 | fs::create_directory(nongsPath); 365 | } 366 | auto result = this->getNongs(songID); 367 | if (!result.has_value()) { 368 | return false; 369 | } 370 | auto nongs = result.value(); 371 | int index = 1; 372 | for (auto const& sfhSong : songs) { 373 | bool update = false; 374 | auto path = nongsPath; 375 | auto unique = nongd::random_string(16); 376 | path.append(std::to_string(songID) + "_" + sfhSong.levelName + "_" + unique + ".mp3"); 377 | for (auto& localSong : nongs.songs) { 378 | if (localSong.songUrl == sfhSong.downloadUrl) { 379 | update = true; 380 | break; 381 | } 382 | } 383 | 384 | auto sfhSongName = sfhSong.songName; 385 | nongd::trim(sfhSongName); 386 | 387 | if (sfhSongName.find_first_of("-") == std::string::npos) { 388 | // probably dash doesn't exist because of unicode filtering, try to insert it by hand 389 | for (size_t i = 0; i < sfhSongName.size() - 1; i++) { 390 | if (sfhSongName.at(i) == ' ' && sfhSongName.at(i + 1) == ' ') { 391 | sfhSongName.insert(i + 1, "-"); 392 | } 393 | } 394 | } 395 | 396 | std::string songName = "-"; 397 | std::string artistName = "-"; 398 | std::stringstream ss; 399 | ss << sfhSongName; 400 | std::string part; 401 | size_t i = 0; 402 | while (std::getline(ss, part, '-')) { 403 | if (i == 0) { 404 | artistName = part; 405 | nongd::right_trim(artistName); 406 | } else { 407 | songName = part; 408 | nongd::left_trim(songName); 409 | } 410 | i++; 411 | } 412 | 413 | if (songName == "-") { 414 | auto temp = songName; 415 | songName = artistName; 416 | artistName = temp; 417 | } 418 | 419 | SongInfo song = { 420 | .path = path, 421 | .songName = songName, 422 | .authorName = artistName, 423 | .songUrl = sfhSong.downloadUrl, 424 | .levelName = sfhSong.levelName 425 | }; 426 | 427 | if (update) { 428 | for(auto& localSong : nongs.songs) { 429 | if (localSong.songUrl == song.songUrl) { 430 | localSong.songName = song.songName; 431 | localSong.authorName = song.authorName; 432 | localSong.levelName = song.levelName; 433 | } 434 | } 435 | } else { 436 | nongs.songs.push_back(song); 437 | } 438 | } 439 | 440 | this->saveNongs(nongs, songID); 441 | return true; 442 | } 443 | 444 | void NongManager::loadSongs() { 445 | auto path = this->getJsonPath(); 446 | if (!fs::exists(path)) { 447 | return; 448 | } 449 | std::ifstream input(path.string()); 450 | std::stringstream buffer; 451 | buffer << input.rdbuf(); 452 | input.close(); 453 | 454 | auto json = matjson::parse(std::string_view(buffer.str())); 455 | m_state = matjson::Serialize::from_json(json); 456 | } -------------------------------------------------------------------------------- /src/managers/nong_manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../types/song_info.hpp" 9 | #include "../types/fetch_status.hpp" 10 | #include "../types/sfh_item.hpp" 11 | #include "../types/nong_state.hpp" 12 | #include "../random_string.hpp" 13 | #include "../trim.hpp" 14 | 15 | using namespace geode::prelude; 16 | 17 | class NongManager : public CCObject { 18 | protected: 19 | inline static NongManager* m_instance = nullptr; 20 | NongState m_state; 21 | std::map> m_getSongInfoCallbacks; 22 | 23 | bool addNongsFromSFH(std::vector const& songs, int songID); 24 | public: 25 | /** 26 | * Only used once, on game launch. Reads the json and loads it into memory. 27 | */ 28 | void loadSongs(); 29 | 30 | /** 31 | * Execute callbacks stored for getSongInfo for a songID, if they exist 32 | */ 33 | void resolveSongInfoCallback(int id); 34 | 35 | /** 36 | * Gets the current manifest version stored in state 37 | */ 38 | int getCurrentManifestVersion(); 39 | 40 | /** 41 | * Gets the current number of song IDs that have been added to the manifest 42 | */ 43 | int getStoredIDCount(); 44 | 45 | /** 46 | * Adds a NONG to the JSON of a songID 47 | * 48 | * @param song the song to add 49 | * @param songID the id of the song 50 | */ 51 | void addNong(SongInfo const& song, int songID); 52 | 53 | /** 54 | * Removes a NONG from the JSON of a songID 55 | * 56 | * @param song the song to remove 57 | * @param songID the id of the song 58 | */ 59 | void deleteNong(SongInfo const& song, int songID); 60 | 61 | /** 62 | * Fetches all NONG data for a certain songID 63 | * 64 | * @param songID the id of the song 65 | * @return the data from the JSON or nullopt if it wasn't created yet 66 | */ 67 | std::optional getNongs(int songID); 68 | 69 | /** 70 | * Fetches the active song from the songID JSON 71 | * 72 | * @param songID the id of the song 73 | * @return the song data or nullopt in case of an error 74 | */ 75 | std::optional getActiveNong(int songID); 76 | 77 | /** 78 | * Fetches the default song from the songID JSON 79 | * 80 | * @param songID the id of the song 81 | * @return the song data or nullopt in case of an error 82 | */ 83 | std::optional getDefaultNong(int songID); 84 | 85 | /** 86 | * Validates any local nongs that have an invalid path 87 | * 88 | * @param songID the id of the song 89 | * 90 | * @return an array of songs that were deleted as result of the validation 91 | */ 92 | std::vector validateNongs(int songID); 93 | 94 | /** 95 | * Saves NONGS to the songID JSON 96 | * 97 | * @param data the data to save 98 | * @param songID the id of the song 99 | */ 100 | void saveNongs(NongData const& data, int songID); 101 | 102 | /** 103 | * Writes song data to the JSON 104 | */ 105 | void writeJson(); 106 | 107 | /** 108 | * Removes all NONG data for a song ID 109 | * 110 | * @param songID the id of the song 111 | */ 112 | void deleteAll(int songID); 113 | 114 | /** 115 | * Formats a size in bytes to a x.xxMB string 116 | * 117 | * @param song the song 118 | * 119 | * @return the formatted size, with the format x.xxMB 120 | */ 121 | std::string getFormattedSize(SongInfo const& song); 122 | 123 | /** 124 | * Calculates the total size of multiple assets, then writes it to a string. 125 | * Runs on a separate thread. Returns the result in the provided callback. 126 | * 127 | * @param songs string of song ids, separated by commas 128 | * @param sfx string of sfx ids, separated by commas 129 | * @param callback callback that handles the computed string 130 | */ 131 | void getMultiAssetSizes(std::string songs, std::string sfx, std::function callback); 132 | 133 | /** 134 | * Fetches song info for an id and creates the default entry in the json. 135 | * 136 | * @param songID the id of the song 137 | * @param fromCallback used to skip fetching song info from MDM (after it has been done once) 138 | */ 139 | void createDefault(int songID, bool fromCallback = false); 140 | 141 | /** 142 | * Creates a default with name unknown and artist unknown. Used for invalid song ids. 143 | * 144 | * @param songID the id of the song 145 | */ 146 | void createUnknownDefault(int songID); 147 | 148 | /** 149 | * Fetches song data from Song File Hub for a songID 150 | * 151 | * @param songID the id of the song 152 | * @param callback a callback that takes a boolean as an argument, which is the status of the request 153 | */ 154 | void fetchSFH(int songID, std::function callback); 155 | 156 | /** 157 | * Downloads a song 158 | * 159 | * @param song the song data 160 | * @param progress a callback that receives the percentage of the download 161 | * @param failed a callback that fires if the download fails or is cancelled. it takes the song data, and the error 162 | */ 163 | void downloadSong(SongInfo const& song, std::function progress, std::function failed); 164 | 165 | /** 166 | * Returns the savefile path 167 | * 168 | * @return the path of the JSON 169 | */ 170 | fs::path getJsonPath(); 171 | 172 | static NongManager* get() { 173 | if (m_instance == nullptr) { 174 | m_instance = new NongManager; 175 | m_instance->retain(); 176 | } 177 | 178 | return m_instance; 179 | } 180 | }; -------------------------------------------------------------------------------- /src/manifest.cpp: -------------------------------------------------------------------------------- 1 | #include "manifest.hpp" 2 | 3 | namespace nongd { 4 | int getManifestVersion() { 5 | return 1; 6 | } 7 | } -------------------------------------------------------------------------------- /src/manifest.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace nongd { 4 | int getManifestVersion(); 5 | } -------------------------------------------------------------------------------- /src/random_string.cpp: -------------------------------------------------------------------------------- 1 | #include "random_string.hpp" 2 | 3 | namespace nongd { 4 | std::string random_string(size_t length) 5 | { 6 | auto randchar = []() -> char 7 | { 8 | const char charset[] = 9 | "0123456789" 10 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 11 | "abcdefghijklmnopqrstuvwxyz"; 12 | const size_t max_index = (sizeof(charset) - 1); 13 | return charset[ rand() % max_index ]; 14 | }; 15 | std::string str(length,0); 16 | std::generate_n(str.begin(), length, randchar); 17 | return str; 18 | } 19 | } -------------------------------------------------------------------------------- /src/random_string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nongd { 7 | /** 8 | * Gracefully stolen from https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c 9 | */ 10 | std::string random_string(size_t length); 11 | } 12 | -------------------------------------------------------------------------------- /src/trim.cpp: -------------------------------------------------------------------------------- 1 | #include "trim.hpp" 2 | 3 | namespace nongd { 4 | void left_trim(std::string &s) { 5 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { 6 | return !std::isspace(ch); 7 | })); 8 | } 9 | 10 | void right_trim(std::string &s) { 11 | s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { 12 | return !std::isspace(ch); 13 | }).base(), s.end()); 14 | } 15 | 16 | void trim(std::string &s) { 17 | right_trim(s); 18 | left_trim(s); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/trim.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nongd { 6 | void left_trim(std::string &s); 7 | void right_trim(std::string &s); 8 | void trim(std::string &s); 9 | } 10 | -------------------------------------------------------------------------------- /src/types/fetch_status.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace nongd { 4 | enum FetchStatus { 5 | SUCCESS, 6 | NOTHING_FOUND, 7 | FAILED 8 | }; 9 | } -------------------------------------------------------------------------------- /src/types/nong_list_type.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | enum NongListType { 4 | Single = 0, 5 | Multiple = 1 6 | }; -------------------------------------------------------------------------------- /src/types/nong_state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "song_info.hpp" 6 | #include "../manifest.hpp" 7 | 8 | struct NongState { 9 | int m_manifestVersion; 10 | std::map m_nongs; 11 | }; 12 | 13 | template<> 14 | struct matjson::Serialize { 15 | static NongState from_json(matjson::Value const& value) { 16 | NongState ret; 17 | ret.m_manifestVersion = value["version"].as_int(); 18 | auto nongs = value["nongs"].as_object(); 19 | for (auto const& kv : nongs) { 20 | int id = stoi(kv.first); 21 | NongData data = matjson::Serialize::from_json(kv.second); 22 | ret.m_nongs[id] = data; 23 | } 24 | 25 | return ret; 26 | } 27 | 28 | static matjson::Value to_json(NongState const& value) { 29 | auto ret = matjson::Object(); 30 | auto nongs = matjson::Object(); 31 | ret["version"] = nongd::getManifestVersion(); 32 | for (auto const& kv : value.m_nongs) { 33 | nongs[std::to_string(kv.first)] = matjson::Serialize::to_json(kv.second); 34 | } 35 | ret["nongs"] = nongs; 36 | return ret; 37 | } 38 | }; -------------------------------------------------------------------------------- /src/types/sfh_item.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct SFHItem { 6 | std::string songName; 7 | std::string downloadUrl; 8 | std::string levelName; 9 | std::string songURL = ""; 10 | }; -------------------------------------------------------------------------------- /src/types/song_info.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../filesystem.hpp" 7 | 8 | using namespace geode::prelude; 9 | 10 | struct SongInfo { 11 | fs::path path; 12 | std::string songName; 13 | std::string authorName; 14 | std::string songUrl; 15 | std::string levelName; 16 | }; 17 | 18 | struct NongData { 19 | fs::path active; 20 | fs::path defaultPath; 21 | std::vector songs; 22 | bool defaultValid; 23 | }; 24 | 25 | template<> 26 | struct matjson::Serialize { 27 | static NongData from_json(matjson::Value const& value) { 28 | std::vector songs; 29 | auto jsonSongs = value["songs"].as_array(); 30 | bool valid = true; 31 | if (value.contains("defaultValid")) { 32 | valid = value["defaultValid"].as_bool(); 33 | } 34 | 35 | for (auto jsonSong : jsonSongs) { 36 | std::string levelName = ""; 37 | if (jsonSong.contains("levelName")) { 38 | levelName = jsonSong["levelName"].as_string(); 39 | } 40 | auto path = fs::path(jsonSong["path"].as_string()); 41 | 42 | SongInfo song = { 43 | .path = path, 44 | .songName = jsonSong["songName"].as_string(), 45 | .authorName = jsonSong["authorName"].as_string(), 46 | .songUrl = jsonSong["songUrl"].as_string(), 47 | .levelName = levelName 48 | }; 49 | songs.push_back(song); 50 | } 51 | 52 | return NongData { 53 | .active = fs::path(value["active"].as_string()), 54 | .defaultPath = fs::path(value["defaultPath"].as_string()), 55 | .songs = songs, 56 | .defaultValid = valid 57 | }; 58 | } 59 | 60 | static matjson::Value to_json(NongData const& value) { 61 | auto ret = matjson::Object(); 62 | auto array = matjson::Array(); 63 | ret["active"] = value.active.string(); 64 | ret["defaultPath"] = value.defaultPath.string(); 65 | ret["defaultValid"] = value.defaultValid; 66 | for (auto song : value.songs) { 67 | auto obj = matjson::Object(); 68 | obj["path"] = song.path.string(); 69 | obj["songName"] = song.songName; 70 | obj["authorName"] = song.authorName; 71 | obj["songUrl"] = song.songUrl; 72 | obj["levelName"] = song.levelName; 73 | 74 | array.push_back(obj); 75 | } 76 | 77 | ret["songs"] = array; 78 | return ret; 79 | } 80 | }; -------------------------------------------------------------------------------- /src/ui/list_cell.cpp: -------------------------------------------------------------------------------- 1 | #include "list_cell.hpp" 2 | 3 | bool JBListCell::init(CCLayer* layer, CCSize const& size) { 4 | m_width = size.width; 5 | m_height = size.height; 6 | m_layer = layer; 7 | this->setContentSize(size); 8 | this->setID("nong-list-cell"); 9 | return true; 10 | } 11 | 12 | void JBListCell::draw() { 13 | reinterpret_cast(this)->StatsCell::draw(); 14 | } -------------------------------------------------------------------------------- /src/ui/list_cell.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using namespace geode::prelude; 6 | 7 | class JBListCell : public CCLayer, public FLAlertLayerProtocol { 8 | protected: 9 | float m_width; 10 | float m_height; 11 | CCLayer* m_layer; 12 | 13 | bool init(CCLayer* layer, CCSize const& size); 14 | void draw() override; 15 | }; 16 | -------------------------------------------------------------------------------- /src/ui/nong_add_popup.cpp: -------------------------------------------------------------------------------- 1 | #include "nong_add_popup.hpp" 2 | 3 | bool NongAddPopup::setup(NongDropdownLayer* parent) { 4 | this->setTitle("Add NONG"); 5 | m_parentPopup = parent; 6 | 7 | auto center = CCDirector::sharedDirector()->getWinSize() / 2; 8 | 9 | m_selectSongButton = CCMenuItemSpriteExtra::create( 10 | ButtonSprite::create("Select mp3..."), 11 | this, 12 | menu_selector(NongAddPopup::openFile) 13 | ); 14 | m_selectSongMenu = CCMenu::create(); 15 | m_selectSongMenu->setID("select-file-menu"); 16 | m_selectSongButton->setID("select-file-button"); 17 | m_selectSongMenu->addChild(this->m_selectSongButton); 18 | m_selectSongMenu->setPosition(center.width, center.height + this->getPopupSize().height / 2 - 75.f); 19 | 20 | m_addSongButton = CCMenuItemSpriteExtra::create( 21 | ButtonSprite::create("Add"), 22 | this, 23 | menu_selector(NongAddPopup::addSong) 24 | ); 25 | m_addSongMenu = CCMenu::create(); 26 | m_addSongMenu->setID("add-song-menu"); 27 | m_addSongButton->setID("add-song-button"); 28 | m_addSongMenu->addChild(this->m_addSongButton); 29 | m_addSongMenu->setPosition(center.width, center.height - this->getPopupSize().height / 2 + 25.f); 30 | 31 | m_containerLayer = CCLayer::create(); 32 | 33 | m_containerLayer->addChild(this->m_selectSongMenu); 34 | m_containerLayer->addChild(this->m_addSongMenu); 35 | m_mainLayer->addChild(this->m_containerLayer); 36 | this->createInputs(); 37 | 38 | return true; 39 | } 40 | 41 | NongAddPopup* NongAddPopup::create(NongDropdownLayer* parent) { 42 | auto ret = new NongAddPopup(); 43 | auto size = ret->getPopupSize(); 44 | if (ret && ret->init(size.width, size.height, parent)) { 45 | ret->autorelease(); 46 | return ret; 47 | } 48 | CC_SAFE_DELETE(ret); 49 | return nullptr; 50 | } 51 | 52 | CCSize NongAddPopup::getPopupSize() { 53 | return { 320.f, 240.f }; 54 | } 55 | 56 | void NongAddPopup::openFile(CCObject* target) { 57 | file::FilePickOptions options = { 58 | std::nullopt, 59 | {} 60 | }; 61 | 62 | file::pickFile(file::PickMode::OpenFile , options, [this](ghc::filesystem::path result) { 63 | auto path = fs::path(result.c_str()); 64 | m_songPath = path; 65 | }, []() { 66 | FLAlertLayer::create("Error", "Failed to open file", "Ok")->show(); 67 | }); 68 | } 69 | 70 | void NongAddPopup::createInputs() { 71 | auto centered = CCDirector::sharedDirector()->getWinSize() / 2; 72 | auto bgSprite = CCScale9Sprite::create( 73 | "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } 74 | ); 75 | bgSprite->setScale(0.7f); 76 | bgSprite->setColor({ 0, 0, 0 }); 77 | bgSprite->setOpacity(75); 78 | bgSprite->setPosition(centered); 79 | bgSprite->setContentSize({ 409.f, 54.f }); 80 | bgSprite->setID("song-name-bg"); 81 | 82 | m_songNameInput = CCTextInputNode::create(250.f, 30.f, "Song name", "bigFont.fnt"); 83 | m_songNameInput->setID("song-name-input"); 84 | m_songNameInput->setPosition(centered); 85 | m_songNameInput->ignoreAnchorPointForPosition(true); 86 | m_songNameInput->m_textField->setAnchorPoint({ 0.5f, 0.5f }); 87 | m_songNameInput->m_placeholderLabel->setAnchorPoint({ 0.5f, 0.5f }); 88 | m_songNameInput->setMaxLabelScale(0.7f); 89 | m_songNameInput->setLabelPlaceholderColor(ccc3(108, 153, 216)); 90 | m_songNameInput->setMouseEnabled(true); 91 | m_songNameInput->setTouchEnabled(true); 92 | 93 | m_containerLayer->addChild(bgSprite); 94 | m_containerLayer->addChild(this->m_songNameInput); 95 | 96 | m_artistNameInput = CCTextInputNode::create(250.f, 30.f, "Artist name", "bigFont.fnt"); 97 | m_artistNameInput->setID("artist-name-input"); 98 | m_artistNameInput->setPosition(centered.width, centered.height - 50.f); 99 | m_artistNameInput->ignoreAnchorPointForPosition(true); 100 | m_artistNameInput->setMaxLabelScale(0.7f); 101 | m_artistNameInput->m_textField->setAnchorPoint({ 0.5f, 0.5f }); 102 | m_artistNameInput->m_placeholderLabel->setAnchorPoint({ 0.5f, 0.5f }); 103 | m_artistNameInput->setLabelPlaceholderColor(ccc3(108, 153, 216)); 104 | 105 | auto bgSprite_artist = CCScale9Sprite::create( 106 | "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } 107 | ); 108 | bgSprite_artist->setScale(0.7f); 109 | bgSprite_artist->setColor({ 0, 0, 0 }); 110 | bgSprite_artist->setOpacity(75); 111 | bgSprite_artist->setPosition(centered.width, centered.height - 50.f); 112 | bgSprite_artist->setContentSize({ 409.f, 54.f }); 113 | bgSprite_artist->setID("artist-name-bg"); 114 | 115 | m_containerLayer->addChild(bgSprite_artist); 116 | m_containerLayer->addChild(this->m_artistNameInput); 117 | } 118 | 119 | void NongAddPopup::addSong(CCObject* target) { 120 | auto artistName = std::string(m_artistNameInput->getString()); 121 | auto songName = std::string(m_songNameInput->getString()); 122 | if (m_songPath.empty()) { 123 | FLAlertLayer::create("Error", "No file selected.", "Ok")->show(); 124 | return; 125 | } 126 | if (!fs::exists(m_songPath)) { 127 | std::stringstream ss; 128 | ss << "The selected file (" << m_songPath.string() << ") does not exist."; 129 | FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); 130 | return; 131 | } 132 | 133 | if (fs::is_directory(m_songPath)) { 134 | FLAlertLayer::create("Error", "You selected a directory.", "Ok")->show(); 135 | return; 136 | } 137 | 138 | if (m_songPath.extension().string() != ".mp3") { 139 | FLAlertLayer::create("Error", "The selected file must be an MP3.", "Ok")->show(); 140 | return; 141 | } 142 | 143 | if (songName == "") { 144 | FLAlertLayer::create("Error", "Song name is empty.", "Ok")->show(); 145 | return; 146 | } 147 | 148 | if (artistName == "") { 149 | FLAlertLayer::create("Error", "Artist name is empty.", "Ok")->show(); 150 | return; 151 | } 152 | 153 | auto unique = nongd::random_string(16); 154 | auto destination = fs::path(Mod::get()->getSaveDir().c_str()) / "nongs"; 155 | if (!fs::exists(destination)) { 156 | fs::create_directory(destination); 157 | } 158 | unique += ".mp3"; 159 | destination = destination / unique; 160 | bool result; 161 | try { 162 | result = fs::copy_file(m_songPath, destination); 163 | } catch (fs::filesystem_error e) { 164 | std::stringstream ss; 165 | ss << "Failed to save song. Please try again! Error: " << e.what(); 166 | FLAlertLayer::create("Error", ss.str().c_str(), "Ok")->show(); 167 | return; 168 | } 169 | if (!result) { 170 | FLAlertLayer::create("Error", "Failed to save song. Please try again!", "Ok")->show(); 171 | return; 172 | } 173 | 174 | SongInfo song = { 175 | .path = destination, 176 | .songName = songName, 177 | .authorName = artistName, 178 | .songUrl = "local", 179 | }; 180 | 181 | m_parentPopup->addSong(song); 182 | this->onClose(this); 183 | } -------------------------------------------------------------------------------- /src/ui/nong_add_popup.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../random_string.hpp" 6 | #include "nong_dropdown_layer.hpp" 7 | 8 | using namespace geode::prelude; 9 | 10 | class NongDropdownLayer; 11 | 12 | class NongAddPopup : public Popup { 13 | protected: 14 | NongDropdownLayer* m_parentPopup; 15 | CCMenuItemSpriteExtra* m_selectSongButton; 16 | CCMenuItemSpriteExtra* m_addSongButton; 17 | CCMenu* m_selectSongMenu; 18 | CCMenu* m_addSongMenu; 19 | CCLayer* m_containerLayer; 20 | 21 | CCTextInputNode* m_songNameInput; 22 | CCTextInputNode* m_artistNameInput; 23 | 24 | fs::path m_songPath; 25 | 26 | bool setup(NongDropdownLayer* parent) override; 27 | void createInputs(); 28 | 29 | CCSize getPopupSize(); 30 | void openFile(CCObject*); 31 | void addSong(CCObject*); 32 | public: 33 | static NongAddPopup* create(NongDropdownLayer* parent); 34 | }; 35 | -------------------------------------------------------------------------------- /src/ui/nong_cell.cpp: -------------------------------------------------------------------------------- 1 | #include "nong_cell.hpp" 2 | 3 | bool NongCell::init(SongInfo info, NongDropdownLayer* parentPopup, CCSize const& size, bool selected, bool isDefault) { 4 | if (!JBListCell::init(parentPopup, size)) return false; 5 | 6 | m_songInfo = info; 7 | m_parentPopup = parentPopup; 8 | 9 | CCMenuItemSpriteExtra* button; 10 | 11 | if (selected) { 12 | auto sprite = ButtonSprite::create("Set", "goldFont.fnt", "GJ_button_02.png"); 13 | sprite->setScale(0.7f); 14 | button = CCMenuItemSpriteExtra::create( 15 | sprite, 16 | this, 17 | // menu_selector(NongCell::onSet) 18 | nullptr 19 | ); 20 | } else { 21 | auto sprite = ButtonSprite::create("Set"); 22 | sprite->setScale(0.7f); 23 | button = CCMenuItemSpriteExtra::create( 24 | sprite, 25 | this, 26 | menu_selector(NongCell::onSet) 27 | ); 28 | } 29 | button->setAnchorPoint(ccp(0.5f, 0.5f)); 30 | button->setID("set-button"); 31 | 32 | auto menu = CCMenu::create(); 33 | menu->addChild(button); 34 | 35 | if (!isDefault) { 36 | auto sprite = CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"); 37 | sprite->setScale(0.7f); 38 | auto deleteButton = CCMenuItemSpriteExtra::create( 39 | sprite, 40 | this, 41 | menu_selector(NongCell::deleteSong) 42 | ); 43 | deleteButton->setID("delete-button"); 44 | menu->addChild(deleteButton); 45 | deleteButton->setPositionX(38.f); 46 | } 47 | 48 | menu->setAnchorPoint(ccp(0, 0)); 49 | menu->setPosition(ccp(267.f, 30.f)); 50 | menu->setID("button-menu"); 51 | this->addChild(menu); 52 | 53 | m_songInfoLayer = CCLayer::create(); 54 | 55 | if (!m_songInfo.levelName.empty()) { 56 | m_levelNameLabel = CCLabelBMFont::create(m_songInfo.levelName.c_str(), "bigFont.fnt"); 57 | m_levelNameLabel->limitLabelWidth(220.f, 0.4f, 0.1f); 58 | m_levelNameLabel->setColor(cc3x(0x00c9ff)); 59 | m_levelNameLabel->setID("level-name"); 60 | } 61 | 62 | m_songNameLabel = CCLabelBMFont::create(m_songInfo.songName.c_str(), "bigFont.fnt"); 63 | m_songNameLabel->limitLabelWidth(220.f, 0.7f, 0.1f); 64 | 65 | if (selected) { 66 | m_songNameLabel->setColor(ccc3(188, 254, 206)); 67 | } 68 | 69 | m_authorNameLabel = CCLabelBMFont::create(m_songInfo.authorName.c_str(), "goldFont.fnt"); 70 | m_authorNameLabel->limitLabelWidth(220.f, 0.7f, 0.1f); 71 | m_authorNameLabel->setID("author-name"); 72 | m_songNameLabel->setID("song-name"); 73 | 74 | if (m_levelNameLabel != nullptr) { 75 | m_songInfoLayer->addChild(m_levelNameLabel); 76 | } 77 | m_songInfoLayer->addChild(m_authorNameLabel); 78 | m_songInfoLayer->addChild(m_songNameLabel); 79 | m_songInfoLayer->setID("song-info"); 80 | auto layout = ColumnLayout::create(); 81 | layout->setAutoScale(false); 82 | if (m_levelNameLabel != nullptr) { 83 | layout->setAxisAlignment(AxisAlignment::Even); 84 | } else { 85 | layout->setAxisAlignment(AxisAlignment::Center); 86 | } 87 | layout->setCrossAxisLineAlignment(AxisAlignment::Start); 88 | m_songInfoLayer->setContentSize(ccp(240.f, this->getContentSize().height - 6.f)); 89 | m_songInfoLayer->setAnchorPoint(ccp(0.f, 0.f)); 90 | m_songInfoLayer->setPosition(ccp(12.f, 1.5f)); 91 | m_songInfoLayer->setLayout(layout); 92 | 93 | this->addChild(m_songInfoLayer); 94 | return true; 95 | } 96 | 97 | void NongCell::onSet(CCObject* target) { 98 | m_parentPopup->setActiveSong(this->m_songInfo); 99 | } 100 | 101 | void NongCell::deleteSong(CCObject* target) { 102 | FLAlertLayer::create(this, "Are you sure?", "Are you sure you want to delete " + this->m_songInfo.songName + " from your NONGs?", "No", "Yes")->show(); 103 | } 104 | 105 | NongCell* NongCell::create(SongInfo info, NongDropdownLayer* parentPopup, CCSize const& size, bool selected, bool isDefault) { 106 | auto ret = new NongCell(); 107 | if (ret && ret->init(info, parentPopup, size, selected, isDefault)) { 108 | return ret; 109 | } 110 | CC_SAFE_DELETE(ret); 111 | return nullptr; 112 | } 113 | 114 | void NongCell::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { 115 | if (btn2) { 116 | m_parentPopup->deleteSong(m_songInfo); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ui/nong_cell.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../types/song_info.hpp" 6 | #include "list_cell.hpp" 7 | #include "nong_dropdown_layer.hpp" 8 | 9 | using namespace geode::prelude; 10 | 11 | class NongDropdownLayer; 12 | 13 | class NongCell : public JBListCell { 14 | protected: 15 | SongInfo m_songInfo; 16 | CCLabelBMFont* m_songNameLabel; 17 | CCLabelBMFont* m_authorNameLabel; 18 | CCLabelBMFont* m_levelNameLabel = nullptr; 19 | CCLayer* m_songInfoLayer; 20 | 21 | NongDropdownLayer* m_parentPopup; 22 | 23 | bool init(SongInfo info, NongDropdownLayer* parentPopup, CCSize const& size, bool selected, bool isDefault); 24 | 25 | virtual void FLAlert_Clicked(FLAlertLayer*, bool btn2); 26 | public: 27 | static NongCell* create(SongInfo info, NongDropdownLayer* parentPopup, CCSize const& size, bool selected, bool isDefault); 28 | void onSet(CCObject*); 29 | void deleteSong(CCObject*); 30 | }; -------------------------------------------------------------------------------- /src/ui/nong_dropdown_layer.cpp: -------------------------------------------------------------------------------- 1 | #include "nong_dropdown_layer.hpp" 2 | 3 | bool NongDropdownLayer::setup(std::vector ids, CustomSongWidget* parent, int defaultSongID) { 4 | m_songIDS = ids; 5 | m_parentWidget = parent; 6 | m_defaultSongID = defaultSongID; 7 | for (auto const& id : m_songIDS) { 8 | auto result = NongManager::get()->getNongs(id); 9 | if (!result.has_value()) { 10 | NongManager::get()->createDefault(id); 11 | NongManager::get()->writeJson(); 12 | result = NongManager::get()->getNongs(id); 13 | } 14 | auto value = result.value(); 15 | m_data[id] = value; 16 | } 17 | if (ids.size() == 1) { 18 | m_currentListType = NongListType::Single; 19 | m_currentSongID = ids[0]; 20 | } else { 21 | m_currentListType = NongListType::Multiple; 22 | } 23 | auto winsize = CCDirector::sharedDirector()->getWinSize(); 24 | 25 | int manifest = NongManager::get()->getCurrentManifestVersion(); 26 | int count = NongManager::get()->getStoredIDCount(); 27 | std::stringstream ss; 28 | ss << "Manifest v" << manifest << ", storing " << count << " unique song IDs."; 29 | 30 | auto manifestLabel = CCLabelBMFont::create(ss.str().c_str(), "chatFont.fnt"); 31 | manifestLabel->setPosition(winsize.width / 2, winsize.height / 2 - 125.f); 32 | manifestLabel->limitLabelWidth(140.f, 0.9f, 0.1f); 33 | manifestLabel->setColor(cc3x(0xc2c2c2)); 34 | manifestLabel->setID("manifest-label"); 35 | m_mainLayer->addChild(manifestLabel); 36 | 37 | auto spr = CCSprite::createWithSpriteFrameName("GJ_downloadBtn_001.png"); 38 | spr->setScale(0.7f); 39 | auto menu = CCMenu::create(); 40 | menu->setID("bottom-right-menu"); 41 | auto downloadBtn = CCMenuItemSpriteExtra::create( 42 | spr, 43 | this, 44 | menu_selector(NongDropdownLayer::fetchSongFileHub) 45 | ); 46 | m_downloadBtn = downloadBtn; 47 | downloadBtn->setPositionY(35.f); 48 | spr = CCSprite::createWithSpriteFrameName("GJ_plusBtn_001.png"); 49 | spr->setScale(0.7f); 50 | auto addBtn = CCMenuItemSpriteExtra::create( 51 | spr, 52 | this, 53 | menu_selector(NongDropdownLayer::openAddPopup) 54 | ); 55 | m_addBtn = addBtn; 56 | spr = CCSprite::createWithSpriteFrameName("GJ_trashBtn_001.png"); 57 | spr->setScale(0.7f); 58 | auto removeBtn = CCMenuItemSpriteExtra::create( 59 | spr, 60 | this, 61 | menu_selector(NongDropdownLayer::deleteAllNongs) 62 | ); 63 | removeBtn->setPositionY(67.f); 64 | m_deleteBtn = removeBtn; 65 | spr = CCSprite::createWithSpriteFrameName("backArrowPlain_01_001.png"); 66 | auto backBtn = CCMenuItemSpriteExtra::create( 67 | spr, 68 | this, 69 | menu_selector(NongDropdownLayer::onBack) 70 | ); 71 | m_backBtn = backBtn; 72 | backBtn->setPosition(ccp(-370.f, 100.f)); 73 | 74 | if (m_currentListType == NongListType::Multiple) { 75 | m_addBtn->setVisible(false); 76 | m_deleteBtn->setVisible(false); 77 | m_downloadBtn->setVisible(false); 78 | m_backBtn->setVisible(false); 79 | } 80 | if (m_data.size() == 1) { 81 | m_backBtn->setVisible(false); 82 | } 83 | menu->addChild(backBtn); 84 | menu->addChild(addBtn); 85 | menu->addChild(downloadBtn); 86 | menu->addChild(removeBtn); 87 | menu->setPosition(winsize.width / 2 + 185.f, winsize.height / 2 - 105.f); 88 | m_mainLayer->addChild(menu); 89 | 90 | menu = CCMenu::create(); 91 | menu->setID("settings-menu"); 92 | auto sprite = CCSprite::createWithSpriteFrameName("GJ_optionsBtn_001.png"); 93 | sprite->setScale(0.8f); 94 | auto settingsButton = CCMenuItemSpriteExtra::create( 95 | sprite, 96 | this, 97 | menu_selector(NongDropdownLayer::onSettings) 98 | ); 99 | settingsButton->setID("settings-button"); 100 | menu->addChild(settingsButton); 101 | menu->setPosition(winsize.width - 30.f, winsize.height - 31.f); 102 | m_mainLayer->addChild(menu); 103 | 104 | this->createList(); 105 | auto listpos = m_listView->getPosition(); 106 | auto leftspr = CCSprite::createWithSpriteFrameName("GJ_commentSide2_001.png"); 107 | leftspr->setPosition(ccp(listpos.x - 162.f, listpos.y)); 108 | leftspr->setScaleY(6.8f); 109 | leftspr->setZOrder(1); 110 | m_mainLayer->addChild(leftspr); 111 | auto rightspr = CCSprite::createWithSpriteFrameName("GJ_commentSide2_001.png"); 112 | rightspr->setPosition(ccp(listpos.x + 162.f, listpos.y)); 113 | rightspr->setScaleY(6.8f); 114 | rightspr->setFlipX(true); 115 | rightspr->setZOrder(1); 116 | m_mainLayer->addChild(rightspr); 117 | auto bottomspr = CCSprite::createWithSpriteFrameName("GJ_commentTop2_001.png"); 118 | bottomspr->setPosition(ccp(listpos.x, listpos.y - 95.f)); 119 | bottomspr->setFlipY(true); 120 | bottomspr->setScaleX(0.934f); 121 | bottomspr->setZOrder(1); 122 | m_mainLayer->addChild(bottomspr); 123 | auto topspr = CCSprite::createWithSpriteFrameName("GJ_commentTop2_001.png"); 124 | topspr->setPosition(ccp(listpos.x, listpos.y + 95.f)); 125 | topspr->setScaleX(0.934f); 126 | topspr->setZOrder(1); 127 | m_mainLayer->addChild(topspr); 128 | auto title = CCSprite::create("JB_ListLogo.png"_spr); 129 | title->setPosition(ccp(winsize.width / 2, winsize.height / 2 + 125.f)); 130 | title->setScale(0.75f); 131 | m_mainLayer->addChild(title); 132 | handleTouchPriority(this); 133 | return true; 134 | } 135 | 136 | void NongDropdownLayer::onSettings(CCObject* sender) { 137 | geode::openSettingsPopup(Mod::get()); 138 | } 139 | 140 | void NongDropdownLayer::onSelectSong(int songID) { 141 | if (m_currentListType == NongListType::Single) { 142 | return; 143 | } 144 | 145 | m_currentSongID = songID; 146 | m_currentListType = NongListType::Single; 147 | this->createList(); 148 | m_addBtn->setVisible(true); 149 | m_deleteBtn->setVisible(true); 150 | m_downloadBtn->setVisible(true); 151 | if (m_data.size() > 1) { 152 | m_backBtn->setVisible(true); 153 | } 154 | } 155 | 156 | void NongDropdownLayer::onBack(CCObject*) { 157 | if (m_currentListType == NongListType::Multiple || m_data.size() == 1) { 158 | return; 159 | } 160 | 161 | m_currentSongID = -1; 162 | m_currentListType = NongListType::Multiple; 163 | this->createList(); 164 | m_addBtn->setVisible(false); 165 | m_deleteBtn->setVisible(false); 166 | m_downloadBtn->setVisible(false); 167 | m_backBtn->setVisible(false); 168 | } 169 | 170 | void NongDropdownLayer::openAddPopup(CCObject* target) { 171 | // #ifdef GEODE_IS_ANDROID 172 | // FLAlertLayer::create("Unavailable", "Adding songs manually is temporarily unavailable on Android.", "Ok")->show(); 173 | // #else 174 | NongAddPopup::create(this)->show(); 175 | // #endif 176 | } 177 | 178 | void NongDropdownLayer::createList() { 179 | switch (m_currentListType) { 180 | case NongListType::Single: { 181 | auto songs = CCArray::create(); 182 | auto activeSong = this->getActiveSong(); 183 | NongData songData = m_data[m_currentSongID]; 184 | 185 | songs->addObject(NongCell::create(activeSong, this, this->getCellSize(), true, activeSong.path == songData.defaultPath)); 186 | 187 | for (auto song : songData.songs) { 188 | if (songData.active == song.path) { 189 | continue; 190 | } 191 | songs->addObject(NongCell::create(song, this, this->getCellSize(), false, song.path == songData.defaultPath)); 192 | } 193 | if (m_listView) { 194 | m_listView->removeFromParent(); 195 | } 196 | 197 | auto list = ListView::create(songs, this->getCellSize().height, this->getCellSize().width, 200.f); 198 | m_mainLayer->addChild(list); 199 | auto winsize = CCDirector::sharedDirector()->getWinSize(); 200 | list->setPosition(winsize.width / 2, winsize.height / 2 - 15.f); 201 | list->ignoreAnchorPointForPosition(false); 202 | m_listView = list; 203 | break; 204 | } 205 | case NongListType::Multiple: { 206 | auto cells = CCArray::create(); 207 | for (auto const& kv : m_data) { 208 | cells->addObject(JBSongCell::create(kv.second, kv.first, this, this->getCellSize())); 209 | } 210 | if (m_listView) { 211 | m_listView->removeFromParent(); 212 | } 213 | 214 | ListView* list = ListView::create(cells, this->getCellSize().height, this->getCellSize().width, 200.f); 215 | m_mainLayer->addChild(list); 216 | auto winsize = CCDirector::sharedDirector()->getWinSize(); 217 | list->setPosition(winsize.width / 2, winsize.height / 2 - 15.f); 218 | list->ignoreAnchorPointForPosition(false); 219 | m_listView = list; 220 | break; 221 | } 222 | } 223 | handleTouchPriority(this); 224 | } 225 | 226 | SongInfo NongDropdownLayer::getActiveSong() { 227 | auto active = NongManager::get()->getActiveNong(m_currentSongID); 228 | if (!active.has_value()) { 229 | m_data[m_currentSongID].active = m_data[m_currentSongID].defaultPath; 230 | NongManager::get()->saveNongs(m_data[m_currentSongID], m_currentSongID); 231 | return NongManager::get()->getActiveNong(m_currentSongID).value(); 232 | } 233 | return active.value(); 234 | } 235 | 236 | CCSize NongDropdownLayer::getCellSize() const { 237 | return { 238 | 320.f, 239 | 60.f 240 | }; 241 | } 242 | 243 | void NongDropdownLayer::setActiveSong(SongInfo const& song) { 244 | auto songs = m_data[m_currentSongID]; 245 | if ( 246 | !fs::exists(song.path) && 247 | song.path != songs.defaultPath && 248 | song.songUrl != "local" 249 | ) { 250 | auto loading = LoadingCircle::create(); 251 | loading->setParentLayer(this); 252 | loading->setFade(true); 253 | loading->show(); 254 | m_fetching = true; 255 | NongManager::get()->downloadSong(song, [this, song, loading](double progress) { 256 | if (progress == 100.f) { 257 | m_fetching = false; 258 | this->updateParentWidget(song); 259 | loading->fadeAndRemove(); 260 | } 261 | }, 262 | [this, loading, songs](SongInfo const& song, std::string const& error) { 263 | loading->fadeAndRemove(); 264 | m_fetching = false; 265 | FLAlertLayer::create("Failed", "Failed to download song", "Ok")->show(); 266 | 267 | for (auto song : songs.songs) { 268 | if (song.path == songs.defaultPath) { 269 | this->setActiveSong(song); 270 | } 271 | } 272 | }); 273 | } 274 | 275 | m_data[m_currentSongID].active = song.path; 276 | 277 | NongManager::get()->saveNongs(m_data[m_currentSongID], m_currentSongID); 278 | 279 | this->updateParentWidget(song); 280 | 281 | this->createList(); 282 | } 283 | 284 | void NongDropdownLayer::updateParentWidget(SongInfo const& song) { 285 | m_parentWidget->m_songInfoObject->m_artistName = song.authorName; 286 | m_parentWidget->m_songInfoObject->m_songName = song.songName; 287 | if (song.songUrl != "local") { 288 | m_parentWidget->m_songInfoObject->m_songUrl = song.songUrl; 289 | } 290 | m_parentWidget->updateSongObject(this->m_parentWidget->m_songInfoObject); 291 | } 292 | 293 | void NongDropdownLayer::deleteSong(SongInfo const& song) { 294 | NongManager::get()->deleteNong(song, m_currentSongID); 295 | auto active = NongManager::get()->getActiveNong(m_currentSongID).value(); 296 | this->updateParentWidget(active); 297 | FLAlertLayer::create("Success", "The song was deleted!", "Ok")->show(); 298 | m_data[m_currentSongID] = NongManager::get()->getNongs(m_currentSongID).value(); 299 | this->createList(); 300 | } 301 | 302 | void NongDropdownLayer::addSong(SongInfo const& song) { 303 | auto data = m_data[m_currentSongID]; 304 | for (auto savedSong : data.songs) { 305 | if (song.path.string() == savedSong.path.string()) { 306 | FLAlertLayer::create("Error", "This NONG already exists! (" + savedSong.songName + ")", "Ok")->show(); 307 | return; 308 | } 309 | } 310 | NongManager::get()->addNong(song, m_currentSongID); 311 | this->updateParentWidget(song); 312 | FLAlertLayer::create("Success", "The song was added!", "Ok")->show(); 313 | m_data[m_currentSongID] = NongManager::get()->getNongs(m_currentSongID).value(); 314 | this->createList(); 315 | } 316 | 317 | void NongDropdownLayer::onSFHFetched(nongd::FetchStatus result) { 318 | switch (result) { 319 | case nongd::FetchStatus::SUCCESS: 320 | FLAlertLayer::create("Success", "The Song File Hub data was fetched successfully!", "Ok")->show(); 321 | m_data[m_currentSongID] = NongManager::get()->getNongs(m_currentSongID).value(); 322 | this->createList(); 323 | break; 324 | case nongd::FetchStatus::NOTHING_FOUND: 325 | FLAlertLayer::create("Failed", "Found no data for this song!", "Ok")->show(); 326 | break; 327 | case nongd::FetchStatus::FAILED: 328 | FLAlertLayer::create("Failed", "Failed to fetch data from Song File Hub!", "Ok")->show(); 329 | break; 330 | } 331 | } 332 | 333 | void NongDropdownLayer::fetchSongFileHub(CCObject*) { 334 | if (m_currentListType == NongListType::Multiple) { 335 | return; 336 | } 337 | createQuickPopup( 338 | "Fetch SFH", 339 | "Do you want to fetch Song File Hub content for " + std::to_string(m_currentSongID) + "?", 340 | "No", "Yes", 341 | [this](auto, bool btn2) { 342 | if (btn2) { 343 | auto loading = LoadingCircle::create(); 344 | loading->setParentLayer(this); 345 | loading->setFade(true); 346 | loading->show(); 347 | m_fetching = true; 348 | NongManager::get()->fetchSFH(m_currentSongID, [this, loading](nongd::FetchStatus result) { 349 | this->onSFHFetched(result); 350 | m_fetching = false; 351 | loading->fadeAndRemove(); 352 | }); 353 | } 354 | } 355 | ); 356 | } 357 | 358 | void NongDropdownLayer::deleteAllNongs(CCObject*) { 359 | createQuickPopup("Delete all nongs", 360 | "Are you sure you want to delete all nongs for this song?", 361 | "No", 362 | "Yes", 363 | [this](auto, bool btn2) { 364 | if (!btn2) { 365 | return; 366 | } 367 | 368 | NongManager::get()->deleteAll(m_currentSongID); 369 | auto data = NongManager::get()->getNongs(m_currentSongID).value(); 370 | m_data[m_currentSongID] = data; 371 | this->updateParentWidget(this->getActiveSong()); 372 | this->createList(); 373 | FLAlertLayer::create("Success", "All nongs were deleted successfully!", "Ok")->show(); 374 | } 375 | ); 376 | } -------------------------------------------------------------------------------- /src/ui/nong_dropdown_layer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../types/song_info.hpp" 7 | #include "../types/fetch_status.hpp" 8 | #include "../types/nong_list_type.hpp" 9 | #include "../managers/nong_manager.hpp" 10 | #include "nong_add_popup.hpp" 11 | #include "nong_cell.hpp" 12 | #include "song_cell.hpp" 13 | #include "Geode/binding/FLAlertLayer.hpp" 14 | #include "Geode/cocos/label_nodes/CCLabelBMFont.h" 15 | #include "Geode/utils/cocos.hpp" 16 | #include 17 | 18 | using namespace geode::prelude; 19 | 20 | class NongDropdownLayer : public Popup, CustomSongWidget*, int> { 21 | protected: 22 | std::map m_data; 23 | std::vector m_songIDS; 24 | int m_currentSongID = -1; 25 | int m_defaultSongID; 26 | Ref m_parentWidget; 27 | ListView* m_listView = nullptr; 28 | NongListType m_currentListType = NongListType::Single; 29 | 30 | CCMenuItemSpriteExtra* m_downloadBtn = nullptr; 31 | CCMenuItemSpriteExtra* m_addBtn = nullptr; 32 | CCMenuItemSpriteExtra* m_deleteBtn = nullptr; 33 | CCMenuItemSpriteExtra* m_backBtn = nullptr; 34 | 35 | bool m_fetching = false; 36 | 37 | bool setup(std::vector ids, CustomSongWidget* parent, int defaultSongID) override; 38 | void createList(); 39 | SongInfo getActiveSong(); 40 | CCSize getCellSize() const; 41 | void deleteAllNongs(CCObject*); 42 | void fetchSongFileHub(CCObject*); 43 | void onSFHFetched(nongd::FetchStatus result); 44 | void onSettings(CCObject*); 45 | void openAddPopup(CCObject*); 46 | public: 47 | void onSelectSong(int songID); 48 | void onBack(CCObject*); 49 | int getSongID(); 50 | void setActiveSong(SongInfo const& song); 51 | void deleteSong(SongInfo const& song); 52 | void addSong(SongInfo const& song); 53 | void updateParentWidget(SongInfo const& song); 54 | 55 | static NongDropdownLayer* create(std::vector ids, CustomSongWidget* parent, int defaultSongID) { 56 | auto ret = new NongDropdownLayer; 57 | if (ret && ret->init(420.f, 280.f, ids, parent, defaultSongID, "GJ_square02.png")) { 58 | ret->autorelease(); 59 | return ret; 60 | } 61 | 62 | CC_SAFE_DELETE(ret); 63 | return nullptr; 64 | } 65 | }; -------------------------------------------------------------------------------- /src/ui/song_cell.cpp: -------------------------------------------------------------------------------- 1 | #include "song_cell.hpp" 2 | 3 | bool JBSongCell::init(NongData data, int id, NongDropdownLayer* parentPopup, CCSize const& size) { 4 | if (!JBListCell::init(parentPopup, size)) return false; 5 | m_parentPopup = parentPopup; 6 | m_songID = id; 7 | for (auto const& song : data.songs) { 8 | if (song.path == data.active) { 9 | m_active = song; 10 | } 11 | } 12 | auto label = CCLabelBMFont::create(m_active.songName.c_str(), "bigFont.fnt"); 13 | label->setAnchorPoint(ccp(0, 0.5f)); 14 | label->limitLabelWidth(240.f, 0.8f, 0.1f); 15 | label->setPosition(ccp(12.f, 40.f)); 16 | this->addChild(label); 17 | m_songNameLabel = label; 18 | auto author = CCLabelBMFont::create(m_active.authorName.c_str(), "goldFont.fnt"); 19 | author->setAnchorPoint(ccp(0, 0.5f)); 20 | author->limitLabelWidth(260.f, 0.6f, 0.1f); 21 | author->setPosition(ccp(12.f, 15.f)); 22 | m_authorNameLabel = author; 23 | this->addChild(author); 24 | auto menu = CCMenu::create(); 25 | auto spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); 26 | spr->setFlipX(true); 27 | spr->setScale(0.8f); 28 | auto btn = CCMenuItemSpriteExtra::create( 29 | spr, 30 | this, 31 | menu_selector(JBSongCell::onSelectSong) 32 | ); 33 | menu->addChild(btn); 34 | this->addChild(menu); 35 | menu->setPosition(ccp(290.f, 30.f)); 36 | return true; 37 | } 38 | 39 | void JBSongCell::onSelectSong(CCObject*) { 40 | m_parentPopup->onSelectSong(m_songID); 41 | } -------------------------------------------------------------------------------- /src/ui/song_cell.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../types/song_info.hpp" 6 | #include "list_cell.hpp" 7 | #include "nong_dropdown_layer.hpp" 8 | 9 | using namespace geode::prelude; 10 | 11 | class NongDropdownLayer; 12 | 13 | class JBSongCell : public JBListCell { 14 | protected: 15 | SongInfo m_active; 16 | CCLabelBMFont* m_songNameLabel; 17 | CCLabelBMFont* m_authorNameLabel; 18 | int m_songID; 19 | // CCLayer* m_songInfoLayer; 20 | 21 | NongDropdownLayer* m_parentPopup; 22 | 23 | bool init(NongData data, int id, NongDropdownLayer* parentPopup, CCSize const& size); 24 | public: 25 | static JBSongCell* create(NongData data, int id, NongDropdownLayer* parentPopup, CCSize const& size) { 26 | auto ret = new JBSongCell(); 27 | if (ret && ret->init(data, id, parentPopup, size)) { 28 | return ret; 29 | } 30 | CC_SAFE_DELETE(ret); 31 | return nullptr; 32 | } 33 | void onSelectSong(CCObject*); 34 | }; --------------------------------------------------------------------------------