From 3ebb3bd05512469c553e8a8aae7d6cb055d7fc4a Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:47:41 +0100 Subject: [PATCH] option to hide homebrew. --- sphaira/CMakeLists.txt | 2 +- sphaira/include/nro.hpp | 3 +- sphaira/include/ui/menus/homebrew.hpp | 20 +- sphaira/source/nro.cpp | 17 +- sphaira/source/option.cpp | 26 +-- sphaira/source/ui/menus/homebrew.cpp | 252 +++++++++++++++----------- 6 files changed, 180 insertions(+), 140 deletions(-) diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 0969a1f..4a7533f 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -190,7 +190,7 @@ FetchContent_Declare(yyjson FetchContent_Declare(minIni GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git - GIT_TAG 11cac8b + GIT_TAG 6e952b6 ) FetchContent_Declare(zstd diff --git a/sphaira/include/nro.hpp b/sphaira/include/nro.hpp index 01199a2..f5959e1 100644 --- a/sphaira/include/nro.hpp +++ b/sphaira/include/nro.hpp @@ -11,6 +11,7 @@ namespace sphaira { struct Hbini { u64 timestamp{}; // timestamp of last launch + bool hidden{}; }; struct MiniNacp { @@ -61,7 +62,7 @@ auto nro_parse(const fs::FsPath& path, NroEntry& entry) -> Result; * nro found. * this does nothing if nested=false. */ -auto nro_scan(const fs::FsPath& path, std::vector& nros, bool hide_spahira, bool nested = true, bool scan_all_dir = true) -> Result; +auto nro_scan(const fs::FsPath& path, std::vector& nros, bool nested = true, bool scan_all_dir = true) -> Result; auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector; auto nro_get_icon(const fs::FsPath& path) -> std::vector; diff --git a/sphaira/include/ui/menus/homebrew.hpp b/sphaira/include/ui/menus/homebrew.hpp index d04188a..9c43e77 100644 --- a/sphaira/include/ui/menus/homebrew.hpp +++ b/sphaira/include/ui/menus/homebrew.hpp @@ -8,6 +8,12 @@ namespace sphaira::ui::menu::homebrew { +enum Filter { + Filter_All, + Filter_HideHidden, + Filter_MAX, +}; + enum SortType { SortType_Updated, SortType_Alphabetical, @@ -43,6 +49,14 @@ struct Menu final : grid::Menu { static Result InstallHomebrew(const fs::FsPath& path, const std::vector& icon); static Result InstallHomebrewFromPath(const fs::FsPath& path); + auto GetEntry(s64 i) -> NroEntry& { + return m_entries[m_entries_current[i]]; + } + + auto GetEntry() -> NroEntry& { + return GetEntry(m_index); + } + private: void SetIndex(s64 index); void InstallHomebrew(); @@ -51,6 +65,7 @@ private: void SortAndFindLastFile(bool scan = false); void FreeEntries(); void OnLayoutChange(); + void DisplayOptions(); auto IsStarEnabled() -> bool { return m_sort.Get() >= SortType_UpdatedStar; @@ -60,6 +75,9 @@ private: static constexpr inline const char* INI_SECTION = "homebrew"; std::vector m_entries{}; + std::vector m_entries_index[Filter_MAX]{}; + std::span m_entries_current{}; + s64 m_index{}; // where i am in the array std::unique_ptr m_list{}; bool m_dirty{}; @@ -67,7 +85,7 @@ private: option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail}; - option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false}; + option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false}; }; } // namespace sphaira::ui::menu::homebrew diff --git a/sphaira/source/nro.cpp b/sphaira/source/nro.cpp index 89b9fc0..f0fb047 100644 --- a/sphaira/source/nro.cpp +++ b/sphaira/source/nro.cpp @@ -79,7 +79,7 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) -> // this function is recursive by 1 level deep // if the nro is in switch/folder/folder2/app.nro it will NOT be found // switch/folder/app.nro for example will work fine. -auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result { +auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector& nros, bool nested, bool scan_all_dir, bool root) -> Result { // we don't need to scan for folders if we are not root u32 dir_open_type = FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize; if (root) { @@ -99,11 +99,6 @@ auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector continue; } - // skip self - if (hide_sphaira && !strncmp(e.name, "sphaira", strlen("sphaira"))) { - continue; - } - if (e.type == FsDirEntryType_Dir) { // assert(!root && "dir should only be scanned on non-root!"); fs::FsPath fullpath; @@ -117,7 +112,7 @@ auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector } else { // slow path... std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name); - nro_scan_internal(fs, fullpath, nros, hide_sphaira, nested, scan_all_dir, false); + nro_scan_internal(fs, fullpath, nros, nested, scan_all_dir, false); } } else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) { fs::FsPath fullpath; @@ -139,9 +134,9 @@ auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector R_SUCCEED(); } -auto nro_scan_internal(const fs::FsPath& path, std::vector& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result { +auto nro_scan_internal(const fs::FsPath& path, std::vector& nros, bool nested, bool scan_all_dir, bool root) -> Result { fs::FsNativeSd fs; - return nro_scan_internal(&fs, path, nros, hide_sphaira, nested, scan_all_dir, root); + return nro_scan_internal(&fs, path, nros, nested, scan_all_dir, root); } auto nro_get_icon_internal(fs::File* f, u64 size, u64 offset) -> std::vector { @@ -198,8 +193,8 @@ auto nro_parse(const fs::FsPath& path, NroEntry& entry) -> Result { return nro_parse_internal(&fs, path, entry); } -auto nro_scan(const fs::FsPath& path, std::vector& nros, bool hide_sphaira, bool nested, bool scan_all_dir) -> Result { - return nro_scan_internal(path, nros, hide_sphaira, nested, scan_all_dir, true); +auto nro_scan(const fs::FsPath& path, std::vector& nros, bool nested, bool scan_all_dir) -> Result { + return nro_scan_internal(path, nros, nested, scan_all_dir, true); } auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector { diff --git a/sphaira/source/option.cpp b/sphaira/source/option.cpp index c4b8f44..c61fa55 100644 --- a/sphaira/source/option.cpp +++ b/sphaira/source/option.cpp @@ -8,28 +8,6 @@ #include namespace sphaira::option { -namespace { - -// these are taken from minini in order to parse a value already loaded in memory. -long getl(const char* LocalBuffer, long def) { - const auto len = strlen(LocalBuffer); - return (len == 0) ? def - : ((len >= 2 && toupper((int)LocalBuffer[1]) == 'X') ? strtol(LocalBuffer, NULL, 16) - : strtol(LocalBuffer, NULL, 10)); -} - -bool getbool(const char* LocalBuffer, bool def) { - const auto c = toupper(LocalBuffer[0]); - - if (c == 'Y' || c == '1' || c == 'T') - return true; - else if (c == 'N' || c == '0' || c == 'F') - return false; - else - return def; -} - -} // namespace template auto OptionBase::GetInternal(const char* name) -> T { @@ -90,9 +68,9 @@ auto OptionBase::LoadFrom(const char* name, const char* value) -> bool { if (m_name == name) { if (m_file) { if constexpr(std::is_same_v) { - m_value = getbool(value, m_default_value); + m_value = ini_parse_getbool(value, m_default_value); } else if constexpr(std::is_same_v) { - m_value = getl(value, m_default_value); + m_value = ini_parse_getl(value, m_default_value); } else if constexpr(std::is_same_v) { m_value = value; } diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index d5487ed..0da6e28 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -53,93 +53,10 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} { this->SetActions( std::make_pair(Button::A, Action{"Launch"_i18n, [this](){ - nro_launch(m_entries[m_index].path); + nro_launch(GetEntry().path); }}), std::make_pair(Button::X, Action{"Options"_i18n, [this](){ - auto options = std::make_unique("Homebrew Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - if (m_entries.size()) { - options->Add("Sort By"_i18n, [this](){ - auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - SidebarEntryArray::Items sort_items; - sort_items.push_back("Updated"_i18n); - sort_items.push_back("Alphabetical"_i18n); - sort_items.push_back("Size"_i18n); - sort_items.push_back("Updated (Star)"_i18n); - sort_items.push_back("Alphabetical (Star)"_i18n); - sort_items.push_back("Size (Star)"_i18n); - - SidebarEntryArray::Items order_items; - order_items.push_back("Descending"_i18n); - order_items.push_back("Ascending"_i18n); - - SidebarEntryArray::Items layout_items; - layout_items.push_back("List"_i18n); - layout_items.push_back("Icon"_i18n); - layout_items.push_back("Grid"_i18n); - - options->Add("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){ - m_sort.Set(index_out); - SortAndFindLastFile(); - }, m_sort.Get()); - - options->Add("Order"_i18n, order_items, [this, order_items](s64& index_out){ - m_order.Set(index_out); - SortAndFindLastFile(); - }, m_order.Get()); - - options->Add("Layout"_i18n, layout_items, [this](s64& index_out){ - m_layout.Set(index_out); - OnLayoutChange(); - }, m_layout.Get()); - - options->Add("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){ - m_hide_sphaira.Set(enable); - }); - }); - - #if 0 - options->Add("Info"_i18n, [this](){ - - }); - #endif - - options->Add("Delete"_i18n, [this](){ - const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].path.toString() + "?"; - App::Push( - buf, - "Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){ - if (op_index && *op_index) { - if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) { - FreeEntry(App::GetVg(), m_entries[m_index]); - m_entries.erase(m_entries.begin() + m_index); - SetIndex(m_index ? m_index - 1 : 0); - } - } - }, m_entries[m_index].image - ); - }, true); - - auto forwarder_entry = options->Add("Install Forwarder"_i18n, [this](){ - if (App::GetInstallPrompt()) { - App::Push( - "WARNING: Installing forwarders will lead to a ban!"_i18n, - "Back"_i18n, "Install"_i18n, 0, [this](auto op_index){ - if (op_index && *op_index) { - InstallHomebrew(); - } - }, m_entries[m_index].image - ); - } else { - InstallHomebrew(); - } - }, true); - - forwarder_entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR)); - } + DisplayOptions(); }}) ); @@ -179,8 +96,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const int image_load_max = 2; int image_load_count = 0; - m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) { - auto& e = m_entries[pos]; + m_list->Draw(vg, theme, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) { + const auto index = m_entries_current[pos]; + auto& e = m_entries[index]; // lazy load image if (image_load_count < image_load_max) { @@ -240,17 +158,17 @@ void Menu::SetIndex(s64 index) { } if (IsStarEnabled()) { - const auto star_path = GenerateStarPath(m_entries[m_index].path); + const auto star_path = GenerateStarPath(GetEntry().path); if (fs::FsNativeSd().FileExists(star_path)) { SetAction(Button::R3, Action{"Unstar"_i18n, [this](){ - fs::FsNativeSd().DeleteFile(GenerateStarPath(m_entries[m_index].path)); - App::Notify("Unstarred "_i18n + m_entries[m_index].GetName()); + fs::FsNativeSd().DeleteFile(GenerateStarPath(GetEntry().path)); + App::Notify("Unstarred "_i18n + GetEntry().GetName()); SortAndFindLastFile(); }}); } else { SetAction(Button::R3, Action{"Star"_i18n, [this](){ - fs::FsNativeSd().CreateFile(GenerateStarPath(m_entries[m_index].path)); - App::Notify("Starred "_i18n + m_entries[m_index].GetName()); + fs::FsNativeSd().CreateFile(GenerateStarPath(GetEntry().path)); + App::Notify("Starred "_i18n + GetEntry().GetName()); SortAndFindLastFile(); }}); } @@ -263,19 +181,19 @@ void Menu::SetIndex(s64 index) { // todo: fix GetFileTimeStampRaw being different to timeGetCurrentTime // log_write("name: %s hbini.ts: %lu file.ts: %lu smaller: %s\n", e.GetName(), e.hbini.timestamp, e.timestamp.modified, e.hbini.timestamp < e.timestamp.modified ? "true" : "false"); - SetTitleSubHeading(m_entries[m_index].path); - this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size())); + SetTitleSubHeading(GetEntry().path); + this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size())); } void Menu::InstallHomebrew() { - const auto& nro = m_entries[m_index]; + const auto& nro = GetEntry(); InstallHomebrew(nro.path, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset)); } void Menu::ScanHomebrew() { TimeStamp ts; FreeEntries(); - nro_scan("/switch", m_entries, m_hide_sphaira.Get()); + nro_scan("/switch", m_entries); log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD()); struct IniUser { @@ -301,7 +219,9 @@ void Menu::ScanHomebrew() { if (user->ini) { if (!strcmp(Key, "timestamp")) { - user->ini->timestamp = atoi(Value); + user->ini->timestamp = ini_parse_getl(Value, 0); + } else if (!strcmp(Key, "hidden")) { + user->ini->hidden = ini_parse_getbool(Value, false); } } @@ -309,6 +229,21 @@ void Menu::ScanHomebrew() { return 1; }, &ini_user, App::PLAYLOG_PATH); + // pre-allocate the max size. + for (auto& index : m_entries_index) { + index.reserve(m_entries.size()); + } + + for (u32 i = 0; i < m_entries.size(); i++) { + auto& e = m_entries[i]; + + m_entries_index[Filter_All].emplace_back(i); + + if (!e.hbini.hidden) { + m_entries_index[Filter_HideHidden].emplace_back(i); + } + } + this->Sort(); SetIndex(0); m_dirty = false; @@ -327,7 +262,10 @@ void Menu::Sort() { const auto sort = m_sort.Get(); const auto order = m_order.Get(); - const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool { + const auto sorter = [this, sort, order](u32 _lhs, u32 _rhs) -> bool { + const auto& lhs = m_entries[_lhs]; + const auto& rhs = m_entries[_rhs]; + const auto name_cmp = [order](const NroEntry& lhs, const NroEntry& rhs) -> bool { auto r = strcasecmp(lhs.GetName(), rhs.GetName()); if (!r) { @@ -403,11 +341,18 @@ void Menu::Sort() { std::unreachable(); }; - std::sort(m_entries.begin(), m_entries.end(), sorter); + if (m_show_hidden.Get()) { + m_entries_current = m_entries_index[Filter_All]; + } else { + m_entries_current = m_entries_index[Filter_HideHidden]; + } + + std::sort(m_entries_current.begin(), m_entries_current.end(), sorter); } void Menu::SortAndFindLastFile(bool scan) { - const auto path = m_entries[m_index].path; + const auto path = GetEntry().path; + if (scan) { ScanHomebrew(); } else { @@ -416,8 +361,8 @@ void Menu::SortAndFindLastFile(bool scan) { SetIndex(0); s64 index = -1; - for (u64 i = 0; i < m_entries.size(); i++) { - if (path == m_entries[i].path) { + for (u64 i = 0; i < m_entries_current.size(); i++) { + if (path == GetEntry(i).path) { index = i; break; } @@ -444,6 +389,9 @@ void Menu::FreeEntries() { } m_entries.clear(); + for (auto& e : m_entries_index) { + e.clear(); + } } void Menu::OnLayoutChange() { @@ -463,4 +411,104 @@ Result Menu::InstallHomebrewFromPath(const fs::FsPath& path) { return InstallHomebrew(path, nro_get_icon(path)); } +void Menu::DisplayOptions() { + auto options = std::make_unique("Homebrew Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->Add("Sort By"_i18n, [this](){ + auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + SidebarEntryArray::Items sort_items; + sort_items.push_back("Updated"_i18n); + sort_items.push_back("Alphabetical"_i18n); + sort_items.push_back("Size"_i18n); + sort_items.push_back("Updated (Star)"_i18n); + sort_items.push_back("Alphabetical (Star)"_i18n); + sort_items.push_back("Size (Star)"_i18n); + + SidebarEntryArray::Items order_items; + order_items.push_back("Descending"_i18n); + order_items.push_back("Ascending"_i18n); + + SidebarEntryArray::Items layout_items; + layout_items.push_back("List"_i18n); + layout_items.push_back("Icon"_i18n); + layout_items.push_back("Grid"_i18n); + + options->Add("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){ + m_sort.Set(index_out); + SortAndFindLastFile(); + }, m_sort.Get()); + + options->Add("Order"_i18n, order_items, [this, order_items](s64& index_out){ + m_order.Set(index_out); + SortAndFindLastFile(); + }, m_order.Get(), "Display entries in Ascending or Descending order."_i18n); + + options->Add("Layout"_i18n, layout_items, [this](s64& index_out){ + m_layout.Set(index_out); + OnLayoutChange(); + }, m_layout.Get(), "Change the layout to List, Icon and Grid."_i18n); + + options->Add("Show hidden"_i18n, m_show_hidden.Get(), [this](bool& enable){ + m_show_hidden.Set(enable); + SortAndFindLastFile(); + }, "Shows all hidden homebrew."_i18n); + }); + + if (!m_entries_current.empty()) { + #if 0 + options->Add("Info"_i18n, [this](){ + + }); + #endif + + options->Add("Hide"_i18n, GetEntry().hbini.hidden, [this](bool& v_out){ + ini_putl(GetEntry().path, "hidden", v_out, App::PLAYLOG_PATH); + ScanHomebrew(); + App::PopToMenu(); + }, "Hides the selected homebrew.\n\n" + "To Unhide homebrew, enable \"Show hidden\" in the sort options."_i18n); + + options->Add("Delete"_i18n, [this](){ + const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?"; + App::Push( + buf, + "Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){ + if (op_index && *op_index) { + if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(GetEntry().path))) { + // todo: remove from list using real index here. + FreeEntry(App::GetVg(), GetEntry()); + ScanHomebrew(); + // m_entries.erase(m_entries.begin() + m_index); + // SetIndex(m_index ? m_index - 1 : 0); + App::PopToMenu(); + } + } + }, GetEntry().image + ); + }, "Perminately delete the selected homebrew.\n\n" + "Files and folders created by the homebrew will still remain. " + "Use the FileBrowser to delete them."_i18n); + + auto forwarder_entry = options->Add("Install Forwarder"_i18n, [this](){ + if (App::GetInstallPrompt()) { + App::Push( + "WARNING: Installing forwarders will lead to a ban!"_i18n, + "Back"_i18n, "Install"_i18n, 0, [this](auto op_index){ + if (op_index && *op_index) { + InstallHomebrew(); + } + }, GetEntry().image + ); + } else { + InstallHomebrew(); + } + }, true); + + forwarder_entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR)); + } +} + } // namespace sphaira::ui::menu::homebrew