public release
This commit is contained in:
1193
sphaira/source/app.cpp
Normal file
1193
sphaira/source/app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
610
sphaira/source/download.cpp
Normal file
610
sphaira/source/download.cpp
Normal 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
57
sphaira/source/evman.cpp
Normal 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
441
sphaira/source/fs.cpp
Normal 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
118
sphaira/source/i18n.cpp
Normal 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
90
sphaira/source/image.cpp
Normal 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
72
sphaira/source/log.cpp
Normal 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
77
sphaira/source/main.cpp
Normal 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
313
sphaira/source/nro.cpp
Normal 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
500
sphaira/source/nxlink.cpp
Normal 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
40
sphaira/source/option.cpp
Normal 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
1175
sphaira/source/owo.cpp
Normal file
File diff suppressed because it is too large
Load Diff
54
sphaira/source/swkbd.cpp
Normal file
54
sphaira/source/swkbd.cpp
Normal 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
|
||||
1156
sphaira/source/ui/error_box.cpp
Normal file
1156
sphaira/source/ui/error_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1545
sphaira/source/ui/menus/appstore.cpp
Normal file
1545
sphaira/source/ui/menus/appstore.cpp
Normal file
File diff suppressed because it is too large
Load Diff
46
sphaira/source/ui/menus/file_viewer.cpp
Normal file
46
sphaira/source/ui/menus/file_viewer.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "ui/menus/file_viewer.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::fileview {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu(const fs::FsPath& path) : MenuBase{path}, m_path{path} {
|
||||
SetAction(Button::B, Action{"Back", [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
std::string buf;
|
||||
if (R_SUCCEEDED(m_fs.OpenFile(m_path, FsOpenMode_Read, &m_file))) {
|
||||
fsFileGetSize(&m_file, &m_file_size);
|
||||
buf.resize(m_file_size + 1);
|
||||
|
||||
u64 read_bytes;
|
||||
fsFileRead(&m_file, m_file_offset, buf.data(), buf.size(), 0, &read_bytes);
|
||||
buf[m_file_size] = '\0';
|
||||
}
|
||||
|
||||
m_scroll_text = std::make_unique<ScrollableText>(buf, 0, 120, 500, 1150-110, 18);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
fsFileClose(&m_file);
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
m_scroll_text->Update(controller, touch);
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
m_scroll_text->Draw(vg, theme);
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::fileview
|
||||
1620
sphaira/source/ui/menus/filebrowser.cpp
Normal file
1620
sphaira/source/ui/menus/filebrowser.cpp
Normal file
File diff suppressed because it is too large
Load Diff
377
sphaira/source/ui/menus/homebrew.cpp
Normal file
377
sphaira/source/ui/menus/homebrew.cpp
Normal file
@@ -0,0 +1,377 @@
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "owo.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <utility>
|
||||
|
||||
namespace sphaira::ui::menu::homebrew {
|
||||
namespace {
|
||||
|
||||
constexpr const char* SORT_STR[] = {
|
||||
"Updated",
|
||||
"Size",
|
||||
"Alphabetical",
|
||||
};
|
||||
|
||||
constexpr const char* ORDER_STR[] = {
|
||||
"Desc",
|
||||
"Asc",
|
||||
};
|
||||
|
||||
// returns seconds as: hh:mm:ss
|
||||
auto TimeFormat(u64 sec) -> std::string {
|
||||
char buf[9];
|
||||
|
||||
const auto s = sec % 60;
|
||||
const auto h = sec / 60 % 60;
|
||||
const auto d = sec / 60 / 60 % 24;
|
||||
|
||||
if (sec < 60) {
|
||||
if (!sec) {
|
||||
return "00:00:00";
|
||||
}
|
||||
std::snprintf(buf, sizeof(buf), "00:00:%02lu", s);
|
||||
} else if (sec < 3600) {
|
||||
std::snprintf(buf, sizeof(buf), "00:%02lu:%02lu", h, s);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%02lu:%02lu:%02lu", d, h, s);
|
||||
}
|
||||
|
||||
return std::string{buf};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
if (m_index < (m_entries.size() - 3)) {
|
||||
SetIndex(m_index + 3);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
} else {
|
||||
SetIndex(m_entries.size() - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
}
|
||||
if (m_index - m_start >= 9) {
|
||||
log_write("moved down\n");
|
||||
m_start += 3;
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_index >= 3) {
|
||||
SetIndex(m_index - 3);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index < m_start ) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||
nro_launch(m_entries[m_index].path);
|
||||
}}),
|
||||
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Homebrew Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
if (m_entries.size()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Sort By"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Sort Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items sort_items;
|
||||
sort_items.push_back("Updated"_i18n);
|
||||
sort_items.push_back("Size"_i18n);
|
||||
sort_items.push_back("Alphabetical"_i18n);
|
||||
|
||||
SidebarEntryArray::Items order_items;
|
||||
order_items.push_back("Decending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
||||
m_sort.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
m_order.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_order.Get()));
|
||||
}));
|
||||
|
||||
#if 0
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Info"_i18n, [this](){
|
||||
|
||||
}));
|
||||
#endif
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].path.toString() + "?";
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
buf,
|
||||
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) {
|
||||
m_entries.erase(m_entries.begin() + m_index);
|
||||
SetIndex(m_index ? m_index - 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
}, true));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
|
||||
m_hide_sphaira.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"WARNING: Installing forwarders will lead to a ban!"_i18n,
|
||||
"Back"_i18n, "Install"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
InstallHomebrew();
|
||||
}
|
||||
}
|
||||
));
|
||||
}, true));
|
||||
}
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
auto vg = App::GetVg();
|
||||
|
||||
for (auto&p : m_entries) {
|
||||
nvgDeleteImage(vg, p.image);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = m_entries.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
|
||||
const auto sb_y = SCROLL / 3.f;
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
// lazy load image
|
||||
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) {
|
||||
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
if (!e.icon.empty()) {
|
||||
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size());
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == cursor_pos) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
const float image_size = 115;
|
||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
const float font_size = 18;
|
||||
const float diff = 32;
|
||||
#if 1
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.GetName());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
|
||||
#else
|
||||
gfx::drawTextArgs(vg, x + 148, y + 35, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.GetName());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 35 + (diff * 1), font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 35 + (diff * 2), font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
|
||||
// gfx::drawTextArgs(vg, x + 148, y + 110, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "PlayCount: %u", e.hbini.launch_count);
|
||||
#endif
|
||||
}
|
||||
nvgRestore(vg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
if (m_entries.empty()) {
|
||||
ScanHomebrew();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(std::size_t index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_start = 0;
|
||||
}
|
||||
|
||||
const auto& e = m_entries[m_index];
|
||||
// TimeCalendarTime caltime;
|
||||
// timeToCalendarTimeWithMyRule()
|
||||
// todo: fix GetFileTimeStampRaw being different to timeGetCurrentTime
|
||||
// log_write("name: %s hbini.ts: %lu file.ts: %lu smaller: %s\n", e.GetName(), e.hbini.timestamp, e.timestamp.modified, e.hbini.timestamp < e.timestamp.modified ? "true" : "false");
|
||||
|
||||
SetTitleSubHeading(m_entries[m_index].path);
|
||||
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
|
||||
}
|
||||
|
||||
void Menu::InstallHomebrew() {
|
||||
const auto& nro = m_entries[m_index];
|
||||
OwoConfig config{};
|
||||
config.nro_path = nro.path.toString();
|
||||
config.nacp = nro.nacp;
|
||||
config.icon = nro.icon;
|
||||
App::Install(config);
|
||||
}
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
TimeStamp ts;
|
||||
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
|
||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSeconds());
|
||||
|
||||
// todo: optimise this. maybe create a file per entry
|
||||
// which would speed up parsing
|
||||
for (auto& e : m_entries) {
|
||||
if (ini_hassection(e.path, App::PLAYLOG_PATH)) {
|
||||
// log_write("has section for: %s\n", e.path);
|
||||
e.hbini.timestamp = ini_getl(e.path, "timestamp", 0, App::PLAYLOG_PATH);
|
||||
}
|
||||
e.image = 0; // images are lazy loaded
|
||||
}
|
||||
|
||||
#if 0
|
||||
struct IniUser {
|
||||
std::vector<NroEntry>& entires;
|
||||
Hbini* ini;
|
||||
std::string last_section;
|
||||
} ini_user { m_entries };
|
||||
|
||||
ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto user = static_cast<IniUser*>(UserData);
|
||||
|
||||
if (user->last_section != Section) {
|
||||
user->last_section = Section;
|
||||
user->ini = nullptr;
|
||||
|
||||
for (auto& e : user->entires) {
|
||||
if (e.path == Section) {
|
||||
user->ini = &e.hbini;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user->ini) {
|
||||
|
||||
}
|
||||
|
||||
// app->
|
||||
log_write("found: %s %s %s\n", Section, Key, Value);
|
||||
return 1;
|
||||
}, &ini_user, App::PLAYLOG_PATH);
|
||||
#endif
|
||||
|
||||
this->Sort();
|
||||
SetIndex(0);
|
||||
}
|
||||
|
||||
void Menu::Sort() {
|
||||
// returns true if lhs should be before rhs
|
||||
const auto sort = m_sort.Get();
|
||||
const auto order = m_order.Get();
|
||||
|
||||
const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
||||
switch (sort) {
|
||||
case SortType_Updated: {
|
||||
auto lhs_timestamp = lhs.hbini.timestamp;
|
||||
auto rhs_timestamp = rhs.hbini.timestamp;
|
||||
if (lhs.timestamp.is_valid && lhs_timestamp < lhs.timestamp.modified) {
|
||||
lhs_timestamp = lhs.timestamp.modified;
|
||||
}
|
||||
if (rhs.timestamp.is_valid && rhs_timestamp < rhs.timestamp.modified) {
|
||||
rhs_timestamp = rhs.timestamp.modified;
|
||||
}
|
||||
|
||||
if (lhs_timestamp == rhs_timestamp) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else if (order == OrderType_Decending) {
|
||||
return lhs_timestamp > rhs_timestamp;
|
||||
} else {
|
||||
return lhs_timestamp < rhs_timestamp;
|
||||
}
|
||||
} break;
|
||||
case SortType_Size: {
|
||||
if (lhs.size == rhs.size) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else if (order == OrderType_Decending) {
|
||||
return lhs.size > rhs.size;
|
||||
} else {
|
||||
return lhs.size < rhs.size;
|
||||
}
|
||||
} break;
|
||||
case SortType_Alphabetical: {
|
||||
if (order == OrderType_Decending) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
std::unreachable();
|
||||
};
|
||||
|
||||
std::sort(m_entries.begin(), m_entries.end(), sorter);
|
||||
}
|
||||
|
||||
void Menu::SortAndFindLastFile() {
|
||||
Sort();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::homebrew
|
||||
533
sphaira/source/ui/menus/irs_menu.cpp
Normal file
533
sphaira/source/ui/menus/irs_menu.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
|
||||
namespace sphaira::ui::menu::irs {
|
||||
namespace {
|
||||
|
||||
// from trial and error
|
||||
constexpr u32 GAIN_MIN = 1;
|
||||
constexpr u32 GAIN_MAX = 16;
|
||||
|
||||
consteval auto generte_iron_palette_table() {
|
||||
std::array<u32, 256> array{};
|
||||
|
||||
const u32 iron_palette[] = {
|
||||
0xff000014, 0xff000025, 0xff00002a, 0xff000032, 0xff000036, 0xff00003e, 0xff000042, 0xff00004f,
|
||||
0xff010055, 0xff010057, 0xff02005c, 0xff03005e, 0xff040063, 0xff050065, 0xff070069, 0xff0a0070,
|
||||
0xff0b0073, 0xff0d0075, 0xff0d0076, 0xff100078, 0xff120079, 0xff15007c, 0xff17007d, 0xff1c0081,
|
||||
0xff200084, 0xff220085, 0xff260087, 0xff280089, 0xff2c008a, 0xff2e008b, 0xff32008d, 0xff38008f,
|
||||
0xff390090, 0xff3c0092, 0xff3e0093, 0xff410094, 0xff420095, 0xff450096, 0xff470096, 0xff4c0097,
|
||||
0xff4f0097, 0xff510097, 0xff540098, 0xff560098, 0xff5a0099, 0xff5c0099, 0xff5f009a, 0xff64009b,
|
||||
0xff66009b, 0xff6a009b, 0xff6c009c, 0xff6f009c, 0xff70009c, 0xff73009d, 0xff75009d, 0xff7a009d,
|
||||
0xff7e009d, 0xff7f009d, 0xff83009d, 0xff84009d, 0xff87009d, 0xff89009d, 0xff8b009d, 0xff91009c,
|
||||
0xff93009c, 0xff96009b, 0xff98009b, 0xff9b009b, 0xff9c009b, 0xff9f009b, 0xffa0009b, 0xffa4009b,
|
||||
0xffa7009a, 0xffa8009a, 0xffaa0099, 0xffab0099, 0xffae0198, 0xffaf0198, 0xffb00198, 0xffb30196,
|
||||
0xffb40296, 0xffb60295, 0xffb70395, 0xffb90495, 0xffba0495, 0xffbb0593, 0xffbc0593, 0xffbf0692,
|
||||
0xffc00791, 0xffc00791, 0xffc10990, 0xffc20a8f, 0xffc30b8e, 0xffc40c8d, 0xffc60d8b, 0xffc81088,
|
||||
0xffc91187, 0xffca1385, 0xffcb1385, 0xffcc1582, 0xffcd1681, 0xffce187e, 0xffcf187c, 0xffd11b78,
|
||||
0xffd21c75, 0xffd21d74, 0xffd32071, 0xffd4216f, 0xffd5236b, 0xffd52469, 0xffd72665, 0xffd92a60,
|
||||
0xffda2b5e, 0xffdb2e5a, 0xffdb2f57, 0xffdd3051, 0xffdd314e, 0xffde3347, 0xffdf3444, 0xffe0373a,
|
||||
0xffe03933, 0xffe13a30, 0xffe23c2a, 0xffe33d26, 0xffe43f20, 0xffe4411d, 0xffe5431b, 0xffe64616,
|
||||
0xffe74715, 0xffe74913, 0xffe84a12, 0xffe84c0f, 0xffe94d0e, 0xffea4e0c, 0xffea4f0c, 0xffeb520a,
|
||||
0xffec5409, 0xffec5608, 0xffec5808, 0xffed5907, 0xffed5b06, 0xffee5c06, 0xffee5d05, 0xffef6004,
|
||||
0xffef6104, 0xfff06303, 0xfff06403, 0xfff16603, 0xfff16603, 0xfff16803, 0xfff16902, 0xfff16b02,
|
||||
0xfff26d01, 0xfff26e01, 0xfff37001, 0xfff37101, 0xfff47300, 0xfff47400, 0xfff47600, 0xfff47a00,
|
||||
0xfff57b00, 0xfff57e00, 0xfff57f00, 0xfff68100, 0xfff68200, 0xfff78400, 0xfff78500, 0xfff88800,
|
||||
0xfff88900, 0xfff88a00, 0xfff88c00, 0xfff98d00, 0xfff98e00, 0xfff98f00, 0xfff99100, 0xfffa9400,
|
||||
0xfffa9500, 0xfffb9800, 0xfffb9900, 0xfffb9c00, 0xfffc9d00, 0xfffca000, 0xfffca100, 0xfffda400,
|
||||
0xfffda700, 0xfffda800, 0xfffdab00, 0xfffdac00, 0xfffdae00, 0xfffeaf00, 0xfffeb100, 0xfffeb400,
|
||||
0xfffeb500, 0xfffeb800, 0xfffeb900, 0xfffeba00, 0xfffebb00, 0xfffebd00, 0xfffebe00, 0xfffec200,
|
||||
0xfffec400, 0xfffec500, 0xfffec700, 0xfffec800, 0xfffeca01, 0xfffeca01, 0xfffecc02, 0xfffecf04,
|
||||
0xfffecf04, 0xfffed106, 0xfffed308, 0xfffed50a, 0xfffed60a, 0xfffed80c, 0xfffed90d, 0xffffdb10,
|
||||
0xffffdc14, 0xffffdd16, 0xffffde1b, 0xffffdf1e, 0xffffe122, 0xffffe224, 0xffffe328, 0xffffe531,
|
||||
0xffffe635, 0xffffe73c, 0xffffe83f, 0xffffea46, 0xffffeb49, 0xffffec50, 0xffffed54, 0xffffee5f,
|
||||
0xffffef67, 0xfffff06a, 0xfffff172, 0xfffff177, 0xfffff280, 0xfffff285, 0xfffff38e, 0xfffff49a,
|
||||
0xfffff59e, 0xfffff5a6, 0xfffff6aa, 0xfffff7b3, 0xfffff7b6, 0xfffff8bd, 0xfffff8c1, 0xfffff9ca,
|
||||
0xfffffad1, 0xfffffad4, 0xfffffcdb, 0xfffffcdf, 0xfffffde5, 0xfffffde8, 0xfffffeee, 0xfffffff6
|
||||
};
|
||||
|
||||
for (u32 i = 0; i < 256; i++) {
|
||||
const auto c = iron_palette[i];
|
||||
array[i] = RGBA8_MAXALPHA((c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
// ARGB Ironbow palette
|
||||
constexpr auto iron_palette = generte_iron_palette_table();
|
||||
|
||||
void irsConvertConfigExToNormal(const IrsImageTransferProcessorExConfig* ex, IrsImageTransferProcessorConfig* nor) {
|
||||
std::memcpy(nor, ex, sizeof(*nor));
|
||||
}
|
||||
|
||||
void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsImageTransferProcessorExConfig* ex) {
|
||||
std::memcpy(ex, nor, sizeof(*nor));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"Irs"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items controller_str;
|
||||
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
|
||||
const auto& e = m_entries[i];
|
||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
text += " (Available)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
text += " (Unsupported)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
text += " (Unconnected)"_i18n;
|
||||
break;
|
||||
}
|
||||
controller_str.emplace_back(text);
|
||||
}
|
||||
|
||||
SidebarEntryArray::Items rotation_str;
|
||||
rotation_str.emplace_back("0 (Sideways)"_i18n);
|
||||
rotation_str.emplace_back("90 (Flat)"_i18n);
|
||||
rotation_str.emplace_back("180 (-Sideways)"_i18n);
|
||||
rotation_str.emplace_back("270 (Upside down)"_i18n);
|
||||
|
||||
SidebarEntryArray::Items colour_str;
|
||||
colour_str.emplace_back("Grey"_i18n);
|
||||
colour_str.emplace_back("Ironbow"_i18n);
|
||||
colour_str.emplace_back("Green"_i18n);
|
||||
colour_str.emplace_back("Red"_i18n);
|
||||
colour_str.emplace_back("Blue"_i18n);
|
||||
|
||||
SidebarEntryArray::Items light_target_str;
|
||||
light_target_str.emplace_back("All leds"_i18n);
|
||||
light_target_str.emplace_back("Bright group"_i18n);
|
||||
light_target_str.emplace_back("Dim group"_i18n);
|
||||
light_target_str.emplace_back("None"_i18n);
|
||||
|
||||
SidebarEntryArray::Items gain_str;
|
||||
for (u32 i = GAIN_MIN; i <= GAIN_MAX; i++) {
|
||||
gain_str.emplace_back(std::to_string(i));
|
||||
}
|
||||
|
||||
SidebarEntryArray::Items is_negative_image_used_str;
|
||||
is_negative_image_used_str.emplace_back("Normal image"_i18n);
|
||||
is_negative_image_used_str.emplace_back("Negative image"_i18n);
|
||||
|
||||
SidebarEntryArray::Items format_str;
|
||||
format_str.emplace_back("320x240"_i18n);
|
||||
format_str.emplace_back("160x120"_i18n);
|
||||
format_str.emplace_back("80x60"_i18n);
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
format_str.emplace_back("40x30"_i18n);
|
||||
format_str.emplace_back("20x15"_i18n);
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](std::size_t& index){
|
||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||
m_index = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_index));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](std::size_t& index){
|
||||
m_rotation = (Rotation)index;
|
||||
}, m_rotation));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](std::size_t& index){
|
||||
m_colour = (Colour)index;
|
||||
updateColourArray();
|
||||
}, m_colour));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](std::size_t& index){
|
||||
m_config.light_target = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.light_target));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](std::size_t& index){
|
||||
m_config.gain = GAIN_MIN + index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.gain - GAIN_MIN));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](std::size_t& index){
|
||||
m_config.is_negative_image_used = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.is_negative_image_used));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](std::size_t& index){
|
||||
m_config.orig_format = index;
|
||||
m_config.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.orig_format));
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](std::size_t& index){
|
||||
// you cannot set trim a larger region than the source
|
||||
if (index < m_config.orig_format) {
|
||||
index = m_config.orig_format;
|
||||
} else {
|
||||
m_config.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}
|
||||
}, m_config.orig_format));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
|
||||
m_config.is_external_light_filter_enabled = enable;
|
||||
UpdateConfig(&m_config);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
|
||||
LoadDefaultConfig();
|
||||
}, true));
|
||||
}});
|
||||
|
||||
if (R_FAILED(m_init_rc = irsInitialize())) {
|
||||
return;
|
||||
}
|
||||
|
||||
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
|
||||
|
||||
// open all handles
|
||||
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
|
||||
irsGetIrCameraHandle(&m_entries[1].m_handle, HidNpadIdType_No2);
|
||||
irsGetIrCameraHandle(&m_entries[2].m_handle, HidNpadIdType_No3);
|
||||
irsGetIrCameraHandle(&m_entries[3].m_handle, HidNpadIdType_No4);
|
||||
irsGetIrCameraHandle(&m_entries[4].m_handle, HidNpadIdType_No5);
|
||||
irsGetIrCameraHandle(&m_entries[5].m_handle, HidNpadIdType_No6);
|
||||
irsGetIrCameraHandle(&m_entries[6].m_handle, HidNpadIdType_No7);
|
||||
irsGetIrCameraHandle(&m_entries[7].m_handle, HidNpadIdType_No8);
|
||||
irsGetIrCameraHandle(&m_entries[8].m_handle, HidNpadIdType_Handheld);
|
||||
// get status of all handles
|
||||
PollCameraStatus(true);
|
||||
// load default config
|
||||
LoadDefaultConfig();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
ResetImage();
|
||||
|
||||
for (auto& e : m_entries) {
|
||||
irsStopImageProcessor(e.m_handle);
|
||||
}
|
||||
|
||||
// this closes all handles
|
||||
irsExit();
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
PollCameraStatus();
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
IrsImageTransferProcessorState state;
|
||||
const auto rc = irsGetImageTransferProcessorState(m_entries[m_index].m_handle, m_irs_buffer.data(), m_irs_buffer.size(), &state);
|
||||
if (R_SUCCEEDED(rc) && state.sampling_number != m_prev_state.sampling_number) {
|
||||
m_prev_state = state;
|
||||
SetSubHeading("Ambient Noise Level: " + std::to_string(m_prev_state.ambient_noise_level));
|
||||
updateColourArray();
|
||||
}
|
||||
|
||||
if (m_image) {
|
||||
float cx{}, cy{};
|
||||
float w{}, h{};
|
||||
float angle{};
|
||||
|
||||
switch (m_rotation) {
|
||||
case Rotation_0: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) - w / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) - h / 2.0;
|
||||
angle = 0;
|
||||
} break;
|
||||
case Rotation_90: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) + h / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) - w / 2.0;
|
||||
angle = 90;
|
||||
} break;
|
||||
case Rotation_180: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) + w / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) + h / 2.0;
|
||||
angle = 180;
|
||||
} break;
|
||||
case Rotation_270: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) - h / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) + w / 2.0;
|
||||
angle = 270;
|
||||
} break;
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgTranslate(vg, cx, cy);
|
||||
const auto paint = nvgImagePattern(vg, 0, 0, w, h, 0, m_image, 1.f);
|
||||
nvgRotate(vg, nvgDegToRad(angle));
|
||||
nvgBeginPath(vg);
|
||||
nvgRect(vg, 0, 0, w, h);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
nvgRestore(vg);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
void Menu::PollCameraStatus(bool statup) {
|
||||
int index = 0;
|
||||
for (auto& e : m_entries) {
|
||||
IrsIrCameraStatus status;
|
||||
if (R_FAILED(irsGetIrCameraStatus(e.m_handle, &status))) {
|
||||
log_write("failed to get ir status\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.status != status || statup) {
|
||||
e.status = status;
|
||||
e.m_update_needed = false;
|
||||
|
||||
log_write("status changed\n");
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
// calling this breaks the handle, kinda
|
||||
#if 0
|
||||
if (R_FAILED(irsCheckFirmwareUpdateNecessity(e.m_handle, &e.m_update_needed))) {
|
||||
log_write("failed to check if update needed: %u\n", e.m_update_needed);
|
||||
} else {
|
||||
if (e.m_update_needed) {
|
||||
log_write("update needed\n");
|
||||
} else {
|
||||
log_write("no update needed\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Available\n", index);
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Unsupported\n", index);
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Unconnected\n", index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::ResetImage() {
|
||||
if (m_image) {
|
||||
nvgDeleteImage(App::GetVg(), m_image);
|
||||
m_image = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::UpdateImage() {
|
||||
ResetImage();
|
||||
m_image = nvgCreateImageRGBA(App::GetVg(), m_irs_width, m_irs_height, NVG_IMAGE_NEAREST, (const unsigned char*)m_rgba.data());
|
||||
}
|
||||
|
||||
void Menu::LoadDefaultConfig() {
|
||||
IrsImageTransferProcessorExConfig ex_config;
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
irsGetDefaultImageTransferProcessorExConfig(&ex_config);
|
||||
} else {
|
||||
IrsImageTransferProcessorConfig nor_config;
|
||||
irsGetDefaultImageTransferProcessorConfig(&nor_config);
|
||||
irsConvertConfigNormalToEx(&nor_config, &ex_config);
|
||||
}
|
||||
|
||||
irsGetMomentProcessorDefaultConfig(&m_moment_config);
|
||||
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
||||
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
||||
|
||||
m_tera_config;
|
||||
m_adaptive_config = {};
|
||||
m_hand_config = {};
|
||||
|
||||
UpdateConfig(&ex_config);
|
||||
}
|
||||
|
||||
void Menu::UpdateConfig(const IrsImageTransferProcessorExConfig* config) {
|
||||
m_config = *config;
|
||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||
|
||||
if (R_FAILED(irsRunMomentProcessor(m_entries[m_index].m_handle, &m_moment_config))) {
|
||||
log_write("failed to irsRunMomentProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunMomentProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunClusteringProcessor(m_entries[m_index].m_handle, &m_clustering_config))) {
|
||||
log_write("failed to irsRunClusteringProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunClusteringProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunPointingProcessor(m_entries[m_index].m_handle))) {
|
||||
log_write("failed to irsRunPointingProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunPointingProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunTeraPluginProcessor(m_entries[m_index].m_handle, &m_tera_config))) {
|
||||
log_write("failed to irsRunTeraPluginProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunTeraPluginProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunIrLedProcessor(m_entries[m_index].m_handle, &m_led_config))) {
|
||||
log_write("failed to irsRunIrLedProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunIrLedProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunAdaptiveClusteringProcessor(m_entries[m_index].m_handle, &m_adaptive_config))) {
|
||||
log_write("failed to irsRunAdaptiveClusteringProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunAdaptiveClusteringProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunHandAnalysis(m_entries[m_index].m_handle, &m_hand_config))) {
|
||||
log_write("failed to irsRunHandAnalysis\n");
|
||||
} else {
|
||||
log_write("did irsRunHandAnalysis\n");
|
||||
}
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
m_init_rc = irsRunImageTransferExProcessor(m_entries[m_index].m_handle, &m_config, 0x10000000);
|
||||
} else {
|
||||
IrsImageTransferProcessorConfig nor;
|
||||
irsConvertConfigExToNormal(&m_config, &nor);
|
||||
m_init_rc = irsRunImageTransferProcessor(m_entries[m_index].m_handle, &nor, 0x10000000);
|
||||
}
|
||||
|
||||
if (R_FAILED(m_init_rc)) {
|
||||
log_write("irs failed to set config!\n");
|
||||
}
|
||||
|
||||
auto format = m_config.orig_format;
|
||||
log_write("IRS CONFIG\n");
|
||||
log_write("\texposure_time: %lu\n", m_config.exposure_time);
|
||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
||||
log_write("\tgain: %u\n", m_config.gain);
|
||||
log_write("\tis_negative_image_used: %u\n", m_config.is_negative_image_used);
|
||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
format = m_config.trimming_format;
|
||||
log_write("\ttrimming_format: %u\n", m_config.trimming_format);
|
||||
log_write("\ttrimming_start_x: %u\n", m_config.trimming_start_x);
|
||||
log_write("\ttrimming_start_y: %u\n", m_config.trimming_start_y);
|
||||
log_write("\tis_external_light_filter_enabled: %u\n", m_config.is_external_light_filter_enabled);
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case IrsImageTransferProcessorFormat_320x240:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_320x240");
|
||||
m_irs_width = 320;
|
||||
m_irs_height = 240;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_160x120:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_160x120");
|
||||
m_irs_width = 160;
|
||||
m_irs_height = 120;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_80x60:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_80x60");
|
||||
m_irs_width = 80;
|
||||
m_irs_height = 60;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_40x30:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_40x30");
|
||||
m_irs_width = 40;
|
||||
m_irs_height = 30;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_20x15:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_20x15");
|
||||
m_irs_width = 20;
|
||||
m_irs_height = 15;
|
||||
break;
|
||||
}
|
||||
|
||||
m_rgba.resize(m_irs_width * m_irs_height);
|
||||
m_irs_buffer.resize(m_irs_width * m_irs_height);
|
||||
m_prev_state.sampling_number = UINT64_MAX;
|
||||
std::fill(m_irs_buffer.begin(), m_irs_buffer.end(), 0);
|
||||
updateColourArray();
|
||||
}
|
||||
|
||||
void Menu::updateColourArray() {
|
||||
const auto ir_width = m_irs_width;
|
||||
const auto ir_height = m_irs_height;
|
||||
const auto colour = m_colour;
|
||||
|
||||
for (u32 y = 0; y < ir_height; y++) {
|
||||
for (u32 x = 0; x < ir_width; x++) {
|
||||
const u32 pos = y * ir_width + x;
|
||||
const u32 pos2 = y * ir_width + x;
|
||||
|
||||
switch (colour) {
|
||||
case Colour_Grey:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], m_irs_buffer[pos2], m_irs_buffer[pos2]);
|
||||
break;
|
||||
case Colour_Ironbow:
|
||||
m_rgba[pos] = iron_palette[m_irs_buffer[pos2]];
|
||||
break;
|
||||
case Colour_Green:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(0, m_irs_buffer[pos2], 0);
|
||||
break;
|
||||
case Colour_Red:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], 0, 0);
|
||||
break;
|
||||
case Colour_Blue:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(0, 0, m_irs_buffer[pos2]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
237
sphaira/source/ui/menus/main_menu.cpp
Normal file
237
sphaira/source/ui/menus/main_menu.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "download.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "web.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::main {
|
||||
namespace {
|
||||
|
||||
#if 0
|
||||
bool parseSearch(const char *parse_string, const char *filter, char* new_string) {
|
||||
char c;
|
||||
u32 offset = 0;
|
||||
const u32 filter_len = std::strlen(filter) - 1;
|
||||
|
||||
while ((c = parse_string[offset++]) != '\0') {
|
||||
if (c == *filter) {
|
||||
for (u32 i = 0; c == filter[i]; i++) {
|
||||
c = parse_string[offset++];
|
||||
if (i == filter_len) {
|
||||
for (u32 j = 0; c != '\"'; j++) {
|
||||
new_string[j] = c;
|
||||
new_string[j+1] = '\0';
|
||||
c = parse_string[offset++];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
MainMenu::MainMenu() {
|
||||
#if 0
|
||||
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sys-patch/releases/latest", [this](std::vector<u8>& data, bool success){
|
||||
data.push_back('\0');
|
||||
auto raw_str = (const char*)data.data();
|
||||
char out_str[0x301];
|
||||
|
||||
if (parseSearch(raw_str, "tag_name\":\"", out_str)) {
|
||||
m_update_version = out_str;
|
||||
if (strcasecmp("v1.5.0", m_update_version.c_str())) {
|
||||
m_update_avaliable = true;
|
||||
}
|
||||
log_write("FOUND IT : %s\n", out_str);
|
||||
}
|
||||
|
||||
if (parseSearch(raw_str, "browser_download_url\":\"", out_str)) {
|
||||
m_update_url = out_str;
|
||||
log_write("FOUND IT : %s\n", out_str);
|
||||
}
|
||||
|
||||
if (parseSearch(raw_str, "body\":\"", out_str)) {
|
||||
m_update_description = out_str;
|
||||
// m_update_description.replace("\r\n\r\n", "\n");
|
||||
log_write("FOUND IT : %s\n", out_str);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
AddOnLPress();
|
||||
AddOnRPress();
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
|
||||
SidebarEntryArray::Items language_items;
|
||||
language_items.push_back("Auto"_i18n);
|
||||
language_items.push_back("English");
|
||||
language_items.push_back("Japanese");
|
||||
language_items.push_back("French");
|
||||
language_items.push_back("German");
|
||||
language_items.push_back("Italian");
|
||||
language_items.push_back("Spanish");
|
||||
language_items.push_back("Chinese");
|
||||
language_items.push_back("Korean");
|
||||
language_items.push_back("Dutch");
|
||||
language_items.push_back("Portuguese");
|
||||
language_items.push_back("Russian");
|
||||
|
||||
options->AddHeader("Header"_i18n);
|
||||
options->AddSpacer();
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
||||
SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
for (auto& p : theme_meta) {
|
||||
theme_items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](std::size_t& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
|
||||
App::SetThemeShuffleEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
|
||||
App::SetNxlinkEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Check for update"_i18n, [this](){
|
||||
App::Notify("Not Implemented"_i18n);
|
||||
}));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this, language_items](std::size_t& index_out){
|
||||
App::SetLanguage(index_out);
|
||||
}, (std::size_t)App::GetLanguage()));
|
||||
|
||||
if (m_update_avaliable) {
|
||||
std::string str = "Update avaliable: "_i18n + m_update_version;
|
||||
options->Add(std::make_shared<SidebarEntryCallback>(str, [this](){
|
||||
App::Notify("Not Implemented"_i18n);
|
||||
}));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
||||
App::SetLogEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||
}));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::irs::Menu>());
|
||||
}));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}));
|
||||
}})
|
||||
);
|
||||
|
||||
m_homebrew_menu = std::make_shared<homebrew::Menu>();
|
||||
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_current_menu = m_homebrew_menu;
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
m_current_menu->SetAction(button, action);
|
||||
}
|
||||
}
|
||||
|
||||
MainMenu::~MainMenu() {
|
||||
|
||||
}
|
||||
|
||||
void MainMenu::Update(Controller* controller, TouchInfo* touch) {
|
||||
m_current_menu->Update(controller, touch);
|
||||
}
|
||||
|
||||
void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
m_current_menu->Draw(vg, theme);
|
||||
}
|
||||
|
||||
void MainMenu::OnFocusGained() {
|
||||
Widget::OnFocusGained();
|
||||
this->SetHidden(false);
|
||||
m_current_menu->OnFocusGained();
|
||||
}
|
||||
|
||||
void MainMenu::OnFocusLost() {
|
||||
m_current_menu->OnFocusLost();
|
||||
}
|
||||
|
||||
void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
m_current_menu->OnFocusLost();
|
||||
if (m_current_menu == m_homebrew_menu) {
|
||||
m_current_menu = menu;
|
||||
RemoveAction(b);
|
||||
} else {
|
||||
m_current_menu = m_homebrew_menu;
|
||||
}
|
||||
|
||||
m_current_menu->OnFocusGained();
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
m_current_menu->SetAction(button, action);
|
||||
}
|
||||
|
||||
if (b == Button::L) {
|
||||
AddOnRPress();
|
||||
} else {
|
||||
AddOnLPress();
|
||||
}
|
||||
}
|
||||
|
||||
void MainMenu::AddOnLPress() {
|
||||
SetAction(Button::L, Action{"Fs"_i18n, [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
|
||||
void MainMenu::AddOnRPress() {
|
||||
SetAction(Button::R, Action{"App"_i18n, [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
}});
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::main
|
||||
94
sphaira/source/ui/menus/menu_base.cpp
Normal file
94
sphaira/source/ui/menus/menu_base.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
namespace sphaira::ui::menu {
|
||||
|
||||
MenuBase::MenuBase(std::string title) : m_title{title} {
|
||||
// this->SetParent(this);
|
||||
this->SetPos(30, 87, 1220 - 30, 646 - 87);
|
||||
m_applet_type = appletGetAppletType();
|
||||
SetAction(Button::START, Action{App::Exit});
|
||||
}
|
||||
|
||||
MenuBase::~MenuBase() {
|
||||
}
|
||||
|
||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
}
|
||||
|
||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
u32 battery_percetange{};
|
||||
|
||||
PsmChargerType charger_type{};
|
||||
NifmInternetConnectionType type{};
|
||||
NifmInternetConnectionStatus status{};
|
||||
u32 strength{};
|
||||
u32 ip{};
|
||||
|
||||
// todo: app thread poll every 1s and this query the result
|
||||
psmGetBatteryChargePercentage(&battery_percetange);
|
||||
psmGetChargerType(&charger_type);
|
||||
nifmGetInternetConnectionStatus(&type, &strength, &status);
|
||||
nifmGetCurrentIpAddress(&ip);
|
||||
|
||||
const float start_y = 70;
|
||||
const float font_size = 22;
|
||||
const float spacing = 30;
|
||||
|
||||
float start_x = 1220;
|
||||
float bounds[4];
|
||||
|
||||
nvgFontSize(vg, font_size);
|
||||
|
||||
#define draw(...) \
|
||||
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||
start_x -= bounds[2] - bounds[0]; \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, __VA_ARGS__); \
|
||||
start_x -= spacing;
|
||||
|
||||
// draw("version %s", APP_VERSION);
|
||||
draw("%u%%", battery_percetange);
|
||||
if (ip) {
|
||||
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF);
|
||||
} else {
|
||||
draw(("No Internet"_i18n).c_str());
|
||||
}
|
||||
if (m_applet_type == AppletType_LibraryApplet || m_applet_type == AppletType_SystemApplet) {
|
||||
draw(("[Applet Mode]"_i18n).c_str());
|
||||
}
|
||||
|
||||
#undef draw
|
||||
|
||||
gfx::drawRect(vg, 30.f, 86.f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, 30.f, 646.0f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
nvgFontSize(vg, 28);
|
||||
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
|
||||
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
||||
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title_sub_heading.c_str());
|
||||
|
||||
// gfx::drawTextArgs(vg, 80, 65, 28.f, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
||||
// gfx::drawTextArgs(vg, 80, 680.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
|
||||
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
|
||||
}
|
||||
|
||||
void MenuBase::SetTitle(std::string title) {
|
||||
m_title = title;
|
||||
}
|
||||
|
||||
void MenuBase::SetTitleSubHeading(std::string sub_heading) {
|
||||
m_title_sub_heading = sub_heading;
|
||||
}
|
||||
|
||||
void MenuBase::SetSubHeading(std::string sub_heading) {
|
||||
m_sub_heading = sub_heading;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu
|
||||
744
sphaira/source/ui/menus/themezer.cpp
Normal file
744
sphaira/source/ui/menus/themezer.cpp
Normal file
@@ -0,0 +1,744 @@
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "download.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <nanovg/stb_image.h>
|
||||
#include <cstring>
|
||||
#include <minizip/unzip.h>
|
||||
#include <yyjson.h>
|
||||
#include "yyjson_helper.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::themezer {
|
||||
namespace {
|
||||
|
||||
// format is /themes/sphaira/Theme Name by Author/theme_name-type.nxtheme
|
||||
constexpr fs::FsPath THEME_FOLDER{"/themes/sphaira/"};
|
||||
constexpr auto CACHE_PATH = "/switch/sphaira/cache/themezer";
|
||||
constexpr auto URL_BASE = "https://switch.cdn.fortheusers.org";
|
||||
|
||||
constexpr const char* REQUEST_TARGET[]{
|
||||
"ResidentMenu",
|
||||
"Entrance",
|
||||
"Flaunch",
|
||||
"Set",
|
||||
"Psl",
|
||||
"MyPage",
|
||||
"Notification"
|
||||
};
|
||||
|
||||
constexpr const char* REQUEST_SORT[]{
|
||||
"downloads",
|
||||
"updated",
|
||||
"likes",
|
||||
"id"
|
||||
};
|
||||
|
||||
constexpr const char* REQUEST_ORDER[]{
|
||||
"desc",
|
||||
"asc"
|
||||
};
|
||||
|
||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
|
||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
|
||||
|
||||
// i know, this is cursed
|
||||
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
std::string api = "https://api.themezer.net/?query=query";
|
||||
std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
|
||||
const char* boolarr[2] = { "false", "true" };
|
||||
|
||||
std::string cmd;
|
||||
std::string p0 = "$nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String";
|
||||
std::string p1 = "nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order";
|
||||
std::string json = "\"nsfw\":"+std::string{boolarr[e.nsfw]}+",\"page\":"+std::to_string(e.page)+",\"limit\":"+std::to_string(e.limit)+",\"sort\":\""+std::string{REQUEST_SORT[e.sort_index]}+"\",\"order\":\""+std::string{REQUEST_ORDER[e.order_index]}+"\"";
|
||||
|
||||
if (is_pack) {
|
||||
cmd = "packList";
|
||||
fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
||||
} else {
|
||||
cmd = "themeList";
|
||||
p0 += ",$target:String";
|
||||
p1 += ",target:$target";
|
||||
if (e.target_index < 7) {
|
||||
json += ",\"target\":\"" + std::string{REQUEST_TARGET[e.target_index]} + "\"";
|
||||
} else {
|
||||
json += ",\"target\":null";
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.creator.empty()) {
|
||||
p0 += ",$creators:[String!]";
|
||||
p1 += ",creators:$creators";
|
||||
json += ",\"creators\":[\"" + e.creator + "\"]";
|
||||
}
|
||||
|
||||
if (!e.query.empty()) {
|
||||
p0 += ",$query:String";
|
||||
p1 += ",query:$query";
|
||||
json += ",\"query\":\"" + e.query + "\"";
|
||||
}
|
||||
|
||||
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables={"+json+"}";
|
||||
}
|
||||
|
||||
auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::string {
|
||||
char url[2048];
|
||||
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query{download%s(id:\"%s\"){filename,url,mimetype}}", is_pack ? "Pack" : "Theme", id.c_str());
|
||||
return url;
|
||||
// https://api.themezer.net/?query=query{downloadPack(id:"11"){filename,url,mimetype}}
|
||||
}
|
||||
|
||||
auto apiBuildUrlDownloadTheme(const ThemeEntry& e) -> std::string {
|
||||
return apiBuildUrlDownloadInternal(e.id, false);
|
||||
}
|
||||
|
||||
auto apiBuildUrlDownloadPack(const PackListEntry& e) -> std::string {
|
||||
return apiBuildUrlDownloadInternal(e.id, true);
|
||||
}
|
||||
|
||||
auto apiBuildFilePack(const PackListEntry& e) -> fs::FsPath {
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "%s/%s By %s/", THEME_FOLDER, e.details.name.c_str(), e.creator.display_name.c_str());
|
||||
return path;
|
||||
}
|
||||
|
||||
auto apiBuildUrlPack(const PackListEntry& e) -> std::string {
|
||||
char url[2048];
|
||||
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={\"id\":\"%s\"}", e.id.c_str());
|
||||
return url;
|
||||
}
|
||||
|
||||
auto apiBuildUrlThemeList(const Config& e) -> std::string {
|
||||
return apiBuildUrlListInternal(e, false);
|
||||
}
|
||||
|
||||
auto apiBuildUrlListPacks(const Config& e) -> std::string {
|
||||
return apiBuildUrlListInternal(e, true);
|
||||
}
|
||||
|
||||
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
|
||||
return path;
|
||||
}
|
||||
|
||||
auto loadThemeImage(ThemeEntry& e) -> void {
|
||||
auto& image = e.preview.lazy_image;
|
||||
|
||||
// already have the image
|
||||
if (e.preview.lazy_image.image) {
|
||||
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
|
||||
return;
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
std::vector<u8> image_buf;
|
||||
|
||||
const auto path = apiBuildIconCache(e);
|
||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||
e.preview.lazy_image.state = ImageDownloadState::Failed;
|
||||
} else {
|
||||
int channels_in_file;
|
||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||
if (buf) {
|
||||
ON_SCOPE_EXIT(stbi_image_free(buf));
|
||||
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
|
||||
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
||||
}
|
||||
}
|
||||
|
||||
if (!image.image) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path);
|
||||
} else {
|
||||
// log_write("loaded image from file: %s\n", path);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
|
||||
if (size && index < (size - 1)) {
|
||||
if (index < (size - step)) {
|
||||
index = index + step;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
} else {
|
||||
index = size - 1;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
}
|
||||
if (index - start >= max) {
|
||||
log_write("moved down\n");
|
||||
start += step;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Creator& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_STR(display_name);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Details& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(name);
|
||||
JSON_SET_STR(description);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Preview& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(original);
|
||||
JSON_SET_STR(thumb);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, DownloadPack& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(filename);
|
||||
JSON_SET_STR(url);
|
||||
JSON_SET_STR(mimetype);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, ThemeEntry& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_OBJ(creator);
|
||||
JSON_SET_OBJ(details);
|
||||
JSON_SET_STR(last_updated);
|
||||
JSON_SET_UINT(dl_count);
|
||||
JSON_SET_UINT(like_count);
|
||||
JSON_SET_ARR_STR(categories);
|
||||
JSON_SET_STR(target);
|
||||
JSON_SET_OBJ(preview);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, PackListEntry& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_OBJ(creator);
|
||||
JSON_SET_OBJ(details);
|
||||
JSON_SET_STR(last_updated);
|
||||
JSON_SET_ARR_STR(categories);
|
||||
JSON_SET_UINT(dl_count);
|
||||
JSON_SET_UINT(like_count);
|
||||
JSON_SET_ARR_OBJ(themes);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Pagination& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_UINT(page);
|
||||
JSON_SET_UINT(limit);
|
||||
JSON_SET_UINT(page_count);
|
||||
JSON_SET_UINT(item_count);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
||||
JSON_INIT_VEC(data, "data");
|
||||
// JSON_GET_OBJ("downloadTheme");
|
||||
JSON_GET_OBJ("downloadPack");
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(filename);
|
||||
JSON_SET_STR(url);
|
||||
JSON_SET_STR(mimetype);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(const std::vector<u8>& data, PackList& e) {
|
||||
JSON_INIT_VEC(data, "data");
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_ARR_OBJ(packList);
|
||||
JSON_SET_OBJ(pagination);
|
||||
);
|
||||
}
|
||||
|
||||
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY_RESULT(fs.GetFsOpenResult(), false);
|
||||
|
||||
DownloadPack download_pack;
|
||||
|
||||
// 1. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
log_write("using url: %s\n", url.c_str());
|
||||
DownloadClearCache(url);
|
||||
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (data.empty()) {
|
||||
log_write("error with download: %s\n", url.c_str());
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
|
||||
from_json(data, download_pack);
|
||||
}
|
||||
|
||||
// 2. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
log_write("starting download: %s\n", download_pack.url.c_str());
|
||||
|
||||
DownloadClearCache(download_pack.url);
|
||||
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
|
||||
|
||||
// create directories
|
||||
fs::FsPath dir_path;
|
||||
std::snprintf(dir_path, sizeof(dir_path), "%s/%s - By %s", THEME_FOLDER, entry.details.name.c_str(), entry.creator.display_name.c_str());
|
||||
fs.CreateDirectoryRecursively(dir_path);
|
||||
|
||||
// 3. extract the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
if (!zfile) {
|
||||
log_write("failed to open zip: %s\n", zip_out);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
char name[512];
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_path = fs::AppendPath(dir_path, name);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_ResultPathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
||||
log_write("failed to open file: %s 0x%04X\n", file_path, rc);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||
|
||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||
log_write("failed to set file size: %s 0x%04X\n", file_path, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
u64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
// log_write("failed to read zip file: %s\n", inzip.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
||||
log_write("failed to write file: %s 0x%04X\n", file_path, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_write("finished install :)\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LazyImage::~LazyImage() {
|
||||
if (image) {
|
||||
nvgDeleteImage(App::GetVg(), image);
|
||||
}
|
||||
}
|
||||
|
||||
Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (ScrollHelperDown(m_index, m_start, 3, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Download theme?"_i18n,
|
||||
"Back"_i18n, "Download"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (page.m_packList.size() && page.m_ready == PageLoadState::Done) {
|
||||
const auto& entry = page.m_packList[m_index];
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this](bool success){
|
||||
// if (success) {
|
||||
// m_entry.status = EntryStatus::Installed;
|
||||
// m_menu.SetDirty();
|
||||
// UpdateOptions();
|
||||
// }
|
||||
}, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
}}),
|
||||
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Themezer Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items sort_items;
|
||||
sort_items.push_back("Downloads"_i18n);
|
||||
sort_items.push_back("Updated"_i18n);
|
||||
sort_items.push_back("Likes"_i18n);
|
||||
sort_items.push_back("ID"_i18n);
|
||||
|
||||
SidebarEntryArray::Items order_items;
|
||||
order_items.push_back("Descending (down)"_i18n);
|
||||
order_items.push_back("Ascending (Up)"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nsfw"_i18n, m_nsfw.Get(), [this](bool& v_out){
|
||||
m_nsfw.Set(v_out);
|
||||
InvalidateAllPages();
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
||||
if (m_sort.Get() != index_out) {
|
||||
m_sort.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
if (m_order.Get() != index_out) {
|
||||
m_order.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}, m_order.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Page"_i18n, [this](){
|
||||
s64 out;
|
||||
if (R_SUCCEEDED(swkbd::ShowNumPad(out, "Enter Page Number", -1, 3))) {
|
||||
if (out < m_page_index_max) {
|
||||
m_page_index = out;
|
||||
PackListDownload();
|
||||
} else {
|
||||
log_write("invalid page number\n");
|
||||
App::Notify("Bad Page"_i18n);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
m_search = out;
|
||||
}
|
||||
}));
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_index >= 3) {
|
||||
SetIndex(m_index - 3);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index < m_start ) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
|
||||
m_page_index++;
|
||||
if (m_page_index >= m_page_index_max) {
|
||||
m_page_index = m_page_index_max - 1;
|
||||
} else {
|
||||
PackListDownload();
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L, Action{"Prev Page"_i18n, [this](){
|
||||
if (m_page_index) {
|
||||
m_page_index--;
|
||||
PackListDownload();
|
||||
}
|
||||
}})
|
||||
);
|
||||
|
||||
m_page_index = 0;
|
||||
m_pages.resize(1);
|
||||
PackListDownload();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
if (m_pages.empty()) {
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& page = m_pages[m_page_index];
|
||||
|
||||
switch (page.m_ready) {
|
||||
case PageLoadState::None:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Not Ready...");
|
||||
return;
|
||||
case PageLoadState::Loading:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading");
|
||||
return;
|
||||
case PageLoadState::Done:
|
||||
break;
|
||||
case PageLoadState::Error:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Error loading page!");
|
||||
return;
|
||||
}
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
|
||||
const auto sb_y = SCROLL / 3.f;
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
const auto index = pos;
|
||||
auto& e = page.m_packList[index];
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == cursor_pos) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
const float xoff = (350 - 320) / 2;
|
||||
const float yoff = (350 - 320) / 2;
|
||||
|
||||
// lazy load image
|
||||
if (e.themes.size()) {
|
||||
auto& theme = e.themes[0];
|
||||
auto& image = e.themes[0].preview.lazy_image;
|
||||
if (!image.image) {
|
||||
switch (image.state) {
|
||||
case ImageDownloadState::None: {
|
||||
const auto path = apiBuildIconCache(theme);
|
||||
log_write("downloading theme!: %s\n", path);
|
||||
|
||||
if (fs::FsNativeSd().FileExists(path)) {
|
||||
loadThemeImage(theme);
|
||||
} else {
|
||||
const auto url = theme.preview.thumb;
|
||||
log_write("downloading url: %s\n", url.c_str());
|
||||
image.state = ImageDownloadState::Progress;
|
||||
DownloadFileAsync(url, path, "", [this, index, &image](std::vector<u8>& data, bool success) {
|
||||
if (success) {
|
||||
image.state = ImageDownloadState::Done;
|
||||
log_write("downloaded themezer image\n");
|
||||
} else {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to download image\n");
|
||||
}
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
} break;
|
||||
case ImageDownloadState::Progress: {
|
||||
|
||||
} break;
|
||||
case ImageDownloadState::Done: {
|
||||
loadThemeImage(theme);
|
||||
} break;
|
||||
case ImageDownloadState::Failed: {
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image);
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.details.name.c_str());
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
void Menu::InvalidateAllPages() {
|
||||
for (auto& e : m_pages) {
|
||||
e.m_packList.clear();
|
||||
e.m_ready = PageLoadState::None;
|
||||
}
|
||||
|
||||
PackListDownload();
|
||||
}
|
||||
|
||||
void Menu::PackListDownload() {
|
||||
const auto page_index = m_page_index + 1;
|
||||
char subheading[128];
|
||||
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu", m_page_index+1, m_page_index_max);
|
||||
SetSubHeading(subheading);
|
||||
|
||||
m_index = 0;
|
||||
m_start = 0;
|
||||
|
||||
// already downloaded
|
||||
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
|
||||
return;
|
||||
}
|
||||
m_pages[m_page_index].m_ready = PageLoadState::Loading;
|
||||
|
||||
Config config;
|
||||
config.page = page_index;
|
||||
config.SetQuery(m_search);
|
||||
config.sort_index = m_sort.Get();
|
||||
config.order_index = m_order.Get();
|
||||
config.nsfw = m_nsfw.Get();
|
||||
const auto packList_url = apiBuildUrlListPacks(config);
|
||||
const auto themeList_url = apiBuildUrlThemeList(config);
|
||||
|
||||
log_write("\npackList_url: %s\n\n", packList_url.c_str());
|
||||
log_write("\nthemeList_url: %s\n\n", themeList_url.c_str());
|
||||
|
||||
DownloadClearCache(packList_url);
|
||||
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){
|
||||
log_write("got themezer data\n");
|
||||
if (!success) {
|
||||
auto& page = m_pages[page_index-1];
|
||||
page.m_ready = PageLoadState::Error;
|
||||
log_write("failed to get themezer data...\n");
|
||||
return;
|
||||
}
|
||||
|
||||
PackList a;
|
||||
from_json(data, a);
|
||||
|
||||
m_pages.resize(a.pagination.page_count);
|
||||
auto& page = m_pages[page_index-1];
|
||||
|
||||
page.m_packList = a.packList;
|
||||
page.m_pagination = a.pagination;
|
||||
page.m_ready = PageLoadState::Done;
|
||||
m_page_index_max = a.pagination.page_count;
|
||||
|
||||
char subheading[128];
|
||||
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu", m_page_index+1, m_page_index_max);
|
||||
SetSubHeading(subheading);
|
||||
|
||||
log_write("a.pagination.page: %u\n", a.pagination.page);
|
||||
log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::themezer
|
||||
142
sphaira/source/ui/notification.cpp
Normal file
142
sphaira/source/ui/notification.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "ui/notification.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "app.hpp"
|
||||
#include <optional>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
NotifEntry::NotifEntry(std::string text, Side side)
|
||||
: m_text{std::move(text)}
|
||||
, m_side{side} {
|
||||
}
|
||||
|
||||
auto NotifEntry::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
||||
m_pos.y = y;
|
||||
Draw(vg, theme);
|
||||
m_count--;
|
||||
return m_count == 0;
|
||||
}
|
||||
|
||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
auto overlay_col = theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour;
|
||||
auto selected_col = theme->elements[ThemeEntryID_SELECTED].colour;
|
||||
auto text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
||||
float font_size = 18.f;
|
||||
// overlay_col.a = 0.2f;
|
||||
// selected_col.a = 0.2f;
|
||||
// text_col.a = 0.2f;
|
||||
|
||||
// auto vg = App::GetVg();
|
||||
if (!m_bounds_measured) {
|
||||
m_bounds_measured = true;
|
||||
m_pos.w = 320.f;
|
||||
m_pos.h = 60.f;
|
||||
|
||||
float bounds[4];
|
||||
nvgFontSize(vg, font_size);
|
||||
nvgTextBounds(vg, 0, 0, this->m_text.c_str(), nullptr, bounds);
|
||||
// m_pos.w = std::max(bounds[2] - bounds[0] + 30.f, m_pos.w);
|
||||
m_pos.w = bounds[2] - bounds[0] + 30.f;
|
||||
|
||||
switch (m_side) {
|
||||
case Side::LEFT:
|
||||
m_pos.x = 4.f;
|
||||
break;
|
||||
case Side::RIGHT:
|
||||
m_pos.x = 1280.f - (m_pos.w + 4.f);// + 30.f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawRectOutline(vg, 4.f, overlay_col, m_pos, selected_col);
|
||||
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
|
||||
}
|
||||
|
||||
auto NotifMananger::OnLayoutChange() -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
}
|
||||
|
||||
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
Draw(vg, theme, m_entries_left);
|
||||
Draw(vg, theme, m_entries_right);
|
||||
}
|
||||
|
||||
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme, Entries& entries) -> void {
|
||||
// auto y = 130.f;
|
||||
auto y = 4.f;
|
||||
std::optional<Entries::iterator> delete_pos{std::nullopt};
|
||||
|
||||
for (auto itr = entries.begin(); itr != entries.end(); ++itr) {
|
||||
itr->Draw(vg, theme, y);
|
||||
if (itr->IsDone() && !delete_pos.has_value()) {
|
||||
delete_pos = itr;
|
||||
}
|
||||
y += itr->GetH() + 15.f;
|
||||
}
|
||||
|
||||
if (delete_pos.has_value()) {
|
||||
entries.erase(*delete_pos, entries.end());
|
||||
}
|
||||
}
|
||||
|
||||
auto NotifMananger::Push(const NotifEntry& entry) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (entry.GetSide()) {
|
||||
case NotifEntry::Side::LEFT:
|
||||
m_entries_left.emplace_front(entry);
|
||||
break;
|
||||
case NotifEntry::Side::RIGHT:
|
||||
m_entries_right.emplace_front(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto NotifMananger::Pop(NotifEntry::Side side) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (side) {
|
||||
case NotifEntry::Side::LEFT:
|
||||
if (!m_entries_left.empty()) {
|
||||
m_entries_left.clear();
|
||||
}
|
||||
break;
|
||||
case NotifEntry::Side::RIGHT:
|
||||
if (!m_entries_right.empty()) {
|
||||
m_entries_right.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto NotifMananger::Clear(NotifEntry::Side side) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (side) {
|
||||
case NotifEntry::Side::LEFT:
|
||||
m_entries_left.clear();
|
||||
break;
|
||||
case NotifEntry::Side::RIGHT:
|
||||
m_entries_right.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto NotifMananger::Clear() -> void {
|
||||
m_entries_left.clear();
|
||||
m_entries_right.clear();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
520
sphaira/source/ui/nvg_util.cpp
Normal file
520
sphaira/source/ui/nvg_util.cpp
Normal file
@@ -0,0 +1,520 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace sphaira::ui::gfx {
|
||||
namespace {
|
||||
|
||||
static constexpr std::array buttons = {
|
||||
std::pair{Button::A, "\uE0E0"},
|
||||
std::pair{Button::B, "\uE0E1"},
|
||||
std::pair{Button::X, "\uE0E2"},
|
||||
std::pair{Button::Y, "\uE0E3"},
|
||||
std::pair{Button::L, "\uE0E4"},
|
||||
std::pair{Button::R, "\uE0E5"},
|
||||
std::pair{Button::L, "\uE0E6"},
|
||||
std::pair{Button::R, "\uE0E7"},
|
||||
std::pair{Button::L2, "\uE0E8"},
|
||||
std::pair{Button::R2, "\uE0E9"},
|
||||
std::pair{Button::UP, "\uE0EB"},
|
||||
std::pair{Button::DOWN, "\uE0EC"},
|
||||
std::pair{Button::LEFT, "\uE0ED"},
|
||||
std::pair{Button::RIGHT, "\uE0EE"},
|
||||
std::pair{Button::START, "\uE0EF"},
|
||||
std::pair{Button::SELECT, "\uE0F0"},
|
||||
// std::pair{Button::LS, "\uE101"},
|
||||
// std::pair{Button::RS, "\uE102"},
|
||||
std::pair{Button::L3, "\uE104"},
|
||||
std::pair{Button::R3, "\uE105"},
|
||||
};
|
||||
|
||||
#define F(a) (a/255.f) // turn range 0-255 to 0.f-1.f range
|
||||
constexpr std::array COLOURS = {
|
||||
std::pair<Colour, NVGcolor>{Colour::BLACK, { F(45.f), F(45.f), F(45.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLACK, { F(50.f), F(50.f), F(50.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::SILVER, { F(128.f), F(128.f), F(128.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::DARK_GREY, { F(70.f), F(70.f), F(70.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::GREY, { F(77.f), F(77.f), F(77.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::WHITE, { F(251.f), F(251.f), F(251.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::CYAN, { F(0.f), F(255.f), F(200.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::TEAL, { F(143.f), F(253.f), F(252.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::BLUE, { F(36.f), F(141.f), F(199.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLUE, { F(26.f), F(188.f), F(252.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::YELLOW, { F(255.f), F(177.f), F(66.f), F(255.f) }},
|
||||
std::pair<Colour, NVGcolor>{Colour::RED, { F(250.f), F(90.f), F(58.f), F(255.f) }}
|
||||
};
|
||||
#undef F
|
||||
|
||||
// NEW ---------------------
|
||||
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGcolor& c, bool rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
||||
}
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGpaint& p, bool rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
||||
}
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
|
||||
float gradientX, gradientY, color;
|
||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
||||
|
||||
const auto strokeWidth = 5.0;
|
||||
auto v2 = vec;
|
||||
v2.x -= strokeWidth / 2.0;
|
||||
v2.y -= strokeWidth / 2.0;
|
||||
v2.w += strokeWidth;
|
||||
v2.h += strokeWidth;
|
||||
const auto corner_radius = 0.5;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgResetScissor(vg);
|
||||
|
||||
// const auto stroke_width = 5.0f;
|
||||
// const auto shadow_corner_radius = 6.0f;
|
||||
const auto shadow_width = 2.0f;
|
||||
const auto shadow_offset = 10.0f;
|
||||
const auto shadow_feather = 10.0f;
|
||||
const auto shadow_opacity = 128.0f;
|
||||
|
||||
// Shadow
|
||||
NVGpaint shadowPaint = nvgBoxGradient(vg,
|
||||
v2.x, v2.y + shadow_width,
|
||||
v2.w, v2.h,
|
||||
corner_radius * 2, shadow_feather,
|
||||
nvgRGBA(0, 0, 0, shadow_opacity * 1.f), nvgRGBA(0, 0, 0, 0));
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgRect(vg, v2.x - shadow_offset, v2.y - shadow_offset,
|
||||
v2.w + shadow_offset * 2, v2.h + shadow_offset * 3);
|
||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||
nvgPathWinding(vg, NVG_HOLE);
|
||||
nvgFillPaint(vg, shadowPaint);
|
||||
nvgFill(vg);
|
||||
|
||||
const auto color1 = nvgRGB(25, 138, 198);
|
||||
const auto color2 = nvgRGB(137, 241, 242);
|
||||
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
|
||||
const auto transparent = nvgRGBA(0, 0, 0, 0);
|
||||
|
||||
const auto pulsationColor = nvgRGBAf((color * color1.r) + (1 - color) * color2.r,
|
||||
(color * color1.g) + (1 - color) * color2.g,
|
||||
(color * color1.b) + (1 - color) * color2.b,
|
||||
1.f);
|
||||
|
||||
const auto border1Paint = nvgRadialGradient(vg,
|
||||
v2.x + gradientX * v2.w, v2.y + gradientY * v2.h,
|
||||
strokeWidth * 10, strokeWidth * 40,
|
||||
borderColor, transparent);
|
||||
|
||||
const auto border2Paint = nvgRadialGradient(vg,
|
||||
v2.x + (1 - gradientX) * v2.w, v2.y + (1 - gradientY) * v2.h,
|
||||
strokeWidth * 10, strokeWidth * 40,
|
||||
borderColor, transparent);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgStrokeColor(vg, pulsationColor);
|
||||
nvgStrokeWidth(vg, strokeWidth);
|
||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||
nvgStroke(vg);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgStrokePaint(vg, border1Paint);
|
||||
nvgStrokeWidth(vg, strokeWidth);
|
||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||
nvgStroke(vg);
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgStrokePaint(vg, border2Paint);
|
||||
nvgStrokeWidth(vg, strokeWidth);
|
||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||
nvgStroke(vg);
|
||||
|
||||
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
|
||||
drawRectIntenal(vg, vec, c, true);
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, corner_radius);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
|
||||
float gradientX, gradientY, color;
|
||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
||||
|
||||
NVGcolor pulsationColor = nvgRGBAf((color * out_col.r) + (1 - color) * out_col.r,
|
||||
(color * out_col.g) + (1 - color) * out_col.g,
|
||||
(color * out_col.b) + (1 - color) * out_col.b,
|
||||
out_col.a);
|
||||
|
||||
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
|
||||
drawRectIntenal(vg, vec, p, false);
|
||||
}
|
||||
|
||||
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawTextIntenal(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
nvgFillColor(vg, c);
|
||||
nvgText(vg, vec.x, vec.y, str, end);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* getButton(const Button want) {
|
||||
for (auto& [key, val] : buttons) {
|
||||
if (key == want) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
NVGcolor getColour(Colour want) {
|
||||
for (auto& [key, val] : COLOURS) {
|
||||
if (key == want) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
|
||||
std::va_list v;
|
||||
va_start(v, str);
|
||||
char buffer[0x100];
|
||||
std::vsnprintf(buffer, sizeof(buffer), str, v);
|
||||
va_end(v);
|
||||
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
||||
}
|
||||
|
||||
static void drawImageInternal(NVGcontext* vg, Vec4 v, int texture, int rounded = 0) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
// drawRect(vg, x, y, w, h, paint);
|
||||
nvgBeginPath(vg);
|
||||
// nvgRect(vg, x, y, w, h);
|
||||
if (rounded == 0) {
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
} else {
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||
}
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, Vec4 v, int texture) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
drawRect(vg, v, paint, false);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImage(vg, Vec4(x, y, w, h), texture);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, Vec4 v, int texture) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
||||
}
|
||||
|
||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGcolor& c, const char* str, int align, const char* end) {
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
nvgFillColor(vg, c);
|
||||
nvgTextBox(vg, x, y, bound, str, end);
|
||||
}
|
||||
|
||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGcolor&& c, const char* str, int align, const char* end) {
|
||||
drawTextBox(vg, x, y, size, bound, c, str, align, end);
|
||||
}
|
||||
|
||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, Colour c, const char* str, int align, const char* end) {
|
||||
drawTextBox(vg, x, y, size, bound, getColour(c), str, align, end);
|
||||
}
|
||||
|
||||
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
|
||||
char buf[0x100];
|
||||
va_list v;
|
||||
va_start(v, str);
|
||||
std::vsnprintf(buf, sizeof(buf), str, v);
|
||||
va_end(v);
|
||||
nvgTextBounds(vg, x, y, buf, nullptr, bounds);
|
||||
}
|
||||
|
||||
// NEW-----------
|
||||
|
||||
void dimBackground(NVGcontext* vg) {
|
||||
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180));
|
||||
// drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(20, 20, 20, 225), false);
|
||||
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, getColour(c), rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, Colour c, bool rounded) {
|
||||
drawRectIntenal(vg, vec, getColour(c), rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor&& c, bool rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor& c, bool rounded) {
|
||||
drawRectIntenal(vg, vec, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor&& c, bool rounded) {
|
||||
drawRectIntenal(vg, vec, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint&& p, bool rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint& p, bool rounded) {
|
||||
drawRectIntenal(vg, vec, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint&& p, bool rounded) {
|
||||
drawRectIntenal(vg, vec, p, rounded);
|
||||
}
|
||||
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, Colour c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, getColour(c));
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, Colour c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, vec, getColour(c));
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor& c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor&& c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, vec, c);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor&& c) {
|
||||
drawRectOutlineInternal(vg, size, out_col, vec, c);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint& p) {
|
||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint&& p) {
|
||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
|
||||
drawRectOutlineInternal(vg, size, out_col, vec, p);
|
||||
}
|
||||
|
||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint&& p) {
|
||||
drawRectOutlineInternal(vg, size, out_col, vec, p);
|
||||
}
|
||||
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, Colour c) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, getColour(c));
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor&& c) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint&& p) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, Colour c) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, Colour c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, Colour c) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, Colour c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, Colour c, const char* str, ...) {
|
||||
std::va_list v;
|
||||
va_start(v, str);
|
||||
char buffer[0x100];
|
||||
std::vsnprintf(buffer, sizeof(buffer), str, v);
|
||||
va_end(v);
|
||||
drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c));
|
||||
}
|
||||
|
||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) {
|
||||
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE));
|
||||
}
|
||||
|
||||
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x) {
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, 24.f);
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
nvgFillColor(vg, c);
|
||||
|
||||
float x = start_x;
|
||||
const float y = 675.f;
|
||||
float bounds[4]{};
|
||||
|
||||
for (const auto& [button, action] : actions) {
|
||||
if (action.IsHidden() || action.m_hint.empty()) {
|
||||
continue;
|
||||
}
|
||||
nvgFontSize(vg, 20.f);
|
||||
nvgTextBounds(vg, x, y, action.m_hint.c_str(), nullptr, bounds);
|
||||
auto len = bounds[2] - bounds[0];
|
||||
nvgText(vg, x, y, action.m_hint.c_str(), nullptr);
|
||||
|
||||
x -= len + 8.f;
|
||||
nvgFontSize(vg, 26.f);
|
||||
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds);
|
||||
len = bounds[2] - bounds[0];
|
||||
nvgText(vg, x, y - 4.f, getButton(button), nullptr);
|
||||
x -= len + 34.f;
|
||||
}
|
||||
}
|
||||
|
||||
// from gc installer
|
||||
void drawDimBackground(NVGcontext* vg) {
|
||||
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
|
||||
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
|
||||
}
|
||||
|
||||
#define HIGHLIGHT_SPEED 350.0
|
||||
|
||||
static double highlightGradientX = 0;
|
||||
static double highlightGradientY = 0;
|
||||
static double highlightColor = 0;
|
||||
|
||||
void updateHighlightAnimation() {
|
||||
const auto currentTime = svcGetSystemTick() * 10 / 192 / 1000;
|
||||
|
||||
// Update variables
|
||||
highlightGradientX = (std::cos((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
|
||||
highlightGradientY = (std::sin((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
|
||||
highlightColor = (std::sin((double)currentTime / HIGHLIGHT_SPEED * 2.0) + 1.0) / 2.0;
|
||||
}
|
||||
|
||||
void getHighlightAnimation(float* gradientX, float* gradientY, float* color) {
|
||||
if (gradientX)
|
||||
*gradientX = (float)highlightGradientX;
|
||||
|
||||
if (gradientY)
|
||||
*gradientY = (float)highlightGradientY;
|
||||
|
||||
if (color)
|
||||
*color = (float)highlightColor;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::gfx
|
||||
146
sphaira/source/ui/option_box.cpp
Normal file
146
sphaira/source/ui/option_box.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
OptionBoxEntry::OptionBoxEntry(const std::string& text, Vec4 pos)
|
||||
: m_text{text} {
|
||||
m_pos = pos;
|
||||
m_text_pos = Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)};
|
||||
}
|
||||
|
||||
auto OptionBoxEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
if (m_selected) {
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
} else {
|
||||
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBoxEntry::Selected(bool enable) -> void {
|
||||
m_selected = enable;
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 295.f;
|
||||
m_pos.x = (1280.f / 2.f) - (m_pos.w / 2.f);
|
||||
m_pos.y = (720.f / 2.f) - (m_pos.h / 2.f);
|
||||
|
||||
auto box = m_pos;
|
||||
box.y += 220.f;
|
||||
box.h -= 220.f;
|
||||
m_entries.emplace_back(a, box);
|
||||
|
||||
Setup(0);
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb)
|
||||
: OptionBox{message, a, b, 0, cb} {
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 295.f;
|
||||
m_pos.x = (1280.f / 2.f) - (m_pos.w / 2.f);
|
||||
m_pos.y = (720.f / 2.f) - (m_pos.h / 2.f);
|
||||
|
||||
auto box = m_pos;
|
||||
box.w /= 2.f;
|
||||
box.y += 220.f;
|
||||
box.h -= 220.f;
|
||||
m_entries.emplace_back(a, box);
|
||||
box.x += box.w;
|
||||
m_entries.emplace_back(b, box);
|
||||
|
||||
Setup(index);
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb)
|
||||
: OptionBox{message, a, b, c, 0, cb} {
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
}
|
||||
|
||||
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const auto old_index = m_index;
|
||||
|
||||
// if (controller->GotDown(Button::LEFT) && m_index) {
|
||||
// m_index--;
|
||||
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) {
|
||||
// m_index++;
|
||||
// }
|
||||
|
||||
// if (old_index != m_index) {
|
||||
// m_entries[old_index].Selected(false);
|
||||
// m_entries[m_index].Selected(true);
|
||||
// }
|
||||
}
|
||||
|
||||
auto OptionBox::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawText(vg, {m_pos.x + (m_pos.w / 2.f), m_pos.y + 110.f}, 26.f, theme->elements[ThemeEntryID_TEXT].colour, m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_TOP);
|
||||
gfx::drawRect(vg, m_spacer_line, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
for (auto&p: m_entries) {
|
||||
p.Draw(vg, theme);
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBox::Setup(std::size_t index) -> void {
|
||||
m_index = std::min(m_entries.size() - 1, index);
|
||||
m_entries[m_index].Selected(true);
|
||||
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
|
||||
|
||||
SetActions(
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index--;
|
||||
m_entries[m_index].Selected(true);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index++;
|
||||
m_entries[m_index].Selected(true);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{[this](){
|
||||
m_callback(m_index);
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{[this](){
|
||||
m_callback({});
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
32
sphaira/source/ui/option_list.cpp
Normal file
32
sphaira/source/ui/option_list.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#include "ui/option_list.hpp"
|
||||
#include "app.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
OptionList::OptionList(Options options)
|
||||
: m_options{std::move(options)} {
|
||||
SetAction(Button::A, Action{"Select", [this](){
|
||||
const auto& [_, func] = m_options[m_index];
|
||||
func();
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::B, Action{"Back", [this](){
|
||||
SetPop();
|
||||
}});
|
||||
}
|
||||
|
||||
auto OptionList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
|
||||
}
|
||||
|
||||
auto OptionList::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto OptionList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
154
sphaira/source/ui/popup_list.cpp
Normal file
154
sphaira/source/ui/popup_list.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index_ref)
|
||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||
|
||||
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
|
||||
if (op_idx) {
|
||||
index_ref = *op_idx;
|
||||
index_str_ref = m_items[index_ref];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, std::string& index_ref)
|
||||
: PopupList{std::move(title), std::move(items), Callback{}} {
|
||||
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
}
|
||||
|
||||
m_callback = [&index_ref, this](auto op_idx) {
|
||||
if (op_idx) {
|
||||
index_ref = m_items[*op_idx];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, std::size_t& index_ref)
|
||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||
|
||||
m_callback = [&index_ref, this](auto op_idx) {
|
||||
if (op_idx) {
|
||||
index_ref = *op_idx;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, Callback cb, std::string index)
|
||||
: PopupList{std::move(title), std::move(items), cb, 0} {
|
||||
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
}
|
||||
}
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t index)
|
||||
: m_title{std::move(title)}
|
||||
, m_items{std::move(items)}
|
||||
, m_callback{cb}
|
||||
, m_index{index} {
|
||||
|
||||
m_pos.w = 1280.f;
|
||||
const float a = std::min(405.f, (60.f * static_cast<float>(m_items.size())));
|
||||
m_pos.h = 80.f + 140.f + a;
|
||||
m_pos.y = 720.f - m_pos.h;
|
||||
m_line_top = m_pos.y + 70.f;
|
||||
m_line_bottom = 720.f - 73.f;
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
|
||||
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
|
||||
|
||||
SetActions(
|
||||
std::make_pair(Button::A, Action{"Select", [this](){
|
||||
if (m_callback) {
|
||||
m_callback(m_index);
|
||||
}
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back", [this](){
|
||||
if (m_callback) {
|
||||
m_callback(std::nullopt);
|
||||
}
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
if (!controller->GotDown(Button::ANY_VERTICAL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
|
||||
m_index++;
|
||||
m_selected_y += m_block.h;
|
||||
} else if (controller->GotDown(Button::UP) && m_index != 0) {
|
||||
m_index--;
|
||||
m_selected_y -= m_block.h;
|
||||
}
|
||||
|
||||
OnLayoutChange();
|
||||
}
|
||||
|
||||
auto PopupList::OnLayoutChange() -> void {
|
||||
if ((m_selected_y + m_block.h) > m_line_bottom) {
|
||||
m_selected_y -= m_block.h;
|
||||
m_index_offset++;
|
||||
m_scrollbar.Move(ScrollBar::Direction::DOWN);
|
||||
} else if (m_selected_y <= m_line_top) {
|
||||
m_selected_y += m_block.h;
|
||||
m_index_offset--;
|
||||
m_scrollbar.Move(ScrollBar::Direction::UP);
|
||||
}
|
||||
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
|
||||
}
|
||||
|
||||
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
||||
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
// todo: cleanup
|
||||
const float x = m_block.x;
|
||||
float y = m_line_top + 1.f + 42.f;
|
||||
const float h = m_block.h;
|
||||
const float w = m_block.w;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
|
||||
|
||||
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
||||
if (m_index == i) {
|
||||
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
||||
gfx::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
} else {
|
||||
gfx::drawRect(vg, x, y, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
}
|
||||
y += h;
|
||||
if (y > m_line_bottom) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
m_scrollbar.Draw(vg, theme);
|
||||
Widget::Draw(vg, theme);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
183
sphaira/source/ui/progress_box.cpp
Normal file
183
sphaira/source/ui/progress_box.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
|
||||
void threadFunc(void* arg) {
|
||||
auto d = static_cast<ProgressBox::ThreadData*>(arg);
|
||||
d->result = d->callback(d->pbox);
|
||||
d->pbox->RequestExit();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
|
||||
SetAction(Button::B, Action{"Back", [this](){
|
||||
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?", "No", "Yes", 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
RequestExit();
|
||||
SetPop();
|
||||
}
|
||||
}));
|
||||
}});
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 430.f;
|
||||
m_pos.x = 255;
|
||||
m_pos.y = 145;
|
||||
|
||||
m_done = done;
|
||||
m_title = title;
|
||||
|
||||
m_thread_data.pbox = this;
|
||||
m_thread_data.callback = callback;
|
||||
if (R_FAILED(threadCreate(&m_thread, threadFunc, &m_thread_data, nullptr, stack_size, prio, cpuid))) {
|
||||
log_write("failed to create thead\n");
|
||||
}
|
||||
if (R_FAILED(threadStart(&m_thread))) {
|
||||
log_write("failed to start thread\n");
|
||||
}
|
||||
}
|
||||
|
||||
ProgressBox::~ProgressBox() {
|
||||
mutexLock(&m_mutex);
|
||||
m_exit_requested = true;
|
||||
mutexUnlock(&m_mutex);
|
||||
|
||||
if (R_FAILED(threadWaitForExit(&m_thread))) {
|
||||
log_write("failed to join thread\n");
|
||||
}
|
||||
if (R_FAILED(threadClose(&m_thread))) {
|
||||
log_write("failed to close thread\n");
|
||||
}
|
||||
|
||||
m_done(m_thread_data.result);
|
||||
}
|
||||
|
||||
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
if (ShouldExit()) {
|
||||
SetPop();
|
||||
}
|
||||
}
|
||||
|
||||
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
const auto title = m_title;
|
||||
const auto transfer = m_transfer;
|
||||
const auto size = m_size;
|
||||
const auto offset = m_offset;
|
||||
mutexUnlock(&m_mutex);
|
||||
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
|
||||
// The pop up shape.
|
||||
// const Vec4 box = { 255, 145, 770, 430 };
|
||||
const Vec4 prog_bar = { 400, 470, 480, 12 };
|
||||
const auto center_x = m_pos.x + m_pos.w/2;
|
||||
|
||||
// shapes.
|
||||
if (offset && size) {
|
||||
gfx::drawRect(vg, prog_bar, gfx::Colour::SILVER);
|
||||
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, gfx::Colour::CYAN);
|
||||
// gfx::drawTextArgs(vg, prog_bar.x + 85, prog_bar.y + 40, 20, 0, gfx::Colour::WHITE, "%u%%", percentage);
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%u%%", percentage);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, center_x, 200, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, title.c_str());
|
||||
// gfx::drawTextArgs(vg, center_x, 260, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "Please do not remove the gamecard or");
|
||||
// gfx::drawTextArgs(vg, center_x, 295, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "power off the system whilst installing.");
|
||||
// gfx::drawTextArgs(vg, center_x, 360, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%.2f MiB/s", 24.0);
|
||||
if (!transfer.empty()) {
|
||||
gfx::drawTextArgs(vg, center_x, 420, 20, NVG_ALIGN_CENTER, gfx::Colour::WHITE, "%s", transfer.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_transfer = transfer;
|
||||
m_size = 0;
|
||||
m_offset = 0;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_size = size;
|
||||
m_offset = offset;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
void ProgressBox::RequestExit() {
|
||||
mutexLock(&m_mutex);
|
||||
m_exit_requested = true;
|
||||
mutexUnlock(&m_mutex);
|
||||
}
|
||||
|
||||
auto ProgressBox::ShouldExit() -> bool {
|
||||
mutexLock(&m_mutex);
|
||||
const auto exit_requested = m_exit_requested;
|
||||
mutexUnlock(&m_mutex);
|
||||
return exit_requested;
|
||||
}
|
||||
|
||||
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
|
||||
FsFile src_file;
|
||||
R_TRY(fs.OpenFile(src_path, FsOpenMode_Read, &src_file));
|
||||
ON_SCOPE_EXIT(fsFileClose(&src_file));
|
||||
|
||||
s64 src_size;
|
||||
R_TRY(fsFileGetSize(&src_file, &src_size));
|
||||
|
||||
// this can fail if it already exists so we ignore the result.
|
||||
// if the file actually failed to be created, the result is implicitly
|
||||
// handled when we try and open it for writing.
|
||||
fs.CreateFile(dst_path, src_size, 0);
|
||||
|
||||
FsFile dst_file;
|
||||
R_TRY(fs.OpenFile(dst_path, FsOpenMode_Write, &dst_file));
|
||||
ON_SCOPE_EXIT(fsFileClose(&dst_file));
|
||||
|
||||
R_TRY(fsFileSetSize(&dst_file, src_size));
|
||||
|
||||
s64 offset{};
|
||||
std::vector<u8> buf(1024*1024*8); // 8MiB
|
||||
|
||||
while (offset < src_size) {
|
||||
if (ShouldExit()) {
|
||||
R_THROW(0xFFFF);
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read));
|
||||
Yield();
|
||||
|
||||
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
Yield();
|
||||
|
||||
offset += bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void ProgressBox::Yield() {
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
109
sphaira/source/ui/scrollable_text.cpp
Normal file
109
sphaira/source/ui/scrollable_text.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "app.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "log.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
ScrollableText::ScrollableText(const std::string& text, float x, float y, float y_clip, float w, float font_size)
|
||||
: m_font_size{font_size}
|
||||
, m_y_off_base{y}
|
||||
, m_y_off{y}
|
||||
, m_clip_y{y_clip}
|
||||
, m_end_w{w}
|
||||
{
|
||||
SetActions(
|
||||
std::make_pair(Button::LS_DOWN, Action{[this](){
|
||||
const auto bound = m_bounds[3];
|
||||
if (bound < m_clip_y) {
|
||||
return;
|
||||
}
|
||||
const auto a = m_y_off_base + m_clip_y;
|
||||
const auto norm = m_bounds[3] - m_bounds[1];
|
||||
const auto b = m_y_off + norm;
|
||||
if (b <= a) {
|
||||
return;
|
||||
}
|
||||
m_y_off -= m_step;
|
||||
m_index++;
|
||||
}}),
|
||||
std::make_pair(Button::LS_UP, Action{[this](){
|
||||
if (m_y_off == m_y_off_base) {
|
||||
return;
|
||||
}
|
||||
m_y_off += m_step;
|
||||
m_index--;
|
||||
}})
|
||||
);
|
||||
|
||||
#if 1
|
||||
// converts '\''n' to '\n' without including <regex> because it bloats
|
||||
// the binary by over 400 KiB lol
|
||||
const auto mini_regex = [](std::string_view str, std::string_view regex, std::string_view replace) -> std::string {
|
||||
std::string out;
|
||||
out.reserve(str.size());
|
||||
u32 i{};
|
||||
|
||||
while (i < str.size()) {
|
||||
if ((i + regex.size()) <= str.size() && !std::memcmp(str.data() + i, regex.data(), regex.size())) {
|
||||
out.append(replace.data(), replace.size());
|
||||
i += regex.size();
|
||||
} else {
|
||||
out.push_back(str[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
m_text = mini_regex(text, "\r", "");
|
||||
m_text = mini_regex(m_text, "\\n", "\n");
|
||||
#else
|
||||
m_text = std::regex_replace(text, std::regex("\\\\n"), "\n");
|
||||
#endif
|
||||
if (m_text.size() > 4096) {
|
||||
m_text.resize(4096);
|
||||
m_text += "...";
|
||||
}
|
||||
|
||||
nvgFontSize(App::GetVg(), m_font_size);
|
||||
nvgTextLineHeight(App::GetVg(), 1.7);
|
||||
nvgTextBoxBounds(App::GetVg(), 110.0F, m_y_off_base, m_end_w, m_text.c_str(), nullptr, m_bounds);
|
||||
// log_write("bounds x: %.2f y: %.2f w: %.2f h: %.2f\n", m_bounds[0], m_bounds[1], m_bounds[2], m_bounds[3]);
|
||||
}
|
||||
|
||||
// void ScrollableText::Update(Controller* controller, TouchInfo* touch) {
|
||||
|
||||
// }
|
||||
|
||||
void ScrollableText::Draw(NVGcontext* vg, Theme* theme) {
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
const Vec4 line_vec(30, 86, 1220, 646);
|
||||
// const Vec4 banner_vec(70, line_vec.y + 20, 848.f, 208.f);
|
||||
const Vec4 banner_vec(70, line_vec.y + 20, m_end_w + (110.0F), 208.f);
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if ((m_bounds[3] - m_bounds[1]) > m_clip_y) {
|
||||
const auto scrollbar_size = m_clip_y;
|
||||
const auto max_index = (m_bounds[3] - m_bounds[1]) / m_step;
|
||||
const auto sb_h = 1.f / max_index * scrollbar_size;
|
||||
const auto in_clip = m_clip_y / m_step - 1;
|
||||
const auto sb_y = m_index;
|
||||
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
gfx::drawRect(vg, banner_vec.w, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, banner_vec.w+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 0, m_y_off_base - m_font_size, 1280, m_clip_y + m_font_size); // clip
|
||||
|
||||
nvgTextLineHeight(App::GetVg(), 1.7);
|
||||
gfx::drawTextBox(vg, banner_vec.x + 40, m_y_off, m_font_size, m_bounds[2] - m_bounds[0], theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str());
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
68
sphaira/source/ui/scrollbar.cpp
Normal file
68
sphaira/source/ui/scrollbar.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "ui/scrollbar.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
ScrollBar::ScrollBar(Vec4 bounds, float entry_height, std::size_t entries)
|
||||
: m_bounds{bounds}
|
||||
, m_entries{entries}
|
||||
, m_entry_height{entry_height} {
|
||||
Setup();
|
||||
}
|
||||
|
||||
auto ScrollBar::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto ScrollBar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
if (m_should_draw) {
|
||||
gfx::drawRect(vg, m_pos, gfx::Colour::RED);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollBar::Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void {
|
||||
m_bounds = bounds;
|
||||
m_entry_height = entry_height;
|
||||
m_entries = entries;
|
||||
Setup();
|
||||
}
|
||||
|
||||
auto ScrollBar::Setup() -> void {
|
||||
m_bounds.y += 5.f;
|
||||
m_bounds.h -= 10.f;
|
||||
|
||||
const float total_size = (m_entry_height) * static_cast<float>(m_entries);
|
||||
if (total_size > m_bounds.h) {
|
||||
m_step_size = total_size / m_entries;
|
||||
m_pos.x = m_bounds.x;
|
||||
m_pos.y = m_bounds.y;
|
||||
m_pos.w = 2.f;
|
||||
m_pos.h = total_size - m_bounds.h;
|
||||
m_should_draw = true;
|
||||
// LOG("total size: %.2f\n", total_size);
|
||||
// LOG("step size: %.2f\n", m_step_size);
|
||||
// LOG("pos y: %.2f\n", m_pos.y);
|
||||
// LOG("pos h: %.2f\n", m_pos.h);
|
||||
} else {
|
||||
// LOG("not big enough for scroll total: %.2f bounds: %.2f\n", total_size, bounds.h);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollBar::Move(Direction direction) -> void {
|
||||
switch (direction) {
|
||||
case Direction::DOWN:
|
||||
if (m_index < (m_entries - 1)) {
|
||||
m_index++;
|
||||
m_pos.y += m_step_size;
|
||||
}
|
||||
break;
|
||||
case Direction::UP:
|
||||
if (m_index != 0) {
|
||||
m_index--;
|
||||
m_pos.y -= m_step_size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
323
sphaira/source/ui/sidebar.cpp
Normal file
323
sphaira/source/ui/sidebar.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "app.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
|
||||
struct SidebarSpacer : SidebarEntryBase {
|
||||
|
||||
};
|
||||
|
||||
struct SidebarHeader : SidebarEntryBase {
|
||||
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SidebarEntryBase::SidebarEntryBase(std::string&& title)
|
||||
: m_title{std::forward<std::string>(title)} {
|
||||
|
||||
}
|
||||
|
||||
auto SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
// draw spacers or highlight box if in focus (selected)
|
||||
if (HasFocus()) {
|
||||
gfx::drawRect(vg, m_pos, nvgRGB(50,50,50));
|
||||
gfx::drawRect(vg, m_pos, nvgRGB(0,0,0));
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
// gfx::drawRect(vg, m_pos.x - 4.f, m_pos.y - 4.f, m_pos.w + 8.f, m_pos.h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
||||
// gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, m_pos.h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
|
||||
gfx::drawRect(vg, m_pos.x, m_pos.y + m_pos.h, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
|
||||
}
|
||||
}
|
||||
|
||||
SidebarEntryBool::SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str, std::string false_str)
|
||||
: SidebarEntryBase{std::move(title)}
|
||||
, m_option{option}
|
||||
, m_callback{cb}
|
||||
, m_true_str{std::move(true_str)}
|
||||
, m_false_str{std::move(false_str)} {
|
||||
|
||||
SetAction(Button::A, Action{"OK", [this](){
|
||||
m_option ^= 1;
|
||||
m_callback(m_option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SidebarEntryBool::SidebarEntryBool(std::string title, bool& option, std::string true_str, std::string false_str)
|
||||
: SidebarEntryBool{std::move(title), option, Callback{} } {
|
||||
m_callback = [](bool& option){
|
||||
option ^= 1;
|
||||
};
|
||||
}
|
||||
|
||||
auto SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
SidebarEntryBase::Draw(vg, theme);
|
||||
|
||||
// if (HasFocus()) {
|
||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
// } else {
|
||||
// }
|
||||
|
||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
|
||||
if (m_option == true) {
|
||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_true_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
||||
} else { // text info
|
||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_false_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
||||
}
|
||||
}
|
||||
|
||||
SidebarEntryCallback::SidebarEntryCallback(std::string title, Callback cb, bool pop_on_click)
|
||||
: SidebarEntryBase{std::move(title)}
|
||||
, m_callback{cb}
|
||||
, m_pop_on_click{pop_on_click} {
|
||||
SetAction(Button::A, Action{"OK", [this](){
|
||||
m_callback();
|
||||
if (m_pop_on_click) {
|
||||
SetPop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto SidebarEntryCallback::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
SidebarEntryBase::Draw(vg, theme);
|
||||
|
||||
// if (HasFocus()) {
|
||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
// } else {
|
||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
// }
|
||||
}
|
||||
|
||||
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, std::string& index)
|
||||
: SidebarEntryArray{std::move(title), std::move(items), Callback{}, 0} {
|
||||
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
}
|
||||
|
||||
m_list_callback = [&index, this]() {
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
m_title, m_items, index, m_index
|
||||
));
|
||||
};
|
||||
|
||||
// m_callback = [&index, this](auto& idx) {
|
||||
// App::Push(std::make_shared<PopupList>(
|
||||
// m_title, m_items, index, idx
|
||||
// ));
|
||||
// };
|
||||
}
|
||||
|
||||
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::string index)
|
||||
: SidebarEntryArray{std::move(title), std::move(items), cb, 0} {
|
||||
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
}
|
||||
}
|
||||
|
||||
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index)
|
||||
: SidebarEntryBase{std::forward<std::string>(title)}
|
||||
, m_items{std::move(items)}
|
||||
, m_callback{cb}
|
||||
, m_index{index} {
|
||||
|
||||
m_list_callback = [this]() {
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
m_title, m_items, [this](auto op_idx){
|
||||
if (op_idx) {
|
||||
m_index = *op_idx;
|
||||
m_callback(m_index);
|
||||
}
|
||||
}, m_index
|
||||
));
|
||||
};
|
||||
|
||||
SetAction(Button::A, Action{"OK", [this](){
|
||||
// m_callback(m_index);
|
||||
m_list_callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
SidebarEntryBase::Draw(vg, theme);
|
||||
|
||||
const auto& text_entry = m_items[m_index];
|
||||
// const auto& colour = HasFocus() ? theme->elements[ThemeEntryID_TEXT_SELECTED].colour : theme->elements[ThemeEntryID_TEXT].colour;
|
||||
const auto& colour = theme->elements[ThemeEntryID_TEXT].colour;
|
||||
|
||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, text_entry.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(std::string title, Side side, Items&& items)
|
||||
: Sidebar{std::move(title), "", side, std::forward<Items>(items)} {
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(std::string title, Side side)
|
||||
: Sidebar{std::move(title), "", side, {}} {
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(std::string title, std::string sub, Side side, Items&& items)
|
||||
: m_title{std::move(title)}
|
||||
, m_sub{std::move(sub)}
|
||||
, m_side{side}
|
||||
, m_items{std::forward<Items>(items)} {
|
||||
switch (m_side) {
|
||||
case Side::LEFT:
|
||||
SetPos(Vec4{0.f, 0.f, 450.f, 720.f});
|
||||
break;
|
||||
|
||||
case Side::RIGHT:
|
||||
SetPos(Vec4{1280.f - 450.f, 0.f, 450.f, 720.f});
|
||||
break;
|
||||
}
|
||||
|
||||
// setup top and bottom bar
|
||||
m_top_bar = Vec4{m_pos.x + 15.f, 86.f, m_pos.w - 30.f, 1.f};
|
||||
m_bottom_bar = Vec4{m_pos.x + 15.f, 646.f, m_pos.w - 30.f, 1.f};
|
||||
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
|
||||
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
|
||||
|
||||
// each item has it's own Action, but we take over B
|
||||
SetAction(Button::B, Action{"Back", [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
m_selected_y = m_base_pos.y;
|
||||
|
||||
if (!m_items.empty()) {
|
||||
// setup positions
|
||||
m_selected_y = m_base_pos.y;
|
||||
// for (auto&p : m_items) {
|
||||
// p->SetPos(m_base_pos);
|
||||
// m_base_pos.y += m_base_pos.h;
|
||||
// }
|
||||
|
||||
// // give focus to first entry.
|
||||
// m_items[m_index]->OnFocusGained();
|
||||
}
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
||||
: Sidebar{std::move(title), sub, side, {}} {
|
||||
}
|
||||
|
||||
|
||||
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
m_items[m_index]->Update(controller, touch);
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
if (m_items[m_index]->ShouldPop()) {
|
||||
SetPop();
|
||||
}
|
||||
|
||||
const auto old_index = m_index;
|
||||
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
|
||||
m_index++;
|
||||
m_selected_y += m_box_size.y;
|
||||
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
|
||||
m_index--;
|
||||
m_selected_y -= m_box_size.y;
|
||||
}
|
||||
|
||||
// if we moved
|
||||
if (m_index != old_index) {
|
||||
m_items[old_index]->OnFocusLost();
|
||||
m_items[m_index]->OnFocusGained();
|
||||
|
||||
// move offset
|
||||
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
|
||||
m_selected_y -= m_box_size.y;
|
||||
m_index_offset++;
|
||||
// LOG("move down\n");
|
||||
} else if (m_selected_y <= m_top_bar.y) {
|
||||
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
|
||||
m_selected_y += m_box_size.y;
|
||||
m_index_offset--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||
return Vec4{
|
||||
va.x, va.y,
|
||||
va.w, vb.y - va.y
|
||||
};
|
||||
}
|
||||
|
||||
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::drawRect(vg, m_pos, nvgRGBA(0, 0, 0, 220));
|
||||
gfx::drawText(vg, m_title_pos, m_title_size, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
||||
if (!m_sub.empty()) {
|
||||
gfx::drawTextArgs(vg, m_pos.x + m_pos.w - 30.f, m_title_pos.y + 10.f, 18, NVG_ALIGN_TOP | NVG_ALIGN_RIGHT, theme->elements[ThemeEntryID_TEXT].colour, m_sub.c_str());
|
||||
}
|
||||
gfx::drawRect(vg, m_top_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, m_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
|
||||
|
||||
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
||||
// m_items[i]->Draw(vg, theme);
|
||||
// }
|
||||
|
||||
for (auto&p : m_items) {
|
||||
p->Draw(vg, theme);
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
|
||||
// draw the buttons. fetch the actions from current item and insert into array.
|
||||
Actions draw_actions{m_actions};
|
||||
const auto& actions_ref = m_items[m_index]->GetActions();
|
||||
draw_actions.insert(actions_ref.cbegin(), actions_ref.cend());
|
||||
|
||||
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour, m_pos.x + m_pos.w - 60.f);
|
||||
}
|
||||
|
||||
auto Sidebar::OnFocusGained() noexcept -> void {
|
||||
Widget::OnFocusGained();
|
||||
SetHidden(false);
|
||||
}
|
||||
|
||||
auto Sidebar::OnFocusLost() noexcept -> void {
|
||||
Widget::OnFocusLost();
|
||||
SetHidden(true);
|
||||
}
|
||||
|
||||
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
|
||||
m_items.emplace_back(entry);
|
||||
m_items.back()->SetPos(m_base_pos);
|
||||
m_base_pos.y += m_base_pos.h;
|
||||
|
||||
// for (auto&p : m_items) {
|
||||
// p->SetPos(base_pos);
|
||||
// m_base_pos.y += m_base_pos.h;
|
||||
// }
|
||||
|
||||
// give focus to first entry.
|
||||
m_items[m_index]->OnFocusGained();
|
||||
}
|
||||
|
||||
void Sidebar::AddSpacer() {
|
||||
|
||||
}
|
||||
|
||||
void Sidebar::AddHeader(std::string name) {
|
||||
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
48
sphaira/source/ui/widget.cpp
Normal file
48
sphaira/source/ui/widget.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
void Widget::Update(Controller* controller, TouchInfo* touch) {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
|
||||
action.Invoke(true);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
}
|
||||
else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) {
|
||||
action.Invoke(false);
|
||||
}
|
||||
else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) {
|
||||
action.Invoke(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::Draw(NVGcontext* vg, Theme* theme) {
|
||||
Actions draw_actions;
|
||||
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (!action.IsHidden()) {
|
||||
draw_actions.emplace(button, action);
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
}
|
||||
|
||||
auto Widget::HasAction(Button button) const -> bool {
|
||||
return m_actions.contains(button);
|
||||
}
|
||||
|
||||
void Widget::SetAction(Button button, Action action) {
|
||||
m_actions.insert_or_assign(button, action);
|
||||
}
|
||||
|
||||
void Widget::RemoveAction(Button button) {
|
||||
if (auto it = m_actions.find(button); it != m_actions.end()) {
|
||||
m_actions.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
54
sphaira/source/web.cpp
Normal file
54
sphaira/source/web.cpp
Normal 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
|
||||
Reference in New Issue
Block a user