public release

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

1193
sphaira/source/app.cpp Normal file

File diff suppressed because it is too large Load Diff

610
sphaira/source/download.cpp Normal file
View File

@@ -0,0 +1,610 @@
#include "download.hpp"
#include "log.hpp"
#include "defines.hpp"
#include "evman.hpp"
#include "fs.hpp"
#include <switch.h>
#include <cstring>
#include <cassert>
#include <vector>
#include <deque>
#include <mutex>
#include <curl/curl.h>
namespace sphaira {
namespace {
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \
if (auto r = curl_easy_setopt(handle, opt, v); r != CURLE_OK) { \
log_write("curl_easy_setopt(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \
} \
#define CURL_SHARE_SETOPT_LOG(handle, opt, v) \
if (auto r = curl_share_setopt(handle, opt, v); r != CURLSHE_OK) { \
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
} \
void DownloadThread(void* p);
void DownloadThreadQueue(void* p);
#define USE_THREAD_QUEUE 1
constexpr auto API_AGENT = "ITotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2C;
constexpr int THREAD_CORE = 1;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
struct UrlCache {
auto AddToCache(const std::string& url, bool force = false) {
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
auto it = std::find(cache.begin(), cache.end(), url);
if (it == cache.end()) {
cache.emplace_back(url);
return true;
} else {
if (force) {
return true;
} else {
return false;
}
}
}
void RemoveFromCache(const std::string& url) {
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
auto it = std::find(cache.begin(), cache.end(), url);
if (it != cache.end()) {
cache.erase(it);
}
}
std::vector<std::string> cache;
Mutex mutex{};
};
struct DataStruct {
std::vector<u8> data;
u64 offset{};
FsFileSystem fs{};
FsFile f{};
s64 file_offset{};
};
struct ThreadEntry {
auto Create() -> Result {
m_curl = curl_easy_init();
R_UNLESS(m_curl != nullptr, 0x1);
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, DownloadThread, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
void Close() {
ueventSignal(&m_uevent);
threadWaitForExit(&m_thread);
threadClose(&m_thread);
if (m_curl) {
curl_easy_cleanup(m_curl);
m_curl = nullptr;
}
}
auto InProgress() -> bool {
return m_in_progress == true;
}
auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
assert(m_in_progress == false && "Setting up thread while active");
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_in_progress) {
return false;
}
m_url = url;
m_file = file;
m_post = post;
m_callback = callback;
m_pcallback = pcallback;
m_in_progress = true;
// log_write("started download :)\n");
ueventSignal(&m_uevent);
return true;
}
CURL* m_curl{};
Thread m_thread{};
std::string m_url{};
std::string m_file{}; // if empty, downloads to buffer
std::string m_post{}; // if empty, downloads to buffer
DownloadCallback m_callback{};
ProgressCallback m_pcallback{};
std::atomic_bool m_in_progress{};
Mutex m_mutex{};
UEvent m_uevent{};
};
struct ThreadQueueEntry {
std::string url;
std::string file;
std::string post;
DownloadCallback callback;
ProgressCallback pcallback;
bool m_delete{};
};
struct ThreadQueue {
std::deque<ThreadQueueEntry> m_entries;
Thread m_thread;
Mutex m_mutex{};
UEvent m_uevent{};
auto Create() -> Result {
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, DownloadThreadQueue, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
void Close() {
ueventSignal(&m_uevent);
threadWaitForExit(&m_thread);
threadClose(&m_thread);
}
auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
ThreadQueueEntry entry{};
entry.url = url;
entry.file = file;
entry.post = post;
entry.callback = callback;
entry.pcallback = pcallback;
switch (prio) {
case DownloadPriority::Normal:
m_entries.emplace_back(entry);
break;
case DownloadPriority::High:
m_entries.emplace_front(entry);
break;
}
ueventSignal(&m_uevent);
return true;
}
};
ThreadEntry g_threads[MAX_THREADS]{};
ThreadQueue g_thread_queue;
UrlCache g_url_cache;
void GetDownloadTempPath(fs::FsPath& buf) {
static Mutex mutex{};
static u64 count{};
mutexLock(&mutex);
const auto count_copy = count;
count++;
mutexUnlock(&mutex);
std::snprintf(buf, sizeof(buf), "/switch/sphaira/cache/download_temp%lu", count_copy);
}
auto ProgressCallbackFunc1(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
if (!g_running) {
return 1;
}
svcSleepThread(YieldType_WithoutCoreMigration);
return 0;
}
auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
if (!g_running) {
return 1;
}
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
auto callback = *static_cast<ProgressCallback*>(clientp);
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
return 1;
}
svcSleepThread(YieldType_WithoutCoreMigration);
return 0;
}
auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<DataStruct*>(userp);
const auto realsize = size * num_files;
// give it more memory
if (data_struct->data.capacity() < data_struct->offset + realsize) {
data_struct->data.reserve(data_struct->data.capacity() + CHUNK_SIZE);
}
data_struct->data.resize(data_struct->offset + realsize);
std::memcpy(data_struct->data.data() + data_struct->offset, contents, realsize);
data_struct->offset += realsize;
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<DataStruct*>(userp);
const auto realsize = size * num_files;
// flush data if incomming data would overflow the buffer
if (data_struct->offset && data_struct->data.size() < data_struct->offset + realsize) {
if (R_FAILED(fsFileWrite(&data_struct->f, data_struct->file_offset, data_struct->data.data(), data_struct->offset, FsWriteOption_None))) {
return 0;
}
data_struct->file_offset += data_struct->offset;
data_struct->offset = 0;
}
// we have a huge chunk! write it directly to file
if (data_struct->data.size() < realsize) {
if (R_FAILED(fsFileWrite(&data_struct->f, data_struct->file_offset, contents, realsize, FsWriteOption_None))) {
return 0;
}
data_struct->file_offset += realsize;
} else {
// buffer data until later
std::memcpy(data_struct->data.data() + data_struct->offset, contents, realsize);
data_struct->offset += realsize;
}
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
fs::FsPath safe_buf;
fs::FsPath tmp_buf;
const bool has_file = !file.empty() && file != "";
const bool has_post = !post.empty() && post != "";
ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } );
if (has_file) {
std::strcpy(safe_buf, file.c_str());
GetDownloadTempPath(tmp_buf);
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false);
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf);
if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create file: %s\n", tmp_buf);
return false;
}
if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
log_write("failed to open file: %s\n", tmp_buf);
return false;
}
}
// reserve the first chunk
chunk.data.reserve(CHUNK_SIZE);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
if (has_post) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str());
log_write("setting post field: %s\n", post.c_str());
}
// progress calls.
if (pcallback) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
}
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
// write calls.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, has_file ? WriteFileCallback : WriteMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk);
// perform download and cleanup after and report the result.
const auto res = curl_easy_perform(curl);
bool success = res == CURLE_OK;
if (has_file) {
if (res == CURLE_OK && chunk.offset) {
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
}
fsFileClose(&chunk.f);
if (res != CURLE_OK) {
fsFsDeleteFile(&chunk.fs, tmp_buf);
} else {
fsFsDeleteFile(&chunk.fs, safe_buf);
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, safe_buf);
if (R_FAILED(fsFsRenameFile(&chunk.fs, tmp_buf, safe_buf))) {
fsFsDeleteFile(&chunk.fs, tmp_buf);
success = false;
}
}
} else {
// empty data if we failed
if (res != CURLE_OK) {
chunk.data.clear();
}
}
log_write("Downloaded %s %s\n", url.c_str(), curl_easy_strerror(res));
return success;
}
auto DownloadInternal(DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
auto curl = curl_easy_init();
if (!curl) {
log_write("curl init failed\n");
return false;
}
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
return DownloadInternal(curl, chunk, pcallback, url, file, post);
}
void DownloadThread(void* p) {
auto data = static_cast<ThreadEntry*>(p);
while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
// log_write("woke up\n");
if (!g_running) {
return;
}
if (R_FAILED(rc)) {
continue;
}
DataStruct chunk;
#if 1
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post);
if (g_running) {
DownloadEventData event_data{data->m_callback, std::move(chunk.data), result};
evman::push(std::move(event_data), false);
} else {
break;
}
#endif
// mutexLock(&data->m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&data->m_mutex));
data->m_in_progress = false;
// notify the queue that there's a space free
ueventSignal(&g_thread_queue.m_uevent);
}
log_write("exited download thread\n");
}
void DownloadThreadQueue(void* p) {
auto data = static_cast<ThreadQueue*>(p);
while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
log_write("[thread queue] woke up\n");
if (!g_running) {
return;
}
if (R_FAILED(rc)) {
continue;
}
mutexLock(&data->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&data->m_mutex));
if (data->m_entries.empty()) {
continue;
}
// find the next avaliable thread
u32 pop_count{};
for (auto& entry : data->m_entries) {
if (!g_running) {
return;
}
bool keep_going{};
for (auto& thread : g_threads) {
if (!g_running) {
return;
}
if (!thread.InProgress()) {
thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post);
// log_write("[dl queue] starting download\n");
// mark entry for deletion
entry.m_delete = true;
pop_count++;
keep_going = true;
break;
}
}
if (!g_running) {
return;
}
if (!keep_going) {
break;
}
}
// delete all entries marked for deletion
for (u32 i = 0; i < pop_count; i++) {
data->m_entries.pop_front();
}
// if (delete_any) {
// data->m_entries.clear();
// data->m_entries.
// data->m_entries.erase(std::remove_if(data->m_entries.begin(), data->m_entries.end(), [](auto& a) {
// return a.m_delete;
// }));
// }
}
log_write("exited download thread queue\n");
}
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]);
}
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
mutexUnlock(&g_mutex_share[data]);
}
} // namespace
auto DownloadInit() -> bool {
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
return false;
}
g_curl_share = curl_share_init();
if (g_curl_share) {
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_LOCKFUNC, my_lock);
CURL_SHARE_SETOPT_LOG(g_curl_share, CURLSHOPT_UNLOCKFUNC, my_unlock);
}
g_running = true;
if (R_FAILED(g_thread_queue.Create())) {
log_write("!failed to create download thread queue\n");
}
for (auto& entry : g_threads) {
if (R_FAILED(entry.Create())) {
log_write("!failed to create download thread\n");
}
}
log_write("finished creating threads\n");
return true;
}
void DownloadExit() {
g_running = false;
g_thread_queue.Close();
for (auto& entry : g_threads) {
entry.Close();
}
if (g_curl_share) {
curl_share_cleanup(g_curl_share);
g_curl_share = {};
}
curl_global_cleanup();
}
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector<u8> {
if (g_url_cache.AddToCache(url)) {
DataStruct chunk{};
if (DownloadInternal(chunk, pcallback, url, "", post)) {
return chunk.data;
}
}
return {};
}
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool {
if (g_url_cache.AddToCache(url)) {
DataStruct chunk{};
if (DownloadInternal(chunk, pcallback, url, out, post)) {
return true;
}
}
return false;
}
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
#if USE_THREAD_QUEUE
if (g_url_cache.AddToCache(url)) {
return g_thread_queue.Add(prio, callback, pcallback, url, "", post);
} else {
return false;
}
#else
// mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
for (auto& entry : g_threads) {
if (!entry.InProgress()) {
return entry.Setup(callback, url);
}
}
log_write("failed to start download, no avaliable threads\n");
return false;
#endif
}
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
#if USE_THREAD_QUEUE
if (g_url_cache.AddToCache(url)) {
return g_thread_queue.Add(prio, callback, pcallback, url, out, post);
} else {
return false;
}
#else
// mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
for (auto& entry : g_threads) {
if (!entry.InProgress()) {
return entry.Setup(callback, url, out);
}
}
log_write("failed to start download, no avaliable threads\n");
return false;
#endif
}
void DownloadClearCache(const std::string& url) {
g_url_cache.AddToCache(url);
g_url_cache.RemoveFromCache(url);
}
} // namespace sphaira

