public release

This commit is contained in:
ITotalJustice
2024-12-16 21:13:05 +00:00
commit 0370e47f7f
248 changed files with 20513 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
#include "ui/menus/file_viewer.hpp"
namespace sphaira::ui::menu::fileview {
namespace {
} // namespace
Menu::Menu(const fs::FsPath& path) : MenuBase{path}, m_path{path} {
SetAction(Button::B, Action{"Back", [this](){
SetPop();
}});
std::string buf;
if (R_SUCCEEDED(m_fs.OpenFile(m_path, FsOpenMode_Read, &m_file))) {
fsFileGetSize(&m_file, &m_file_size);
buf.resize(m_file_size + 1);
u64 read_bytes;
fsFileRead(&m_file, m_file_offset, buf.data(), buf.size(), 0, &read_bytes);
buf[m_file_size] = '\0';
}
m_scroll_text = std::make_unique<ScrollableText>(buf, 0, 120, 500, 1150-110, 18);
}
Menu::~Menu() {
fsFileClose(&m_file);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_scroll_text->Update(controller, touch);
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
m_scroll_text->Draw(vg, theme);
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::fileview

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "ui/menus/homebrew.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 "owo.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include <minIni.h>
#include <utility>
namespace sphaira::ui::menu::homebrew {
namespace {
constexpr const char* SORT_STR[] = {
"Updated",
"Size",
"Alphabetical",
};
constexpr const char* ORDER_STR[] = {
"Desc",
"Asc",
};
// returns seconds as: hh:mm:ss
auto TimeFormat(u64 sec) -> std::string {
char buf[9];
const auto s = sec % 60;
const auto h = sec / 60 % 60;
const auto d = sec / 60 / 60 % 24;
if (sec < 60) {
if (!sec) {
return "00:00:00";
}
std::snprintf(buf, sizeof(buf), "00:00:%02lu", s);
} else if (sec < 3600) {
std::snprintf(buf, sizeof(buf), "00:%02lu:%02lu", h, s);
} else {
std::snprintf(buf, sizeof(buf), "%02lu:%02lu:%02lu", d, h, s);
}
return std::string{buf};
}
} // namespace
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_index < (m_entries.size() - 1)) {
if (m_index < (m_entries.size() - 3)) {
SetIndex(m_index + 3);
App::PlaySoundEffect(SoundEffect_Scroll);
} else {
SetIndex(m_entries.size() - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
}
if (m_index - m_start >= 9) {
log_write("moved down\n");
m_start += 3;
}
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_index >= 3) {
SetIndex(m_index - 3);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_start ) {
// log_write("moved up\n");
m_start -= 3;
}
}
}}),
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
nro_launch(m_entries[m_index].path);
}}),
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()) {
options->Add(std::make_shared<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Sort Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items sort_items;
sort_items.push_back("Updated"_i18n);
sort_items.push_back("Size"_i18n);
sort_items.push_back("Alphabetical"_i18n);
SidebarEntryArray::Items order_items;
order_items.push_back("Decending"_i18n);
order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
}, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
m_order.Set(index_out);
SortAndFindLastFile();
}, m_order.Get()));
}));
#if 0
options->Add(std::make_shared<SidebarEntryCallback>("Info"_i18n, [this](){
}));
#endif
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].path.toString() + "?";
App::Push(std::make_shared<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))) {
m_entries.erase(m_entries.begin() + m_index);
SetIndex(m_index ? m_index - 1 : 0);
}
}
}
));
}, true));
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
m_hide_sphaira.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
App::Push(std::make_shared<OptionBox>(
"WARNING: Installing forwarders will lead to a ban!"_i18n,
"Back"_i18n, "Install"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
InstallHomebrew();
}
}
));
}, true));
}
}})
);
}
Menu::~Menu() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
nvgDeleteImage(vg, p.image);
}
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = m_entries.size();
const u64 cursor_pos = m_index;
// only draw scrollbar if needed
if (nro_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
auto& e = m_entries[pos];
// lazy load image
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) {
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
if (!e.icon.empty()) {
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size());
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image);
nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, h); // clip
{
const float font_size = 18;
const float diff = 32;
#if 1
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.GetName());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
#else
gfx::drawTextArgs(vg, x + 148, y + 35, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.GetName());
gfx::drawTextArgs(vg, x + 148, y + 35 + (diff * 1), font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
gfx::drawTextArgs(vg, x + 148, y + 35 + (diff * 2), font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
// gfx::drawTextArgs(vg, x + 148, y + 110, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "PlayCount: %u", e.hbini.launch_count);
#endif
}
nvgRestore(vg);
}
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
if (m_entries.empty()) {
ScanHomebrew();
}
}
void Menu::SetIndex(std::size_t index) {
m_index = index;
if (!m_index) {
m_start = 0;
}
const auto& e = m_entries[m_index];
// TimeCalendarTime caltime;
// timeToCalendarTimeWithMyRule()
// 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()));
}
void Menu::InstallHomebrew() {
const auto& nro = m_entries[m_index];
OwoConfig config{};
config.nro_path = nro.path.toString();
config.nacp = nro.nacp;
config.icon = nro.icon;
App::Install(config);
}
void Menu::ScanHomebrew() {
TimeStamp ts;
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSeconds());
// todo: optimise this. maybe create a file per entry
// which would speed up parsing
for (auto& e : m_entries) {
if (ini_hassection(e.path, App::PLAYLOG_PATH)) {
// log_write("has section for: %s\n", e.path);
e.hbini.timestamp = ini_getl(e.path, "timestamp", 0, App::PLAYLOG_PATH);
}
e.image = 0; // images are lazy loaded
}
#if 0
struct IniUser {
std::vector<NroEntry>& entires;
Hbini* ini;
std::string last_section;
} ini_user { m_entries };
ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto user = static_cast<IniUser*>(UserData);
if (user->last_section != Section) {
user->last_section = Section;
user->ini = nullptr;
for (auto& e : user->entires) {
if (e.path == Section) {
user->ini = &e.hbini;
break;
}
}
}
if (user->ini) {
}
// app->
log_write("found: %s %s %s\n", Section, Key, Value);
return 1;
}, &ini_user, App::PLAYLOG_PATH);
#endif
this->Sort();
SetIndex(0);
}
void Menu::Sort() {
// returns true if lhs should be before rhs
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 {
switch (sort) {
case SortType_Updated: {
auto lhs_timestamp = lhs.hbini.timestamp;
auto rhs_timestamp = rhs.hbini.timestamp;
if (lhs.timestamp.is_valid && lhs_timestamp < lhs.timestamp.modified) {
lhs_timestamp = lhs.timestamp.modified;
}
if (rhs.timestamp.is_valid && rhs_timestamp < rhs.timestamp.modified) {
rhs_timestamp = rhs.timestamp.modified;
}
if (lhs_timestamp == rhs_timestamp) {
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
} else if (order == OrderType_Decending) {
return lhs_timestamp > rhs_timestamp;
} else {
return lhs_timestamp < rhs_timestamp;
}
} break;
case SortType_Size: {
if (lhs.size == rhs.size) {
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
} else if (order == OrderType_Decending) {
return lhs.size > rhs.size;
} else {
return lhs.size < rhs.size;
}
} break;
case SortType_Alphabetical: {
if (order == OrderType_Decending) {
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
} else {
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
}
} break;
}
std::unreachable();
};
std::sort(m_entries.begin(), m_entries.end(), sorter);
}
void Menu::SortAndFindLastFile() {
Sort();
}
} // namespace sphaira::ui::menu::homebrew

View File

