diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..301cd8c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +name: Build Geode Mod + +on: + workflow_dispatch: + push: + branches: + - "master" + +jobs: + build: + strategy: + fail-fast: false + matrix: + config: + - name: Windows + os: windows-latest + + - name: macOS + os: macos-latest + + - name: Android32 + os: ubuntu-latest + target: Android32 + + - name: Android64 + os: ubuntu-latest + target: Android64 + + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Build the mod + uses: geode-sdk/build-geode-mod@main + with: + build-config: RelWithDebInfo + export-pdb: true + combine: true + target: ${{ matrix.config.target }} + + package: + name: Package builds + runs-on: ubuntu-latest + needs: ['build'] + + steps: + - uses: geode-sdk/build-geode-mod/combine@main + id: build + + - uses: actions/upload-artifact@v4 + with: + name: Build Output + path: ${{ steps.build.outputs.build-output }} + + release: + name: Release the mod + runs-on: ubuntu-latest + needs: ['package'] + + steps: + - uses: actions/checkout@v4 + + - uses: hiimjustin000/release-geode-mod@main + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8babefa --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Macos be like +**/.DS_Store + +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Ignore build folders +**/build +# Ignore platform specific build folders +build-*/ + +# Workspace files are user-specific +*.sublime-workspace + +# ILY vscode +**/.vscode +.idea/ + +# clangd +.cache/ + +# Visual Studio +.vs/ + +# Bruh +logo.pdn diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2c7e932 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.21) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") +set(CMAKE_CXX_VISIBILITY_PRESET hidden) + +project(SearchHistory VERSION 1.0.0) + +add_library(${PROJECT_NAME} SHARED + src/main.cpp + src/SearchHistory.cpp + src/SearchHistoryNode.cpp + src/SearchHistoryPopup.cpp +) + +if (NOT DEFINED ENV{GEODE_SDK}) + message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") +else() + message(STATUS "Found Geode: $ENV{GEODE_SDK}") +endif() + +add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) + +setup_geode_mod(${PROJECT_NAME}) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1828c58 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 hiimjustin000 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a39c891 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Search History +A mod that allows you to view your search history. + +## Features +- A button in the level search menu that allows you to view your search history +- A popup that shows your search history, including a filter text box, and entries that can be used to search again + +## Credits +- [Kabslantivity](https://gdbrowser.com/u/17597362) - Initial idea for the mod +- [at4pm](https://gdbrowser.com/u/27791517) - Additional ideas for the mod +- [hiimjustin000](https://gdbrowser.com/u/7466002) - Creator of the mod + +## Gallery +![Search History Popup 1](./resources/image1.png) +![Search History Popup 2](./resources/image2.png) + +# License +This mod is licensed under the [MIT License](./LICENSE). \ No newline at end of file diff --git a/about.md b/about.md new file mode 100644 index 0000000..f9b48b0 --- /dev/null +++ b/about.md @@ -0,0 +1,15 @@ +# Search History +A mod that allows you to view your search history. + +## Features +- A button in the level search menu that allows you to view your search history +- A popup that shows your search history, including a filter text box, and entries that can be used to search again + +## Credits +- [Kabslantivity](user:17597362) - Initial idea for the mod +- [at4pm](user:27791517) - Additional ideas for the mod +- [hiimjustin000](user:7466002) - Creator of the mod + +## Gallery +![Search History Popup 1](hiimjustin000.search_history/image1.png?scale=0.6)\ +![Search History Popup 2](hiimjustin000.search_history/image2.png?scale=0.6) \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e80a1cc --- /dev/null +++ b/changelog.md @@ -0,0 +1,3 @@ +# Search History Changelog +## v1.0.0 (2024-08-07) +- Initial release \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..33272af Binary files /dev/null and b/logo.png differ diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..f5dfe8e --- /dev/null +++ b/mod.json @@ -0,0 +1,51 @@ +{ + "geode": "3.4.0", + "gd": { + "android": "2.206", + "win": "2.206", + "mac": "2.206" + }, + "version": "v1.0.0", + "id": "hiimjustin000.search_history", + "name": "Search History", + "developer": "hiimjustin000", + "description": "A mod that allows you to view your search history.", + "repository": "https://github.com/hiimjustin000/SearchHistory", + "dependencies": [ + { + "id": "geode.node-ids", + "version": ">=v1.12.0", + "importance": "required" + } + ], + "resources": { + "sprites": [ + "resources/*.png" + ], + "spritesheets": { + "HistorySheet": [ + "resources/HistorySheet/*.png" + ] + } + }, + "settings": { + "12-hour-time": { + "name": "12-Hour Time", + "description": "Whether or not to use 12-hour time on the time label.", + "type": "bool", + "default": false + }, + "white-time": { + "name": "White Time", + "description": "Whether or not to color the time label white.", + "type": "bool", + "default": false + } + }, + "tags": [ + "enhancement", + "interface", + "offline", + "utility" + ] +} diff --git a/resources/HistorySheet/SH_epicCoin2_001.png b/resources/HistorySheet/SH_epicCoin2_001.png new file mode 100644 index 0000000..67bc89e Binary files /dev/null and b/resources/HistorySheet/SH_epicCoin2_001.png differ diff --git a/resources/HistorySheet/SH_epicCoin3_001.png b/resources/HistorySheet/SH_epicCoin3_001.png new file mode 100644 index 0000000..06d07c7 Binary files /dev/null and b/resources/HistorySheet/SH_epicCoin3_001.png differ diff --git a/resources/HistorySheet/SH_epicCoin_001.png b/resources/HistorySheet/SH_epicCoin_001.png new file mode 100644 index 0000000..e796137 Binary files /dev/null and b/resources/HistorySheet/SH_epicCoin_001.png differ diff --git a/resources/HistorySheet/SH_featuredCoin_001.png b/resources/HistorySheet/SH_featuredCoin_001.png new file mode 100644 index 0000000..5d36bef Binary files /dev/null and b/resources/HistorySheet/SH_featuredCoin_001.png differ diff --git a/resources/HistorySheet/SH_originalIcon_001.png b/resources/HistorySheet/SH_originalIcon_001.png new file mode 100644 index 0000000..68420e0 Binary files /dev/null and b/resources/HistorySheet/SH_originalIcon_001.png differ diff --git a/resources/HistorySheet/SH_twoPlayerBtn_001.png b/resources/HistorySheet/SH_twoPlayerBtn_001.png new file mode 100644 index 0000000..1fc010e Binary files /dev/null and b/resources/HistorySheet/SH_twoPlayerBtn_001.png differ diff --git a/resources/SH_historyBtn_001.png b/resources/SH_historyBtn_001.png new file mode 100644 index 0000000..50e95e9 Binary files /dev/null and b/resources/SH_historyBtn_001.png differ diff --git a/resources/image1.png b/resources/image1.png new file mode 100644 index 0000000..12e4cae Binary files /dev/null and b/resources/image1.png differ diff --git a/resources/image2.png b/resources/image2.png new file mode 100644 index 0000000..49d7aeb Binary files /dev/null and b/resources/image2.png differ diff --git a/src/SearchHistory.cpp b/src/SearchHistory.cpp new file mode 100644 index 0000000..eb10576 --- /dev/null +++ b/src/SearchHistory.cpp @@ -0,0 +1,56 @@ +#include "SearchHistory.hpp" + +void SearchHistory::add(GJSearchObject* search, time_t time, int type) { + auto history = Mod::get()->getSavedValue>("search-history"); + + auto difficultyStrings = search->m_difficulty != "-" ? string::split(search->m_difficulty, ",") : std::vector(); + auto difficulties = std::vector(); + for (auto const& str : difficultyStrings) { + difficulties.push_back(std::stoi(str)); + } + + auto lengthStrings = search->m_length != "-" ? string::split(search->m_length, ",") : std::vector(); + auto lengths = std::vector(); + for (auto const& str : lengthStrings) { + lengths.push_back(std::stoi(str)); + } + + history.insert(history.begin(), { + .time = time, + .type = type, + .query = search->m_searchQuery, + .difficulties = difficulties, + .lengths = lengths, + .uncompleted = search->m_uncompletedFilter, + .completed = search->m_completedFilter, + .featured = search->m_featuredFilter, + .original = search->m_originalFilter, + .twoPlayer = search->m_twoPlayerFilter, + .coins = search->m_coinsFilter, + .epic = search->m_epicFilter, + .legendary = search->m_legendaryFilter, + .mythic = search->m_mythicFilter, + .song = search->m_songFilter, + .customSong = search->m_customSongFilter, + .songID = search->m_songID, + .demonFilter = (int)search->m_demonFilter, + .noStar = search->m_noStarFilter, + .star = search->m_starFilter + }); + + Mod::get()->setSavedValue("search-history", history); +} + +void SearchHistory::clear() { + Mod::get()->setSavedValue("search-history", std::vector()); +} + +std::vector SearchHistory::get() { + return Mod::get()->getSavedValue>("search-history"); +} + +void SearchHistory::remove(int index) { + auto history = Mod::get()->getSavedValue>("search-history"); + history.erase(history.begin() + index); + Mod::get()->setSavedValue("search-history", history); +} diff --git a/src/SearchHistory.hpp b/src/SearchHistory.hpp new file mode 100644 index 0000000..dee2fb1 --- /dev/null +++ b/src/SearchHistory.hpp @@ -0,0 +1,123 @@ +#include + +using namespace geode::prelude; + +struct SearchHistoryObject { + int64_t time; + int type; + std::string query; + std::vector difficulties; + std::vector lengths; + bool uncompleted; + bool completed; + bool featured; + bool original; + bool twoPlayer; + bool coins; + bool epic; + bool legendary; + bool mythic; + bool song; + bool customSong; + int songID; + int demonFilter; + bool noStar; + bool star; +}; + +class SearchHistory { +public: + static void add(GJSearchObject*, time_t, int); + static void clear(); + static std::vector get(); + static void remove(int); +}; + +template<> +struct matjson::Serialize> { + static std::vector from_json(matjson::Value const& value) { + auto vec = std::vector(); + + for (auto const& elem : value.as_array()) { + auto difficulties = std::vector(); + for (auto const& e : elem["difficulties"].as_array()) { + difficulties.push_back(e.as_int()); + } + + auto lengths = std::vector(); + for (auto const& e : elem["lengths"].as_array()) { + lengths.push_back(e.as_int()); + } + + vec.push_back({ + .time = (int64_t)elem["time"].as_double(), + .type = elem["type"].as_int(), + .query = elem["query"].as_string(), + .difficulties = difficulties, + .lengths = lengths, + .uncompleted = elem["uncompleted"].as_bool(), + .completed = elem["completed"].as_bool(), + .featured = elem["featured"].as_bool(), + .original = elem["original"].as_bool(), + .twoPlayer = elem["two-player"].as_bool(), + .coins = elem["coins"].as_bool(), + .epic = elem["epic"].as_bool(), + .legendary = elem["legendary"].as_bool(), + .mythic = elem["mythic"].as_bool(), + .song = elem["song"].as_bool(), + .customSong = elem["custom-song"].as_bool(), + .songID = elem["song-id"].as_int(), + .demonFilter = elem["demon-filter"].as_int(), + .noStar = elem["no-star"].as_bool(), + .star = elem["star"].as_bool() + }); + } + + return vec; + } + + static matjson::Value to_json(std::vector const& vec) { + auto arr = matjson::Array(); + + for (auto const& obj : vec) { + auto difficulties = matjson::Array(); + for (int const& e : obj.difficulties) { + difficulties.push_back(e); + } + + auto lengths = matjson::Array(); + for (int const& e : obj.lengths) { + lengths.push_back(e); + } + + arr.push_back(matjson::Object { + { "time", obj.time }, + { "type", obj.type }, + { "query", obj.query }, + { "difficulties", difficulties }, + { "lengths", lengths }, + { "uncompleted", obj.uncompleted }, + { "completed", obj.completed }, + { "featured", obj.featured }, + { "original", obj.original }, + { "two-player", obj.twoPlayer }, + { "coins", obj.coins }, + { "epic", obj.epic }, + { "legendary", obj.legendary }, + { "mythic", obj.mythic }, + { "song", obj.song }, + { "custom-song", obj.customSong }, + { "song-id", obj.songID }, + { "demon-filter", obj.demonFilter }, + { "no-star", obj.noStar }, + { "star", obj.star } + }); + } + + return arr; + } + + static bool is_json(matjson::Value const& value) { + return value.is_array(); + } +}; diff --git a/src/SearchHistoryNode.cpp b/src/SearchHistoryNode.cpp new file mode 100644 index 0000000..a60430f --- /dev/null +++ b/src/SearchHistoryNode.cpp @@ -0,0 +1,217 @@ +#include "SearchHistoryNode.hpp" + +SearchHistoryNode* SearchHistoryNode::create(SearchHistoryObject const& object, int index, SearchCallback search, RemoveCallback remove, bool h12, bool white) { + auto ret = new SearchHistoryNode(); + if (ret->init(object, index, search, remove, h12, white)) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +bool SearchHistoryNode::init(SearchHistoryObject const& object, int index, SearchCallback search, RemoveCallback remove, bool h12, bool white) { + if (!CCNode::init()) return false; + + setContentSize({ 400.0f, 100.0f }); + + m_object = object; + m_searchCallback = search; + m_removeCallback = remove; + m_index = index; + + auto background = CCScale9Sprite::create("GJ_square01.png", { 0, 0, 80, 80 }); + background->setContentSize({ 400.0f, 100.0f }); + background->setPosition(200.0f, 50.0f); + addChild(background); + + auto queryLabel = CCLabelBMFont::create(object.query.empty() ? "(No Query)" : object.query.c_str(), "bigFont.fnt"); + queryLabel->setColor(object.query.empty() ? ccColor3B { 127, 127, 127 } : ccColor3B { 255, 255, 255 }); + queryLabel->setScale(0.75f); + queryLabel->setAnchorPoint({ 0.0f, 0.5f }); + queryLabel->setPositionX(100.0f); + queryLabel->limitLabelWidth(225.0f, 0.75f, 0.1f); + addChild(queryLabel); + + auto type = object.type; + switch (type) { + case 0: { + queryLabel->setPositionY(80.0f); + auto levelSprite = CCSprite::createWithSpriteFrameName("GJ_viewLevelsBtn_001.png"); + levelSprite->setScale(1.75f); + levelSprite->setPosition({ 50.0f, 50.0f }); + addChild(levelSprite); + break; + } + case 1: { + queryLabel->setPositionY(65.0f); + auto listSprite = CCSprite::createWithSpriteFrameName("GJ_viewListsBtn_001.png"); + listSprite->setScale(1.75f); + listSprite->setPosition({ 50.0f, 50.0f }); + addChild(listSprite); + break; + } + case 2: { + queryLabel->setPositionY(50.0f); + auto userSprite = CCSprite::createWithSpriteFrameName("GJ_profileButton_001.png"); + userSprite->setScale(1.45f); + userSprite->setPosition({ 50.0f, 50.0f }); + addChild(userSprite); + break; + } + } + + auto buttonMenu = CCMenu::create(); + buttonMenu->setContentSize({ 400.0f, 100.0f }); + buttonMenu->setPosition(0.0f, 0.0f); + addChild(buttonMenu); + + auto removeButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_deleteBtn_001.png", 0.5f, [this](auto) { + createQuickPopup("Remove Search", "Are you sure you want to remove this search history entry?", "No", "Yes", [this](auto, bool btn2) { + if (btn2) m_removeCallback(m_index); + }); + }); + removeButton->setPosition(queryLabel->getScaledContentSize().width + 120.0f, queryLabel->getPositionY()); + buttonMenu->addChild(removeButton); + + auto searchButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_undoBtn_001.png", 0.6f, [this](auto) { + createQuickPopup("Search", "Are you sure you want to search with this query?", "No", "Yes", [this](auto, bool btn2) { + if (btn2) m_searchCallback(m_object); + }); + }); + searchButton->setPosition(queryLabel->getScaledContentSize().width + 150.0f, queryLabel->getPositionY()); + buttonMenu->addChild(searchButton); + + if (type < 2) { + if (type < 1) { + auto filtersNode = CCNode::create(); + filtersNode->setAnchorPoint({ 0.0f, 0.5f }); + filtersNode->setContentSize({ 300.0f, 15.0f }); + filtersNode->setPosition(100.0f, 55.0f); + filtersNode->setLayout(RowLayout::create()->setGap(3.0f)->setAxisAlignment(AxisAlignment::Start)->setAutoScale(false)); + addChild(filtersNode); + + if (object.uncompleted) filtersNode->addChild(CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png")); + if (object.completed) filtersNode->addChild(CCSprite::createWithSpriteFrameName("GJ_completesIcon_001.png")); + if (object.featured) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_featuredCoin_001.png"_spr)); + if (object.original) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_originalIcon_001.png"_spr)); + if (object.twoPlayer) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_twoPlayerBtn_001.png"_spr)); + if (object.coins) filtersNode->addChild(CCSprite::createWithSpriteFrameName("GJ_coinsIcon2_001.png")); + if (object.epic) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_epicCoin_001.png"_spr)); + if (object.mythic) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_epicCoin2_001.png"_spr)); + if (object.legendary) filtersNode->addChild(CCSprite::createWithSpriteFrameName("SH_epicCoin3_001.png"_spr)); + if (object.noStar) filtersNode->addChild(CCSprite::createWithSpriteFrameName("GJ_starsIcon_gray_001.png")); + + for (auto child : CCArrayExt(filtersNode->getChildren())) { + child->setScale(0.7f); + } + + if (filtersNode->getChildrenCount() == 0) { + filtersNode->removeFromParent(); + auto noFiltersLabel = CCLabelBMFont::create("(No Filters)", "bigFont.fnt"); + noFiltersLabel->setAnchorPoint({ 0.0f, 0.5f }); + noFiltersLabel->setColor({ 127, 127, 127 }); + noFiltersLabel->setScale(0.5f); + noFiltersLabel->setPosition(100.0f, 55.0f); + addChild(noFiltersLabel); + } + else filtersNode->updateLayout(); + } + + auto difficultiesNode = CCNode::create(); + difficultiesNode->setAnchorPoint({ 0.0f, 0.5f }); + difficultiesNode->setContentSize({ 300.0f, 15.0f }); + difficultiesNode->setPosition(100.0f, 35.0f + (type >= 1 ? 5.0f : 0.0f)); + difficultiesNode->setLayout(RowLayout::create()->setGap(3.0f)->setAxisAlignment(AxisAlignment::Start)->setAutoScale(false)); + addChild(difficultiesNode); + + for (auto difficulty : object.difficulties) { + auto difficultyFrame = std::string(); + switch (difficulty) { + case -1: difficultyFrame = "diffIcon_00_btn_001.png"; break; + case -2: difficultyFrame = "diffIcon_06_btn_001.png"; break; + case -3: difficultyFrame = "diffIcon_auto_btn_001.png"; break; + default: difficultyFrame = fmt::format("diffIcon_{:02d}_btn_001.png", difficulty); + } + + auto difficultySprite = CCSprite::createWithSpriteFrameName(difficultyFrame.c_str()); + difficultySprite->setScale(0.5f); + difficultiesNode->addChild(difficultySprite); + } + + if (std::find(object.difficulties.begin(), object.difficulties.end(), -2) != object.difficulties.end() && object.demonFilter > 0) { + auto demonFilterSprite = CCSprite::createWithSpriteFrameName(fmt::format("diffIcon_{:02d}_btn_001.png", object.demonFilter).c_str()); + demonFilterSprite->setScale(0.5f); + difficultiesNode->addChild(demonFilterSprite); + } + + if (type < 1) for (auto time : object.lengths) { + auto length = std::string(); + switch (time) { + case 0: length = "Tiny"; break; + case 1: length = "Short"; break; + case 2: length = "Medium"; break; + case 3: length = "Long"; break; + case 4: length = "XL"; break; + case 5: length = "Plat."; break; + } + + auto lengthLabel = CCLabelBMFont::create(length.c_str(), "bigFont.fnt"); + lengthLabel->setScale(0.3f); + difficultiesNode->addChild(lengthLabel); + } + + if (object.star) { + auto starSprite = CCSprite::createWithSpriteFrameName("GJ_starsIcon_001.png"); + starSprite->setScale(0.7f); + difficultiesNode->addChild(starSprite); + } + + if (difficultiesNode->getChildrenCount() == 0) { + difficultiesNode->removeFromParent(); + auto noDifficultiesLabel = CCLabelBMFont::create("(No Difficulties/Lengths)", "bigFont.fnt"); + noDifficultiesLabel->setAnchorPoint({ 0.0f, 0.5f }); + noDifficultiesLabel->setColor({ 127, 127, 127 }); + noDifficultiesLabel->setScale(0.5f); + noDifficultiesLabel->setPosition(100.0f, 35.0f + (type >= 1 ? 5.0f : 0.0f)); + addChild(noDifficultiesLabel); + } + else difficultiesNode->updateLayout(); + + if (type < 1 && object.song) { + auto songSprite = CCSprite::createWithSpriteFrameName("GJ_musicIcon_001.png"); + songSprite->setScale(0.65f); + songSprite->setPosition({ 100.0f + (songSprite->getScaledContentSize().width / 2), 15.0f }); + addChild(songSprite); + + auto songLabel = CCLabelBMFont::create( + (object.customSong ? std::to_string(object.songID) : LevelTools::getAudioTitle(object.songID - 1)).c_str(), "bigFont.fnt"); + songLabel->setScale(0.4f); + songLabel->setAnchorPoint({ 0.0f, 0.5f }); + songLabel->setPosition(120.0f, 15.0f); + songLabel->limitLabelWidth(280.0f, 0.4f, 0.1f); + addChild(songLabel); + } else if (type < 1) { + auto noSongLabel = CCLabelBMFont::create("(No Song)", "bigFont.fnt"); + noSongLabel->setAnchorPoint({ 0.0f, 0.5f }); + noSongLabel->setColor({ 127, 127, 127 }); + noSongLabel->setScale(0.5f); + noSongLabel->setPosition(100.0f, 15.0f); + addChild(noSongLabel); + } + } + + std::stringstream ss; + auto time = object.time; + ss << std::put_time(std::localtime(&time), h12 ? "%Y-%m-%d %I:%M:%S %p" : "%Y-%m-%d %H:%M:%S"); + + auto timeLabel = CCLabelBMFont::create(ss.str().c_str(), "chatFont.fnt"); + timeLabel->setColor(white ? ccColor3B { 255, 255, 255 } : ccColor3B { 51, 51, 51 }); + timeLabel->setOpacity(white ? 200 : 152); + timeLabel->setScale(0.5f); + timeLabel->setAnchorPoint({ 1.0f, 0.0f }); + timeLabel->setPosition(394.0f, 6.0f); + addChild(timeLabel); + + return true; +} diff --git a/src/SearchHistoryNode.hpp b/src/SearchHistoryNode.hpp new file mode 100644 index 0000000..4de7498 --- /dev/null +++ b/src/SearchHistoryNode.hpp @@ -0,0 +1,16 @@ +#include "SearchHistory.hpp" + +typedef MiniFunction SearchCallback; +typedef MiniFunction RemoveCallback; + +class SearchHistoryNode : public CCNode { +protected: + SearchHistoryObject m_object; + SearchCallback m_searchCallback; + RemoveCallback m_removeCallback; + int m_index; + + bool init(SearchHistoryObject const&, int, SearchCallback, RemoveCallback, bool, bool); +public: + static SearchHistoryNode* create(SearchHistoryObject const&, int, SearchCallback, RemoveCallback, bool, bool); +}; diff --git a/src/SearchHistoryPopup.cpp b/src/SearchHistoryPopup.cpp new file mode 100644 index 0000000..91932df --- /dev/null +++ b/src/SearchHistoryPopup.cpp @@ -0,0 +1,101 @@ +#include "SearchHistoryPopup.hpp" + +SearchHistoryPopup* SearchHistoryPopup::create(SearchHistoryCallback callback) { + auto ret = new SearchHistoryPopup(); + if (ret->initAnchored(440.0f, 290.0f, callback, "GJ_square02.png")) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +bool SearchHistoryPopup::setup(SearchHistoryCallback callback) { + setTitle("Search History", "bigFont.fnt", 0.53f); + + m_searchCallback = callback; + + auto background = CCScale9Sprite::create("square02_001.png", { 0, 0, 80, 80 }); + background->setContentSize({ 400.0f, 195.0f }); + background->setPosition(220.0f, 117.5f); + background->setOpacity(127); + m_mainLayer->addChild(background); + + m_scrollLayer = ScrollLayer::create({ 400.0f, 195.0f }); + m_scrollLayer->setPosition(20.0f, 20.0f); + m_scrollLayer->m_contentLayer->setLayout( + ColumnLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ->setAutoGrowAxis(195.0f) + ->setGap(2.5f) + ); + m_mainLayer->addChild(m_scrollLayer); + + m_prevButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_arrow_01_001.png", 1.0f, [this](auto) { + page(m_page - 1); + }); + m_prevButton->setPosition(-34.5f, 145.0f); + m_buttonMenu->addChild(m_prevButton); + + auto nextButtonSprite = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + nextButtonSprite->setFlipX(true); + m_nextButton = CCMenuItemExt::createSpriteExtra(nextButtonSprite, [this](auto) { + page(m_page + 1); + }); + m_nextButton->setPosition(474.5f, 145.0f); + m_buttonMenu->addChild(m_nextButton); + + auto clearButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_deleteBtn_001.png", 0.6f, [this](auto) { + createQuickPopup("Clear History", "Are you sure you want to clear your search history?", "No", "Yes", [this](auto, bool btn2) { + if (btn2) { + SearchHistory::clear(); + page(0); + } + }); + }); + clearButton->setPosition(420.0f, 270.0f); + m_buttonMenu->addChild(clearButton); + + m_searchInput = TextInput::create(400.0f, "Search History..."); + m_searchInput->setCommonFilter(CommonFilter::Any); + m_searchInput->setTextAlign(TextInputAlign::Left); + m_searchInput->setPosition(220.0f, 235.0f); + m_searchInput->setCallback([this](auto) { page(0); }); + m_mainLayer->addChild(m_searchInput); + + page(0); + + return true; +} + +void SearchHistoryPopup::page(int p) { + m_scrollLayer->m_contentLayer->removeAllChildren(); + + auto history = std::vector(); + auto query = string::toLower(m_searchInput->getString()); + for (auto const& object : SearchHistory::get()) { + if (string::toLower(object.query).find(query) != std::string::npos) history.push_back(object); + } + + auto count = history.size(); + m_prevButton->setVisible(p > 0); + m_nextButton->setVisible(p < (count > 0 ? (count - 1) / 5 : 0)); + + auto h12 = Mod::get()->getSettingValue("12-hour-time"); + auto white = Mod::get()->getSettingValue("white-time"); + for (int i = p * 5; i < (p + 1) * 5 && i < count; i++) { + m_scrollLayer->m_contentLayer->addChild(SearchHistoryNode::create(history[i], i, [this](SearchHistoryObject const& object) { + m_searchCallback(object); + onClose(nullptr); + }, [this](int index) { + SearchHistory::remove(index); + page(m_page); + }, h12, white)); + } + + m_scrollLayer->m_contentLayer->updateLayout(); + m_scrollLayer->scrollToTop(); + + m_page = p; +} diff --git a/src/SearchHistoryPopup.hpp b/src/SearchHistoryPopup.hpp new file mode 100644 index 0000000..e1e2c7a --- /dev/null +++ b/src/SearchHistoryPopup.hpp @@ -0,0 +1,19 @@ +#include "SearchHistoryNode.hpp" + +typedef MiniFunction SearchHistoryCallback; + +class SearchHistoryPopup : public geode::Popup { +protected: + ScrollLayer* m_scrollLayer; + SearchHistoryCallback m_searchCallback; + TextInput* m_searchInput; + CCMenuItemSpriteExtra* m_prevButton; + CCMenuItemSpriteExtra* m_nextButton; + int m_page; + + bool setup(SearchHistoryCallback) override; +public: + static SearchHistoryPopup* create(SearchHistoryCallback); + + void page(int); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c939122 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,74 @@ +#include "SearchHistoryPopup.hpp" + +#include +class $modify(MyLevelSearchLayer, LevelSearchLayer) { + bool init(int type) { + if (!LevelSearchLayer::init(type)) return false; + + auto historyButtonSprite = CircleButtonSprite::createWithSprite("SH_historyBtn_001.png"_spr); + historyButtonSprite->getTopNode()->setScale(1.0f); + historyButtonSprite->setScale(0.8f); + auto historyButton = CCMenuItemExt::createSpriteExtra(historyButtonSprite, [this](auto) { + SearchHistoryPopup::create([this](SearchHistoryObject const& object) { + auto glm = GameLevelManager::sharedState(); + if (object.type == 0) { + glm->setBoolForKey(object.uncompleted, "uncompleted_filter"); + glm->setBoolForKey(object.completed, "completed_filter"); + glm->setBoolForKey(object.original, "original_filter"); + glm->setBoolForKey(object.coins, "coin_filter"); + glm->setBoolForKey(object.twoPlayer, "twoP_filter"); + glm->setBoolForKey(object.song, "enable_songFilter"); + glm->setBoolForKey(object.noStar, "nostar_filter"); + glm->setBoolForKey(object.featured, "featured_filter"); + glm->setBoolForKey(object.epic, "epic_filter"); + glm->setBoolForKey(object.mythic, "legendary_filter"); // Mythic and legendary are swapped, nice job RobTop + glm->setBoolForKey(object.legendary, "mythic_filter"); + glm->setBoolForKey(object.customSong, "customsong_filter"); + glm->setIntForKey(object.songID, "song_filter"); + } + + if (object.type == 0 || object.type == 1) { + if (glm->getBoolForKey("star_filter") != object.star) toggleStar(nullptr); + for (int i = 0; i < 8; i++) { + auto diff = i; + switch (i) { + case 0: diff = -1; break; + case 6: diff = -2; break; + case 7: diff = -3; break; + } + toggleDifficultyNum(i, std::find(object.difficulties.begin(), object.difficulties.end(), diff) != object.difficulties.end()); + if (i != 6) continue; + + auto demonToggled = m_difficultyDict->valueForKey(getDiffKey(i))->boolValue(); + m_demonTypeButton->setEnabled(demonToggled); + m_demonTypeButton->setVisible(demonToggled); + if (auto dibFilter = getChildByIDRecursive("hiimjustin000.demons_in_between/quick-search-button")) dibFilter->setVisible(demonToggled); + } + if (object.type == 0) for (int i = 0; i < 6; i++) { + toggleTimeNum(i, std::find(object.lengths.begin(), object.lengths.end(), i) != object.lengths.end()); + } + demonFilterSelectClosed(object.demonFilter); + } + + m_searchInput->setString(object.query); + })->show(); + }); + historyButton->setID("search-history-button"_spr); + + auto otherFilterMenu = getChildByID("other-filter-menu"); + otherFilterMenu->addChild(historyButton); + otherFilterMenu->updateLayout(); + + return true; + } + + void onSearch(CCObject* sender) { + LevelSearchLayer::onSearch(sender); + SearchHistory::add(getSearchObject(SearchType::Search, m_searchInput->getString()), time(0), m_type); + } + + void onSearchUser(CCObject* sender) { + LevelSearchLayer::onSearchUser(sender); + SearchHistory::add(getSearchObject(SearchType::Users, m_searchInput->getString()), time(0), 2); + } +};