57
sphaira/source/evman.cpp Normal file
View File

@@ -0,0 +1,57 @@
#include "evman.hpp"
#include <mutex>
#include <optional>
#include <algorithm>
#include <list>
namespace sphaira::evman {
namespace {
std::mutex mutex{};
std::list<EventData> events;
void remove_if_matching(const EventData& e, bool remove_matching) {
if (remove_matching) {
events.remove_if([&e](const EventData&a) { return e.index() == a.index(); });
}
}
} // namespace
auto push(const EventData& e, bool remove_matching) -> bool {
std::scoped_lock lock(mutex);
remove_if_matching(e, remove_matching);
events.push_back(e);
return true;
}
auto push(EventData&& e, bool remove_matching) -> bool {
std::scoped_lock lock(mutex);
remove_if_matching(e, remove_matching);
events.emplace_back(std::forward<EventData>(e));
return true;
}
auto count() -> std::size_t {
std::scoped_lock lock(mutex);
return events.size();
}
auto pop() -> std::optional<EventData> {
std::scoped_lock lock(mutex);
if (events.empty()) {
return std::nullopt;
}
auto e = events.front();
events.pop_front();
return e;
}
auto popall() -> std::list<EventData> {
std::scoped_lock lock(mutex);
auto list_copy = events;
events.clear();
return list_copy;
}
} // namespace sphaira::evman

441
sphaira/source/fs.cpp Normal file
View File