@@ -0,0 +1,533 @@
#include "ui/menus/irs_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
#include <array>
namespace sphaira::ui::menu::irs {
namespace {
// from trial and error
constexpr u32 GAIN_MIN = 1;
constexpr u32 GAIN_MAX = 16;
consteval auto generte_iron_palette_table() {
std::array<u32, 256> array{};
const u32 iron_palette[] = {
0xff000014, 0xff000025, 0xff00002a, 0xff000032, 0xff000036, 0xff00003e, 0xff000042, 0xff00004f,
0xff010055, 0xff010057, 0xff02005c, 0xff03005e, 0xff040063, 0xff050065, 0xff070069, 0xff0a0070,
0xff0b0073, 0xff0d0075, 0xff0d0076, 0xff100078, 0xff120079, 0xff15007c, 0xff17007d, 0xff1c0081,
0xff200084, 0xff220085, 0xff260087, 0xff280089, 0xff2c008a, 0xff2e008b, 0xff32008d, 0xff38008f,
0xff390090, 0xff3c0092, 0xff3e0093, 0xff410094, 0xff420095, 0xff450096, 0xff470096, 0xff4c0097,
0xff4f0097, 0xff510097, 0xff540098, 0xff560098, 0xff5a0099, 0xff5c0099, 0xff5f009a, 0xff64009b,
0xff66009b, 0xff6a009b, 0xff6c009c, 0xff6f009c, 0xff70009c, 0xff73009d, 0xff75009d, 0xff7a009d,
0xff7e009d, 0xff7f009d, 0xff83009d, 0xff84009d, 0xff87009d, 0xff89009d, 0xff8b009d, 0xff91009c,
0xff93009c, 0xff96009b, 0xff98009b, 0xff9b009b, 0xff9c009b, 0xff9f009b, 0xffa0009b, 0xffa4009b,
0xffa7009a, 0xffa8009a, 0xffaa0099, 0xffab0099, 0xffae0198, 0xffaf0198, 0xffb00198, 0xffb30196,
0xffb40296, 0xffb60295, 0xffb70395, 0xffb90495, 0xffba0495, 0xffbb0593, 0xffbc0593, 0xffbf0692,
0xffc00791, 0xffc00791, 0xffc10990, 0xffc20a8f, 0xffc30b8e, 0xffc40c8d, 0xffc60d8b, 0xffc81088,
0xffc91187, 0xffca1385, 0xffcb1385, 0xffcc1582, 0xffcd1681, 0xffce187e, 0xffcf187c, 0xffd11b78,
0xffd21c75, 0xffd21d74, 0xffd32071, 0xffd4216f, 0xffd5236b, 0xffd52469, 0xffd72665, 0xffd92a60,
0xffda2b5e, 0xffdb2e5a, 0xffdb2f57, 0xffdd3051, 0xffdd314e, 0xffde3347, 0xffdf3444, 0xffe0373a,
0xffe03933, 0xffe13a30, 0xffe23c2a, 0xffe33d26, 0xffe43f20, 0xffe4411d, 0xffe5431b, 0xffe64616,
0xffe74715, 0xffe74913, 0xffe84a12, 0xffe84c0f, 0xffe94d0e, 0xffea4e0c, 0xffea4f0c, 0xffeb520a,
0xffec5409, 0xffec5608, 0xffec5808, 0xffed5907, 0xffed5b06, 0xffee5c06, 0xffee5d05, 0xffef6004,
0xffef6104, 0xfff06303, 0xfff06403, 0xfff16603, 0xfff16603, 0xfff16803, 0xfff16902, 0xfff16b02,
0xfff26d01, 0xfff26e01, 0xfff37001, 0xfff37101, 0xfff47300, 0xfff47400, 0xfff47600, 0xfff47a00,
0xfff57b00, 0xfff57e00, 0xfff57f00, 0xfff68100, 0xfff68200, 0xfff78400, 0xfff78500, 0xfff88800,
0xfff88900, 0xfff88a00, 0xfff88c00, 0xfff98d00, 0xfff98e00, 0xfff98f00, 0xfff99100, 0xfffa9400,
0xfffa9500, 0xfffb9800, 0xfffb9900, 0xfffb9c00, 0xfffc9d00, 0xfffca000, 0xfffca100, 0xfffda400,
0xfffda700, 0xfffda800, 0xfffdab00, 0xfffdac00, 0xfffdae00, 0xfffeaf00, 0xfffeb100, 0xfffeb400,
0xfffeb500, 0xfffeb800, 0xfffeb900, 0xfffeba00, 0xfffebb00, 0xfffebd00, 0xfffebe00, 0xfffec200,
0xfffec400, 0xfffec500, 0xfffec700, 0xfffec800, 0xfffeca01, 0xfffeca01, 0xfffecc02, 0xfffecf04,
0xfffecf04, 0xfffed106, 0xfffed308, 0xfffed50a, 0xfffed60a, 0xfffed80c, 0xfffed90d, 0xffffdb10,
0xffffdc14, 0xffffdd16, 0xffffde1b, 0xffffdf1e, 0xffffe122, 0xffffe224, 0xffffe328, 0xffffe531,
0xffffe635, 0xffffe73c, 0xffffe83f, 0xffffea46, 0xffffeb49, 0xffffec50, 0xffffed54, 0xffffee5f,
0xffffef67, 0xfffff06a, 0xfffff172, 0xfffff177, 0xfffff280, 0xfffff285, 0xfffff38e, 0xfffff49a,
0xfffff59e, 0xfffff5a6, 0xfffff6aa, 0xfffff7b3, 0xfffff7b6, 0xfffff8bd, 0xfffff8c1, 0xfffff9ca,
0xfffffad1, 0xfffffad4, 0xfffffcdb, 0xfffffcdf, 0xfffffde5, 0xfffffde8, 0xfffffeee, 0xfffffff6
};
for (u32 i = 0; i < 256; i++) {
const auto c = iron_palette[i];
array[i] = RGBA8_MAXALPHA((c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF);
}
return array;
}
// ARGB Ironbow palette
constexpr auto iron_palette = generte_iron_palette_table();
void irsConvertConfigExToNormal(const IrsImageTransferProcessorExConfig* ex, IrsImageTransferProcessorConfig* nor) {
std::memcpy(nor, ex, sizeof(*nor));
}
void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsImageTransferProcessorExConfig* ex) {
std::memcpy(ex, nor, sizeof(*nor));
}
} // namespace
Menu::Menu() : MenuBase{"Irs"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items controller_str;
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
const auto& e = m_entries[i];
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
switch (e.status) {
case IrsIrCameraStatus_Available:
text += " (Available)"_i18n;
break;
case IrsIrCameraStatus_Unsupported:
text += " (Unsupported)"_i18n;
break;
case IrsIrCameraStatus_Unconnected:
text += " (Unconnected)"_i18n;
break;
}
controller_str.emplace_back(text);
}
SidebarEntryArray::Items rotation_str;
rotation_str.emplace_back("0 (Sideways)"_i18n);
rotation_str.emplace_back("90 (Flat)"_i18n);
rotation_str.emplace_back("180 (-Sideways)"_i18n);
rotation_str.emplace_back("270 (Upside down)"_i18n);
SidebarEntryArray::Items colour_str;
colour_str.emplace_back("Grey"_i18n);
colour_str.emplace_back("Ironbow"_i18n);
colour_str.emplace_back("Green"_i18n);
colour_str.emplace_back("Red"_i18n);
colour_str.emplace_back("Blue"_i18n);
SidebarEntryArray::Items light_target_str;
light_target_str.emplace_back("All leds"_i18n);
light_target_str.emplace_back("Bright group"_i18n);
light_target_str.emplace_back("Dim group"_i18n);
light_target_str.emplace_back("None"_i18n);
SidebarEntryArray::Items gain_str;
for (u32 i = GAIN_MIN; i <= GAIN_MAX; i++) {
gain_str.emplace_back(std::to_string(i));
}
SidebarEntryArray::Items is_negative_image_used_str;
is_negative_image_used_str.emplace_back("Normal image"_i18n);
is_negative_image_used_str.emplace_back("Negative image"_i18n);
SidebarEntryArray::Items format_str;
format_str.emplace_back("320x240"_i18n);
format_str.emplace_back("160x120"_i18n);
format_str.emplace_back("80x60"_i18n);
if (hosversionAtLeast(4,0,0)) {
format_str.emplace_back("40x30"_i18n);
format_str.emplace_back("20x15"_i18n);
}
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](std::size_t& index){
irsStopImageProcessor(m_entries[m_index].m_handle);
m_index = index;
UpdateConfig(&m_config);
}, m_index));
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](std::size_t& index){
m_rotation = (Rotation)index;
}, m_rotation));
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](std::size_t& index){
m_colour = (Colour)index;
updateColourArray();
}, m_colour));
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](std::size_t& index){
m_config.light_target = index;
UpdateConfig(&m_config);
}, m_config.light_target));
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](std::size_t& index){
m_config.gain = GAIN_MIN + index;
UpdateConfig(&m_config);
}, m_config.gain - GAIN_MIN));
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](std::size_t& index){
m_config.is_negative_image_used = index;
UpdateConfig(&m_config);
}, m_config.is_negative_image_used));
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](std::size_t& index){
m_config.orig_format = index;
m_config.trimming_format = index;
UpdateConfig(&m_config);
}, m_config.orig_format));
if (hosversionAtLeast(4,0,0)) {
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](std::size_t& index){
// you cannot set trim a larger region than the source
if (index < m_config.orig_format) {
index = m_config.orig_format;
} else {
m_config.trimming_format = index;
UpdateConfig(&m_config);
}
}, m_config.orig_format));
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
m_config.is_external_light_filter_enabled = enable;
UpdateConfig(&m_config);
}, "Enabled"_i18n, "Disabled"_i18n));
}
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
LoadDefaultConfig();
}, true));
}});
if (R_FAILED(m_init_rc = irsInitialize())) {
return;
}
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
// open all handles
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
irsGetIrCameraHandle(&m_entries[1].m_handle, HidNpadIdType_No2);
irsGetIrCameraHandle(&m_entries[2].m_handle, HidNpadIdType_No3);
irsGetIrCameraHandle(&m_entries[3].m_handle, HidNpadIdType_No4);
irsGetIrCameraHandle(&m_entries[4].m_handle, HidNpadIdType_No5);
irsGetIrCameraHandle(&m_entries[5].m_handle, HidNpadIdType_No6);
irsGetIrCameraHandle(&m_entries[6].m_handle, HidNpadIdType_No7);
irsGetIrCameraHandle(&m_entries[7].m_handle, HidNpadIdType_No8);
irsGetIrCameraHandle(&m_entries[8].m_handle, HidNpadIdType_Handheld);
// get status of all handles
PollCameraStatus(true);
// load default config
LoadDefaultConfig();
}
Menu::~Menu() {
ResetImage();
for (auto& e : m_entries) {
irsStopImageProcessor(e.m_handle);
}
// this closes all handles
irsExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
PollCameraStatus();
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
IrsImageTransferProcessorState state;
const auto rc = irsGetImageTransferProcessorState(m_entries[m_index].m_handle, m_irs_buffer.data(), m_irs_buffer.size(), &state);
if (R_SUCCEEDED(rc) && state.sampling_number != m_prev_state.sampling_number) {
m_prev_state = state;
SetSubHeading("Ambient Noise Level: " + std::to_string(m_prev_state.ambient_noise_level));
updateColourArray();
}
if (m_image) {
float cx{}, cy{};
float w{}, h{};
float angle{};
switch (m_rotation) {
case Rotation_0: {
const auto scale_x = m_pos.w / float(m_irs_width);
const auto scale_y = m_pos.h / float(m_irs_height);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.0) - w / 2.0;
cy = (m_pos.y + m_pos.h / 2.0) - h / 2.0;
angle = 0;
} break;
case Rotation_90: {
const auto scale_x = m_pos.w / float(m_irs_height);
const auto scale_y = m_pos.h / float(m_irs_width);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.0) + h / 2.0;
cy = (m_pos.y + m_pos.h / 2.0) - w / 2.0;
angle = 90;
} break;
case Rotation_180: {
const auto scale_x = m_pos.w / float(m_irs_width);
const auto scale_y = m_pos.h / float(m_irs_height);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.0) + w / 2.0;
cy = (m_pos.y + m_pos.h / 2.0) + h / 2.0;
angle = 180;
} break;
case Rotation_270: {
const auto scale_x = m_pos.w / float(m_irs_height);
const auto scale_y = m_pos.h / float(m_irs_width);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.0) - h / 2.0;
cy = (m_pos.y + m_pos.h / 2.0) + w / 2.0;
angle = 270;
} break;
}
nvgSave(vg);
nvgTranslate(vg, cx, cy);
const auto paint = nvgImagePattern(vg, 0, 0, w, h, 0, m_image, 1.f);
nvgRotate(vg, nvgDegToRad(angle));
nvgBeginPath(vg);
nvgRect(vg, 0, 0, w, h);
nvgFillPaint(vg, paint);
nvgFill(vg);
nvgRestore(vg);
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
void Menu::PollCameraStatus(bool statup) {
int index = 0;
for (auto& e : m_entries) {
IrsIrCameraStatus status;
if (R_FAILED(irsGetIrCameraStatus(e.m_handle, &status))) {
log_write("failed to get ir status\n");
continue;
}
if (e.status != status || statup) {
e.status = status;
e.m_update_needed = false;
log_write("status changed\n");
switch (e.status) {
case IrsIrCameraStatus_Available:
if (hosversionAtLeast(4,0,0)) {
// calling this breaks the handle, kinda
#if 0
if (R_FAILED(irsCheckFirmwareUpdateNecessity(e.m_handle, &e.m_update_needed))) {
log_write("failed to check if update needed: %u\n", e.m_update_needed);
} else {
if (e.m_update_needed) {
log_write("update needed\n");
} else {
log_write("no update needed\n");
}
}
#endif
}
log_write("irs index: %d status: IrsIrCameraStatus_Available\n", index);
break;
case IrsIrCameraStatus_Unsupported:
log_write("irs index: %d status: IrsIrCameraStatus_Unsupported\n", index);
break;
case IrsIrCameraStatus_Unconnected:
log_write("irs index: %d status: IrsIrCameraStatus_Unconnected\n", index);
break;
}
}
index++;
}
}
void Menu::ResetImage() {
if (m_image) {
nvgDeleteImage(App::GetVg(), m_image);
m_image = 0;
}
}
void Menu::UpdateImage() {
ResetImage();
m_image = nvgCreateImageRGBA(App::GetVg(), m_irs_width, m_irs_height, NVG_IMAGE_NEAREST, (const unsigned char*)m_rgba.data());
}
void Menu::LoadDefaultConfig() {
IrsImageTransferProcessorExConfig ex_config;
if (hosversionAtLeast(4,0,0)) {
irsGetDefaultImageTransferProcessorExConfig(&ex_config);
} else {
IrsImageTransferProcessorConfig nor_config;
irsGetDefaultImageTransferProcessorConfig(&nor_config);
irsConvertConfigNormalToEx(&nor_config, &ex_config);
}
irsGetMomentProcessorDefaultConfig(&m_moment_config);
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
irsGetIrLedProcessorDefaultConfig(&m_led_config);
m_tera_config;
m_adaptive_config = {};
m_hand_config = {};
UpdateConfig(&ex_config);
}
void Menu::UpdateConfig(const IrsImageTransferProcessorExConfig* config) {
m_config = *config;
irsStopImageProcessor(m_entries[m_index].m_handle);
if (R_FAILED(irsRunMomentProcessor(m_entries[m_index].m_handle, &m_moment_config))) {
log_write("failed to irsRunMomentProcessor\n");
} else {
log_write("did irsRunMomentProcessor\n");
}
if (R_FAILED(irsRunClusteringProcessor(m_entries[m_index].m_handle, &m_clustering_config))) {
log_write("failed to irsRunClusteringProcessor\n");
} else {
log_write("did irsRunClusteringProcessor\n");
}
if (R_FAILED(irsRunPointingProcessor(m_entries[m_index].m_handle))) {
log_write("failed to irsRunPointingProcessor\n");
} else {
log_write("did irsRunPointingProcessor\n");
}
if (R_FAILED(irsRunTeraPluginProcessor(m_entries[m_index].m_handle, &m_tera_config))) {
log_write("failed to irsRunTeraPluginProcessor\n");
} else {
log_write("did irsRunTeraPluginProcessor\n");
}
if (R_FAILED(irsRunIrLedProcessor(m_entries[m_index].m_handle, &m_led_config))) {
log_write("failed to irsRunIrLedProcessor\n");
} else {
log_write("did irsRunIrLedProcessor\n");
}
if (R_FAILED(irsRunAdaptiveClusteringProcessor(m_entries[m_index].m_handle, &m_adaptive_config))) {
log_write("failed to irsRunAdaptiveClusteringProcessor\n");
} else {
log_write("did irsRunAdaptiveClusteringProcessor\n");
}
if (R_FAILED(irsRunHandAnalysis(m_entries[m_index].m_handle, &m_hand_config))) {
log_write("failed to irsRunHandAnalysis\n");
} else {
log_write("did irsRunHandAnalysis\n");
}
if (hosversionAtLeast(4,0,0)) {
m_init_rc = irsRunImageTransferExProcessor(m_entries[m_index].m_handle, &m_config, 0x10000000);
} else {
IrsImageTransferProcessorConfig nor;
irsConvertConfigExToNormal(&m_config, &nor);
m_init_rc = irsRunImageTransferProcessor(m_entries[m_index].m_handle, &nor, 0x10000000);
}
if (R_FAILED(m_init_rc)) {
log_write("irs failed to set config!\n");
}
auto format = m_config.orig_format;
log_write("IRS CONFIG\n");
log_write("\texposure_time: %lu\n", m_config.exposure_time);
log_write("\tlight_target: %u\n", m_config.light_target);
log_write("\tgain: %u\n", m_config.gain);
log_write("\tis_negative_image_used: %u\n", m_config.is_negative_image_used);
log_write("\tlight_target: %u\n", m_config.light_target);
if (hosversionAtLeast(4,0,0)) {
format = m_config.trimming_format;
log_write("\ttrimming_format: %u\n", m_config.trimming_format);
log_write("\ttrimming_start_x: %u\n", m_config.trimming_start_x);
log_write("\ttrimming_start_y: %u\n", m_config.trimming_start_y);
log_write("\tis_external_light_filter_enabled: %u\n", m_config.is_external_light_filter_enabled);
}
switch (format) {
case IrsImageTransferProcessorFormat_320x240:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_320x240");
m_irs_width = 320;
m_irs_height = 240;
break;
case IrsImageTransferProcessorFormat_160x120:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_160x120");
m_irs_width = 160;
m_irs_height = 120;
break;
case IrsImageTransferProcessorFormat_80x60:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_80x60");
m_irs_width = 80;
m_irs_height = 60;
break;
case IrsImageTransferProcessorFormat_40x30:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_40x30");
m_irs_width = 40;
m_irs_height = 30;
break;
case IrsImageTransferProcessorFormat_20x15:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_20x15");
m_irs_width = 20;
m_irs_height = 15;
break;
}
m_rgba.resize(m_irs_width * m_irs_height);
m_irs_buffer.resize(m_irs_width * m_irs_height);
m_prev_state.sampling_number = UINT64_MAX;
std::fill(m_irs_buffer.begin(), m_irs_buffer.end(), 0);
updateColourArray();
}
void Menu::updateColourArray() {
const auto ir_width = m_irs_width;
const auto ir_height = m_irs_height;
const auto colour = m_colour;
for (u32 y = 0; y < ir_height; y++) {
for (u32 x = 0; x < ir_width; x++) {
const u32 pos = y * ir_width + x;
const u32 pos2 = y * ir_width + x;
switch (colour) {
case Colour_Grey:
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], m_irs_buffer[pos2], m_irs_buffer[pos2]);
break;
case Colour_Ironbow:
m_rgba[pos] = iron_palette[m_irs_buffer[pos2]];
break;
case Colour_Green:
m_rgba[pos] = RGBA8_MAXALPHA(0, m_irs_buffer[pos2], 0);
break;
case Colour_Red:
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], 0, 0);
break;
case Colour_Blue:
m_rgba[pos] = RGBA8_MAXALPHA(0, 0, m_irs_buffer[pos2]);
break;
}
}
}
UpdateImage();
}
} // namespace sphaira::ui::menu::irs

