From 3545f557fcfbb1e6efdb11e5ad1267150b1d8a8b Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Thu, 1 May 2025 15:14:50 +0100 Subject: [PATCH] add scrolling text to popup list, handling clipping inside scrolling text, game menu changes - added delete entity in game menu - added list meta records to game menu --- sphaira/include/ui/popup_list.hpp | 2 + sphaira/include/yati/nx/ncm.hpp | 3 + sphaira/source/ui/error_box.cpp | 2 +- sphaira/source/ui/menus/appstore.cpp | 13 ++-- sphaira/source/ui/menus/game_menu.cpp | 96 ++++++++++++++++++++++++--- sphaira/source/ui/menus/homebrew.cpp | 25 +++---- sphaira/source/ui/menus/themezer.cpp | 11 +-- sphaira/source/ui/popup_list.cpp | 7 +- sphaira/source/ui/scrolling_text.cpp | 15 ++++- sphaira/source/yati/nx/ncm.cpp | 32 +++++++++ 10 files changed, 157 insertions(+), 49 deletions(-) diff --git a/sphaira/include/ui/popup_list.hpp b/sphaira/include/ui/popup_list.hpp index 2fe3e72..272dd2c 100644 --- a/sphaira/include/ui/popup_list.hpp +++ b/sphaira/include/ui/popup_list.hpp @@ -1,6 +1,7 @@ #pragma once #include "ui/widget.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include @@ -39,6 +40,7 @@ private: s64 m_starting_index{}; std::unique_ptr m_list{}; + ScrollingText m_scroll_text{}; float m_yoff{}; float m_line_top{}; diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp index 5efcd0e..1287bd1 100644 --- a/sphaira/include/yati/nx/ncm.hpp +++ b/sphaira/include/yati/nx/ncm.hpp @@ -31,6 +31,9 @@ union ExtendedHeader { NcmDataPatchMetaExtendedHeader data_patch; }; +auto GetMetaTypeStr(u8 meta_type) -> const char*; +auto GetStorageIdStr(u8 storage_id) -> const char*; + auto GetAppId(u8 meta_type, u64 id) -> u64; auto GetAppId(const NcmContentMetaKey& key) -> u64; auto GetAppId(const PackagedContentMeta& meta) -> u64; diff --git a/sphaira/source/ui/error_box.cpp b/sphaira/source/ui/error_box.cpp index cbd0b25..750c0fb 100644 --- a/sphaira/source/ui/error_box.cpp +++ b/sphaira/source/ui/error_box.cpp @@ -37,7 +37,7 @@ auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void { gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140"); if (m_code.has_value()) { const auto code = m_code.value(); - gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Error code: 0x%X Module: %u Description: 0x%X Value: 0x%X", code, R_MODULE(code), R_DESCRIPTION(code), R_VALUE(code)); + gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X Value: 0x%X", code, R_MODULE(code), R_DESCRIPTION(code), R_VALUE(code)); } else { gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "An error occurred"_i18n.c_str()); } diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 9528b2c..a0c7663 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -1044,15 +1044,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto text_off = 148; const auto text_x = x + text_off; const auto text_clip_w = w - 30.f - text_off; - nvgSave(vg); - nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip - { - const float font_size = 18; - m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str()); - m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str()); - m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str()); - } - nvgRestore(vg); + const float font_size = 18; + m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str()); float i_size = 22; switch (e.status) { diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 7bb73df..cba9db5 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -6,9 +6,11 @@ #include "ui/error_box.hpp" #include "ui/option_box.hpp" #include "ui/progress_box.hpp" +#include "ui/popup_list.hpp" #include "ui/nvg_util.hpp" #include "defines.hpp" #include "i18n.hpp" +#include "yati/nx/ncm.hpp" #include #include @@ -16,6 +18,8 @@ namespace sphaira::ui::menu::game { namespace { +using MetaEntries = std::vector; + Result Notify(Result rc, const std::string& error_message) { if (R_FAILED(rc)) { App::Push(std::make_shared(rc, @@ -28,6 +32,17 @@ Result Notify(Result rc, const std::string& error_message) { return rc; } +Result GetMetaEntries(const Entry& e, MetaEntries& out) { + s32 count; + R_TRY(nsCountApplicationContentMeta(e.app_id, &count)); + + out.resize(count); + R_TRY(nsListApplicationContentMetaStatus(e.app_id, 0, out.data(), out.size(), &count)); + + out.resize(count); + R_SUCCEED(); +} + // also sets the status to error. void FakeNacpEntry(Entry& e) { e.status = NacpLoadStatus::Error; @@ -89,10 +104,13 @@ Menu::Menu() : MenuBase{"Games"_i18n} { SetPop(); }}), std::make_pair(Button::A, Action{"Launch"_i18n, [this](){ + if (m_entries.empty()) { + return; + } LaunchEntry(m_entries[m_index]); }}), std::make_pair(Button::X, Action{"Options"_i18n, [this](){ - auto options = std::make_shared("Homebrew Options"_i18n, Sidebar::Side::RIGHT); + auto options = std::make_shared("Game Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(options)); if (m_entries.size()) { @@ -117,6 +135,40 @@ Menu::Menu() : MenuBase{"Games"_i18n} { )); })); + options->Add(std::make_shared("List meta records"_i18n, [this](){ + MetaEntries meta_entries; + const auto rc = GetMetaEntries(m_entries[m_index], meta_entries); + if (R_FAILED(rc)) { + App::Push(std::make_shared(rc, + i18n::get("Failed to list application meta entries") + )); + return; + } + + if (meta_entries.empty()) { + App::Notify("No meta entries found...\n"); + return; + } + + PopupList::Items items; + for (auto& e : meta_entries) { + char buf[256]; + std::snprintf(buf, sizeof(buf), "Type: %s Storage: %s [%016lX][v%u]", ncm::GetMetaTypeStr(e.meta_type), ncm::GetStorageIdStr(e.storageID), e.application_id, e.version); + items.emplace_back(buf); + } + + App::Push(std::make_shared( + "Entries", items, [this, meta_entries](auto op_index){ + #if 0 + if (op_index) { + const auto& e = meta_entries[*op_index]; + } + #endif + } + )); + })); + + // completely deletes the application record and all data. options->Add(std::make_shared("Delete"_i18n, [this](){ const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?"; App::Push(std::make_shared( @@ -133,6 +185,20 @@ Menu::Menu() : MenuBase{"Games"_i18n} { }, m_entries[m_index].image )); }, true)); + + // removes installed data but keeps the record, basically archiving. + options->Add(std::make_shared("Delete entity"_i18n, [this](){ + const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?"; + App::Push(std::make_shared( + buf, + "Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){ + if (op_index && *op_index) { + const auto rc = nsDeleteApplicationEntity(m_entries[m_index].app_id); + Notify(rc, "Failed to delete application"); + } + }, m_entries[m_index].image + )); + }, true)); } }}) ); @@ -197,15 +263,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto text_off = 148; const auto text_x = x + text_off; const auto text_clip_w = w - 30.f - text_off; - nvgSave(vg); - nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip - { - const float font_size = 18; - m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName()); - m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); - m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); - } - nvgRestore(vg); + const float font_size = 18; + m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); }); } @@ -249,7 +310,20 @@ void Menu::ScanHomebrew() { } 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); + #if 0 + u8 unk_x09 = record_list[i].unk_x09; + u64 unk_x0a;// = record_list[i].unk_x0a; + u8 unk_x10 = record_list[i].unk_x10; + u64 unk_x11;// = record_list[i].unk_x11; + memcpy(&unk_x0a, record_list[i].unk_x0a, sizeof(record_list[i].unk_x0a)); + memcpy(&unk_x11, record_list[i].unk_x11, sizeof(record_list[i].unk_x11)); + log_write("ID: %016lx got type: %u unk_x09: %u unk_x0a: %zu unk_x10: %u unk_x11: %zu\n", record_list[i].application_id, record_list[i].type, + unk_x09, + unk_x0a, + unk_x10, + unk_x11 + ); + #endif m_entries.emplace_back(record_list[i].application_id); } diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index cc57c9b..29c6521 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -176,23 +176,18 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto text_off = 148; const auto text_x = x + text_off; const auto text_clip_w = w - 30.f - text_off; - nvgSave(vg); - nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip - { - bool has_star = false; - if (IsStarEnabled()) { - if (!e.has_star.has_value()) { - e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path)); - } - has_star = e.has_star.value(); + bool has_star = false; + if (IsStarEnabled()) { + if (!e.has_star.has_value()) { + e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path)); } - - const float font_size = 18; - m_scroll_name.DrawArgs(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName()); - m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); - m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); + has_star = e.has_star.value(); } - nvgRestore(vg); + + const float font_size = 18; + m_scroll_name.DrawArgs(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); }); } diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 04c634c..103de56 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -610,14 +610,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto text_x = x + xoff; const auto text_clip_w = w - 30.f - xoff; - nvgSave(vg); - nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip - { - const float font_size = 18; - m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str()); - m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str()); - } - nvgRestore(vg); + const float font_size = 18; + m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str()); + m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str()); }); } diff --git a/sphaira/source/ui/popup_list.cpp b/sphaira/source/ui/popup_list.cpp index 9e34c7b..68c8f01 100644 --- a/sphaira/source/ui/popup_list.cpp +++ b/sphaira/source/ui/popup_list.cpp @@ -113,7 +113,8 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void { m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) { const auto& [x, y, w, h] = v; auto colour = ThemeEntryID_TEXT; - if (m_index == i) { + const auto selected = m_index == i; + if (selected) { gfx::drawRectOutline(vg, theme, 4.f, v); } else { if (i != m_items.size() - 1) { @@ -126,7 +127,9 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void { gfx::drawText(vg, x + w - m_text_xoffset, y + (h / 2.f), 20.f, "\uE14B", NULL, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(colour)); } - gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour)); + const auto text_x = x + m_text_xoffset; + const auto text_clip_w = w - 60.f - m_text_xoffset; + m_scroll_text.Draw(vg, selected, text_x, y + (h / 2.f), text_clip_w, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), m_items[i]); }); Widget::Draw(vg, theme); diff --git a/sphaira/source/ui/scrolling_text.cpp b/sphaira/source/ui/scrolling_text.cpp index 5854702..f51dac1 100644 --- a/sphaira/source/ui/scrolling_text.cpp +++ b/sphaira/source/ui/scrolling_text.cpp @@ -14,11 +14,20 @@ auto GetTextScrollSpeed() -> float { } } +void DrawClipped(NVGcontext* vg, const Vec4& clip, float x, float y, float size, int align, const NVGcolor& colour, const std::string& str) { + nvgSave(vg); + nvgIntersectScissor(vg, clip.x, clip.y, clip.w, clip.h); // clip + gfx::drawText(vg, x, y, size, colour, str.c_str(), align); + nvgRestore(vg); +} + } // namespace void ScrollingText::Draw(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry) { + const Vec4 clip{x, 0, w, 720}; + if (!focus) { - gfx::drawText(vg, x, y, size, colour, text_entry.c_str(), align); + DrawClipped(vg, clip, x, y, size, align, colour, text_entry); return; } @@ -56,8 +65,8 @@ void ScrollingText::Draw(NVGcontext* vg, bool focus, float x, float y, float w, } } - const Vec2 pos{x - m_text_xoff, y}; - gfx::drawText(vg, pos, size, colour, value_str.c_str(), align); + x -= m_text_xoff; + DrawClipped(vg, clip, x, y, size, align, colour, value_str); } void ScrollingText::DrawArgs(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) { diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp index ba6ecf9..2013ded 100644 --- a/sphaira/source/yati/nx/ncm.cpp +++ b/sphaira/source/yati/nx/ncm.cpp @@ -7,6 +7,38 @@ namespace { } // namespace +auto GetMetaTypeStr(u8 meta_type) -> const char* { + switch (meta_type) { + case NcmContentMetaType_Unknown: return "Unknown"; + case NcmContentMetaType_SystemProgram: return "SystemProgram"; + case NcmContentMetaType_SystemData: return "SystemData"; + case NcmContentMetaType_SystemUpdate: return "SystemUpdate"; + case NcmContentMetaType_BootImagePackage: return "BootImagePackage"; + case NcmContentMetaType_BootImagePackageSafe: return "BootImagePackageSafe"; + case NcmContentMetaType_Application: return "Application"; + case NcmContentMetaType_Patch: return "Patch"; + case NcmContentMetaType_AddOnContent: return "AddOnContent"; + case NcmContentMetaType_Delta: return "Delta"; + case NcmContentMetaType_DataPatch: return "DataPatch"; + } + + return "Unknown"; +} + +auto GetStorageIdStr(u8 storage_id) -> const char* { + switch (storage_id) { + case NcmStorageId_None: return "None"; + case NcmStorageId_Host: return "Host"; + case NcmStorageId_GameCard: return "GameCard"; + case NcmStorageId_BuiltInSystem: return "BuiltInSystem"; + case NcmStorageId_BuiltInUser: return "BuiltInUser"; + case NcmStorageId_SdCard: return "SdCard"; + case NcmStorageId_Any: return "Any"; + } + + return "Unknown"; +} + auto GetAppId(u8 meta_type, u64 id) -> u64 { if (meta_type == NcmContentMetaType_Patch) { return id ^ 0x800;