@@ -0,0 +1,441 @@
#include "fs.hpp"
#include "defines.hpp"
#include "ui/nvg_util.hpp"
#include "log.hpp"
#include <switch.h>
#include <cstdio>
#include <cstring>
#include <vector>
#include <string_view>
#include <algorithm>
#include <ranges>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <ftw.h>
namespace fs {
namespace {
// these folders and internals cannot be modified
constexpr std::string_view READONLY_ROOT_FOLDERS[]{
"/atmosphere/automatic_backups",
"/bootloader/res",
"/bootloader/sys",
"/backup", // some people never back this up...
"/Nintendo", // Nintendo private folder
"/Nintendo/Contents",
"/Nintendo/save",
"/emuMMC", // emunand
"/warmboot_mariko",
};
// these files and folders cannot be modified
constexpr std::string_view READONLY_FILES[]{
"/", // don't allow deleting root
"/atmosphere", // don't allow deleting all of /atmosphere
"/atmosphere/hbl.nsp",
"/atmosphere/package3",
"/atmosphere/reboot_payload.bin",
"/atmosphere/stratosphere.romfs",
"/bootloader", // don't allow deleting all of /bootloader
"/bootloader/hekate_ipl.ini",
"/switch", // don't allow deleting all of /switch
"/hbmenu.nro", // breaks hbl
"/payload.bin", // some modchips need this
"/boot.dat", // sxos
"/license.dat", // sxos
"/switch/prod.keys",
"/switch/title.keys",
"/switch/reboot_to_payload.nro",
};
bool is_read_only_root(std::string_view path) {
for (auto p : READONLY_ROOT_FOLDERS) {
if (path.starts_with(p)) {
return true;
}
}
return false;
}
bool is_read_only_file(std::string_view path) {
for (auto p : READONLY_FILES) {
if (path == p) {
return true;
}
}
return false;
}
bool is_read_only(std::string_view path) {
if (is_read_only_root(path)) {
return true;
}
if (is_read_only_file(path)) {
return true;
}
return false;
}
} // namespace
FsPath AppendPath(const FsPath& root_path, const FsPath& file_path) {
FsPath path;
if (root_path[std::strlen(root_path) - 1] != '/') {
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path.s);
} else {
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path.s);
}
return path;
}
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return fsFsCreateFile(fs, path, size, option);
}
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return fsFsCreateDirectory(fs, path);
}
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
auto path_view = std::string_view{_path};
FsPath path{"/"};
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
std::strncat(path, dir.data(), dir.size());
Result rc;
if (fs) {
rc = CreateDirectory(fs, path, ignore_read_only);
} else {
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create folder: %s\n", path);
return rc;
}
// log_write("created_directory: %s\n", path);
std::strcat(path, "/");
}
R_SUCCEED();
}
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
size_t off = 0;
while (true) {
const auto first = std::strchr(_path + off, '/');
if (!first) {
R_SUCCEED();
}
off = (first - _path.s) + 1;
FsPath path;
std::strncpy(path, _path, off);
Result rc;
if (fs) {
rc = CreateDirectory(fs, path, ignore_read_only);
} else {
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create folder recursively: %s\n", path);
return rc;
}
// log_write("created_directory recursively: %s\n", path);
}
}
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteFile(fs, path);
}
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteDirectory(fs, path);
}
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteDirectoryRecursively(fs, path);
}
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return fsFsRenameFile(fs, src, dst);
}
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return fsFsRenameDirectory(fs, src, dst);
}
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out) {
return fsFsGetEntryType(fs, path, out);
}
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out) {
return fsFsGetFileTimeStampRaw(fs, path, out);
}
bool FileExists(FsFileSystem* fs, const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(fs, path, &type), false);
return type == FsDirEntryType_File;
}
bool DirExists(FsFileSystem* fs, const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(fs, path, &type), false);
return type == FsDirEntryType_Dir;
}
Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>& out) {
FsNative fs{_fs, false};
R_TRY(fs.GetFsOpenResult());
FsFile f;
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
s64 size;
R_TRY(fsFileGetSize(&f, &size));
out.resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&f, 0, out.data(), out.size(), FsReadOption_None, &bytes_read));
R_UNLESS(bytes_read == out.size(), 1);
R_SUCCEED();
}
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
FsNative fs{_fs, false};
R_TRY(fs.GetFsOpenResult());
if (auto rc = fs.CreateFile(path, in.size(), 0, ignore_read_only); R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
return rc;
}
FsFile f;
R_TRY(fs.OpenFile(path, FsOpenMode_Write, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileSetSize(&f, in.size()));
R_TRY(fsFileWrite(&f, 0, in.data(), in.size(), FsWriteOption_None));
R_SUCCEED();
}
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(fs, src, data));
return write_entire_file(fs, dst, data, ignore_read_only);
}
Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
auto fd = open(path, O_CREAT | S_IRUSR | S_IWUSR);
if (fd == -1) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
close(fd);
R_SUCCEED();
}
Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
if (mkdir(path, 0777)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return CreateDirectoryRecursively(nullptr, path, ignore_read_only);
}
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
return CreateDirectoryRecursivelyWithPath(nullptr, path, ignore_read_only);
}
Result DeleteFile(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
if (remove(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result DeleteDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return DeleteFile(path, ignore_read_only);
}
// ftw / ntfw isn't found by linker...
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
#if 0
// const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) -> int {
const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag) -> int {
return remove(fpath);
};
// todo: check for reasonable max fd limit
// if (nftw(path, unlink_cb, 16, FTW_DEPTH)) {
if (ftw(path, unlink_cb, 16)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
#else
R_THROW(0xFFFF);
#endif
}
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
if (rename(src, dst)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
return RenameFile(src, dst, ignore_read_only);
}
Result GetEntryType(const FsPath& path, FsDirEntryType* out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
R_SUCCEED();
}
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
out->is_valid = true;
out->created = st.st_ctim.tv_sec;
out->modified = st.st_mtim.tv_sec;
out->accessed = st.st_atim.tv_sec;
R_SUCCEED();
}
bool FileExists(const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(path, &type), false);
return type == FsDirEntryType_File;
}
bool DirExists(const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(path, &type), false);
return type == FsDirEntryType_Dir;
}
Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
auto f = std::fopen(path, "rb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
std::fseek(f, 0, SEEK_END);
const auto size = std::ftell(f);
std::rewind(f);
out.resize(size);
std::fread(out.data(), 1, out.size(), f);
R_SUCCEED();
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
auto f = std::fopen(path, "wb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
std::fwrite(in.data(), 1, in.size(), f);
R_SUCCEED();
}
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(src, data));
return write_entire_file(dst, data, ignore_read_only);
}
} // namespace fs

118
sphaira/source/i18n.cpp Normal file
View File