View File

@@ -0,0 +1,237 @@
#include "ui/menus/main_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "app.hpp"
#include "log.hpp"
#include "download.hpp"
#include "defines.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "web.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::main {
namespace {
#if 0
bool parseSearch(const char *parse_string, const char *filter, char* new_string) {
char c;
u32 offset = 0;
const u32 filter_len = std::strlen(filter) - 1;
while ((c = parse_string[offset++]) != '\0') {
if (c == *filter) {
for (u32 i = 0; c == filter[i]; i++) {
c = parse_string[offset++];
if (i == filter_len) {
for (u32 j = 0; c != '\"'; j++) {
new_string[j] = c;
new_string[j+1] = '\0';
c = parse_string[offset++];
}
return true;
}
}
}
}
return false;
}
#endif
} // namespace
MainMenu::MainMenu() {
#if 0
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sys-patch/releases/latest", [this](std::vector<u8>& data, bool success){
data.push_back('\0');
auto raw_str = (const char*)data.data();
char out_str[0x301];
if (parseSearch(raw_str, "tag_name\":\"", out_str)) {
m_update_version = out_str;
if (strcasecmp("v1.5.0", m_update_version.c_str())) {
m_update_avaliable = true;
}
log_write("FOUND IT : %s\n", out_str);
}
if (parseSearch(raw_str, "browser_download_url\":\"", out_str)) {
m_update_url = out_str;
log_write("FOUND IT : %s\n", out_str);
}
if (parseSearch(raw_str, "body\":\"", out_str)) {
m_update_description = out_str;
// m_update_description.replace("\r\n\r\n", "\n");
log_write("FOUND IT : %s\n", out_str);
}
});
#endif
AddOnLPress();
AddOnRPress();
this->SetActions(
std::make_pair(Button::START, Action{App::Exit}),
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items language_items;
language_items.push_back("Auto"_i18n);
language_items.push_back("English");
language_items.push_back("Japanese");
language_items.push_back("French");
language_items.push_back("German");
language_items.push_back("Italian");
language_items.push_back("Spanish");
language_items.push_back("Chinese");
language_items.push_back("Korean");
language_items.push_back("Dutch");
language_items.push_back("Portuguese");
language_items.push_back("Russian");
options->AddHeader("Header"_i18n);
options->AddSpacer();
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](std::size_t& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
App::SetThemeShuffleEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
App::SetNxlinkEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryCallback>("Check for update"_i18n, [this](){
App::Notify("Not Implemented"_i18n);
}));
}));
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this, language_items](std::size_t& index_out){
App::SetLanguage(index_out);
}, (std::size_t)App::GetLanguage()));
if (m_update_avaliable) {
std::string str = "Update avaliable: "_i18n + m_update_version;
options->Add(std::make_shared<SidebarEntryCallback>(str, [this](){
App::Notify("Not Implemented"_i18n);
}));
}
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
App::Push(std::make_shared<menu::themezer::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<menu::irs::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
WebShow("https://lite.duckduckgo.com/lite");
}));
}));
}})
);
m_homebrew_menu = std::make_shared<homebrew::Menu>();
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
m_current_menu = m_homebrew_menu;
for (auto [button, action] : m_actions) {
m_current_menu->SetAction(button, action);
}
}
MainMenu::~MainMenu() {
}
void MainMenu::Update(Controller* controller, TouchInfo* touch) {
m_current_menu->Update(controller, touch);
}
void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
m_current_menu->Draw(vg, theme);
}
void MainMenu::OnFocusGained() {
Widget::OnFocusGained();
this->SetHidden(false);
m_current_menu->OnFocusGained();
}
void MainMenu::OnFocusLost() {
m_current_menu->OnFocusLost();
}
void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
m_current_menu->OnFocusLost();
if (m_current_menu == m_homebrew_menu) {
m_current_menu = menu;
RemoveAction(b);
} else {
m_current_menu = m_homebrew_menu;
}
m_current_menu->OnFocusGained();
for (auto [button, action] : m_actions) {
m_current_menu->SetAction(button, action);
}
if (b == Button::L) {
AddOnRPress();
} else {
AddOnLPress();
}
}
void MainMenu::AddOnLPress() {
SetAction(Button::L, Action{"Fs"_i18n, [this]{
OnLRPress(m_filebrowser_menu, Button::L);
}});
}
void MainMenu::AddOnRPress() {
SetAction(Button::R, Action{"App"_i18n, [this]{
OnLRPress(m_app_store_menu, Button::R);
}});
}
} // namespace sphaira::ui::menu::main

View File

@@ -0,0 +1,94 @@
#include "app.hpp"
#include "log.hpp"
#include "ui/menus/menu_base.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
namespace sphaira::ui::menu {
MenuBase::MenuBase(std::string title) : m_title{title} {
// this->SetParent(this);
this->SetPos(30, 87, 1220 - 30, 646 - 87);
m_applet_type = appletGetAppletType();
SetAction(Button::START, Action{App::Exit});
}
MenuBase::~MenuBase() {
}
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
}
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme);
u32 battery_percetange{};
PsmChargerType charger_type{};
NifmInternetConnectionType type{};
NifmInternetConnectionStatus status{};
u32 strength{};
u32 ip{};
// todo: app thread poll every 1s and this query the result
psmGetBatteryChargePercentage(&battery_percetange);
psmGetChargerType(&charger_type);
nifmGetInternetConnectionStatus(&type, &strength, &status);
nifmGetCurrentIpAddress(&ip);
const float start_y = 70;
const float font_size = 22;
const float spacing = 30;
float start_x = 1220;
float bounds[4];
nvgFontSize(vg, font_size);
#define draw(...) \
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
start_x -= bounds[2] - bounds[0]; \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, __VA_ARGS__); \
start_x -= spacing;
// draw("version %s", APP_VERSION);
draw("%u%%", battery_percetange);
if (ip) {
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF);
} else {
draw(("No Internet"_i18n).c_str());
}
if (m_applet_type == AppletType_LibraryApplet || m_applet_type == AppletType_SystemApplet) {
draw(("[Applet Mode]"_i18n).c_str());
}
#undef draw
gfx::drawRect(vg, 30.f, 86.f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, 30.f, 646.0f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
nvgFontSize(vg, 28);
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title_sub_heading.c_str());
// gfx::drawTextArgs(vg, 80, 65, 28.f, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
// gfx::drawTextArgs(vg, 80, 680.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
}
void MenuBase::SetTitle(std::string title) {
m_title = title;
}
void MenuBase::SetTitleSubHeading(std::string sub_heading) {
m_title_sub_heading = sub_heading;
}
void MenuBase::SetSubHeading(std::string sub_heading) {
m_sub_heading = sub_heading;
}
} // namespace sphaira::ui::menu

