option to hide homebrew.

This commit is contained in:
ITotalJustice
2025-07-31 18:47:41 +01:00
parent 25e19b22f7
commit 3ebb3bd055
6 changed files with 180 additions and 140 deletions

View File

@@ -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

View File

@@ -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<NroEntry>& nros, bool hide_spahira, bool nested = true, bool scan_all_dir = true) -> Result;
auto nro_scan(const fs::FsPath& path, std::vector<NroEntry>& nros, bool nested = true, bool scan_all_dir = true) -> Result;
auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector<u8>;
auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8>;

View File

@@ -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<u8>& 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<NroEntry> m_entries{};
std::vector<u32> m_entries_index[Filter_MAX]{};
std::span<u32> m_entries_current{};
s64 m_index{}; // where i am in the array
std::unique_ptr<List> 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

View File

@@ -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<NroEntry>& 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<NroEntry>& 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<NroEntry>
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<NroEntry>
} 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<NroEntry>
R_SUCCEED();
}
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& 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<u8> {
@@ -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<NroEntry>& 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<NroEntry>& 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<u8> {

View File

@@ -8,28 +8,6 @@
#include <cstdlib>
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<typename T>
auto OptionBase<T>::GetInternal(const char* name) -> T {
@@ -90,9 +68,9 @@ auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
if (m_name == name) {
if (m_file) {
if constexpr(std::is_same_v<T, bool>) {
m_value = getbool(value, m_default_value);
m_value = ini_parse_getbool(value, m_default_value);
} else if constexpr(std::is_same_v<T, long>) {
m_value = getl(value, m_default_value);
m_value = ini_parse_getl(value, m_default_value);
} else if constexpr(std::is_same_v<T, std::string>) {
m_value = value;
}

View File

@@ -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<Sidebar>("Homebrew Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
if (m_entries.size()) {
options->Add<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_unique<Sidebar>("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<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
}, m_sort.Get());
options->Add<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
m_order.Set(index_out);
SortAndFindLastFile();
}, m_order.Get());
options->Add<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
m_layout.Set(index_out);
OnLayoutChange();
}, m_layout.Get());
options->Add<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
m_hide_sphaira.Set(enable);
});
});
#if 0
options->Add<SidebarEntryCallback>("Info"_i18n, [this](){
});
#endif
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].path.toString() + "?";
App::Push<OptionBox>(
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<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
if (App::GetInstallPrompt()) {
App::Push<OptionBox>(
"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<Sidebar>("Homebrew Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_unique<Sidebar>("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<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
}, m_sort.Get());
options->Add<SidebarEntryArray>("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<SidebarEntryArray>("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<SidebarEntryBool>("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<SidebarEntryCallback>("Info"_i18n, [this](){
});
#endif
options->Add<SidebarEntryBool>("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<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?";
App::Push<OptionBox>(
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<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
if (App::GetInstallPrompt()) {
App::Push<OptionBox>(
"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