@@ -0,0 +1,118 @@
#include "i18n.hpp"
#include "fs.hpp"
#include "log.hpp"
#include <yyjson.h>
#include <vector>
namespace sphaira::i18n {
namespace {
std::vector<u8> g_i18n_data;
yyjson_doc* json;
yyjson_val* root;
std::string get(const char* str, size_t len) {
if (!json || !root) {
log_write("no json or root\n");
return str;
}
auto key = yyjson_obj_getn(root, str, len);
if (!key) {
log_write("\tfailed to find key: [%.*s]\n", len, str);
return str;
}
auto val = yyjson_get_str(key);
auto val_len = yyjson_get_len(key);
if (!val || !val_len) {
log_write("\tfailed to get value: [%.*s]\n", len, str);
return str;
}
return {val, val_len};
}
} // namespace
bool init(long index) {
R_TRY_RESULT(romfsInit(), false);
ON_SCOPE_EXIT( romfsExit() );
u64 languageCode;
SetLanguage setLanguage = SetLanguage_ENGB;
switch (index) {
case 0: // auto
if (R_SUCCEEDED(setGetSystemLanguage(&languageCode))) {
setMakeLanguage(languageCode, &setLanguage);
}
break;
case 1: setLanguage = SetLanguage_ENGB; break; // "English"
case 2: setLanguage = SetLanguage_JA; break; // "Japanese"
case 3: setLanguage = SetLanguage_FR; break; // "French"
case 4: setLanguage = SetLanguage_DE; break; // "German"
case 5: setLanguage = SetLanguage_IT; break; // "Italian"
case 6: setLanguage = SetLanguage_ES; break; // "Spanish"
case 7: setLanguage = SetLanguage_ZHCN; break; // "Chinese"
case 8: setLanguage = SetLanguage_KO; break; // "Korean"
case 9: setLanguage = SetLanguage_NL; break; // "Dutch"
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
}
std::string lang_name;
switch (setLanguage) {
case SetLanguage_JA: lang_name = "ja"; break;
case SetLanguage_FR: lang_name = "fr"; break;
case SetLanguage_DE: lang_name = "de"; break;
case SetLanguage_IT: lang_name = "it"; break;
case SetLanguage_ES: lang_name = "es"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_KO: lang_name = "ko"; break;
case SetLanguage_NL: lang_name = "nl"; break;
case SetLanguage_PT: lang_name = "pt"; break;
case SetLanguage_RU: lang_name = "ru"; break;
case SetLanguage_ZHTW: lang_name = "zh"; break;
default: lang_name = "en"; break;
}
const fs::FsPath path = "romfs:/i18n/" + lang_name + ".json";
if (R_SUCCEEDED(fs::FsStdio().read_entire_file(path, g_i18n_data))) {
json = yyjson_read((const char*)g_i18n_data.data(), g_i18n_data.size(), YYJSON_READ_ALLOW_TRAILING_COMMAS|YYJSON_READ_ALLOW_COMMENTS|YYJSON_READ_ALLOW_INVALID_UNICODE);
if (json) {
root = yyjson_doc_get_root(json);
if (root) {
log_write("opened json: %s\n", path.s);
return true;
} else {
log_write("failed to find root\n");
}
} else {
log_write("failed open json\n");
}
} else {
log_write("failed to read file\n");
}
return false;
}
void exit() {
if (json) {
yyjson_doc_free(json);
json = nullptr;
}
g_i18n_data.clear();
}
} // namespace sphaira::i18n
namespace literals {
std::string operator"" _i18n(const char* str, size_t len) {
return sphaira::i18n::get(str, len);
}
} // namespace literals

90
sphaira/source/image.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "image.hpp"
// disable warnings for stb
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
#pragma GCC diagnostic ignored "-Warray-bounds="
#include "nanovg/stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_WRITE_STATIC
#define STBI_WRITE_NO_STDIO
#include "stb_image_write.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#define STB_IMAGE_RESIZE_STATIC
#include "stb_image_resize2.h"
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
#include "log.hpp"
#include <cstring>
namespace sphaira {
namespace {
constexpr int BPP = 4;
auto ImageLoadInternal(stbi_uc* image_data, int x, int y) -> ImageResult {
if (image_data) {
log_write("loaded image: w: %d h: %d\n", x, y);
ImageResult result{};
result.data.resize(x*y*BPP);
result.w = x;
result.h = y;
std::memcpy(result.data.data(), image_data, result.data.size());
stbi_image_free(image_data);
return result;
}
log_write("failed image load\n");
return {};
}
} // namespace
auto ImageLoadFromMemory(std::span<const u8> data) -> ImageResult {
int x, y, channels;
return ImageLoadInternal(stbi_load_from_memory(data.data(), data.size(), &x, &y, &channels, BPP), x, y);
}
auto ImageLoadFromFile(const fs::FsPath& file) -> ImageResult {
log_write("doing file load\n");
int x, y, channels;
return ImageLoadInternal(stbi_load(file, &x, &y, &channels, BPP), x, y);
}
auto ImageResize(std::span<const u8> data, int inx, int iny, int outx, int outy) -> ImageResult {
log_write("doing resize inx: %d iny: %d outx: %d outy: %d\n", inx, iny, outx, outy);
std::vector<u8> resized_data(outx*outy*BPP);
// if (stbir_resize_uint8(data.data(), inx, iny, inx * BPP, resized_data.data(), outx, outy, outx*BPP, BPP)) {
if (stbir_resize_uint8_linear(data.data(), inx, iny, inx * BPP, resized_data.data(), outx, outy, outx*BPP, (stbir_pixel_layout)BPP)) {
log_write("did resize\n");
return { resized_data, outx, outy };
}
log_write("failed resize\n");
return {};
}
auto ImageConvertToJpg(std::span<const u8> data, int x, int y) -> ImageResult {
std::vector<u8> out;
out.reserve(x*y*BPP);
log_write("doing jpeg convert\n");
const auto cb = [](void *context, void *data, int size) -> void {
auto buf = static_cast<std::vector<u8>*>(context);
const auto offset = buf->size();
buf->resize(offset + size);
std::memcpy(buf->data() + offset, data, size);
};
if (stbi_write_jpg_to_func(cb, &out, x, y, 4, data.data(), 93)) {
// out.shrink_to_fit();
log_write("did jpg convert\n");
return { out, x, y };
}
log_write("failed jpg convert\n");
return {};
}
} // namespace sphaira

72
sphaira/source/log.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include "log.hpp"
#include <cstdio>
#include <cstdarg>
#include <unistd.h>
#include <mutex>
#include <switch.h>
#if sphaira_USE_LOG
namespace {
constexpr const char* logpath = "/config/sphaira/log.txt";
std::FILE* file{};
int nxlink_socket{};
std::mutex mutex{};
} // namespace
auto log_file_init() -> bool {
std::scoped_lock lock{mutex};
if (file) {
return false;
}
file = std::fopen(logpath, "w");
return file != nullptr;
}
auto log_nxlink_init() -> bool {
std::scoped_lock lock{mutex};
if (nxlink_socket) {
return false;
}
nxlink_socket = nxlinkConnectToHost(true, false);
return nxlink_socket != 0;
}
void log_file_exit() {
std::scoped_lock lock{mutex};
if (file) {
std::fclose(file);
file = nullptr;
}
}
void log_nxlink_exit() {
std::scoped_lock lock{mutex};
if (nxlink_socket) {
close(nxlink_socket);
nxlink_socket = 0;
}
}
void log_write(const char* s, ...) {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
return;
}
std::va_list v{};
va_start(v, s);
if (file) {
std::vfprintf(file, s, v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(s, v);
}
va_end(v);
}
#endif

77
sphaira/source/main.cpp Normal file
View File

