fix memleak when deleting homebrew, add game menu.
This commit is contained in:
@@ -49,6 +49,7 @@ add_executable(sphaira
|
|||||||
source/ui/menus/usb_menu.cpp
|
source/ui/menus/usb_menu.cpp
|
||||||
source/ui/menus/ftp_menu.cpp
|
source/ui/menus/ftp_menu.cpp
|
||||||
source/ui/menus/gc_menu.cpp
|
source/ui/menus/gc_menu.cpp
|
||||||
|
source/ui/menus/game_menu.cpp
|
||||||
|
|
||||||
source/ui/error_box.cpp
|
source/ui/error_box.cpp
|
||||||
source/ui/notification.cpp
|
source/ui/notification.cpp
|
||||||
|
|||||||
62
sphaira/include/ui/menus/game_menu.hpp
Normal file
62
sphaira/include/ui/menus/game_menu.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "option.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::game {
|
||||||
|
|
||||||
|
enum class NacpLoadStatus {
|
||||||
|
// not yet attempted to be loaded.
|
||||||
|
None,
|
||||||
|
// loaded, ready to parse.
|
||||||
|
Loaded,
|
||||||
|
// failed to load, do not attempt to load again!
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
u64 app_id{};
|
||||||
|
char display_version[0x10]{};
|
||||||
|
NacpLanguageEntry lang{};
|
||||||
|
int image{};
|
||||||
|
|
||||||
|
std::unique_ptr<NsApplicationControlData> control{};
|
||||||
|
u64 control_size{};
|
||||||
|
NacpLoadStatus status{NacpLoadStatus::None};
|
||||||
|
|
||||||
|
auto GetName() const -> const char* {
|
||||||
|
return lang.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetAuthor() const -> const char* {
|
||||||
|
return lang.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetDisplayVersion() const -> const char* {
|
||||||
|
return display_version;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
void ScanHomebrew();
|
||||||
|
void FreeEntries();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Entry> m_entries{};
|
||||||
|
s64 m_index{}; // where i am in the array
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::game
|
||||||
@@ -30,23 +30,25 @@ struct Menu final : MenuBase {
|
|||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
|
||||||
|
return m_entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
|
||||||
|
static Result InstallHomebrewFromPath(const fs::FsPath& path);
|
||||||
|
|
||||||
|
private:
|
||||||
void SetIndex(s64 index);
|
void SetIndex(s64 index);
|
||||||
void InstallHomebrew();
|
void InstallHomebrew();
|
||||||
void ScanHomebrew();
|
void ScanHomebrew();
|
||||||
void Sort();
|
void Sort();
|
||||||
void SortAndFindLastFile();
|
void SortAndFindLastFile();
|
||||||
|
void FreeEntries();
|
||||||
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
|
|
||||||
return m_entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto IsStarEnabled() -> bool {
|
auto IsStarEnabled() -> bool {
|
||||||
return m_sort.Get() >= SortType_UpdatedStar;
|
return m_sort.Get() >= SortType_UpdatedStar;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
|
|
||||||
static Result InstallHomebrewFromPath(const fs::FsPath& path);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "ui/menus/usb_menu.hpp"
|
#include "ui/menus/usb_menu.hpp"
|
||||||
#include "ui/menus/ftp_menu.hpp"
|
#include "ui/menus/ftp_menu.hpp"
|
||||||
#include "ui/menus/gc_menu.hpp"
|
#include "ui/menus/gc_menu.hpp"
|
||||||
|
#include "ui/menus/game_menu.hpp"
|
||||||
|
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
@@ -1518,6 +1519,10 @@ void App::DisplayMiscOptions(bool left_side) {
|
|||||||
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Games"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::game::Menu>());
|
||||||
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||||
App::Push(std::make_shared<ui::menu::themezer::Menu>());
|
App::Push(std::make_shared<ui::menu::themezer::Menu>());
|
||||||
}));
|
}));
|
||||||
|
|||||||
287
sphaira/source/ui/menus/game_menu.cpp
Normal file
287
sphaira/source/ui/menus/game_menu.cpp
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
#include "app.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "ui/menus/game_menu.hpp"
|
||||||
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/error_box.hpp"
|
||||||
|
#include "ui/option_box.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::game {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Result Notify(Result rc, const std::string& error_message) {
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
App::Push(std::make_shared<ui::ErrorBox>(rc,
|
||||||
|
i18n::get(error_message.c_str())
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
App::Notify("Success");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// also sets the status to error.
|
||||||
|
void FakeNacpEntry(Entry& e) {
|
||||||
|
e.status = NacpLoadStatus::Error;
|
||||||
|
// fake the nacp entry
|
||||||
|
std::strcpy(e.lang.name, "Corrupted");
|
||||||
|
std::strcpy(e.lang.author, "Corrupted");
|
||||||
|
std::strcpy(e.display_version, "0.0.0");
|
||||||
|
e.control.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadControlImage(Entry& e) {
|
||||||
|
if (!e.image && e.control) {
|
||||||
|
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
|
||||||
|
e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
|
||||||
|
e.control.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
||||||
|
if (e.status == NacpLoadStatus::None) {
|
||||||
|
e.control = std::make_unique<NsApplicationControlData>();
|
||||||
|
if (R_FAILED(nsGetApplicationControlData(NsApplicationControlSource_Storage, e.app_id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
|
||||||
|
FakeNacpEntry(e);
|
||||||
|
} else {
|
||||||
|
NacpLanguageEntry* lang{};
|
||||||
|
if (R_FAILED(nsGetApplicationDesiredLanguage(&e.control->nacp, &lang)) || !lang) {
|
||||||
|
FakeNacpEntry(e);
|
||||||
|
} else {
|
||||||
|
e.lang = *lang;
|
||||||
|
std::memcpy(e.display_version, e.control->nacp.display_version, sizeof(e.display_version));
|
||||||
|
e.status = NacpLoadStatus::Loaded;
|
||||||
|
|
||||||
|
if (force_image_load) {
|
||||||
|
LoadControlImage(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||||
|
nvgDeleteImage(vg, e.image);
|
||||||
|
e.image = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LaunchEntry(const Entry& e) {
|
||||||
|
const auto rc = appletRequestLaunchApplication(e.app_id, nullptr);
|
||||||
|
Notify(rc, "Failed to launch application");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Menu::Menu() : MenuBase{"Games"_i18n} {
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||||
|
LaunchEntry(m_entries[m_index]);
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
auto options = std::make_shared<Sidebar>("Homebrew Options"_i18n, Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
if (m_entries.size()) {
|
||||||
|
#if 0
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Info"_i18n, [this](){
|
||||||
|
|
||||||
|
}));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Launch random game"_i18n, [this](){
|
||||||
|
const auto random_index = randomGet64() % std::size(m_entries);
|
||||||
|
auto& e = m_entries[random_index];
|
||||||
|
LoadControlEntry(e, true);
|
||||||
|
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
"Launch "_i18n + e.GetName(),
|
||||||
|
"Back"_i18n, "Launch"_i18n, 1, [this, &e](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
LaunchEntry(e);
|
||||||
|
}
|
||||||
|
}, e.image
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||||
|
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
buf,
|
||||||
|
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
const auto rc = nsDeleteApplicationCompletely(m_entries[m_index].app_id);
|
||||||
|
if (R_SUCCEEDED(Notify(rc, "Failed to delete application"))) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Enable auto delete"_i18n, [this](){
|
||||||
|
const auto rc = nsEnableApplicationAutoDelete(m_entries[m_index].app_id);
|
||||||
|
Notify(rc, "Failed to enable auto delete");
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Disable auto delete"_i18n, [this](){
|
||||||
|
const auto rc = nsDisableApplicationAutoDelete(m_entries[m_index].app_id);
|
||||||
|
Notify(rc, "Failed to disable auto delete");
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Withdraw update request"_i18n, [this](){
|
||||||
|
const auto rc = nsWithdrawApplicationUpdateRequest(m_entries[m_index].app_id);
|
||||||
|
Notify(rc, "Failed to withdraw update request");
|
||||||
|
}));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, 110, 370, 155};
|
||||||
|
const Vec2 pad{10, 10};
|
||||||
|
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||||
|
nsInitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
FreeEntries();
|
||||||
|
nsExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||||
|
if (touch && m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
// max images per frame, in order to not hit io / gpu too hard.
|
||||||
|
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) {
|
||||||
|
const auto& [x, y, w, h] = v;
|
||||||
|
auto& e = m_entries[pos];
|
||||||
|
|
||||||
|
if (e.status == NacpLoadStatus::None) {
|
||||||
|
LoadControlEntry(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy load image
|
||||||
|
if (image_load_count < image_load_max) {
|
||||||
|
if (LoadControlImage(e)) {
|
||||||
|
image_load_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
if (pos == m_index) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
DrawElement(v, ThemeEntryID_GRID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float image_size = 115;
|
||||||
|
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 5);
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||||
|
{
|
||||||
|
const float font_size = 18;
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
|
||||||
|
}
|
||||||
|
nvgRestore(vg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
ScanHomebrew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SetIndex(s64 index) {
|
||||||
|
m_index = index;
|
||||||
|
if (!m_index) {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: set subheadering.
|
||||||
|
char title_id[33];
|
||||||
|
std::snprintf(title_id, sizeof(title_id), "%016lX", m_entries[m_index].app_id);
|
||||||
|
SetTitleSubHeading(title_id);
|
||||||
|
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::ScanHomebrew() {
|
||||||
|
constexpr auto ENTRY_CHUNK_COUNT = 1000;
|
||||||
|
TimeStamp ts;
|
||||||
|
|
||||||
|
FreeEntries();
|
||||||
|
m_entries.reserve(ENTRY_CHUNK_COUNT);
|
||||||
|
|
||||||
|
std::vector<NsApplicationRecord> record_list(ENTRY_CHUNK_COUNT);
|
||||||
|
s32 offset{};
|
||||||
|
while (true) {
|
||||||
|
s32 record_count{};
|
||||||
|
if (R_FAILED(nsListApplicationRecord(record_list.data(), record_list.size(), offset, &record_count))) {
|
||||||
|
log_write("failed to list application records at offset: %d\n", offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// finished parsing all entries.
|
||||||
|
if (!record_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (s32 i = 0; i < record_count; i++) {
|
||||||
|
// log_write("ID: %016lx got type: %u\n", record_list[i].application_id, record_list[i].type);
|
||||||
|
m_entries.emplace_back(record_list[i].application_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += record_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
|
||||||
|
SetIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::FreeEntries() {
|
||||||
|
auto vg = App::GetVg();
|
||||||
|
|
||||||
|
for (auto&p : m_entries) {
|
||||||
|
FreeEntry(vg, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::game
|
||||||
@@ -24,6 +24,11 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FreeEntry(NVGcontext* vg, NroEntry& e) {
|
||||||
|
nvgDeleteImage(vg, e.image);
|
||||||
|
e.image = 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||||
@@ -80,6 +85,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
|||||||
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
|
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
|
||||||
if (op_index && *op_index) {
|
if (op_index && *op_index) {
|
||||||
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) {
|
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);
|
m_entries.erase(m_entries.begin() + m_index);
|
||||||
SetIndex(m_index ? m_index - 1 : 0);
|
SetIndex(m_index ? m_index - 1 : 0);
|
||||||
}
|
}
|
||||||
@@ -114,11 +120,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Menu::~Menu() {
|
Menu::~Menu() {
|
||||||
auto vg = App::GetVg();
|
FreeEntries();
|
||||||
|
|
||||||
for (auto&p : m_entries) {
|
|
||||||
nvgDeleteImage(vg, p.image);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
@@ -238,6 +240,7 @@ void Menu::InstallHomebrew() {
|
|||||||
|
|
||||||
void Menu::ScanHomebrew() {
|
void Menu::ScanHomebrew() {
|
||||||
TimeStamp ts;
|
TimeStamp ts;
|
||||||
|
FreeEntries();
|
||||||
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
|
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
|
||||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
||||||
|
|
||||||
@@ -394,6 +397,16 @@ void Menu::SortAndFindLastFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Menu::FreeEntries() {
|
||||||
|
auto vg = App::GetVg();
|
||||||
|
|
||||||
|
for (auto&p : m_entries) {
|
||||||
|
FreeEntry(vg, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
|
Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
|
||||||
OwoConfig config{};
|
OwoConfig config{};
|
||||||
config.nro_path = path.toString();
|
config.nro_path = path.toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user