From 945d1f3ae68206aa528dc0e245a53e2dfe586aa5 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:04:27 +0000 Subject: [PATCH] add updater, remove white theme (it was unfished), remove more dead code, bump version for release --- assets/romfs/themes/white_theme.ini | 23 --- sphaira/CMakeLists.txt | 2 +- sphaira/include/i18n.hpp | 2 + sphaira/include/ui/menus/main_menu.hpp | 11 +- sphaira/source/app.cpp | 23 +++ sphaira/source/i18n.cpp | 8 +- sphaira/source/ui/menus/appstore.cpp | 2 +- sphaira/source/ui/menus/homebrew.cpp | 32 ---- sphaira/source/ui/menus/main_menu.cpp | 217 +++++++++++++++++++------ 9 files changed, 209 insertions(+), 111 deletions(-) delete mode 100644 assets/romfs/themes/white_theme.ini diff --git a/assets/romfs/themes/white_theme.ini b/assets/romfs/themes/white_theme.ini deleted file mode 100644 index ff1fdf5..0000000 --- a/assets/romfs/themes/white_theme.ini +++ /dev/null @@ -1,23 +0,0 @@ -[meta] -name="White not finished" -author=TotalJustice -version=1.0.0 -preview=romfs:/theme/preview.jpg - -[theme] -background=0xEBEBEBff -cursor=romfs:/theme/cursor.png -cursor_drag=romfs:/theme/cursor_drag.png -grid=0x46464630 -selected=0x464646ff -selected_overlay=0x00ffc8ff -text=0x2D2D2Dff -text_selected=0x3A50F0ff - -icon_audio=romfs:/theme/icon_audio.png -icon_video=romfs:/theme/icon_video.png -icon_image=romfs:/theme/icon_image.png -icon_file=romfs:/theme/icon_file.png -icon_folder=romfs:/theme/icon_folder.png -icon_zip=romfs:/theme/icon_zip.png -icon_nro=romfs:/theme/icon_nro.png diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 8ff9228..1388201 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.13) -set(sphaira_VERSION 0.3.0) +set(sphaira_VERSION 0.4.0) project(sphaira VERSION ${sphaira_VERSION} diff --git a/sphaira/include/i18n.hpp b/sphaira/include/i18n.hpp index 44ec020..5b03658 100644 --- a/sphaira/include/i18n.hpp +++ b/sphaira/include/i18n.hpp @@ -7,6 +7,8 @@ namespace sphaira::i18n { bool init(long index); void exit(); +std::string get(const char* str); + } // namespace sphaira::i18n inline namespace literals { diff --git a/sphaira/include/ui/menus/main_menu.hpp b/sphaira/include/ui/menus/main_menu.hpp index e417231..cf7ec99 100644 --- a/sphaira/include/ui/menus/main_menu.hpp +++ b/sphaira/include/ui/menus/main_menu.hpp @@ -7,6 +7,15 @@ namespace sphaira::ui::menu::main { +enum class UpdateState { + // still downloading json from github + Pending, + // no update available. + None, + // update available! + Update, +}; + // this holds 2 menus and allows for switching between them struct MainMenu final : Widget { MainMenu(); @@ -31,7 +40,7 @@ private: std::string m_update_url{}; std::string m_update_version{}; std::string m_update_description{}; - bool m_update_avaliable{}; + UpdateState m_update_state{UpdateState::Pending}; }; } // namespace sphaira::ui::menu::main diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index e95e2d6..28e8bcf 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1046,6 +1046,29 @@ App::~App() { } else { log_write("success with copying over root file!\n"); } + } else if (IsHbmenu()) { + // check we have a version that's newer than current. + fs::FsNativeSd fs; + NacpStruct sphaira_nacp; + fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro"; + Result rc; + + rc = nro_get_nacp(sphaira_path, sphaira_nacp); + if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { + sphaira_path = "/switch/sphaira.nro"; + rc = nro_get_nacp(sphaira_path, sphaira_nacp); + } + + // found sphaira, now lets get compare version + if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { + if (std::strcmp(APP_VERSION, sphaira_nacp.display_version) < 0) { + if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path, true))) { + log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc)); + } else { + log_write("success with updating hbmenu!\n"); + } + } + } } if (App::GetNxlinkEnable()) { diff --git a/sphaira/source/i18n.cpp b/sphaira/source/i18n.cpp index 08527ae..cbc0090 100644 --- a/sphaira/source/i18n.cpp +++ b/sphaira/source/i18n.cpp @@ -11,7 +11,7 @@ std::vector g_i18n_data; yyjson_doc* json; yyjson_val* root; -std::string get(const char* str, size_t len) { +std::string get_internal(const char* str, size_t len) { if (!json || !root) { log_write("no json or root\n"); return str; @@ -117,12 +117,16 @@ void exit() { g_i18n_data.clear(); } +std::string get(const char* str) { + return get_internal(str, std::strlen(str)); +} + } // namespace sphaira::i18n namespace literals { std::string operator"" _i18n(const char* str, size_t len) { - return sphaira::i18n::get(str, len); + return sphaira::i18n::get_internal(str, len); } } // namespace literals diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 66da279..a23da93 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -1438,7 +1438,7 @@ void Menu::Sort() { char subheader[128]{}; - std::snprintf(subheader, sizeof(subheader), "Sort: %s | Filter: %s | Order: %s", SORT_STR[m_sort], FILTER_STR[m_filter], ORDER_STR[m_order]); + std::snprintf(subheader, sizeof(subheader), "Sort: %s | Filter: %s | Order: %s", i18n::get(SORT_STR[m_sort]), i18n::get(FILTER_STR[m_filter]), i18n::get(ORDER_STR[m_order])); SetTitleSubHeading(subheader); std::sort(m_entries_current.begin(), m_entries_current.end(), sorter); diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index 80eb26a..2bd4cdd 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -17,38 +17,6 @@ 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 diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index c6cc3de..798e501 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -1,76 +1,187 @@ #include "ui/menus/main_menu.hpp" +#include "ui/menus/irs_menu.hpp" +#include "ui/menus/themezer.hpp" + #include "ui/sidebar.hpp" #include "ui/popup_list.hpp" #include "ui/option_box.hpp" +#include "ui/progress_box.hpp" +#include "ui/error_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 +#include +#include 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; +auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool { + static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"}; + constexpr auto chunk_size = 1024 * 512; // 512KiB - 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++]; + fs::FsNativeSd fs; + R_TRY_RESULT(fs.GetFsOpenResult(), false); + + // 1. download the zip + if (!pbox->ShouldExit()) { + pbox->NewTransfer("Downloading "_i18n + version); + log_write("starting download: %s\n", url.c_str()); + + DownloadClearCache(url); + if (!DownloadFile(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)); + + // 2. 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; + fs::FsPath file_path; + if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, file_path, sizeof(file_path), 0, 0, 0, 0)) { + log_write("failed to get current info\n"); + return false; + } + + if (file_path[0] != '/') { + file_path = fs::AppendPath("/", file_path); + } + + Result rc; + if (file_path[strlen(file_path) -1] == '/') { + if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_ResultPathAlreadyExists) { + log_write("failed to create folder: %s 0x%04X\n", file_path, rc); + return false; + } + } else { + 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 buf(chunk_size); + u64 offset{}; + while (offset < info.uncompressed_size) { + if (pbox->ShouldExit()) { + return false; } - return true; + + 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; } } } } - return false; + log_write("finished update :)\n"); + return true; } -#endif } // namespace MainMenu::MainMenu() { - #if 0 - DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sys-patch/releases/latest", [this](std::vector& data, bool success){ - data.push_back('\0'); - auto raw_str = (const char*)data.data(); - char out_str[0x301]; + DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector& data, bool success){ + m_update_state = UpdateState::None; + auto json = yyjson_read((const char*)data.data(), data.size(), 0); + R_UNLESS(json, false); + ON_SCOPE_EXIT(yyjson_doc_free(json)); - 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); - } + auto root = yyjson_doc_get_root(json); + R_UNLESS(root, false); - if (parseSearch(raw_str, "browser_download_url\":\"", out_str)) { - m_update_url = out_str; - log_write("FOUND IT : %s\n", out_str); - } + auto tag_key = yyjson_obj_get(root, "tag_name"); + R_UNLESS(tag_key, false); - 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); - } + const auto version = yyjson_get_str(tag_key); + R_UNLESS(version, false); + R_UNLESS(std::strcmp(APP_VERSION, version) < 0, false); + + auto assets = yyjson_obj_get(root, "assets"); + R_UNLESS(assets, false); + + auto idx0 = yyjson_arr_get(assets, 0); + R_UNLESS(idx0, false); + + auto url_key = yyjson_obj_get(idx0, "browser_download_url"); + R_UNLESS(url_key, false); + + const auto url = yyjson_get_str(url_key); + R_UNLESS(url, false); + + m_update_version = version; + m_update_url = url; + m_update_state = UpdateState::Update; + log_write("found url: %s\n", url); + App::Notify("Update avaliable: "_i18n + m_update_version); + + return true; }); - #endif AddOnLPress(); AddOnRPress(); @@ -128,22 +239,26 @@ MainMenu::MainMenu() { options->Add(std::make_shared("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){ App::SetNxlinkEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Check for update"_i18n, [this](){ - App::Notify("Not Implemented"_i18n); - })); + + if (m_update_state == UpdateState::Update) { + options->Add(std::make_shared("Download update: "_i18n + m_update_version, [this](){ + App::Push(std::make_shared("Downloading "_i18n + m_update_version, [this](auto pbox){ + return InstallUpdate(pbox, m_update_url, m_update_version); + }, [this](bool success){ + if (success) { + m_update_state = UpdateState::None; + } else { + App::Push(std::make_shared(MAKERESULT(351, 1), "Failed to download update")); + } + }, 2)); + })); + } })); options->Add(std::make_shared("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(str, [this](){ - App::Notify("Not Implemented"_i18n); - })); - } - options->Add(std::make_shared("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){ App::SetLogEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n));