@@ -0,0 +1,77 @@
#include <switch.h>
#include <memory>
#include "app.hpp"
#include "log.hpp"
int main(int argc, char** argv) {
if (!argc || !argv) {
return 1;
}
auto app = std::make_unique<sphaira::App>(argv[0]);
app->Loop();
return 0;
}
extern "C" {
void userAppInit(void) {
Result rc;
// https://github.com/mtheall/ftpd/blob/e27898f0c3101522311f330e82a324861e0e3f7e/source/switch/init.c#L31
const SocketInitConfig socket_config_application = {
.tcp_tx_buf_size = 1024 * 64,
.tcp_rx_buf_size = 1024 * 64,
.tcp_tx_buf_max_size = 1024 * 1024 * 4,
.tcp_rx_buf_max_size = 1024 * 1024 * 4,
.udp_tx_buf_size = 0x2400, // same as default
.udp_rx_buf_size = 0xA500, // same as default
.sb_efficiency = 8,
.num_bsd_sessions = 3,
.bsd_service_type = BsdServiceType_Auto,
};
const SocketInitConfig socket_config_applet = {
.tcp_tx_buf_size = 1024 * 32,
.tcp_rx_buf_size = 1024 * 64,
.tcp_tx_buf_max_size = 1024 * 256,
.tcp_rx_buf_max_size = 1024 * 256,
.udp_tx_buf_size = 0x2400, // same as default
.udp_rx_buf_size = 0xA500, // same as default
.sb_efficiency = 4,
.num_bsd_sessions = 3,
.bsd_service_type = BsdServiceType_Auto,
};
const auto is_application = sphaira::App::IsApplication();
const auto socket_config = is_application ? socket_config_application : socket_config_applet;
if (R_FAILED(rc = appletLockExit()))
diagAbortWithResult(rc);
if (R_FAILED(rc = socketInitialize(&socket_config)))
diagAbortWithResult(rc);
if (R_FAILED(rc = plInitialize(PlServiceType_User)))
diagAbortWithResult(rc);
if (R_FAILED(rc = psmInitialize()))
diagAbortWithResult(rc);
if (R_FAILED(rc = nifmInitialize(NifmServiceType_User)))
diagAbortWithResult(rc);
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
diagAbortWithResult(rc);
log_nxlink_init();
}
void userAppExit(void) {
log_nxlink_exit();
accountExit();
nifmExit();
psmExit();
plExit();
socketExit();
appletUnlockExit();
}
} // extern "C"

313
sphaira/source/nro.cpp Normal file
View File

@@ -0,0 +1,313 @@
#include "nro.hpp"
#include "defines.hpp"
#include "evman.hpp"
#include "app.hpp"
#include "log.hpp"
#include <switch.h>
#include <vector>
#include <cstring>
#include <string_view>
#include <minIni.h>
namespace sphaira {
namespace {
enum {
Module_Nro = 421,
};
enum NroError {
NroError_BadMagic = MAKERESULT(Module_Nro, 1),
NroError_BadSize = MAKERESULT(Module_Nro, 2),
};
struct NroData {
NroStart start;
NroHeader header;
};
auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entry) -> Result {
entry.path = path;
// todo: special sorting for fw 2.0.0 to make it not look like shit
if (hosversionAtLeast(3,0,0)) {
// it doesn't matter if we fail
entry.timestamp.is_valid = false;
fs.GetFileTimeStampRaw(entry.path, &entry.timestamp);
// if (R_FAILED(fsFsGetFileTimeStampRaw(fs, entry.path, &entry.timestamp))) {
// // log_write("failed to get timestamp for: %s\n", path);
// }
}
FsFile f;
R_TRY(fs.OpenFile(entry.path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileGetSize(&f, &entry.size));
NroData data;
u64 bytes_read;
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
NroAssetHeader asset;
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
// some .nro (vgedit) have bad nacp, fake the nacp
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(entry.nacp)) {
std::memset(&entry.nacp, 0, sizeof(entry.nacp));
// get the name without the .nro
const auto file_name = std::strrchr(path, '/') + 1;
const auto file_name_len = std::strlen(file_name);
for (auto& lang : entry.nacp.lang) {
std::strncpy(lang.name, file_name, file_name_len - 4);
std::strcpy(lang.author, "Unknown");
}
std::strcpy(entry.nacp.display_version, "Unknown");
entry.is_nacp_valid = false;
} else {
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &entry.nacp, sizeof(entry.nacp), FsReadOption_None, &bytes_read));
entry.is_nacp_valid = true;
log_write("got nacp\n");
}
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
R_SUCCEED();
}
// this function is recursive by 1 level deep
// if the nro is in switch/folder/folder2/app.nro it will NOT be found
// switch/folder/app.nro for example will work fine.
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
// we don't need to scan for folders if we are not root
u32 dir_open_type = FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize;
if (root) {
dir_open_type |= FsDirOpenMode_ReadDirs;
}
FsDir d;
R_TRY(fs.OpenDirectory(path, dir_open_type, &d));
ON_SCOPE_EXIT(fs.DirClose(&d));
s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count));
// return early if empty
R_UNLESS(count > 0, 0x0);
// we won't run out of memory here
std::vector<FsDirectoryEntry> entries(count);
R_TRY(fs.DirRead(&d, &count, entries.size(), entries.data()));
// size may of changed
entries.resize(count);
for (const auto& e : entries) {
// skip hidden files / folders
if ('.' == e.name[0]) {
continue;
}
// skip self
if (hide_sphaira && !strncmp(e.name, "sphaira", strlen("sphaira"))) {
continue;
}
if (e.type == FsDirEntryType_Dir) {
// assert(!root && "dir should only be scanned on non-root!");
fs::FsPath fullpath;
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path, e.name, e.name);
// fast path for detecting an nro in a folder
NroEntry entry;
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
// log_write("NRO: fast path for: %s\n", fullpath);
nros.emplace_back(entry);
} else {
// slow path...
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
}
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
fs::FsPath fullpath;
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
NroEntry entry;
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
nros.emplace_back(entry);
if (!root && !scan_all_dir) {
// log_write("NRO: slow path for: %s\n", fullpath);
R_SUCCEED();
}
} else {
log_write("error when trying to parse %s\n", fullpath);
}
}
}
R_SUCCEED();
}
auto nro_get_icon_internal(FsFile* f, u64 size, u64 offset) -> std::vector<u8> {
std::vector<u8> icon;
u64 bytes_read{};
icon.resize(size);
R_TRY_RESULT(fsFileRead(f, offset, icon.data(), icon.size(), FsReadOption_None, &bytes_read), {});
R_UNLESS(bytes_read == icon.size(), {});
return icon;
}
auto launch_internal(const std::string& path, const std::string& argv) -> Result {
R_TRY(envSetNextLoad(path.c_str(), argv.c_str()));
log_write("set launch with path: %s argv: %s\n", path.c_str(), argv.c_str());
evman::push(evman::LaunchNroEventData{path, argv});
R_SUCCEED();
}
} // namespace
/*
NRO INFO PAGE:
- icon
- name
- author
- path
- filesize
- launch count
- timestamp created
- timestamp modified
*/
auto nro_verify(std::span<const u8> data) -> Result {
NroData nro;
R_UNLESS(data.size() >= sizeof(nro), NroError_BadSize);
memcpy(&nro, data.data(), sizeof(nro));
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_SUCCEED();
}
auto nro_parse(const fs::FsPath& path, NroEntry& entry) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return nro_parse_internal(fs, path, entry);
}
auto nro_scan(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir) -> Result {
return nro_scan_internal(path, nros, hide_sphaira, nested, scan_all_dir, true);
}
auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
return nro_get_icon_internal(&f, size, offset);
}
auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY_RESULT(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read), {});
R_UNLESS(data.header.magic == NROHEADER_MAGIC, {});
R_TRY_RESULT(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {});
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, {});
return nro_get_icon_internal(&f, asset.icon.size, asset.icon.offset);
}
auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
R_SUCCEED();
}
auto nro_launch(std::string path, std::string args) -> Result {
if (path.empty()) {
return 1;
}
// keeps compat with hbloader
// https://github.com/ITotalJustice/Gamecard-Installer-NX/blob/master/source/main.c#L73
// https://github.com/ITotalJustice/Gamecard-Installer-NX/blob/d549c5f916dea814fa0a7e5dc8c903fa3044ba15/source/main.c#L29
if (!path.starts_with("sdmc:")) {
path = "sdmc:" + path;
}
if (args.empty()) {
args = nro_add_arg(path);
} else {
args = nro_add_arg(path) + ' ' + args;
}
return launch_internal(path, args);
}
auto nro_add_arg(std::string arg) -> std::string {
if (arg.contains(' ')) {
return '\"' + arg + '\"';
}
return arg;
}
auto nro_add_arg_file(std::string arg) -> std::string {
if (!arg.starts_with("sdmc:")) {
arg = "sdmc:" + arg;
}
if (arg.contains(' ')) {
return '\"' + arg + '\"';
}
return arg;
}
auto nro_normalise_path(const std::string& p) -> std::string {
if (p.starts_with("sdmc:")) {
return p.substr(5);
}
return p;
}
} // namespace sphaira

