public release
This commit is contained in:
1156
sphaira/source/ui/error_box.cpp
Normal file
1156
sphaira/source/ui/error_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1545
sphaira/source/ui/menus/appstore.cpp
Normal file
1545
sphaira/source/ui/menus/appstore.cpp
Normal file
File diff suppressed because it is too large
Load Diff
46
sphaira/source/ui/menus/file_viewer.cpp
Normal file
46
sphaira/source/ui/menus/file_viewer.cpp
Normal 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
|
||||
1620
sphaira/source/ui/menus/filebrowser.cpp
Normal file
1620
sphaira/source/ui/menus/filebrowser.cpp
Normal file
File diff suppressed because it is too large
Load Diff
377
sphaira/source/ui/menus/homebrew.cpp
Normal file
377
sphaira/source/ui/menus/homebrew.cpp
Normal 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
|
||||
533
sphaira/source/ui/menus/irs_menu.cpp
Normal file
533
sphaira/source/ui/menus/irs_menu.cpp
Normal 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
|
||||
237
sphaira/source/ui/menus/main_menu.cpp
Normal file
237
sphaira/source/ui/menus/main_menu.cpp
Normal 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
|
||||
94
sphaira/source/ui/menus/menu_base.cpp
Normal file
94
sphaira/source/ui/menus/menu_base.cpp
Normal 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
|
||||
744
sphaira/source/ui/menus/themezer.cpp
Normal file
744
sphaira/source/ui/menus/themezer.cpp
Normal 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
|
||||
142
sphaira/source/ui/notification.cpp
Normal file
142
sphaira/source/ui/notification.cpp
Normal 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
|
||||
520
sphaira/source/ui/nvg_util.cpp
Normal file
520
sphaira/source/ui/nvg_util.cpp
Normal 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
|
||||
146
sphaira/source/ui/option_box.cpp
Normal file
146
sphaira/source/ui/option_box.cpp
Normal 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
|
||||
32
sphaira/source/ui/option_list.cpp
Normal file
32
sphaira/source/ui/option_list.cpp
Normal 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
|
||||
154
sphaira/source/ui/popup_list.cpp
Normal file
154
sphaira/source/ui/popup_list.cpp
Normal 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
|
||||
183
sphaira/source/ui/progress_box.cpp
Normal file
183
sphaira/source/ui/progress_box.cpp
Normal 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
|
||||
109
sphaira/source/ui/scrollable_text.cpp
Normal file
109
sphaira/source/ui/scrollable_text.cpp
Normal 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
|
||||
68
sphaira/source/ui/scrollbar.cpp
Normal file
68
sphaira/source/ui/scrollbar.cpp
Normal 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
|
||||
323
sphaira/source/ui/sidebar.cpp
Normal file
323
sphaira/source/ui/sidebar.cpp
Normal 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
|
||||
48
sphaira/source/ui/widget.cpp
Normal file
48
sphaira/source/ui/widget.cpp
Normal 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
|
||||
Reference in New Issue
Block a user