View File

@@ -0,0 +1,744 @@
#include "ui/menus/themezer.hpp"
#include "ui/progress_box.hpp"
#include "ui/option_box.hpp"
#include "ui/sidebar.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "download.hpp"
#include "ui/nvg_util.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include <minIni.h>
#include <nanovg/stb_image.h>
#include <cstring>
#include <minizip/unzip.h>
#include <yyjson.h>
#include "yyjson_helper.hpp"
namespace sphaira::ui::menu::themezer {
namespace {
// format is /themes/sphaira/Theme Name by Author/theme_name-type.nxtheme
constexpr fs::FsPath THEME_FOLDER{"/themes/sphaira/"};
constexpr auto CACHE_PATH = "/switch/sphaira/cache/themezer";
constexpr auto URL_BASE = "https://switch.cdn.fortheusers.org";
constexpr const char* REQUEST_TARGET[]{
"ResidentMenu",
"Entrance",
"Flaunch",
"Set",
"Psl",
"MyPage",
"Notification"
};
constexpr const char* REQUEST_SORT[]{
"downloads",
"updated",
"likes",
"id"
};
constexpr const char* REQUEST_ORDER[]{
"desc",
"asc"
};
// https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
// i know, this is cursed
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
std::string api = "https://api.themezer.net/?query=query";
std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
const char* boolarr[2] = { "false", "true" };
std::string cmd;
std::string p0 = "$nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String";
std::string p1 = "nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order";
std::string json = "\"nsfw\":"+std::string{boolarr[e.nsfw]}+",\"page\":"+std::to_string(e.page)+",\"limit\":"+std::to_string(e.limit)+",\"sort\":\""+std::string{REQUEST_SORT[e.sort_index]}+"\",\"order\":\""+std::string{REQUEST_ORDER[e.order_index]}+"\"";
if (is_pack) {
cmd = "packList";
fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
} else {
cmd = "themeList";
p0 += ",$target:String";
p1 += ",target:$target";
if (e.target_index < 7) {
json += ",\"target\":\"" + std::string{REQUEST_TARGET[e.target_index]} + "\"";
} else {
json += ",\"target\":null";
}
}
if (!e.creator.empty()) {
p0 += ",$creators:[String!]";
p1 += ",creators:$creators";
json += ",\"creators\":[\"" + e.creator + "\"]";
}
if (!e.query.empty()) {
p0 += ",$query:String";
p1 += ",query:$query";
json += ",\"query\":\"" + e.query + "\"";
}
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables={"+json+"}";
}
auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::string {
char url[2048];
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query{download%s(id:\"%s\"){filename,url,mimetype}}", is_pack ? "Pack" : "Theme", id.c_str());
return url;
// https://api.themezer.net/?query=query{downloadPack(id:"11"){filename,url,mimetype}}
}
auto apiBuildUrlDownloadTheme(const ThemeEntry& e) -> std::string {
return apiBuildUrlDownloadInternal(e.id, false);
}
auto apiBuildUrlDownloadPack(const PackListEntry& e) -> std::string {
return apiBuildUrlDownloadInternal(e.id, true);
}
auto apiBuildFilePack(const PackListEntry& e) -> fs::FsPath {
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s/%s By %s/", THEME_FOLDER, e.details.name.c_str(), e.creator.display_name.c_str());
return path;
}
auto apiBuildUrlPack(const PackListEntry& e) -> std::string {
char url[2048];
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={\"id\":\"%s\"}", e.id.c_str());
return url;
}
auto apiBuildUrlThemeList(const Config& e) -> std::string {
return apiBuildUrlListInternal(e, false);
}
auto apiBuildUrlListPacks(const Config& e) -> std::string {
return apiBuildUrlListInternal(e, true);
}
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
return path;
}
auto loadThemeImage(ThemeEntry& e) -> void {
auto& image = e.preview.lazy_image;
// already have the image
if (e.preview.lazy_image.image) {
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
return;
}
auto vg = App::GetVg();
fs::FsNativeSd fs;
std::vector<u8> image_buf;
const auto path = apiBuildIconCache(e);
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
e.preview.lazy_image.state = ImageDownloadState::Failed;
} else {
int channels_in_file;
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
if (buf) {
ON_SCOPE_EXIT(stbi_image_free(buf));
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
}
}
if (!image.image) {
image.state = ImageDownloadState::Failed;
log_write("failed to load image from file: %s\n", path);
} else {
// log_write("loaded image from file: %s\n", path);
}
}
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
if (size && index < (size - 1)) {
if (index < (size - step)) {
index = index + step;
App::PlaySoundEffect(SoundEffect_Scroll);
} else {
index = size - 1;
App::PlaySoundEffect(SoundEffect_Scroll);
}
if (index - start >= max) {
log_write("moved down\n");
start += step;
}
return true;
}
return false;
}
void from_json(yyjson_val* json, Creator& e) {
JSON_OBJ_ITR(
JSON_SET_STR(id);
JSON_SET_STR(display_name);
);
}
void from_json(yyjson_val* json, Details& e) {
JSON_OBJ_ITR(
JSON_SET_STR(name);
JSON_SET_STR(description);
);
}
void from_json(yyjson_val* json, Preview& e) {
JSON_OBJ_ITR(
JSON_SET_STR(original);
JSON_SET_STR(thumb);
);
}
void from_json(yyjson_val* json, DownloadPack& e) {
JSON_OBJ_ITR(
JSON_SET_STR(filename);
JSON_SET_STR(url);
JSON_SET_STR(mimetype);
);
}
void from_json(yyjson_val* json, ThemeEntry& e) {
JSON_OBJ_ITR(
JSON_SET_STR(id);
JSON_SET_OBJ(creator);
JSON_SET_OBJ(details);
JSON_SET_STR(last_updated);
JSON_SET_UINT(dl_count);
JSON_SET_UINT(like_count);
JSON_SET_ARR_STR(categories);
JSON_SET_STR(target);
JSON_SET_OBJ(preview);
);
}
void from_json(yyjson_val* json, PackListEntry& e) {
JSON_OBJ_ITR(
JSON_SET_STR(id);
JSON_SET_OBJ(creator);
JSON_SET_OBJ(details);
JSON_SET_STR(last_updated);
JSON_SET_ARR_STR(categories);
JSON_SET_UINT(dl_count);
JSON_SET_UINT(like_count);
JSON_SET_ARR_OBJ(themes);
);
}
void from_json(yyjson_val* json, Pagination& e) {
JSON_OBJ_ITR(
JSON_SET_UINT(page);
JSON_SET_UINT(limit);
JSON_SET_UINT(page_count);
JSON_SET_UINT(item_count);
);
}
void from_json(const std::vector<u8>& data, DownloadPack& e) {
JSON_INIT_VEC(data, "data");
// JSON_GET_OBJ("downloadTheme");
JSON_GET_OBJ("downloadPack");
JSON_OBJ_ITR(
JSON_SET_STR(filename);
JSON_SET_STR(url);
JSON_SET_STR(mimetype);
);
}
void from_json(const std::vector<u8>& data, PackList& e) {
JSON_INIT_VEC(data, "data");
JSON_OBJ_ITR(
JSON_SET_ARR_OBJ(packList);
JSON_SET_OBJ(pagination);
);
}
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB
fs::FsNativeSd fs;
R_TRY_RESULT(fs.GetFsOpenResult(), false);
DownloadPack download_pack;
// 1. download the zip
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
log_write("starting download\n");
const auto url = apiBuildUrlDownloadPack(entry);
log_write("using url: %s\n", url.c_str());
DownloadClearCache(url);
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
if (pbox->ShouldExit()) {
return false;
}
pbox->UpdateTransfer(dlnow, dltotal);
return true;
});
if (data.empty()) {
log_write("error with download: %s\n", url.c_str());
// push popup error box
return false;
}
from_json(data, download_pack);
}
// 2. download the zip
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
log_write("starting download: %s\n", download_pack.url.c_str());
DownloadClearCache(download_pack.url);
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
if (pbox->ShouldExit()) {
return false;
}
pbox->UpdateTransfer(dlnow, dltotal);
return true;
})) {
log_write("error with download\n");
// push popup error box
return false;
}
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
// create directories
fs::FsPath dir_path;
std::snprintf(dir_path, sizeof(dir_path), "%s/%s - By %s", THEME_FOLDER, entry.details.name.c_str(), entry.creator.display_name.c_str());
fs.CreateDirectoryRecursively(dir_path);
// 3. extract the zip
if (!pbox->ShouldExit()) {
auto zfile = unzOpen64(zip_out);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out);
return false;
}
ON_SCOPE_EXIT(unzClose(zfile));
unz_global_info64 pglobal_info;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
return false;
}
for (int i = 0; i < pglobal_info.number_entry; i++) {
if (i > 0) {
if (UNZ_OK != unzGoToNextFile(zfile)) {
log_write("failed to unzGoToNextFile\n");
return false;
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
unz_file_info64 info;
char name[512];
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
log_write("failed to get current info\n");
return false;
}
const auto file_path = fs::AppendPath(dir_path, name);
Result rc;
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
return false;
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", file_path, rc);
return false;
}
std::vector<char> buf(chunk_size);
u64 offset{};
while (offset < info.uncompressed_size) {
if (pbox->ShouldExit()) {
return false;
}
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
if (bytes_read <= 0) {
// log_write("failed to read zip file: %s\n", inzip.c_str());
return false;
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", file_path, rc);
return false;
}
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
}
}
}
log_write("finished install :)\n");
return true;
}
} // namespace
LazyImage::~LazyImage() {
if (image) {
nvgDeleteImage(App::GetVg(), image);
}
}
Menu::Menu() : MenuBase{"Themezer"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this]{
SetPop();
}});
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
const auto& page = m_pages[m_page_index];
if (ScrollHelperDown(m_index, m_start, 3, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
App::Push(std::make_shared<OptionBox>(
"Download theme?"_i18n,
"Back"_i18n, "Download"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
const auto& page = m_pages[m_page_index];
if (page.m_packList.size() && page.m_ready == PageLoadState::Done) {
const auto& entry = page.m_packList[m_index];
const auto url = apiBuildUrlDownloadPack(entry);
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
return InstallTheme(pbox, entry);
}, [this](bool success){
// if (success) {
// m_entry.status = EntryStatus::Installed;
// m_menu.SetDirty();
// UpdateOptions();
// }
}, 2));
}
}
}
));
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Themezer Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items sort_items;
sort_items.push_back("Downloads"_i18n);
sort_items.push_back("Updated"_i18n);
sort_items.push_back("Likes"_i18n);
sort_items.push_back("ID"_i18n);
SidebarEntryArray::Items order_items;
order_items.push_back("Descending (down)"_i18n);
order_items.push_back("Ascending (Up)"_i18n);
options->Add(std::make_shared<SidebarEntryBool>("Nsfw"_i18n, m_nsfw.Get(), [this](bool& v_out){
m_nsfw.Set(v_out);
InvalidateAllPages();
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
if (m_sort.Get() != index_out) {
m_sort.Set(index_out);
InvalidateAllPages();
}
}, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
if (m_order.Get() != index_out) {
m_order.Set(index_out);
InvalidateAllPages();
}
}, m_order.Get()));
options->Add(std::make_shared<SidebarEntryCallback>("Page"_i18n, [this](){
s64 out;
if (R_SUCCEEDED(swkbd::ShowNumPad(out, "Enter Page Number", -1, 3))) {
if (out < m_page_index_max) {
m_page_index = out;
PackListDownload();
} else {
log_write("invalid page number\n");
App::Notify("Bad Page"_i18n);
}
}
}));
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
m_search = out;
}
}));
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_index >= 3) {
SetIndex(m_index - 3);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_start ) {
// log_write("moved up\n");
m_start -= 3;
}
}
}}),
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
m_page_index++;
if (m_page_index >= m_page_index_max) {
m_page_index = m_page_index_max - 1;
} else {
PackListDownload();
}
}}),
std::make_pair(Button::L, Action{"Prev Page"_i18n, [this](){
if (m_page_index) {
m_page_index--;
PackListDownload();
}
}})
);
m_page_index = 0;
m_pages.resize(1);
PackListDownload();
}
Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
if (m_pages.empty()) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!");
return;
}
auto& page = m_pages[m_page_index];
switch (page.m_ready) {
case PageLoadState::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Not Ready...");
return;
case PageLoadState::Loading:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading");
return;
case PageLoadState::Done:
break;
case PageLoadState::Error:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Error loading page!");
return;
}
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
const u64 cursor_pos = m_index;
// only draw scrollbar if needed
if (nro_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
nvgSave(vg);
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
const auto index = pos;
auto& e = page.m_packList[index];
auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
const float xoff = (350 - 320) / 2;
const float yoff = (350 - 320) / 2;
// lazy load image
if (e.themes.size()) {
auto& theme = e.themes[0];
auto& image = e.themes[0].preview.lazy_image;
if (!image.image) {
switch (image.state) {
case ImageDownloadState::None: {
const auto path = apiBuildIconCache(theme);
log_write("downloading theme!: %s\n", path);
if (fs::FsNativeSd().FileExists(path)) {
loadThemeImage(theme);
} else {
const auto url = theme.preview.thumb;
log_write("downloading url: %s\n", url.c_str());
image.state = ImageDownloadState::Progress;
DownloadFileAsync(url, path, "", [this, index, &image](std::vector<u8>& data, bool success) {
if (success) {
image.state = ImageDownloadState::Done;
log_write("downloaded themezer image\n");
} else {
image.state = ImageDownloadState::Failed;
log_write("failed to download image\n");
}
}, nullptr, DownloadPriority::High);
}
} break;
case ImageDownloadState::Progress: {
} break;
case ImageDownloadState::Done: {
loadThemeImage(theme);
} break;
case ImageDownloadState::Failed: {
} break;
}
} else {
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image);
}
}
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.details.name.c_str());
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
}
}
nvgRestore(vg);
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
void Menu::InvalidateAllPages() {
for (auto& e : m_pages) {
e.m_packList.clear();
e.m_ready = PageLoadState::None;
}
PackListDownload();
}
void Menu::PackListDownload() {
const auto page_index = m_page_index + 1;
char subheading[128];
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu", m_page_index+1, m_page_index_max);
SetSubHeading(subheading);
m_index = 0;
m_start = 0;
// already downloaded
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
return;
}
m_pages[m_page_index].m_ready = PageLoadState::Loading;
Config config;
config.page = page_index;
config.SetQuery(m_search);
config.sort_index = m_sort.Get();
config.order_index = m_order.Get();
config.nsfw = m_nsfw.Get();
const auto packList_url = apiBuildUrlListPacks(config);
const auto themeList_url = apiBuildUrlThemeList(config);
log_write("\npackList_url: %s\n\n", packList_url.c_str());
log_write("\nthemeList_url: %s\n\n", themeList_url.c_str());
DownloadClearCache(packList_url);
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){
log_write("got themezer data\n");
if (!success) {
auto& page = m_pages[page_index-1];
page.m_ready = PageLoadState::Error;
log_write("failed to get themezer data...\n");
return;
}
PackList a;
from_json(data, a);
m_pages.resize(a.pagination.page_count);
auto& page = m_pages[page_index-1];
page.m_packList = a.packList;
page.m_pagination = a.pagination;
page.m_ready = PageLoadState::Done;
m_page_index_max = a.pagination.page_count;
char subheading[128];
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu", m_page_index+1, m_page_index_max);
SetSubHeading(subheading);
log_write("a.pagination.page: %u\n", a.pagination.page);
log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
}, nullptr, DownloadPriority::High);
}
} // namespace sphaira::ui::menu::themezer