500
sphaira/source/nxlink.cpp Normal file
View File

@@ -0,0 +1,500 @@
// very quick and dirty way to talk to nxlink-pc
#include "nxlink.h"
#include "defines.hpp"
#include "nro.hpp"
#include "log.hpp"
#include "fs.hpp"
#include <cstring>
#include <vector>
#include <mutex>
#include <string>
// #include <string_view>
#include <zlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
namespace {
using Socket = int;
constexpr s32 SERVER_PORT = NXLINK_SERVER_PORT;
constexpr s32 CLIENT_PORT = NXLINK_CLIENT_PORT;
constexpr s32 ZLIB_CHUNK = 0x4000;
constexpr s32 ERR_OK = 0;
constexpr s32 ERR_FILE = -1;
constexpr s32 ERR_SPACE = -2;
constexpr s32 ERR_MEM = -3;
constexpr const char UDP_MAGIC_SERVER[] = {"nxboot"};
constexpr const char UDP_MAGIC_CLIENT[] = {"bootnx"};
Thread g_thread{};
std::mutex g_mutex{};
std::atomic_bool g_quit{false};
bool g_is_running{false};
NxlinkCallback g_callback{};
struct SocketWrapper {
SocketWrapper(Socket socket) : sock{socket} {
this->nonBlocking();
}
SocketWrapper(int af, int type, int proto) {
this->sock = socket(af, type, proto);
this->nonBlocking();
}
~SocketWrapper() {
if (this->sock > 0) {
shutdown(this->sock, SHUT_RDWR);
close(this->sock);
}
}
void nonBlocking() {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
}
Socket operator=(Socket s) { return this->sock = s; }
operator int() { return this->sock; }
Socket sock{};
};
struct ZlibWrapper {
ZlibWrapper() { inflateInit(&strm); }
ZlibWrapper(u8* out, size_t size) {
inflateInit(&strm);
strm.avail_out = size;
strm.next_out = out;
}
~ZlibWrapper() { inflateEnd(&strm); }
z_stream* operator&() { return &this->strm; }
int Inflate(int flush) { return inflate(&this->strm, flush); }
void Setup(u8* in, size_t size) {
strm.next_in = in;
strm.avail_in = size;
}
z_stream strm{};
};
void WriteCallbackNone(NxlinkCallbackType type) {
if (!g_callback) {
return;
}
NxlinkCallbackData data = {type};
g_callback(&data);
}
void WriteCallbackFile(NxlinkCallbackType type, const char* name) {
if (!g_callback) {
return;
}
NxlinkCallbackData data = {type};
std::strcpy(data.file.filename, name);
g_callback(&data);
}
void WriteCallbackProgress(NxlinkCallbackType type, s64 offset, s64 size) {
if (!g_callback) {
return;
}
NxlinkCallbackData data = {type};
data.progress.offset = offset;
data.progress.size = size;
g_callback(&data);
}
auto recvall(int sock, void* buf, int size) -> bool {
auto p = static_cast<u8*>(buf);
int got{}, left{size};
while (!g_quit && got < size) {
const auto len = recv(sock, p + got, left, 0);
if (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
}
svcSleepThread(YieldType_WithoutCoreMigration);
} else {
got += len;
left -= len;
}
}
return !g_quit;
}
auto sendall(Socket sock, const void* buf, int size) -> bool {
auto p = static_cast<const u8*>(buf);
int sent{}, left{size};
while (!g_quit && sent < size) {
const auto len = send(sock, p + sent, left, 0);
if (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
}
svcSleepThread(YieldType_WithoutCoreMigration);
}
sent += len;
left -= len;
}
return !g_quit;
}
auto get_file_data(Socket sock, int max) -> std::vector<u8> {
std::vector<u8> buf(max);
std::vector<u8> chunk(ZLIB_CHUNK);
ZlibWrapper zlib{buf.data(), buf.size()};
u32 want{};
while (zlib.strm.total_out < buf.size()) {
if (g_quit) {
return {};
}
if (!recvall(sock, &want, sizeof(want))) {
return {};
}
if (want > chunk.size()) {
want = chunk.size();
}
if (!recvall(sock, chunk.data(), want)) {
return {};
}
WriteCallbackProgress(NxlinkCallbackType_WriteProgress, want, max);
zlib.Setup(chunk.data(), want);
zlib.Inflate(Z_NO_FLUSH);
}
return buf;
}
#if 0
auto create_directories(fs::FsNative& fs, const std::string& path) -> Result {
std::size_t pos{};
// no sane person creates 20 directories deep
for (int i = 0; i < 20; i++) {
pos = path.find_first_of("/", pos);
if (pos == std::string::npos) {
break;
}
pos++;
fs::FsPath safe_buf;
std::strcpy(safe_buf, path.substr(0, pos).c_str());
const auto rc = fs.CreateDirectory(safe_buf);
R_UNLESS(R_SUCCEEDED(rc) || rc == FsError_ResultPathAlreadyExists, rc);
}
R_SUCCEED();
}
#endif
void loop(void* args) {
log_write("in nxlink thread func\n");
const sockaddr_in servaddr{
.sin_family = AF_INET,
.sin_port = htons(SERVER_PORT),
.sin_addr = htonl(INADDR_ANY),
};
Result rc;
const auto poll_network_change = []() -> bool {
static u32 current_addr, subnet_mask, gateway, primary_dns_server, secondary_dns_server;
u32 t_current_addr, t_subnet_mask, t_gateway, t_primary_dns_server, t_secondary_dns_server;
if (R_FAILED(nifmGetCurrentIpConfigInfo(&t_current_addr, &t_subnet_mask, &t_gateway, &t_primary_dns_server, &t_secondary_dns_server))) {
return true;
}
if (current_addr != t_current_addr || subnet_mask != t_subnet_mask || gateway != t_gateway || primary_dns_server != t_primary_dns_server || secondary_dns_server != t_secondary_dns_server) {
current_addr = t_current_addr;
subnet_mask = t_subnet_mask;
gateway = t_gateway;
primary_dns_server = t_primary_dns_server;
secondary_dns_server = t_secondary_dns_server;
return true;
}
return false;
};
while (!g_quit) {
svcSleepThread(33'333'333);
if (poll_network_change()) {
continue;
}
SocketWrapper sock{AF_INET, SOCK_STREAM, 0};
SocketWrapper sock_udp(AF_INET, SOCK_DGRAM, 0);
if (sock < 0 || sock_udp < 0) {
log_write("failed to get sock/sock_udp: 0x%X\n", socketGetLastResult());
continue;
}
u32 tmpval = 1;
if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
continue;
}
if (0 > setsockopt(sock_udp, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
continue;
}
if (0 > bind(sock, (const sockaddr*)&servaddr, sizeof(servaddr))) {
log_write("failed to get bind(sock): 0x%X\n", socketGetLastResult());
continue;
}
if (0 > bind(sock_udp, (const sockaddr*)&servaddr, sizeof(servaddr))) {
log_write("failed to get bind(sock_udp): 0x%X\n", socketGetLastResult());
continue;
}
if (0 > listen(sock, 10)) {
log_write("failed to get listen: 0x%X\n", socketGetLastResult());
continue;
}
sockaddr_in sa_remote{};
while (!g_quit) {
svcSleepThread(33'333'333);
if (poll_network_change()) {
break;
}
char recvbuf[256];
socklen_t from_len = sizeof(sa_remote);
const auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len);
if (udp_len > 0 && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
// log_write("got udp len: %d - %.*s\n", udp_len, udp_len, recvbuf);
sa_remote.sin_family = AF_INET;
sa_remote.sin_port = htons(NXLINK_CLIENT_PORT);
sendto(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), 0, (sockaddr*)&sa_remote, sizeof(sa_remote));
}
socklen_t accept_len = sizeof(sa_remote);
SocketWrapper connfd = accept(sock, (sockaddr*)&sa_remote, &accept_len);
if (connfd < 0) {
continue;
}
WriteCallbackNone(NxlinkCallbackType_Connected);
u32 namelen{};
if (!recvall(connfd, &namelen, sizeof(namelen))) {
log_write("failed to get name: 0x%X\n", socketGetLastResult());
continue;
}
fs::FsPath name{};
if (namelen > sizeof(name)) {
log_write("namelen is bigger than name: 0x%X\n", socketGetLastResult());
continue;
}
if (!recvall(connfd, name, namelen)) {
log_write("failed to get name: 0x%X\n", socketGetLastResult());
continue;
}
log_write("got name: %s\n", name);
u32 filesize{};
if (!recvall(connfd, &filesize, sizeof(filesize))) {
log_write("failed to get filesize: 0x%X\n", socketGetLastResult());
continue;
}
// check that we have enough space
s64 sd_storage_space_free;
if (R_FAILED(fs::FsNativeSd().GetFreeSpace("/", &sd_storage_space_free)) || filesize >= sd_storage_space_free) {
sendall(connfd, &ERR_SPACE, sizeof(ERR_SPACE));
continue;
}
// tell nxlink that we want this file
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
log_write("failed to tell nxlink that we want the file: 0x%X\n", socketGetLastResult());
continue;
}
// todo: verify nro magic here
WriteCallbackFile(NxlinkCallbackType_WriteBegin, name);
const auto file_data = get_file_data(connfd, filesize);
WriteCallbackFile(NxlinkCallbackType_WriteEnd, name);
if (file_data.empty()) {
continue;
}
fs::FsPath path;
// if (!name_view.starts_with("/") && !name_view.starts_with("sdmc:/")) {
if (name[0] != '/' && strncasecmp(name, "sdmc:/", std::strlen("sdmc:/"))) {
path = "/switch/" + name;
} else {
path = name;
}
// std::strcat(temp_path, "~");
fs::FsNativeSd fs;
if (R_FAILED(rc = fs.GetFsOpenResult())) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to open fs: 0x%X\n", socketGetLastResult());
continue;
}
// if (R_FAILED(rc = create_directories(fs, path))) {
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to create directories: %X\n", rc);
continue;
}
// this is the path we will write to
const auto temp_path = path + "~";
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_ResultPathAlreadyExists) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to create file: %X\n", rc);
continue;
}
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
{
FsFile f;
if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to open file %X\n", rc);
continue;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, file_data.size()))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to set file size: 0x%X\n", socketGetLastResult());
continue;
}
u64 offset = 0;
while (offset < file_data.size()) {
svcSleepThread(YieldType_WithoutCoreMigration);
u64 chunk_size = ZLIB_CHUNK;
if (offset + chunk_size > file_data.size()) {
chunk_size = file_data.size() - offset;
}
if (R_FAILED(rc = fsFileWrite(&f, offset, file_data.data() + offset, chunk_size, FsWriteOption_None))) {
break;
}
offset += chunk_size;
}
// if (R_FAILED(rc = fsFileWrite(&f, 0, file_data.data(), file_data.size(), FsWriteOption_None))) {
if (R_FAILED(rc)) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to write: 0x%X\n", socketGetLastResult());
continue;
}
}
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_ResultPathNotFound) {
log_write("failed to delete %X\n", rc);
continue;
}
if (R_FAILED(rc = fs.RenameFile(temp_path, path))) {
log_write("failed to rename %X\n", rc);
continue;
}
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
log_write("failed to send ok message: 0x%X\n", socketGetLastResult());
continue;
}
if (R_SUCCEEDED(sphaira::nro_verify(file_data))) {
std::string args{};
// try and get args
u32 args_len{};
char args_buf[256]{};
if (recvall(connfd, &args_len, sizeof(args_len))) {
args_len = std::min<u32>(args_len, sizeof(args_buf));
if (recvall(connfd, args_buf, args_len) && args_len > 0) {
// change NULL into spaces
for (u32 i = 0; i < args_len; i++) {
if (args_buf[i] == '\0') {
args_buf[i] = ' ';
}
}
args += args_buf;
}
}
// this allows for nxlink server to be activated
char nxlinked[17]{};
std::snprintf(nxlinked, sizeof(nxlinked), "%08X_NXLINK_", sa_remote.sin_addr.s_addr);
if (!args.empty()) {
args += ' ';
}
args += nxlinked;
// log_write("launching with: %s %s\n", path.c_str(), args.c_str());
if (R_SUCCEEDED(sphaira::nro_launch(path, args))) {
g_quit = true;
}
}
}
}
}
} // namespace
extern "C" {
bool nxlinkInitialize(NxlinkCallback callback) {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
return false;
}
g_callback = callback;
g_quit = false;
if (R_FAILED(threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", socketGetLastResult());
return false;
}
if (R_FAILED(threadStart(&g_thread))) {
log_write("failed to start nxlink thread: 0x%X\n", socketGetLastResult());
threadClose(&g_thread);
return false;
}
return g_is_running = true;
}
void nxlinkExit() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
g_is_running = false;
}
g_quit = true;
threadWaitForExit(&g_thread);
threadClose(&g_thread);
}
} // extern "C"

40
sphaira/source/option.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include <minIni.h>
#include <type_traits>
#include "option.hpp"
#include "app.hpp"
namespace sphaira::option {
template<typename T>
auto OptionBase<T>::Get() -> T {
if (!m_value.has_value()) {
if constexpr(std::is_same_v<T, bool>) {
m_value = ini_getbool(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
m_value = ini_getl(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH];
ini_gets(m_section.c_str(), m_name.c_str(), m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
m_value = buf;
}
}
return m_value.value();
}
template<typename T>
void OptionBase<T>::Set(T value) {
m_value = value;
if constexpr(std::is_same_v<T, bool>) {
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
ini_puts(m_section.c_str(), m_name.c_str(), value.c_str(), App::CONFIG_PATH);
}
}
template struct OptionBase<bool>;
template struct OptionBase<long>;
template struct OptionBase<std::string>;
} // namespace sphaira::option

1175
sphaira/source/owo.cpp Normal file

File diff suppressed because it is too large Load Diff

54
sphaira/source/swkbd.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "swkbd.hpp"
#include "defines.hpp"
#include <cstdlib>
namespace sphaira::swkbd {
namespace {
struct Config {
char out_text[FS_MAX_PATH]{};
bool numpad{};
};
Result ShowInternal(Config& cfg, const char* guide, s64 len_min, s64 len_max) {
SwkbdConfig c;
R_TRY(swkbdCreate(&c, 0));
swkbdConfigMakePresetDefault(&c);
swkbdConfigSetInitialCursorPos(&c, 1);
if (cfg.numpad) {
swkbdConfigSetType(&c, SwkbdType_NumPad);
}
if (guide) {
swkbdConfigSetGuideText(&c, guide);
}
if (len_min >= 0) {
swkbdConfigSetStringLenMin(&c, len_min);
}
if (len_max >= 0) {
swkbdConfigSetStringLenMax(&c, len_max);
}
return swkbdShow(&c, cfg.out_text, sizeof(cfg.out_text));
}
} // namespace
Result ShowText(std::string& out, const char* guide, s64 len_min, s64 len_max) {
Config cfg;
R_TRY(ShowInternal(cfg, guide, len_min, len_max));
out = cfg.out_text;
R_SUCCEED();
}
Result ShowNumPad(s64& out, const char* guide, s64 len_min, s64 len_max) {
Config cfg;
R_TRY(ShowInternal(cfg, guide, len_min, len_max));
out = std::atoll(cfg.out_text);
R_SUCCEED();
}
} // namespace sphaira::swkbd

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

54
sphaira/source/web.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "web.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <cstring>
namespace sphaira {
auto WebShow(const std::string& url, bool show_error) -> Result {
// showError("Running in applet mode\nPlease launch hbmenu by holding R on an APP (e.g. a game) NOT an applet (e.g. Gallery)", "", 0);
// showError("Error: Nag active, check more details", "Browser won't launch if supernag is active\n\nUse gagorder or switch-sys-tweak (the latter is bundled with BrowseNX) to disable supernag.", 0);
// log_write("web show with url: %s\n", url.c_str());
// return 0;
WebCommonConfig config{};
WebCommonReply reply{};
WebExitReason reason{};
AccountUid account_uid{};
char last_url[FS_MAX_PATH]{};
size_t last_url_len{};
// WebBackgroundKind_Unknown1 = shows background
// WebBackgroundKind_Unknown2 = shows background faded
if (R_FAILED(accountTrySelectUserWithoutInteraction(&account_uid, false))) { log_write("failed: accountTrySelectUserWithoutInteraction\n"); }
if (R_FAILED(webPageCreate(&config, url.c_str()))) { log_write("failed: webPageCreate\n"); }
if (R_FAILED(webConfigSetWhitelist(&config, "^http"))) { log_write("failed: webConfigSetWhitelist\n"); }
if (R_FAILED(webConfigSetEcClientCert(&config, true))) { log_write("failed: webConfigSetEcClientCert\n"); }
if (R_FAILED(webConfigSetScreenShot(&config, true))) { log_write("failed: webConfigSetScreenShot\n"); }
if (R_FAILED(webConfigSetBootDisplayKind(&config, WebBootDisplayKind_Black))) { log_write("failed: webConfigSetBootDisplayKind\n"); }
if (R_FAILED(webConfigSetBackgroundKind(&config, WebBackgroundKind_Default))) { log_write("failed: webConfigSetBackgroundKind\n"); }
if (R_FAILED(webConfigSetPointer(&config, true))) { log_write("failed: webConfigSetPointer\n"); }
if (R_FAILED(webConfigSetLeftStickMode(&config, WebLeftStickMode_Pointer))) { log_write("failed: webConfigSetLeftStickMode\n"); }
// if (R_FAILED(webConfigSetBootAsMediaPlayer(&config, true))) { log_write("failed: webConfigSetBootAsMediaPlayer\n"); }
if (R_FAILED(webConfigSetJsExtension(&config, true))) { log_write("failed: webConfigSetJsExtension\n"); }
if (R_FAILED(webConfigSetMediaPlayerAutoClose(&config, true))) { log_write("failed: webConfigSetMediaPlayerAutoClose\n"); }
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
if (R_FAILED(webConfigSetFooterFixedKind(&config, WebFooterFixedKind_Hidden))) { log_write("failed: webConfigSetFooterFixedKind\n"); }
if (R_FAILED(webConfigSetPageFade(&config, true))) { log_write("failed: webConfigSetPageFade\n"); }
if (R_FAILED(webConfigSetPageScrollIndicator(&config, true))) { log_write("failed: webConfigSetPageScrollIndicator\n"); }
// if (R_FAILED(webConfigSetMediaPlayerSpeedControl(&config, true))) { log_write("failed: webConfigSetMediaPlayerSpeedControl\n"); }
if (R_FAILED(webConfigSetBootMode(&config, WebSessionBootMode_AllForeground))) { log_write("failed: webConfigSetBootMode\n"); }
if (R_FAILED(webConfigSetTransferMemory(&config, true))) { log_write("failed: webConfigSetTransferMemory\n"); }
if (R_FAILED(webConfigSetTouchEnabledOnContents(&config, true))) { log_write("failed: webConfigSetTouchEnabledOnContents\n"); }
// if (R_FAILED(webConfigSetMediaPlayerUi(&config, true))) { log_write("failed: webConfigSetMediaPlayerUi\n"); }
// if (R_FAILED(webConfigSetWebAudio(&config, true))) { log_write("failed: webConfigSetWebAudio\n"); }
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
if (R_FAILED(webConfigSetBootLoadingIcon(&config, true))) { log_write("failed: webConfigSetBootLoadingIcon\n"); }
if (R_FAILED(webConfigSetUid(&config, account_uid))) { log_write("failed: webConfigSetUid\n"); }
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
log_write("last url: %s\n", last_url);
R_SUCCEED();
}
} // namespace sphaira