View File

@@ -0,0 +1,142 @@
#include "ui/notification.hpp"
#include "ui/nvg_util.hpp"
#include "defines.hpp"
#include "app.hpp"
#include <optional>
namespace sphaira::ui {
NotifEntry::NotifEntry(std::string text, Side side)
: m_text{std::move(text)}
, m_side{side} {
}
auto NotifEntry::OnLayoutChange() -> void {
}
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
m_pos.y = y;
Draw(vg, theme);
m_count--;
return m_count == 0;
}
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
auto overlay_col = theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour;
auto selected_col = theme->elements[ThemeEntryID_SELECTED].colour;
auto text_col = theme->elements[ThemeEntryID_TEXT].colour;
float font_size = 18.f;
// overlay_col.a = 0.2f;
// selected_col.a = 0.2f;
// text_col.a = 0.2f;
// auto vg = App::GetVg();
if (!m_bounds_measured) {
m_bounds_measured = true;
m_pos.w = 320.f;
m_pos.h = 60.f;
float bounds[4];
nvgFontSize(vg, font_size);
nvgTextBounds(vg, 0, 0, this->m_text.c_str(), nullptr, bounds);
// m_pos.w = std::max(bounds[2] - bounds[0] + 30.f, m_pos.w);
m_pos.w = bounds[2] - bounds[0] + 30.f;
switch (m_side) {
case Side::LEFT:
m_pos.x = 4.f;
break;
case Side::RIGHT:
m_pos.x = 1280.f - (m_pos.w + 4.f);// + 30.f);
break;
}
}
gfx::drawRectOutline(vg, 4.f, overlay_col, m_pos, selected_col);
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
}
auto NotifMananger::OnLayoutChange() -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
}
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
Draw(vg, theme, m_entries_left);
Draw(vg, theme, m_entries_right);
}
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme, Entries& entries) -> void {
// auto y = 130.f;
auto y = 4.f;
std::optional<Entries::iterator> delete_pos{std::nullopt};
for (auto itr = entries.begin(); itr != entries.end(); ++itr) {
itr->Draw(vg, theme, y);
if (itr->IsDone() && !delete_pos.has_value()) {
delete_pos = itr;
}
y += itr->GetH() + 15.f;
}
if (delete_pos.has_value()) {
entries.erase(*delete_pos, entries.end());
}
}
auto NotifMananger::Push(const NotifEntry& entry) -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (entry.GetSide()) {
case NotifEntry::Side::LEFT:
m_entries_left.emplace_front(entry);
break;
case NotifEntry::Side::RIGHT:
m_entries_right.emplace_front(entry);
break;
}
}
auto NotifMananger::Pop(NotifEntry::Side side) -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (side) {
case NotifEntry::Side::LEFT:
if (!m_entries_left.empty()) {
m_entries_left.clear();
}
break;
case NotifEntry::Side::RIGHT:
if (!m_entries_right.empty()) {
m_entries_right.clear();
}
break;
}
}
auto NotifMananger::Clear(NotifEntry::Side side) -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (side) {
case NotifEntry::Side::LEFT:
m_entries_left.clear();
break;
case NotifEntry::Side::RIGHT:
m_entries_right.clear();
break;
}
}
auto NotifMananger::Clear() -> void {
m_entries_left.clear();
m_entries_right.clear();
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,520 @@
#include "ui/nvg_util.hpp"
#include <cstddef>
#include <cstdio>
#include <cstdarg>
#include <array>
#include <utility>
#include <algorithm>
#include <cmath>
namespace sphaira::ui::gfx {
namespace {
static constexpr std::array buttons = {
std::pair{Button::A, "\uE0E0"},
std::pair{Button::B, "\uE0E1"},
std::pair{Button::X, "\uE0E2"},
std::pair{Button::Y, "\uE0E3"},
std::pair{Button::L, "\uE0E4"},
std::pair{Button::R, "\uE0E5"},
std::pair{Button::L, "\uE0E6"},
std::pair{Button::R, "\uE0E7"},
std::pair{Button::L2, "\uE0E8"},
std::pair{Button::R2, "\uE0E9"},
std::pair{Button::UP, "\uE0EB"},
std::pair{Button::DOWN, "\uE0EC"},
std::pair{Button::LEFT, "\uE0ED"},
std::pair{Button::RIGHT, "\uE0EE"},
std::pair{Button::START, "\uE0EF"},
std::pair{Button::SELECT, "\uE0F0"},
// std::pair{Button::LS, "\uE101"},
// std::pair{Button::RS, "\uE102"},
std::pair{Button::L3, "\uE104"},
std::pair{Button::R3, "\uE105"},
};
#define F(a) (a/255.f) // turn range 0-255 to 0.f-1.f range
constexpr std::array COLOURS = {
std::pair<Colour, NVGcolor>{Colour::BLACK, { F(45.f), F(45.f), F(45.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLACK, { F(50.f), F(50.f), F(50.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::SILVER, { F(128.f), F(128.f), F(128.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::DARK_GREY, { F(70.f), F(70.f), F(70.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::GREY, { F(77.f), F(77.f), F(77.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::WHITE, { F(251.f), F(251.f), F(251.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::CYAN, { F(0.f), F(255.f), F(200.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::TEAL, { F(143.f), F(253.f), F(252.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::BLUE, { F(36.f), F(141.f), F(199.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLUE, { F(26.f), F(188.f), F(252.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::YELLOW, { F(255.f), F(177.f), F(66.f), F(255.f) }},
std::pair<Colour, NVGcolor>{Colour::RED, { F(250.f), F(90.f), F(58.f), F(255.f) }}
};
#undef F
// NEW ---------------------
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGcolor& c, bool rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
} else {
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
}
nvgFillColor(vg, c);
nvgFill(vg);
}
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGpaint& p, bool rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
} else {
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
}
nvgFillPaint(vg, p);
nvgFill(vg);
}
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
float gradientX, gradientY, color;
getHighlightAnimation(&gradientX, &gradientY, &color);
const auto strokeWidth = 5.0;
auto v2 = vec;
v2.x -= strokeWidth / 2.0;
v2.y -= strokeWidth / 2.0;
v2.w += strokeWidth;
v2.h += strokeWidth;
const auto corner_radius = 0.5;
nvgSave(vg);
nvgResetScissor(vg);
// const auto stroke_width = 5.0f;
// const auto shadow_corner_radius = 6.0f;
const auto shadow_width = 2.0f;
const auto shadow_offset = 10.0f;
const auto shadow_feather = 10.0f;
const auto shadow_opacity = 128.0f;
// Shadow
NVGpaint shadowPaint = nvgBoxGradient(vg,
v2.x, v2.y + shadow_width,
v2.w, v2.h,
corner_radius * 2, shadow_feather,
nvgRGBA(0, 0, 0, shadow_opacity * 1.f), nvgRGBA(0, 0, 0, 0));
nvgBeginPath(vg);
nvgRect(vg, v2.x - shadow_offset, v2.y - shadow_offset,
v2.w + shadow_offset * 2, v2.h + shadow_offset * 3);
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
nvgPathWinding(vg, NVG_HOLE);
nvgFillPaint(vg, shadowPaint);
nvgFill(vg);
const auto color1 = nvgRGB(25, 138, 198);
const auto color2 = nvgRGB(137, 241, 242);
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
const auto transparent = nvgRGBA(0, 0, 0, 0);
const auto pulsationColor = nvgRGBAf((color * color1.r) + (1 - color) * color2.r,
(color * color1.g) + (1 - color) * color2.g,
(color * color1.b) + (1 - color) * color2.b,
1.f);
const auto border1Paint = nvgRadialGradient(vg,
v2.x + gradientX * v2.w, v2.y + gradientY * v2.h,
strokeWidth * 10, strokeWidth * 40,
borderColor, transparent);
const auto border2Paint = nvgRadialGradient(vg,
v2.x + (1 - gradientX) * v2.w, v2.y + (1 - gradientY) * v2.h,
strokeWidth * 10, strokeWidth * 40,
borderColor, transparent);
nvgBeginPath(vg);
nvgStrokeColor(vg, pulsationColor);
nvgStrokeWidth(vg, strokeWidth);
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
nvgStroke(vg);
nvgBeginPath(vg);
nvgStrokePaint(vg, border1Paint);
nvgStrokeWidth(vg, strokeWidth);
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
nvgStroke(vg);
nvgBeginPath(vg);
nvgStrokePaint(vg, border2Paint);
nvgStrokeWidth(vg, strokeWidth);
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
nvgStroke(vg);
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
drawRectIntenal(vg, vec, c, true);
nvgBeginPath(vg);
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, corner_radius);
nvgFillColor(vg, c);
nvgFill(vg);
nvgRestore(vg);
}
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
float gradientX, gradientY, color;
getHighlightAnimation(&gradientX, &gradientY, &color);
NVGcolor pulsationColor = nvgRGBAf((color * out_col.r) + (1 - color) * out_col.r,
(color * out_col.g) + (1 - color) * out_col.g,
(color * out_col.b) + (1 - color) * out_col.b,
out_col.a);
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
drawRectIntenal(vg, vec, p, false);
}
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
nvgBeginPath(vg);
nvgMoveTo(vg, aX, aY);
nvgLineTo(vg, bX, bY);
nvgLineTo(vg, cX, cY);
nvgFillColor(vg, c);
nvgFill(vg);
}
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
nvgBeginPath(vg);
nvgMoveTo(vg, aX, aY);
nvgLineTo(vg, bX, bY);
nvgLineTo(vg, cX, cY);
nvgFillPaint(vg, p);
nvgFill(vg);
}
inline void drawTextIntenal(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
nvgFillColor(vg, c);
nvgText(vg, vec.x, vec.y, str, end);
}
} // namespace
const char* getButton(const Button want) {
for (auto& [key, val] : buttons) {
if (key == want) {
return val;
}
}
std::unreachable();
}
NVGcolor getColour(Colour want) {
for (auto& [key, val] : COLOURS) {
if (key == want) {
return val;
}
}
std::unreachable();
}
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
std::va_list v;
va_start(v, str);
char buffer[0x100];
std::vsnprintf(buffer, sizeof(buffer), str, v);
va_end(v);
drawText(vg, x, y, size, buffer, nullptr, align, c);
}
static void drawImageInternal(NVGcontext* vg, Vec4 v, int texture, int rounded = 0) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
// drawRect(vg, x, y, w, h, paint);
nvgBeginPath(vg);
// nvgRect(vg, x, y, w, h);
if (rounded == 0) {
nvgRect(vg, v.x, v.y, v.w, v.h);
} else {
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
}
nvgFillPaint(vg, paint);
nvgFill(vg);
}
void drawImage(NVGcontext* vg, Vec4 v, int texture) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
drawRect(vg, v, paint, false);
}
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImage(vg, Vec4(x, y, w, h), texture);
}
void drawImageRounded(NVGcontext* vg, Vec4 v, int texture) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
nvgFillPaint(vg, paint);
nvgFill(vg);
}
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImageRounded(vg, Vec4(x, y, w, h), texture);
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGcolor& c, const char* str, int align, const char* end) {
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
nvgFillColor(vg, c);
nvgTextBox(vg, x, y, bound, str, end);
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGcolor&& c, const char* str, int align, const char* end) {
drawTextBox(vg, x, y, size, bound, c, str, align, end);
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, Colour c, const char* str, int align, const char* end) {
drawTextBox(vg, x, y, size, bound, getColour(c), str, align, end);
}
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
char buf[0x100];
va_list v;
va_start(v, str);
std::vsnprintf(buf, sizeof(buf), str, v);
va_end(v);
nvgTextBounds(vg, x, y, buf, nullptr, bounds);
}
// NEW-----------
void dimBackground(NVGcontext* vg) {
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180));
// drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(20, 20, 20, 225), false);
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) {
drawRectIntenal(vg, {x,y,w,h}, getColour(c), rounded);
}
void drawRect(NVGcontext* vg, Vec4 vec, Colour c, bool rounded) {
drawRectIntenal(vg, vec, getColour(c), rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor&& c, bool rounded) {
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
}
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor& c, bool rounded) {
drawRectIntenal(vg, vec, c, rounded);
}
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor&& c, bool rounded) {
drawRectIntenal(vg, vec, c, rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint&& p, bool rounded) {
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
}
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint& p, bool rounded) {
drawRectIntenal(vg, vec, p, rounded);
}
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint&& p, bool rounded) {
drawRectIntenal(vg, vec, p, rounded);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, Colour c) {
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, getColour(c));
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, Colour c) {
drawRectOutlineInternal(vg, size, out_col, vec, getColour(c));
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor& c) {
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor&& c) {
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
drawRectOutlineInternal(vg, size, out_col, vec, c);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor&& c) {
drawRectOutlineInternal(vg, size, out_col, vec, c);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint& p) {
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint&& p) {
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
drawRectOutlineInternal(vg, size, out_col, vec, p);
}
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint&& p) {
drawRectOutlineInternal(vg, size, out_col, vec, p);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, Colour c) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, getColour(c));
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor&& c) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint&& p) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
}
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, Colour c) {
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
}
void drawText(NVGcontext* vg, float x, float y, float size, Colour c, const char* str, int align, const char* end) {
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
}
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, Colour c) {
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
}
void drawText(NVGcontext* vg, Vec2 vec, float size, Colour c, const char* str, int align, const char* end) {
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
}
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
}
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
drawTextIntenal(vg, vec, size, str, end, align, c);
}
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
drawTextIntenal(vg, vec, size, str, end, align, c);
}
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor& c, const char* str, int align, const char* end) {
drawTextIntenal(vg, vec, size, str, end, align, c);
}
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
drawTextIntenal(vg, vec, size, str, end, align, c);
}
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, Colour c, const char* str, ...) {
std::va_list v;
va_start(v, str);
char buffer[0x100];
std::vsnprintf(buffer, sizeof(buffer), str, v);
va_end(v);
drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c));
}
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) {
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE));
}
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x) {
nvgBeginPath(vg);
nvgFontSize(vg, 24.f);
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
nvgFillColor(vg, c);
float x = start_x;
const float y = 675.f;
float bounds[4]{};
for (const auto& [button, action] : actions) {
if (action.IsHidden() || action.m_hint.empty()) {
continue;
}
nvgFontSize(vg, 20.f);
nvgTextBounds(vg, x, y, action.m_hint.c_str(), nullptr, bounds);
auto len = bounds[2] - bounds[0];
nvgText(vg, x, y, action.m_hint.c_str(), nullptr);
x -= len + 8.f;
nvgFontSize(vg, 26.f);
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds);
len = bounds[2] - bounds[0];
nvgText(vg, x, y - 4.f, getButton(button), nullptr);
x -= len + 34.f;
}
}
// from gc installer
void drawDimBackground(NVGcontext* vg) {
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
}
#define HIGHLIGHT_SPEED 350.0
static double highlightGradientX = 0;
static double highlightGradientY = 0;
static double highlightColor = 0;
void updateHighlightAnimation() {
const auto currentTime = svcGetSystemTick() * 10 / 192 / 1000;
// Update variables
highlightGradientX = (std::cos((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
highlightGradientY = (std::sin((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
highlightColor = (std::sin((double)currentTime / HIGHLIGHT_SPEED * 2.0) + 1.0) / 2.0;
}
void getHighlightAnimation(float* gradientX, float* gradientY, float* color) {
if (gradientX)
*gradientX = (float)highlightGradientX;
if (gradientY)
*gradientY = (float)highlightGradientY;
if (color)
*color = (float)highlightColor;
}
} // namespace sphaira::ui::gfx

View File

@@ -0,0 +1,146 @@
#include "ui/option_box.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
namespace sphaira::ui {
OptionBoxEntry::OptionBoxEntry(const std::string& text, Vec4 pos)
: m_text{text} {
m_pos = pos;
m_text_pos = Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)};
}
auto OptionBoxEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
if (m_selected) {
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
} else {
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
}
}
auto OptionBoxEntry::Selected(bool enable) -> void {
m_selected = enable;
}
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
: m_message{message}
, m_callback{cb} {
m_pos.w = 770.f;
m_pos.h = 295.f;
m_pos.x = (1280.f / 2.f) - (m_pos.w / 2.f);
m_pos.y = (720.f / 2.f) - (m_pos.h / 2.f);
auto box = m_pos;
box.y += 220.f;
box.h -= 220.f;
m_entries.emplace_back(a, box);
Setup(0);
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb)
: OptionBox{message, a, b, 0, cb} {
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb)
: m_message{message}
, m_callback{cb} {
m_pos.w = 770.f;
m_pos.h = 295.f;
m_pos.x = (1280.f / 2.f) - (m_pos.w / 2.f);
m_pos.y = (720.f / 2.f) - (m_pos.h / 2.f);
auto box = m_pos;
box.w /= 2.f;
box.y += 220.f;
box.h -= 220.f;
m_entries.emplace_back(a, box);
box.x += box.w;
m_entries.emplace_back(b, box);
Setup(index);
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb)
: OptionBox{message, a, b, c, 0, cb} {
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb)
: m_message{message}
, m_callback{cb} {
}
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
// return;
// }
// const auto old_index = m_index;
// if (controller->GotDown(Button::LEFT) && m_index) {
// m_index--;
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) {
// m_index++;
// }
// if (old_index != m_index) {
// m_entries[old_index].Selected(false);
// m_entries[m_index].Selected(true);
// }
}
auto OptionBox::OnLayoutChange() -> void {
}
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
gfx::drawText(vg, {m_pos.x + (m_pos.w / 2.f), m_pos.y + 110.f}, 26.f, theme->elements[ThemeEntryID_TEXT].colour, m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_TOP);
gfx::drawRect(vg, m_spacer_line, theme->elements[ThemeEntryID_TEXT].colour);
for (auto&p: m_entries) {
p.Draw(vg, theme);
}
}
auto OptionBox::Setup(std::size_t index) -> void {
m_index = std::min(m_entries.size() - 1, index);
m_entries[m_index].Selected(true);
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
SetActions(
std::make_pair(Button::LEFT, Action{[this](){
if (m_index) {
m_entries[m_index].Selected(false);
m_index--;
m_entries[m_index].Selected(true);
}
}}),
std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1)) {
m_entries[m_index].Selected(false);
m_index++;
m_entries[m_index].Selected(true);
}
}}),
std::make_pair(Button::A, Action{[this](){
m_callback(m_index);
SetPop();
}}),
std::make_pair(Button::B, Action{[this](){
m_callback({});
SetPop();
}})
);
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,32 @@
#include "ui/option_list.hpp"
#include "app.hpp"
#include "ui/nvg_util.hpp"
namespace sphaira::ui {
OptionList::OptionList(Options options)
: m_options{std::move(options)} {
SetAction(Button::A, Action{"Select", [this](){
const auto& [_, func] = m_options[m_index];
func();
SetPop();
}});
SetAction(Button::B, Action{"Back", [this](){
SetPop();
}});
}
auto OptionList::Update(Controller* controller, TouchInfo* touch) -> void {
}
auto OptionList::OnLayoutChange() -> void {
}
auto OptionList::Draw(NVGcontext* vg, Theme* theme) -> void {
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,154 @@
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
namespace sphaira::ui {
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index_ref)
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
if (op_idx) {
index_ref = *op_idx;
index_str_ref = m_items[index_ref];
}
};
}
PopupList::PopupList(std::string title, Items items, std::string& index_ref)
: PopupList{std::move(title), std::move(items), Callback{}} {
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
}
m_callback = [&index_ref, this](auto op_idx) {
if (op_idx) {
index_ref = m_items[*op_idx];
}
};
}
PopupList::PopupList(std::string title, Items items, std::size_t& index_ref)
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
m_callback = [&index_ref, this](auto op_idx) {
if (op_idx) {
index_ref = *op_idx;
}
};
}
PopupList::PopupList(std::string title, Items items, Callback cb, std::string index)
: PopupList{std::move(title), std::move(items), cb, 0} {
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
}
}
PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t index)
: m_title{std::move(title)}
, m_items{std::move(items)}
, m_callback{cb}
, m_index{index} {
m_pos.w = 1280.f;
const float a = std::min(405.f, (60.f * static_cast<float>(m_items.size())));
m_pos.h = 80.f + 140.f + a;
m_pos.y = 720.f - m_pos.h;
m_line_top = m_pos.y + 70.f;
m_line_bottom = 720.f - 73.f;
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
SetActions(
std::make_pair(Button::A, Action{"Select", [this](){
if (m_callback) {
m_callback(m_index);
}
SetPop();
}}),
std::make_pair(Button::B, Action{"Back", [this](){
if (m_callback) {
m_callback(std::nullopt);
}
SetPop();
}})
);
}
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
if (!controller->GotDown(Button::ANY_VERTICAL)) {
return;
}
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_block.h;
} else if (controller->GotDown(Button::UP) && m_index != 0) {
m_index--;
m_selected_y -= m_block.h;
}
OnLayoutChange();
}
auto PopupList::OnLayoutChange() -> void {
if ((m_selected_y + m_block.h) > m_line_bottom) {
m_selected_y -= m_block.h;
m_index_offset++;
m_scrollbar.Move(ScrollBar::Direction::DOWN);
} else if (m_selected_y <= m_line_top) {
m_selected_y += m_block.h;
m_index_offset--;
m_scrollbar.Move(ScrollBar::Direction::UP);
}
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
}
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
// todo: cleanup
const float x = m_block.x;
float y = m_line_top + 1.f + 42.f;
const float h = m_block.h;
const float w = m_block.w;
nvgSave(vg);
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
if (m_index == i) {
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
gfx::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].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->elements[ThemeEntryID_TEXT_SELECTED].colour);
} else {
gfx::drawRect(vg, x, y, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, x, y + h, w, 1.f, theme->elements[ThemeEntryID_TEXT].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->elements[ThemeEntryID_TEXT].colour);
}
y += h;
if (y > m_line_bottom) {
break;
}
}
nvgRestore(vg);
m_scrollbar.Draw(vg, theme);
Widget::Draw(vg, theme);
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,183 @@
#include "ui/progress_box.hpp"
#include "ui/option_box.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
namespace sphaira::ui {
namespace {
void threadFunc(void* arg) {
auto d = static_cast<ProgressBox::ThreadData*>(arg);
d->result = d->callback(d->pbox);
d->pbox->RequestExit();
}
} // namespace
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
SetAction(Button::B, Action{"Back", [this](){
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?", "No", "Yes", 1, [this](auto op_index){
if (op_index && *op_index) {
RequestExit();
SetPop();
}
}));
}});
m_pos.w = 770.f;
m_pos.h = 430.f;
m_pos.x = 255;
m_pos.y = 145;
m_done = done;
m_title = title;
m_thread_data.pbox = this;
m_thread_data.callback = callback;
if (R_FAILED(threadCreate(&m_thread, threadFunc, &m_thread_data, nullptr, stack_size, prio, cpuid))) {
log_write("failed to create thead\n");
}
if (R_FAILED(threadStart(&m_thread))) {
log_write("failed to start thread\n");
}
}
ProgressBox::~ProgressBox() {
mutexLock(&m_mutex);
m_exit_requested = true;
mutexUnlock(&m_mutex);
if (R_FAILED(threadWaitForExit(&m_thread))) {
log_write("failed to join thread\n");
}
if (R_FAILED(threadClose(&m_thread))) {
log_write("failed to close thread\n");
}
m_done(m_thread_data.result);
}
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
if (ShouldExit()) {
SetPop();
}
}
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex);
const auto title = m_title;
const auto transfer = m_transfer;
const auto size = m_size;
const auto offset = m_offset;
mutexUnlock(&m_mutex);
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
// The pop up shape.
// const Vec4 box = { 255, 145, 770, 430 };
const Vec4 prog_bar = { 400, 470, 480, 12 };
const auto center_x = m_pos.x + m_pos.w/2;
// shapes.
if (offset && size) {
gfx::drawRect(vg, prog_bar, gfx::Colour::SILVER);
const u32 percentage = ((double)offset / (double)size) * 100.0;
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, gfx::Colour::CYAN);
// gfx::drawTextArgs(vg, prog_bar.x + 85, prog_bar.y + 40, 20, 0, gfx::Colour::WHITE, "%u%%", percentage);
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%u%%", percentage);
}
gfx::drawTextArgs(vg, center_x, 200, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, title.c_str());
// gfx::drawTextArgs(vg, center_x, 260, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "Please do not remove the gamecard or");
// gfx::drawTextArgs(vg, center_x, 295, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "power off the system whilst installing.");
// gfx::drawTextArgs(vg, center_x, 360, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%.2f MiB/s", 24.0);
if (!transfer.empty()) {
gfx::drawTextArgs(vg, center_x, 420, 20, NVG_ALIGN_CENTER, gfx::Colour::WHITE, "%s", transfer.c_str());
}
}
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
mutexLock(&m_mutex);
m_transfer = transfer;
m_size = 0;
m_offset = 0;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
mutexLock(&m_mutex);
m_size = size;
m_offset = offset;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
void ProgressBox::RequestExit() {
mutexLock(&m_mutex);
m_exit_requested = true;
mutexUnlock(&m_mutex);
}
auto ProgressBox::ShouldExit() -> bool {
mutexLock(&m_mutex);
const auto exit_requested = m_exit_requested;
mutexUnlock(&m_mutex);
return exit_requested;
}
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
FsFile src_file;
R_TRY(fs.OpenFile(src_path, FsOpenMode_Read, &src_file));
ON_SCOPE_EXIT(fsFileClose(&src_file));
s64 src_size;
R_TRY(fsFileGetSize(&src_file, &src_size));
// this can fail if it already exists so we ignore the result.
// if the file actually failed to be created, the result is implicitly
// handled when we try and open it for writing.
fs.CreateFile(dst_path, src_size, 0);
FsFile dst_file;
R_TRY(fs.OpenFile(dst_path, FsOpenMode_Write, &dst_file));
ON_SCOPE_EXIT(fsFileClose(&dst_file));
R_TRY(fsFileSetSize(&dst_file, src_size));
s64 offset{};
std::vector<u8> buf(1024*1024*8); // 8MiB
while (offset < src_size) {
if (ShouldExit()) {
R_THROW(0xFFFF);
}
u64 bytes_read;
R_TRY(fsFileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read));
Yield();
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
Yield();
offset += bytes_read;
}
R_SUCCEED();
}
void ProgressBox::Yield() {
svcSleepThread(YieldType_WithoutCoreMigration);
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,109 @@
#include "ui/scrollable_text.hpp"
#include "app.hpp"
#include "ui/nvg_util.hpp"
#include "log.hpp"
#include <cstring>
namespace sphaira::ui {
ScrollableText::ScrollableText(const std::string& text, float x, float y, float y_clip, float w, float font_size)
: m_font_size{font_size}
, m_y_off_base{y}
, m_y_off{y}
, m_clip_y{y_clip}
, m_end_w{w}
{
SetActions(
std::make_pair(Button::LS_DOWN, Action{[this](){
const auto bound = m_bounds[3];
if (bound < m_clip_y) {
return;
}
const auto a = m_y_off_base + m_clip_y;
const auto norm = m_bounds[3] - m_bounds[1];
const auto b = m_y_off + norm;
if (b <= a) {
return;
}
m_y_off -= m_step;
m_index++;
}}),
std::make_pair(Button::LS_UP, Action{[this](){
if (m_y_off == m_y_off_base) {
return;
}
m_y_off += m_step;
m_index--;
}})
);
#if 1
// converts '\''n' to '\n' without including <regex> because it bloats
// the binary by over 400 KiB lol
const auto mini_regex = [](std::string_view str, std::string_view regex, std::string_view replace) -> std::string {
std::string out;
out.reserve(str.size());
u32 i{};
while (i < str.size()) {
if ((i + regex.size()) <= str.size() && !std::memcmp(str.data() + i, regex.data(), regex.size())) {
out.append(replace.data(), replace.size());
i += regex.size();
} else {
out.push_back(str[i]);
i++;
}
}
return out;
};
m_text = mini_regex(text, "\r", "");
m_text = mini_regex(m_text, "\\n", "\n");
#else
m_text = std::regex_replace(text, std::regex("\\\\n"), "\n");
#endif
if (m_text.size() > 4096) {
m_text.resize(4096);
m_text += "...";
}
nvgFontSize(App::GetVg(), m_font_size);
nvgTextLineHeight(App::GetVg(), 1.7);
nvgTextBoxBounds(App::GetVg(), 110.0F, m_y_off_base, m_end_w, m_text.c_str(), nullptr, m_bounds);
// log_write("bounds x: %.2f y: %.2f w: %.2f h: %.2f\n", m_bounds[0], m_bounds[1], m_bounds[2], m_bounds[3]);
}
// void ScrollableText::Update(Controller* controller, TouchInfo* touch) {
// }
void ScrollableText::Draw(NVGcontext* vg, Theme* theme) {
Widget::Draw(vg, theme);
const Vec4 line_vec(30, 86, 1220, 646);
// const Vec4 banner_vec(70, line_vec.y + 20, 848.f, 208.f);
const Vec4 banner_vec(70, line_vec.y + 20, m_end_w + (110.0F), 208.f);
// only draw scrollbar if needed
if ((m_bounds[3] - m_bounds[1]) > m_clip_y) {
const auto scrollbar_size = m_clip_y;
const auto max_index = (m_bounds[3] - m_bounds[1]) / m_step;
const auto sb_h = 1.f / max_index * scrollbar_size;
const auto in_clip = m_clip_y / m_step - 1;
const auto sb_y = m_index;
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
gfx::drawRect(vg, banner_vec.w, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, banner_vec.w+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
nvgSave(vg);
nvgScissor(vg, 0, m_y_off_base - m_font_size, 1280, m_clip_y + m_font_size); // clip
nvgTextLineHeight(App::GetVg(), 1.7);
gfx::drawTextBox(vg, banner_vec.x + 40, m_y_off, m_font_size, m_bounds[2] - m_bounds[0], theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str());
nvgRestore(vg);
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,68 @@
#include "ui/scrollbar.hpp"
#include "ui/nvg_util.hpp"
namespace sphaira::ui {
ScrollBar::ScrollBar(Vec4 bounds, float entry_height, std::size_t entries)
: m_bounds{bounds}
, m_entries{entries}
, m_entry_height{entry_height} {
Setup();
}
auto ScrollBar::OnLayoutChange() -> void {
}
auto ScrollBar::Draw(NVGcontext* vg, Theme* theme) -> void {
if (m_should_draw) {
gfx::drawRect(vg, m_pos, gfx::Colour::RED);
}
}
auto ScrollBar::Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void {
m_bounds = bounds;
m_entry_height = entry_height;
m_entries = entries;
Setup();
}
auto ScrollBar::Setup() -> void {
m_bounds.y += 5.f;
m_bounds.h -= 10.f;
const float total_size = (m_entry_height) * static_cast<float>(m_entries);
if (total_size > m_bounds.h) {
m_step_size = total_size / m_entries;
m_pos.x = m_bounds.x;
m_pos.y = m_bounds.y;
m_pos.w = 2.f;
m_pos.h = total_size - m_bounds.h;
m_should_draw = true;
// LOG("total size: %.2f\n", total_size);
// LOG("step size: %.2f\n", m_step_size);
// LOG("pos y: %.2f\n", m_pos.y);
// LOG("pos h: %.2f\n", m_pos.h);
} else {
// LOG("not big enough for scroll total: %.2f bounds: %.2f\n", total_size, bounds.h);
}
}
auto ScrollBar::Move(Direction direction) -> void {
switch (direction) {
case Direction::DOWN:
if (m_index < (m_entries - 1)) {
m_index++;
m_pos.y += m_step_size;
}
break;
case Direction::UP:
if (m_index != 0) {
m_index--;
m_pos.y -= m_step_size;
}
break;
}
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,323 @@
#include "ui/sidebar.hpp"
#include "app.hpp"
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
namespace sphaira::ui {
namespace {
struct SidebarSpacer : SidebarEntryBase {
};
struct SidebarHeader : SidebarEntryBase {
};
} // namespace
SidebarEntryBase::SidebarEntryBase(std::string&& title)
: m_title{std::forward<std::string>(title)} {
}
auto SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme) -> void {
// draw spacers or highlight box if in focus (selected)
if (HasFocus()) {
gfx::drawRect(vg, m_pos, nvgRGB(50,50,50));
gfx::drawRect(vg, m_pos, nvgRGB(0,0,0));
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
// gfx::drawRect(vg, m_pos.x - 4.f, m_pos.y - 4.f, m_pos.w + 8.f, m_pos.h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
// gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, m_pos.h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
gfx::drawRect(vg, m_pos.x, m_pos.y + m_pos.h, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
}
}
SidebarEntryBool::SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str, std::string false_str)
: SidebarEntryBase{std::move(title)}
, m_option{option}
, m_callback{cb}
, m_true_str{std::move(true_str)}
, m_false_str{std::move(false_str)} {
SetAction(Button::A, Action{"OK", [this](){
m_option ^= 1;
m_callback(m_option);
}
});
}
SidebarEntryBool::SidebarEntryBool(std::string title, bool& option, std::string true_str, std::string false_str)
: SidebarEntryBool{std::move(title), option, Callback{} } {
m_callback = [](bool& option){
option ^= 1;
};
}
auto SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme) -> void {
SidebarEntryBase::Draw(vg, theme);
// if (HasFocus()) {
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
// } else {
// }
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
if (m_option == true) {
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_true_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
} else { // text info
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_false_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
}
}
SidebarEntryCallback::SidebarEntryCallback(std::string title, Callback cb, bool pop_on_click)
: SidebarEntryBase{std::move(title)}
, m_callback{cb}
, m_pop_on_click{pop_on_click} {
SetAction(Button::A, Action{"OK", [this](){
m_callback();
if (m_pop_on_click) {
SetPop();
}
}
});
}
auto SidebarEntryCallback::Draw(NVGcontext* vg, Theme* theme) -> void {
SidebarEntryBase::Draw(vg, theme);
// if (HasFocus()) {
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
// } else {
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
// }
}
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, std::string& index)
: SidebarEntryArray{std::move(title), std::move(items), Callback{}, 0} {
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
}
m_list_callback = [&index, this]() {
App::Push(std::make_shared<PopupList>(
m_title, m_items, index, m_index
));
};
// m_callback = [&index, this](auto& idx) {
// App::Push(std::make_shared<PopupList>(
// m_title, m_items, index, idx
// ));
// };
}
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::string index)
: SidebarEntryArray{std::move(title), std::move(items), cb, 0} {
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
}
}
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index)
: SidebarEntryBase{std::forward<std::string>(title)}
, m_items{std::move(items)}
, m_callback{cb}
, m_index{index} {
m_list_callback = [this]() {
App::Push(std::make_shared<PopupList>(
m_title, m_items, [this](auto op_idx){
if (op_idx) {
m_index = *op_idx;
m_callback(m_index);
}
}, m_index
));
};
SetAction(Button::A, Action{"OK", [this](){
// m_callback(m_index);
m_list_callback();
}
});
}
auto SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme) -> void {
SidebarEntryBase::Draw(vg, theme);
const auto& text_entry = m_items[m_index];
// const auto& colour = HasFocus() ? theme->elements[ThemeEntryID_TEXT_SELECTED].colour : theme->elements[ThemeEntryID_TEXT].colour;
const auto& colour = theme->elements[ThemeEntryID_TEXT].colour;
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, text_entry.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
}
Sidebar::Sidebar(std::string title, Side side, Items&& items)
: Sidebar{std::move(title), "", side, std::forward<Items>(items)} {
}
Sidebar::Sidebar(std::string title, Side side)
: Sidebar{std::move(title), "", side, {}} {
}
Sidebar::Sidebar(std::string title, std::string sub, Side side, Items&& items)
: m_title{std::move(title)}
, m_sub{std::move(sub)}
, m_side{side}
, m_items{std::forward<Items>(items)} {
switch (m_side) {
case Side::LEFT:
SetPos(Vec4{0.f, 0.f, 450.f, 720.f});
break;
case Side::RIGHT:
SetPos(Vec4{1280.f - 450.f, 0.f, 450.f, 720.f});
break;
}
// setup top and bottom bar
m_top_bar = Vec4{m_pos.x + 15.f, 86.f, m_pos.w - 30.f, 1.f};
m_bottom_bar = Vec4{m_pos.x + 15.f, 646.f, m_pos.w - 30.f, 1.f};
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
// each item has it's own Action, but we take over B
SetAction(Button::B, Action{"Back", [this](){
SetPop();
}});
m_selected_y = m_base_pos.y;
if (!m_items.empty()) {
// setup positions
m_selected_y = m_base_pos.y;
// for (auto&p : m_items) {
// p->SetPos(m_base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// // give focus to first entry.
// m_items[m_index]->OnFocusGained();
}
}
Sidebar::Sidebar(std::string title, std::string sub, Side side)
: Sidebar{std::move(title), sub, side, {}} {
}
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
m_items[m_index]->Update(controller, touch);
Widget::Update(controller, touch);
if (m_items[m_index]->ShouldPop()) {
SetPop();
}
const auto old_index = m_index;
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_box_size.y;
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
m_index--;
m_selected_y -= m_box_size.y;
}
// if we moved
if (m_index != old_index) {
m_items[old_index]->OnFocusLost();
m_items[m_index]->OnFocusGained();
// move offset
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
m_selected_y -= m_box_size.y;
m_index_offset++;
// LOG("move down\n");
} else if (m_selected_y <= m_top_bar.y) {
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
m_selected_y += m_box_size.y;
m_index_offset--;
}
}
}
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
return Vec4{
va.x, va.y,
va.w, vb.y - va.y
};
}
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, m_pos, nvgRGBA(0, 0, 0, 220));
gfx::drawText(vg, m_title_pos, m_title_size, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
if (!m_sub.empty()) {
gfx::drawTextArgs(vg, m_pos.x + m_pos.w - 30.f, m_title_pos.y + 10.f, 18, NVG_ALIGN_TOP | NVG_ALIGN_RIGHT, theme->elements[ThemeEntryID_TEXT].colour, m_sub.c_str());
}
gfx::drawRect(vg, m_top_bar, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, m_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
nvgSave(vg);
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
// m_items[i]->Draw(vg, theme);
// }
for (auto&p : m_items) {
p->Draw(vg, theme);
}
nvgRestore(vg);
// draw the buttons. fetch the actions from current item and insert into array.
Actions draw_actions{m_actions};
const auto& actions_ref = m_items[m_index]->GetActions();
draw_actions.insert(actions_ref.cbegin(), actions_ref.cend());
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour, m_pos.x + m_pos.w - 60.f);
}
auto Sidebar::OnFocusGained() noexcept -> void {
Widget::OnFocusGained();
SetHidden(false);
}
auto Sidebar::OnFocusLost() noexcept -> void {
Widget::OnFocusLost();
SetHidden(true);
}
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
m_items.emplace_back(entry);
m_items.back()->SetPos(m_base_pos);
m_base_pos.y += m_base_pos.h;
// for (auto&p : m_items) {
// p->SetPos(base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// give focus to first entry.
m_items[m_index]->OnFocusGained();
}
void Sidebar::AddSpacer() {
}
void Sidebar::AddHeader(std::string name) {
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,48 @@
#include "ui/widget.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
namespace sphaira::ui {
void Widget::Update(Controller* controller, TouchInfo* touch) {
for (const auto& [button, action] : m_actions) {
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
action.Invoke(true);
App::PlaySoundEffect(SoundEffect_Focus);
}
else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) {
action.Invoke(false);
}
else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) {
action.Invoke(true);
}
}
}
void Widget::Draw(NVGcontext* vg, Theme* theme) {
Actions draw_actions;
for (const auto& [button, action] : m_actions) {
if (!action.IsHidden()) {
draw_actions.emplace(button, action);
}
}
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
}
auto Widget::HasAction(Button button) const -> bool {
return m_actions.contains(button);
}
void Widget::SetAction(Button button, Action action) {
m_actions.insert_or_assign(button, action);
}
void Widget::RemoveAction(Button button) {
if (auto it = m_actions.find(button); it != m_actions.end()) {
m_actions.erase(it);
}
}
} // namespace sphaira::ui