diff --git a/.gitignore b/.gitignore index 62aea4b..2402560 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ *.elf *.rpx -*.wuhb \ No newline at end of file +*.wuhb + +*~ diff --git a/external/imgui b/external/imgui index 4087d84..1ed50c2 160000 --- a/external/imgui +++ b/external/imgui @@ -1 +1 @@ -Subproject commit 4087d84ad0761d004a9bc5c5b389f67df75998d7 +Subproject commit 1ed50c2acfe85ef061a9b805ee127946cc3a1481 diff --git a/src/App.cpp b/src/App.cpp index 4fe5016..d3a58b8 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -65,7 +65,7 @@ namespace App { bool isRunning; - static uint32_t procCallbackAcquire(void *content) { + static uint32_t procCallbackAcquire(void *) { if (Camera::is_initialized()) Camera::open(); @@ -165,8 +165,9 @@ namespace App { auto &style = ImGui::GetStyle(); style.ScaleAllSizes(3); - // style.WindowBorderSize = 0.0f; - // style.WindowPadding = {6.0f, 6.0f}; + + style.FramePadding = {20, 15}; + style.ItemSpacing = {24, 18}; style.FrameRounding = 12.0f; style.ChildRounding = 0.0f; @@ -175,8 +176,8 @@ namespace App { load_imgui_theme(); - auto &colors = style.Colors; #ifdef DEBUG_BG_COLOR + auto &colors = style.Colors; colors[ImGuiCol_WindowBg] = {0.5, 0.0f, 0.0f, 1.0f}; #endif @@ -233,17 +234,7 @@ namespace App { cout << "Hello world from Themiify!" << endl; initialize_imgui(); -#if 0 - for (int i = 0; i < SDL_NumJoysticks(); i++) { - SDL_GameController *controller = nullptr; - if (SDL_IsGameController(i)) { - controller = SDL_GameControllerOpen(i); - if (controller) { - controllers.push_back(controller); - } - } - } -#endif + Camera::initialize(renderer); Camera::open(); @@ -352,24 +343,6 @@ namespace App { } break; } - /*case SDL_SYSWMEVENT: { - auto *msg = e.syswm.msg; - if (!msg) - break; - - switch (msg->msg.wiiu.event) { - case SDL_WIIU_SYSWM_SWKBD_OK_FINISH_EVENT: { - swkbd_ok_selected = true; - break; - } - - default: - break; - } - - - }*/ - default: break; } diff --git a/src/DownloadManager.cpp b/src/DownloadManager.cpp index 0ef5180..b55d375 100644 --- a/src/DownloadManager.cpp +++ b/src/DownloadManager.cpp @@ -43,19 +43,15 @@ namespace DownloadManager { success_function_t success_func; failure_function_t failure_func; - CURL* utheme_easy = nullptr; - CURL* thumbnail_easy = nullptr; + CURL* easy = nullptr; - std::filebuf utheme_file; - std::filebuf thumbnail_file; + std::filebuf file; - std::filesystem::path thumbnail_output; + std::array error_buffer; - bool utheme_content_started = false; - bool thumbnail_content_started = false; + bool content_started = false; - bool utheme_done = false; - bool thumbnail_done = false; + bool done = false; Download(Download&&) = delete; @@ -66,36 +62,22 @@ namespace DownloadManager { success_func{std::move(success_func_)}, failure_func{std::move(failure_func_)} { - create_directories(info->utheme_output.parent_path()); - create_directories(info->thumbnail_output.parent_path()); + create_directories(info->filename.parent_path()); - if (!utheme_file.open(info->utheme_output, std::ios::out | std::ios::binary | std::ios::trunc)) - throw std::runtime_error{"could not open "s + info->utheme_output.string()}; + if (!file.open(info->filename, std::ios::out | std::ios::binary | std::ios::trunc)) + throw std::runtime_error{"could not open "s + info->filename.string()}; - if (!thumbnail_file.open(info->thumbnail_output, std::ios::out | std::ios::binary | std::ios::trunc)) - throw std::runtime_error{"could not open "s + info->thumbnail_output.string()}; - - setup_easy(utheme_easy, info->utheme_url, this, true); - setup_easy(thumbnail_easy, info->thumbnail_url, this, false); + error_buffer[0] = '\0'; + setup_easy(); } ~Download() { - if (utheme_easy) { - curl_easy_cleanup(utheme_easy); - utheme_easy = nullptr; - } - - if (thumbnail_easy) { - curl_easy_cleanup(thumbnail_easy); - thumbnail_easy = nullptr; - } + if (easy) + curl_easy_cleanup(easy); } - static void setup_easy(CURL*& easy, - const std::string& url, - Download* self, - bool is_utheme) + void setup_easy() { easy = curl_easy_init(); if (!easy) @@ -104,47 +86,37 @@ namespace DownloadManager { if (!user_agent.empty()) curl_easy_setopt(easy, CURLOPT_USERAGENT, user_agent.c_str()); - curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); + curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(easy, CURLOPT_URL, info->url.c_str()); curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(easy, CURLOPT_AUTOREFERER, 1L); - curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(easy, CURLOPT_ACCEPT_ENCODING, ""); curl_easy_setopt(easy, CURLOPT_TRANSFER_ENCODING, 1L); curl_easy_setopt(easy, CURLOPT_BUFFERSIZE, 65536L); curl_easy_setopt(easy, CURLOPT_TCP_NODELAY, 0L); curl_easy_setopt(easy, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, error_buffer.data()); - curl_easy_setopt(easy, CURLOPT_WRITEDATA, self); - curl_easy_setopt(easy, - CURLOPT_WRITEFUNCTION, - is_utheme ? utheme_write_callback : thumbnail_write_callback); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, this); + curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(easy, CURLOPT_XFERINFODATA, self); + curl_easy_setopt(easy, CURLOPT_XFERINFODATA, this); curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, progress_callback); } - static size_t utheme_write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) + static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { auto* self = static_cast(userdata); const size_t total = size * nmemb; - self->utheme_content_started = true; + self->content_started = true; - return self->utheme_file.sputn(ptr, total); + return self->file.sputn(ptr, total); } - static size_t thumbnail_write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) - { - auto* self = static_cast(userdata); - const size_t total = size * nmemb; - - self->thumbnail_content_started = true; - - return self->thumbnail_file.sputn(ptr, total); - } - - static int progress_callback(void* userdata, + static int progress_callback(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, @@ -152,51 +124,44 @@ namespace DownloadManager { { auto* self = static_cast(userdata); - if (self->utheme_content_started && dltotal) + if (self->content_started && dltotal) self->info->progress = float(dlnow) / float(dltotal); double speed = 0.0; - if (self->utheme_easy) - curl_easy_getinfo(self->utheme_easy, CURLINFO_SPEED_DOWNLOAD_T, &speed); + if (self->easy) + curl_easy_getinfo(self->easy, CURLINFO_SPEED_DOWNLOAD_T, &speed); self->info->speed = static_cast(speed); return 0; } - bool owns(CURL* easy) const + bool owns(CURL* easy_) const { - return easy == utheme_easy || easy == thumbnail_easy; + return easy_ == easy; } - void mark_done(CURL* easy) + void mark_done() { - if (easy == utheme_easy) - utheme_done = true; - else if (easy == thumbnail_easy) - thumbnail_done = true; + done = true; } bool is_done() const { - return utheme_done && thumbnail_done; + return done; } void finish() { - utheme_file.close(); - thumbnail_file.close(); - + file.close(); if (success_func) success_func(*info); } void finish(const std::exception& e) noexcept try { - utheme_file.close(); - thumbnail_file.close(); - + file.close(); if (failure_func) failure_func(e); } @@ -225,13 +190,9 @@ namespace DownloadManager { ~Resources() noexcept { - for (auto& d : downloads) { - if (d.utheme_easy) - curl_multi_remove_handle(multi, d.utheme_easy); - - if (d.thumbnail_easy) - curl_multi_remove_handle(multi, d.thumbnail_easy); - } + for (auto& d : downloads) + if (d.easy) + curl_multi_remove_handle(multi, d.easy); if (multi) { curl_multi_cleanup(multi); @@ -249,12 +210,11 @@ namespace DownloadManager { if (msg->msg != CURLMSG_DONE) continue; - CURL* completed_easy = msg->easy_handle; - + // Find the Download that corresponds to the handle auto completed = std::ranges::find_if( downloads, - [completed_easy](const Download& d) { - return d.owns(completed_easy); + [msg](const Download& d) { + return d.owns(msg->easy_handle); } ); @@ -263,12 +223,16 @@ namespace DownloadManager { throw std::logic_error{"BUG: transfer not found"}; if (msg->data.result != CURLE_OK) { + std::string url = completed->info->url; throw std::runtime_error{ - curl_easy_strerror(msg->data.result) + curl_easy_strerror(msg->data.result) + " ["s + url + "]"s }; } - completed->mark_done(completed_easy); + completed->mark_done(); + curl_multi_remove_handle(multi, completed->easy); + completed->finish(); + downloads.erase(completed); } catch (std::exception& e) { cerr << "DownloadManager::Resources::process(): ERROR: " @@ -279,25 +243,11 @@ namespace DownloadManager { completed->finish(e); if (completed != downloads.end()) { - if (completed->utheme_easy) - curl_multi_remove_handle(multi, completed->utheme_easy); - - if (completed->thumbnail_easy) - curl_multi_remove_handle(multi, completed->thumbnail_easy); + if (completed->easy) + curl_multi_remove_handle(multi, completed->easy); downloads.erase(completed); } - - continue; - } - - if (completed != downloads.end()) { - curl_multi_remove_handle(multi, completed_easy); - - if (completed->is_done()) { - completed->finish(); - downloads.erase(completed); - } } } } @@ -307,28 +257,18 @@ namespace DownloadManager { return infos; } - bool add(const std::string& label, - const std::string& utheme_url, - const std::string& thumbnail_url, - const std::filesystem::path& utheme_output, - const std::filesystem::path& thumbnail_output, + bool add(const std::string& url, + const std::filesystem::path& filename, success_function_t success_func, failure_function_t failure_func) { for (auto& entry : infos) { - if (entry->utheme_url == utheme_url) + if (entry->url == url) return false; } - - auto sanitized_utheme_output = sanitize(utheme_output); - auto sanitized_thumbnail_output = sanitize(thumbnail_output); - auto info = std::make_shared( - label, - utheme_url, - thumbnail_url, - sanitized_utheme_output, - sanitized_thumbnail_output, + url, + filename, 0.0f, 0, State::queued @@ -344,15 +284,11 @@ namespace DownloadManager { auto& download = downloads.back(); - curl_multi_add_handle(multi, download.utheme_easy); - curl_multi_add_handle(multi, download.thumbnail_easy); + curl_multi_add_handle(multi, download.easy); cout << "Added download:" - << "\n " << label - << "\n " << utheme_url - << "\n " << thumbnail_url - << "\n " << utheme_output - << "\n " << thumbnail_output + << "\n " << url + << "\n " << filename << endl; return true; @@ -402,31 +338,23 @@ namespace DownloadManager { TRACE_FUNC; } - void clear_finished() + void clear_all() { res->infos.clear(); } - bool add(const std::string& label, - const std::string& utheme_url, - const std::string& thumbnail_url, - const std::filesystem::path& utheme_output, - const std::filesystem::path& thumbnail_output, + bool add(const std::string& url, + const std::filesystem::path& filename, success_function_t success_func, failure_function_t failure_func) { TRACE_FUNC; assert(res); - return res->add( - label, - utheme_url, - thumbnail_url, - utheme_output, - thumbnail_output, - std::move(success_func), - std::move(failure_func) - ); + return res->add(url, + filename, + std::move(success_func), + std::move(failure_func)); } void pause(const std::string& url) @@ -435,6 +363,7 @@ namespace DownloadManager { assert(res); cout << "Pausing " << url << endl; + // TODO } void cancel(const std::string& url) @@ -443,12 +372,17 @@ namespace DownloadManager { assert(res); cout << "Canceling " << url << endl; + // TODO } - const std::vector>& get_infos() + std::shared_ptr + get_info(const std::string& url) { assert(res); - return res->get_infos(); + for (auto& info : res->infos) + if (info->url == url) + return info; + return {}; } } diff --git a/src/DownloadManager.h b/src/DownloadManager.h index d1c6983..8323819 100644 --- a/src/DownloadManager.h +++ b/src/DownloadManager.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -28,16 +28,11 @@ namespace DownloadManager { }; struct Info { - - std::string label; - std::string utheme_url; - std::string thumbnail_url; - std::filesystem::path utheme_output; - std::filesystem::path thumbnail_output; + std::string url; + std::filesystem::path filename; float progress = 0; std::uint64_t speed = 0; State state; - }; // struct Info @@ -68,17 +63,14 @@ namespace DownloadManager { cancel_all(); void - clear_finished(); + clear_all(); bool - add(const std::string& label, - const std::string& utheme_url, - const std::string& thumbnail_url, - const std::filesystem::path& utheme_output, - const std::filesystem::path& thumbnail_output, - success_function_t success_func, - failure_function_t failure_func); + add(const std::string& url, + const std::filesystem::path& filename, + success_function_t success_func = {}, + failure_function_t failure_func = {}); void pause(const std::string& url); @@ -86,8 +78,6 @@ namespace DownloadManager { void cancel(const std::string& url); - - const std::vector>& - get_infos(); - + std::shared_ptr + get_info(const std::string& url); } diff --git a/src/ImageLoader.cpp b/src/ImageLoader.cpp index 3e79d22..5445ecb 100644 --- a/src/ImageLoader.cpp +++ b/src/ImageLoader.cpp @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -293,9 +293,10 @@ namespace ImageLoader { entry.headers = curl_slist_append(entry.headers, "Accept: image/*"); + curl_easy_setopt(entry.easy, CURLOPT_VERBOSE, 1L); curl_easy_setopt(entry.easy, CURLOPT_URL, location.c_str()); curl_easy_setopt(entry.easy, CURLOPT_HTTPHEADER, entry.headers); - curl_easy_setopt(entry.easy, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(entry.easy, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(entry.easy, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(entry.easy, CURLOPT_AUTOREFERER, 1L); curl_easy_setopt(entry.easy, CURLOPT_ACCEPT_ENCODING, ""); @@ -314,22 +315,28 @@ namespace ImageLoader { else if (location.starts_with("ui/")) { const auto path = content_prefix / location; - entry.img = IMG_Load(path.string().c_str()); + entry.img = IMG_Load(path.c_str()); if (!entry.img) throw std::runtime_error{IMG_GetError()}; entry.state = LoadState::loaded; } + else if (location.starts_with("fs:/")) { + entry.img = IMG_Load(location.c_str()); + if (!entry.img) + throw std::runtime_error{IMG_GetError()}; + entry.state = LoadState::loaded; + } else { throw std::runtime_error{"invalid location"}; } } catch (std::exception& e) { - std::print(cerr, - "ERROR: ImageLoader::process_one_request(): location=\"{}\", exception={}", - location, - e.what()); - + std::println(cerr, + "ERROR: ImageLoader::process_one_request(): location=\"{}\", exception={}", + location, + e.what()); + entry.state = LoadState::error; } } @@ -435,6 +442,7 @@ namespace ImageLoader { try { if (msg->data.result != CURLE_OK) { throw std::runtime_error{ + "[" + entry->location + "] " + curl_easy_strerror(msg->data.result) }; } @@ -543,4 +551,4 @@ namespace ImageLoader { return "unknown (" + std::to_string(static_cast(st)) + ")"; } } -} +} diff --git a/src/NavBar.cpp b/src/NavBar.cpp index 40711ab..f404e1a 100644 --- a/src/NavBar.cpp +++ b/src/NavBar.cpp @@ -7,10 +7,10 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#include + #include "NavBar.h" #include "App.h" -#include "screens/ManageThemesScreen.h" -#include "screens/HomeScreen.h" #include "utils.h" #include @@ -19,6 +19,12 @@ #include #include +// Define this to help seeing the padding and spacing values for windows. +// #define DEBUG_BG_COLOR + +using std::cout; +using std::endl; + namespace NavBar { SDL_Texture *logo_tex; @@ -82,7 +88,16 @@ namespace NavBar { StyleVar no_child_border{ImGuiStyleVar_ChildBorderSize, 0.0f}; StyleVar item_spacing{ImGuiStyleVar_ItemSpacing, {0.0f, 12.0f}}; - Child nav_bar{"NavBar", {160.0f, 0.0f}, ImGuiChildFlags_NavFlattened}; + // auto viewport = ImGui::GetMainViewport(); + +#ifdef DEBUG_BG_COLOR + StyleColor navbar_bg{ImGuiCol_ChildBg, {0.5f, 0.0f, 0.0f, 1.0f}}; +#endif + const float navbar_width = 160; + + Child nav_bar{"NavBar", {navbar_width, 0}, + ImGuiChildFlags_NavFlattened, + ImGuiWindowFlags_NoScrollbar}; if (!nav_bar) return; @@ -91,51 +106,65 @@ namespace NavBar { StyleVar no_frame_rounding{ImGuiStyleVar_FrameRounding, 0}; StyleVar no_frame_padding{ImGuiStyleVar_FramePadding, {0, 0}}; - ImGui::Image(logo_tex, ImVec2(152.4f, 138.0f)); + ImGui::Image(logo_tex, {152.4f, 138.0f}); + + const auto &style = ImGui::GetStyle(); + const auto available = ImGui::GetContentRegionAvail(); + const int num_buttons = 5; + const ImVec2 button_size = {148, 96}; + const float total_empty = + available.y + - num_buttons * button_size.y + - style.SeparatorSize; + const float spacing = total_empty / num_buttons; + + StyleVar button_spacing{ImGuiStyleVar_ItemSpacing, {0, spacing}}; - if (Child buttons_box{"ButtonsBox", {}, ImGuiChildFlags_NavFlattened}) { +#ifdef DEBUG_BG_COLOR + StyleColor buttons_box_bg{ImGuiCol_ChildBg, {0.0f, 0.5f, 0.0f, 1.0f}}; +#endif + if (Child buttons_box{"ButtonsBox", {navbar_width, 0}, ImGuiChildFlags_NavFlattened}) { if (current_tab == Tab::home) { - ImGui::ImageButton("home_button_active", home_button_active_tex, ImVec2(148, 96)); + ImGui::ImageButton("home_button_active", home_button_active_tex, button_size); } else { - // Implement the App::ImageButton overload in the App namespace to add the sound effect and rumble when clicked - if (ImGui::ImageButton("home_button_normal", home_button_normal_tex, ImVec2(148, 96))) { + // TODO: Implement the App::ImageButton overload in the App namespace to + // add the sound effect and rumble when clicked + if (ImGui::ImageButton("home_button_normal", home_button_normal_tex, button_size)) { current_tab = Tab::home; - HomeScreen::force_refresh(); } } if (current_tab == Tab::manage_themes) { - ImGui::ImageButton("installed_button_active", manage_themes_button_active_tex, ImVec2(148, 96)); + ImGui::ImageButton("installed_button_active", manage_themes_button_active_tex, button_size); } else { - if (ImGui::ImageButton("installed_button_normal", manage_themes_button_normal_tex, ImVec2(148, 96))) { + if (ImGui::ImageButton("installed_button_normal", manage_themes_button_normal_tex, button_size)) { current_tab = Tab::manage_themes; } } if (current_tab == Tab::themezer) { - ImGui::ImageButton("themezer_button_active", themezer_button_active_tex, ImVec2(148, 96)); + ImGui::ImageButton("themezer_button_active", themezer_button_active_tex, button_size); } else { - if (ImGui::ImageButton("themezer_button_normal", themezer_button_normal_tex, ImVec2(148, 96))) { + if (ImGui::ImageButton("themezer_button_normal", themezer_button_normal_tex, button_size)) { current_tab = Tab::themezer; - ManageThemesScreen::force_refresh(); } } if (current_tab == Tab::settings) { - ImGui::ImageButton("settings_button_active", settings_button_active_tex, ImVec2(148, 96)); + ImGui::ImageButton("settings_button_active", settings_button_active_tex, button_size); } else { - if (ImGui::ImageButton("settings_button_normal", settings_button_normal_tex, ImVec2(148, 96))) { + if (ImGui::ImageButton("settings_button_normal", settings_button_normal_tex, button_size)) { current_tab = Tab::settings; } } ImGui::Separator(); - if (ImGui::ImageButton("exit_button_normal", exit_button_normal_tex, ImVec2(148, 96))) { + if (ImGui::ImageButton("exit_button_normal", exit_button_normal_tex, button_size)) { App::quit(); } } diff --git a/src/ThemezerAPI.cpp b/src/ThemezerAPI.cpp index 467d1da..1b431be 100644 --- a/src/ThemezerAPI.cpp +++ b/src/ThemezerAPI.cpp @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -124,6 +124,7 @@ query Themes($order: SortOrder, $paginationArgs: PaginationInput, $query: String username } collagePreview { + tinyUrl thumbUrl } downloadCount @@ -231,6 +232,7 @@ query($hexId: String!) { tags { name } + description } } } diff --git a/src/ThemezerAPI.h b/src/ThemezerAPI.h index 7aa3970..505fa36 100644 --- a/src/ThemezerAPI.h +++ b/src/ThemezerAPI.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -34,6 +34,7 @@ namespace ThemezerAPI { std::string username; } creator; struct { + std::string tinyUrl; // 320x180 std::string thumbUrl; // 426x240 } collagePreview; unsigned downloadCount; @@ -84,6 +85,7 @@ namespace ThemezerAPI { std::vector tags; + std::optional description; }; // struct WiiuThemeFull diff --git a/src/graphql.cpp b/src/graphql.cpp index cf80451..c9ed291 100644 --- a/src/graphql.cpp +++ b/src/graphql.cpp @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -59,7 +59,7 @@ namespace graphql { curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(handle, CURLOPT_AUTOREFERER, 1L); - curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, ""); curl_easy_setopt(handle, CURLOPT_TRANSFER_ENCODING, 1L); curl_easy_setopt(handle, CURLOPT_BUFFERSIZE, 65536L); diff --git a/src/installer.cpp b/src/installer.cpp index 3c01c0d..3b256e8 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -8,6 +8,7 @@ */ #include +#include #include #include #include @@ -22,7 +23,8 @@ #include #include -#include +#include +#include #include #include @@ -62,11 +64,6 @@ namespace Installer { return ss.str(); } - static std::string GetString(const glz::generic& obj, const std::string& key) - { - return obj.at(key).get(); - } - std::filesystem::path GetMenuContentPath() { uint64_t menuTitleID = _SYSGetSystemApplicationTitleId(SYSTEM_APP_ID_WII_U_MENU); @@ -112,108 +109,213 @@ namespace Installer { cout << "Successfully cached file to: " << outputPath << endl; } - int GetThemeMetadata(const std::filesystem::path &themePath, theme_data *themeData) { - zip_t *themeArchive; - zip_error_t error; - int err; - - if (!(themeArchive = zip_open(themePath.c_str(), 0, &err))) { - zip_error_init_with_code(&error, err); - cerr << "Cannot open theme archive. Error Code: " - << zip_error_strerror(&error) << endl; - zip_error_fini(&error); - return 0; - } - - zip_file_t *themeMetadataFile; - if (!(themeMetadataFile = zip_fopen(themeArchive, "metadata.json", ZIP_RDONLY))) { - zip_error_init_with_code(&error, err); - cerr << "Cannot open theme metadata. Error Code: " - << zip_error_strerror(&error) << endl; - zip_error_fini(&error); - zip_close(themeArchive); - return 0; - } + struct MetadataJson { + UThemeMetadata Metadata; + }; - zip_stat_t metadataStatData; - if (zip_stat(themeArchive, "metadata.json", 0, &metadataStatData) != 0) { - zip_error_init_with_code(&error, err); - cerr << "Cannot stat theme metadata! Error Code: " - << zip_error_strerror(&error) << endl; - zip_error_fini(&error); - zip_fclose(themeMetadataFile); - zip_close(themeArchive); - return 0; - } + struct LegacyMetadataJson { + struct { + std::string themeName; + std::string themeAuthor; + std::string themeID; + std::string themeIDPath; + std::string themeVersion; + std::string themeInstallPath; + } ThemeData; + }; - std::string buffer(metadataStatData.size, '\0'); - zip_fread(themeMetadataFile, buffer.data(), metadataStatData.size); - zip_fclose(themeMetadataFile); - zip_close(themeArchive); + bool GetUThemeMetadata(const std::filesystem::path &themePath, + UThemeMetadata &umeta) { + try { + zip_t *themeArchive; + zip_error_t error; + int err; - glz::generic themeMetadata; - if (auto err = glz::read_json(themeMetadata, buffer)) { - cerr << "Failed to parse metadata.json: " - << glz::format_error(err, buffer) << endl; - return 0; - } + umeta = {}; - auto& metadata = themeMetadata.at("Metadata"); + if (!(themeArchive = zip_open(themePath.c_str(), 0, &err))) { + zip_error_init_with_code(&error, err); + cerr << "Cannot open theme archive. Error Code: " + << zip_error_strerror(&error) << endl; + zip_error_fini(&error); + return false; + } - themeData->themeID = GetString(metadata, "themeID"); + zip_file_t *themeMetadataFile; + if (!(themeMetadataFile = zip_fopen(themeArchive, "metadata.json", ZIP_RDONLY))) { + zip_error_init_with_code(&error, err); + cerr << "Cannot open theme metadata. Error Code: " + << zip_error_strerror(&error) << endl; + zip_error_fini(&error); + zip_close(themeArchive); + return false; + } - std::string themeIDPathStr = themeData->themeID; - themeIDPathStr.erase(std::remove(themeIDPathStr.begin(), themeIDPathStr.end(), ':'), themeIDPathStr.end()); - themeData->themeIDPath = themeIDPathStr; + zip_stat_t metadataStatData; + if (zip_stat(themeArchive, "metadata.json", 0, &metadataStatData) != 0) { + zip_error_init_with_code(&error, err); + cerr << "Cannot stat theme metadata! Error Code: " + << zip_error_strerror(&error) << endl; + zip_error_fini(&error); + zip_fclose(themeMetadataFile); + zip_close(themeArchive); + return 0; + } - themeData->themeName = GetString(metadata, "themeName"); - themeData->themeAuthor = GetString(metadata, "themeAuthor"); - themeData->themeVersion = GetString(metadata, "themeVersion"); + std::string buffer(metadataStatData.size, '\0'); + zip_fread(themeMetadataFile, buffer.data(), metadataStatData.size); + zip_fclose(themeMetadataFile); + zip_close(themeArchive); - return 1; + MetadataJson metaJson; + glz::ex::read_json(metaJson, buffer); + umeta = std::move(metaJson.Metadata); + return true; + } + catch (std::exception& e) { + cerr << "ERROR: " << e.what() << endl; + return false; + } } - int GetInstalledThemeMetadata(const std::filesystem::path &installedThemeJsonPath, - installed_theme_data *themeData) { - std::ifstream installedThemeJson{installedThemeJsonPath}; + bool GetInstalledThemeMetadata(const std::filesystem::path &installedThemePath, + InstalledThemeMetadata &imeta) { - if (!installedThemeJson.is_open()) { - cerr << "Cannot open installed theme's json file." << endl; - return 0; - } + static const std::array image_extensions{".webp", ".jpg", ".png"}; + try { + imeta = {}; + imeta.themePath = installedThemePath; + try { + for (auto& entry + : std::filesystem::recursive_directory_iterator{imeta.themePath}) { + if (entry.is_regular_file()) + imeta.files.push_back(entry.path()); + } + std::ranges::sort(imeta.files, {}, as_lower_case); + } + catch (std::exception &e) { + cerr << "Error listing files inside theme. SD card may be corrupted." << endl; + } - std::string jsonStr = ReadWholeFile(installedThemeJson); + static const std::array image_names{ + "preview-collage", + "preview-launcher", + "preview-warawara" + }; + for (auto name : image_names) { + auto preview_base = imeta.themePath / name; + for (auto ext : image_extensions) { + auto preview = preview_base; + preview += ext; + if (exists(preview)) { + imeta.previewPaths.push_back(preview); + break; + } + } + } + MetadataJson metaJson; + glz::ex::read_file_json(metaJson, + (installedThemePath / "metadata.json").string(), + std::string{}); + imeta.uthemeMetadata = std::move(metaJson.Metadata); + + // If there was no preview image on the theme folder, look for a cached thumbnail. + if (imeta.themePath.empty() && imeta.uthemeMetadata.themeID) { + auto preview = theme_id_to_cached_thumbnail_path(*imeta.uthemeMetadata.themeID); + if (exists(preview)) + imeta.previewPaths.push_back(preview); + } - glz::generic installedThemeMetadata; - if (auto err = glz::read_json(installedThemeMetadata, jsonStr)) { - cerr << "Failed to parse installed theme json: " - << glz::format_error(err, jsonStr) << endl; - return 0; + return true; } + catch (std::exception& e) { + // cout << "Failed to get new theme metadata: " << e.what() << endl; + + // Fallback: find a json (in SD:/themiify/installed/) that matches this theme. + auto folder_name = installedThemePath.filename(); + try { + for (auto entry + : std::filesystem::directory_iterator{THEMIIFY_INSTALLED_THEMES}) { + if (!entry.is_regular_file()) + continue; + if (entry.path().extension() != ".json") + continue; + try { + LegacyMetadataJson legacyMetaJson; + glz::ex::read_file_json(legacyMetaJson, entry.path().string(), std::string{}); + auto& leg_meta = legacyMetaJson.ThemeData; + if (leg_meta.themeID.empty()) // not a Themezer theme, skip it + continue; + auto meta_folder_name = std::filesystem::path{leg_meta.themeInstallPath}.filename(); + if (folder_name == meta_folder_name) { + imeta.uthemeMetadata.themeID = leg_meta.themeID; + imeta.uthemeMetadata.themeName = leg_meta.themeName; + imeta.uthemeMetadata.themeAuthor = leg_meta.themeAuthor; + imeta.uthemeMetadata.themeVersion = leg_meta.themeVersion; + imeta.legacyMetadataPath = entry.path(); + auto preview = + theme_id_to_cached_thumbnail_path(*imeta.uthemeMetadata.themeID); + if (exists(preview)) + imeta.previewPaths.push_back(preview); + return true; + } + } + catch (std::exception& e3) { + cout << "Failed to parse " << entry.path() + << ": " << e3.what() << endl; + } + } + // No matching metadata found, just use the folder name as the name. + imeta.uthemeMetadata.themeName = folder_name; + return true; + } + catch (std::exception& e2) { + cerr << "ERROR: while looking for themiify metadata: " << e2.what() << endl; + return false; + } + return false; + } + } - auto& data = installedThemeMetadata.at("ThemeData"); - - themeData->themeID = GetString(data, "themeID"); - themeData->themeIDPath = GetString(data, "themeIDPath"); - themeData->themeName = GetString(data, "themeName"); - themeData->themeAuthor = GetString(data, "themeAuthor"); - themeData->themeVersion = GetString(data, "themeVersion"); - themeData->installedThemePath = GetString(data, "themeInstallPath"); - - return 1; + std::vector GetInstalledThemes(std::stop_token& stopper) { + std::vector result; + try { + for (auto& entry : std::filesystem::directory_iterator{THEMES_ROOT}) { + if (stopper.stop_requested()) + break; + if (!entry.is_directory()) + continue; + InstalledThemeMetadata meta; + if (GetInstalledThemeMetadata(entry.path(), meta)) + result.push_back(std::move(meta)); + } + if (stopper.stop_requested()) + return result; + // Now sort them by path. + std::ranges::sort(result, + {}, + [](const InstalledThemeMetadata& meta) -> std::string + { + return as_lower_case(meta.themePath); + }); + } + catch (std::exception &e) { + cerr << "ERROR in Installer::GetInstalledThemes(): " + << e.what() << endl; + } + return result; } void InstallTheme(std::stop_token &stopper, - const std::filesystem::path &themePath, - theme_data themeData, + const std::filesystem::path &uthemePath, + UThemeMetadata themeMetadata, progress_function_t progressCallback, success_function_t successCallback, error_function_t errorCallback) { zip_t *themeArchive = nullptr; zip_file_t *patchFile = nullptr; - std::filesystem::path modpackPath; - std::filesystem::path installPath; + std::filesystem::path themePath; try { @@ -238,7 +340,7 @@ namespace Installer { throwIfStopped(); - if (!(themeArchive = zip_open(themePath.c_str(), 0, &err))) { + if (!(themeArchive = zip_open(uthemePath.c_str(), 0, &err))) { zip_error_init_with_code(&error, err); std::string msg = "Cannot open theme archive:"s + zip_error_strerror(&error); zip_error_fini(&error); @@ -247,11 +349,11 @@ namespace Installer { throwIfStopped(); - reportProgress(std::format("Installing {}...", themeData.themeName)); + reportProgress(std::format("Installing {}...", themeMetadata.themeName)); - modpackPath = sanitize(THEMES_ROOT / (themeData.themeName + " (" + themeData.themeIDPath + ")")); + themePath = GetThemePath(themeMetadata); - reportProgress(std::format("Installing theme to: \"{}\"", modpackPath.string())); + reportProgress(std::format("Installing theme to: \"{}\"", themePath.string())); int64_t numEntries; if ((numEntries = zip_get_num_entries(themeArchive, ZIP_FL_UNCHANGED)) < 0) { @@ -259,12 +361,14 @@ namespace Installer { throw std::runtime_error{"themeArchive is NULL"}; } + const std::string AllMessage_ = "AllMessage_"; for (uint64_t i = 0; i < static_cast(numEntries); ++i) { throwIfStopped(); std::filesystem::path menuFilePath; - std::string entryName = zip_get_name(themeArchive, i, ZIP_FL_ENC_RAW); + std::filesystem::path entryName = zip_get_name(themeArchive, i, ZIP_FL_ENC_RAW); + std::string entryStem = entryName.stem().string(); if (entryName == "Men.bps") { menuFilePath = MEN_PATH; @@ -275,15 +379,8 @@ namespace Installer { else if (entryName == "cafe_barista_men.bps") { menuFilePath = CAFE_BARISTA_MEN_PATH; } - else if (entryName.contains("AllMessage")) { - const std::string allMessageStr = "AllMessage_"; - const std::string extensionStr = ".bps"; - - std::string regionLangStr = entryName.substr( - allMessageStr.size(), - entryName.size() - allMessageStr.size() - extensionStr.size() - ); - + else if (entryStem.starts_with(AllMessage_) && entryName.extension() == ".bps") { + std::string regionLangStr = entryStem.substr(AllMessage_.size(), 4); auto it = regionLangMap.find(regionLangStr); if (it == regionLangMap.end()) { reportProgress(std::format("Unknown AllMessage Region and Language: \"{}\"", regionLangStr)); @@ -293,13 +390,15 @@ namespace Installer { menuFilePath = it->second; } - if (entryName != "metadata.json") { - reportProgress(std::format("menuFilePath: \"{}\"", menuFilePath.string())); + if (!menuFilePath.empty()) { + // Found a known patch. + reportProgress(std::format("Handling patch for \"{}\"", + menuFilePath.string())); auto menuPath = menuContentPath / menuFilePath; auto cachePath = THEMIIFY_ROOT / "cache" / menuFilePath; - auto patchPath = std::filesystem::path{entryName}; - auto outputPath = modpackPath / "content" / menuFilePath; + auto patchPath = entryName; + auto outputPath = themePath / "content" / menuFilePath; CreateParentDirectories(cachePath); @@ -320,6 +419,7 @@ namespace Installer { zip_error_strerror(&error)); zip_error_fini(&error); zip_fclose(patchFile); + patchFile = nullptr; throw std::runtime_error{msg}; } @@ -338,6 +438,7 @@ namespace Installer { if (!inputFile.is_open()) { inputFile.clear(); zip_fclose(patchFile); + patchFile = nullptr; // NOTE: don't error out, just report reportProgress(std::format("Could not open source file for \"{}\"", patchPath.string())); @@ -367,6 +468,7 @@ namespace Installer { zip_fread(patchFile, patchData.data(), patchData.size()); zip_fclose(patchFile); + patchFile = nullptr; throwIfStopped(); @@ -400,6 +502,44 @@ namespace Installer { static_cast(result))}; } } + else { + // Not a known patch: if it's not a .bps file, just copy it verbatim + // to themePath. + if (entryName.extension() != ".bps") { + reportProgress(format("Copying file: \"{}\"", entryName.string())); + if (!(patchFile = zip_fopen(themeArchive, + entryName.c_str(), + ZIP_RDONLY))) { + zip_error_init_with_code(&error, err); + std::string msg = std::format("Cannot open \"{}\"!. Error: {}", + entryName.string(), + zip_error_strerror(&error)); + zip_error_fini(&error); + throw std::runtime_error{msg}; + } + auto outputPath = themePath / entryName; + if (outputPath.has_parent_path()) + create_directories(outputPath.parent_path()); + std::filebuf output; + if (!output.open(outputPath, + std::ios::out | std::ios::binary | std::ios::trunc)) + throw std::runtime_error{"Could not create output file: \""s + + outputPath.string() + "\""s}; + std::vector buf(65536); + zip_int64_t read = 0; + zip_int64_t total = 0; + while ((read = zip_fread(patchFile, buf.data(), buf.size())) > 0) { + output.sputn(buf.data(), read); + total += read; + } + output.close(); + zip_fclose(patchFile); + patchFile = nullptr; + reportProgress(std::format("File written to \"{}\" ({} bytes)", + outputPath.string(), + total)); + } + } } zip_close(themeArchive); @@ -407,50 +547,19 @@ namespace Installer { throwIfStopped(); - installPath = THEMIIFY_INSTALLED_THEMES / (themeData.themeIDPath + ".json"); - - reportProgress(std::format("Creating install metadata: \"{}\"", - installPath.string())); - CreateParentDirectories(installPath); - - // TODO: use a struct - glz::generic installedThemeJson; - installedThemeJson["ThemeData"]["themeName"] = themeData.themeName; - installedThemeJson["ThemeData"]["themeAuthor"] = themeData.themeAuthor; - installedThemeJson["ThemeData"]["themeID"] = themeData.themeID; - installedThemeJson["ThemeData"]["themeIDPath"] = themeData.themeIDPath; - installedThemeJson["ThemeData"]["themeVersion"] = themeData.themeVersion; - installedThemeJson["ThemeData"]["themeInstallPath"] = modpackPath; - - auto jsonStr = glz::write(installedThemeJson); - if (!jsonStr) { - throw std::runtime_error{std::format("Failed to generate install metadata for \"{}\"", - themeData.themeName)}; - } - else { - std::ofstream outFile(installPath); - if (outFile.is_open()) { - outFile << *jsonStr; - outFile.close(); - reportProgress(std::format("Finished install for \"{}\".", - themeData.themeName)); - } - else { - throw std::runtime_error{std::format("Failed to save install metadata for \"{}\"", - themeData.themeName)}; - } - } - OSEnableHomeButtonMenu(TRUE); if (successCallback) successCallback(); } catch (std::exception &e) { + if (patchFile) + zip_fclose(patchFile); if (themeArchive) zip_close(themeArchive); - cerr << "Deleting theme: " << modpackPath << " and " << installPath << endl; - DeleteTheme(modpackPath, installPath); + cerr << "Deleting partially installed theme: " << themePath << endl; + if (!themePath.empty()) + DeletePath(themePath); if (errorCallback) errorCallback(e); else @@ -459,24 +568,22 @@ namespace Installer { } } - bool DeleteTheme(const std::filesystem::path &modpackPath, - const std::filesystem::path &installPath) { - std::filesystem::path thumbnailPath; - if (!modpackPath.empty()) - DeletePath(modpackPath); - if (!installPath.empty()) { - DeletePath(installPath); - // Dumb hack but I don't wanna change more stuff - thumbnailPath = THEMIIFY_THUMBNAILS / installPath.stem(); - thumbnailPath.replace_extension(".webp"); - DeletePath(thumbnailPath); + void DeleteTheme(const InstalledThemeMetadata& meta) { + // If there's a cached thumbnail, delete it. + if (meta.uthemeMetadata.themeID) { + auto thumbnail = theme_id_to_cached_thumbnail_path(*meta.uthemeMetadata.themeID); + if (exists(thumbnail)) + DeletePath(thumbnail); } - if (exists(modpackPath) && exists(installPath) && exists(thumbnailPath)) { - return false; + // If there's legacy metadata, delete it. + if (!meta.legacyMetadataPath.empty()) { + if (exists(meta.legacyMetadataPath)) + DeletePath(meta.legacyMetadataPath); } - return true; + // Finally, delete the theme path with all its contents. + DeletePath(meta.themePath); } std::string GetStyleMiiUConfigPath() { @@ -492,48 +599,56 @@ namespace Installer { return std::string(environmentPathBuffer) + "/plugins/config/style-mii-u.json"; } - bool SetCurrentTheme(const std::string &themeName, const std::string &themeID) { - std::string styleMiiUConfigPath = GetStyleMiiUConfigPath(); + bool SetCurrentTheme(const InstalledThemeMetadata &meta) { + try { + cout << "Trying to set current theme: " << meta.themePath << endl; + auto styleMiiUConfigPath = GetStyleMiiUConfigPath(); - std::ifstream configFile(styleMiiUConfigPath); - if (!configFile.is_open()) { - cerr << "Failed to open config file: " << styleMiiUConfigPath << endl; - return false; - } + std::ifstream configFile(styleMiiUConfigPath); + if (!configFile.is_open()) { + cerr << "Failed to open config file: " << styleMiiUConfigPath << endl; + return false; + } - std::string jsonStr = ReadWholeFile(configFile); - configFile.close(); + std::string jsonStr = ReadWholeFile(configFile); + configFile.close(); - glz::generic configJson; - if (auto err = glz::read_json(configJson, jsonStr)) { - cerr << "Failed to parse config file: " + glz::generic configJson; + if (auto err = glz::read_json(configJson, jsonStr)) { + cerr << "Failed to parse config file: " << glz::format_error(err, jsonStr) << endl; - return false; - } + return false; + } - configJson["storageitems"]["enabledThemes"] = sanitize_element(themeName + " (" + themeID + ")"); + configJson["storageitems"]["enabledThemes"] = meta.themePath.filename().string(); - auto outputJson = glz::write(configJson); - if (!outputJson) { - cerr << "Failed to serialize config json" << endl; - return false; - } + auto outputJson = glz::write(configJson); + if (!outputJson) { + cerr << "Failed to serialize config json" << endl; + return false; + } - std::ofstream outFile(styleMiiUConfigPath, std::ios::trunc); - if (!outFile.is_open()) { - cerr << "Failed to open for write: " << styleMiiUConfigPath << endl; - return false; - } + std::ofstream outFile{styleMiiUConfigPath, std::ios::trunc | std::ios::out}; + if (!outFile.is_open()) { + cerr << "Failed to open for write: " << styleMiiUConfigPath << endl; + return false; + } - outFile << *outputJson; - outFile.close(); + outFile << *outputJson; + outFile.close(); - std::println("Succesfully set {} as current StyleMiiU theme!", themeName); + std::println("Succesfully set {} as current StyleMiiU theme!", + meta.themePath.string()); - return true; + return true; + } + catch (std::exception& e) { + cerr << "ERROR in SetCurrentTheme(): " << e.what() << endl; + return false; + } } - std::string GetCurrentTheme() { + std::string GetCurrentThemeName() { std::string styleMiiUConfigPath = GetStyleMiiUConfigPath(); std::ifstream configFile(styleMiiUConfigPath); @@ -555,4 +670,23 @@ namespace Installer { return configJson.at("storageitems").at("enabledThemes").get(); } + std::optional + GetCurrentTheme() { + // TODO: handle shuffle mode + auto themeName = GetCurrentThemeName(); + auto themePath = THEMES_ROOT / themeName; + if (!exists(themePath)) + return {}; + InstalledThemeMetadata result; + if (!GetInstalledThemeMetadata(themePath, result)) + return {}; + return {std::move(result)}; + } + + std::filesystem::path + GetThemePath(const UThemeMetadata& meta) + { + return THEMES_ROOT / make_theme_folder_name(meta.themeName, meta.themeID); + } + } // namespace Installer diff --git a/src/installer.h b/src/installer.h index 32d3b8b..7b28499 100644 --- a/src/installer.h +++ b/src/installer.h @@ -11,29 +11,34 @@ #include #include #include -#include +#include #include +#include +#include namespace Installer { - struct theme_data { - std::string themeID; - std::string themeIDPath; + struct UThemeMetadata { + std::optional themeID; std::string themeName; - std::string themeAuthor; - std::string themeVersion; + std::optional themeAuthor; + std::optional themeVersion; }; - struct installed_theme_data { - std::string themeID; - std::string themeIDPath; - std::string themeName; - std::string themeAuthor; - std::string themeVersion; - std::filesystem::path installedThemePath; + struct InstalledThemeMetadata { + UThemeMetadata uthemeMetadata; + std::filesystem::path themePath; + std::vector previewPaths; + std::filesystem::path legacyMetadataPath; + std::vector files; }; - int GetThemeMetadata(const std::filesystem::path &themePath, theme_data *themeData); - int GetInstalledThemeMetadata(const std::filesystem::path &installedThemeJsonPath, installed_theme_data *themeData); + bool GetUThemeMetadata(const std::filesystem::path &themePath, + UThemeMetadata &meta); + + bool GetInstalledThemeMetadata(const std::filesystem::path &installedThemePath, + InstalledThemeMetadata &imeta); + + std::vector GetInstalledThemes(std::stop_token& stopper); using progress_function_sig = void (const std::string &msg); using progress_function_t = std::function; @@ -46,11 +51,14 @@ namespace Installer { void InstallTheme(std::stop_token &stopper, const std::filesystem::path &themePath, - theme_data themeData, + UThemeMetadata themeMetadata, progress_function_t progressCallback, success_function_t successCallback, error_function_t errorCallback); - bool DeleteTheme(const std::filesystem::path &modpackPath, const std::filesystem::path &installPath); - bool SetCurrentTheme(const std::string &themeName, const std::string &themeIDPath); - std::string GetCurrentTheme(); + void DeleteTheme(const InstalledThemeMetadata& meta); + bool SetCurrentTheme(const InstalledThemeMetadata &meta); + std::string GetCurrentThemeName(); + std::optional GetCurrentTheme(); + + std::filesystem::path GetThemePath(const UThemeMetadata& meta); } // namespace Installer diff --git a/src/main.cpp b/src/main.cpp index 9aa8478..f6bcfb2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -12,13 +12,13 @@ #include #include -int main(int argc, char **argv) +int main() { #ifdef __WIIU__ wiiu_init_stdout(); wiiu_init_stderr(); #endif - + App::initialize(); App::run(); App::finalize(); diff --git a/src/screens/DeleteThemePopup.cpp b/src/screens/DeleteThemePopup.cpp index c1d061c..829bee0 100644 --- a/src/screens/DeleteThemePopup.cpp +++ b/src/screens/DeleteThemePopup.cpp @@ -7,6 +7,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#include #include #include @@ -15,6 +16,7 @@ #include "DeleteThemePopup.h" #include "ManageThemesScreen.h" +#include "../IconsFontAwesome4.h" using namespace std::literals; @@ -29,14 +31,12 @@ namespace DeleteThemePopup { bool popup_queued; const std::string popup_id = "DeleteThemePopup"s; - Installer::installed_theme_data installedThemeData; - std::filesystem::path themeJsonPath; + Installer::InstalledThemeMetadata installedThemeData; - void show(Installer::installed_theme_data installed_theme_data, std::filesystem::path theme_json_path) { + void open(const Installer::InstalledThemeMetadata &installed_theme_data) { popup_queued = true; state = State::shown; installedThemeData = installed_theme_data; - themeJsonPath = theme_json_path; } void process_ui() { @@ -52,8 +52,12 @@ namespace DeleteThemePopup { auto center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); - Popup popup{popup_id, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoMove}; + PopupModal popup{popup_id, nullptr, + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize}; if (!popup) { state = State::hidden; @@ -68,23 +72,34 @@ namespace DeleteThemePopup { ImGui::Text("Delete Confirmation"); } - ImGui::TextWrapped("Would you like to delete \"%s\"?", installedThemeData.themeName.c_str()); + ImGui::TextWrapped("Would you like to delete \"%s\"?", + installedThemeData.uthemeMetadata.themeName.c_str()); - ImGui::Spacing(); + // Show two buttons with same size. + const ImVec2 available = ImGui::GetContentRegionAvail(); - ImVec2 button_size{180.0f, 60.0f}; + const std::string delete_label = ICON_FA_TRASH " Delete"; + const std::string cancel_label = ICON_FA_TIMES " Cancel"; + + const ImVec2 delete_size = ImGui::CalcTextSize(delete_label); + const ImVec2 cancel_size = ImGui::CalcTextSize(cancel_label); + + const ImVec2 button_size = + ImVec2{ std::fmax(delete_size.x, cancel_size.x), + std::fmax(delete_size.y, cancel_size.y) } + + 2 * style.FramePadding; float spacing = style.ItemSpacing.x; - float total_width = button_size.x * 2.0f + spacing; + float total_width = 2 * button_size.x + spacing; - float start_x = (ImGui::GetContentRegionAvail().x - total_width) * 0.5f; + float start_x = (available.x - total_width) / 2; - if (start_x > 0.0f) + if (start_x > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); - if (ImGui::Button("Delete", button_size)) { - Installer::DeleteTheme(installedThemeData.installedThemePath, themeJsonPath); - ManageThemesScreen::force_refresh(); + if (ImGui::Button(delete_label, button_size)) { + Installer::DeleteTheme(installedThemeData); + ManageThemesScreen::refresh_installed_themes(); ImGui::CloseCurrentPopup(); state = State::hidden; } @@ -92,11 +107,9 @@ namespace DeleteThemePopup { ImGui::SameLine(); - if (ImGui::Button("Cancel", button_size)) { + if (ImGui::Button(cancel_label, button_size)) { ImGui::CloseCurrentPopup(); state = State::hidden; } - - ImGui::Spacing(); } } diff --git a/src/screens/DeleteThemePopup.h b/src/screens/DeleteThemePopup.h index 39bac17..395e9ab 100644 --- a/src/screens/DeleteThemePopup.h +++ b/src/screens/DeleteThemePopup.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -14,7 +14,7 @@ #include "../installer.h" namespace DeleteThemePopup { - void show(Installer::installed_theme_data installed_theme_data, std::filesystem::path theme_json_path); + void open(const Installer::InstalledThemeMetadata &installed_theme_data); void process_ui(); } diff --git a/src/screens/DownloadThemePopup.cpp b/src/screens/DownloadThemePopup.cpp index 5981fa2..2a04647 100644 --- a/src/screens/DownloadThemePopup.cpp +++ b/src/screens/DownloadThemePopup.cpp @@ -7,9 +7,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#include #include #include #include +#include #include #include @@ -19,6 +21,7 @@ #include "../utils.h" #include "../DownloadManager.h" #include "../humanize.hpp" +#include "../IconsFontAwesome4.h" #include "../installer.h" using std::cout; @@ -28,6 +31,7 @@ using namespace std::literals; namespace DownloadThemePopup { enum class State { hidden, + queued, confirmation, downloading, error, @@ -36,184 +40,225 @@ namespace DownloadThemePopup { State state; - bool popup_queued; const std::string popup_id = "Download Theme"; - std::filesystem::path utheme_path; + std::string utheme_url; + std::filesystem::path utheme_filename; - ThemezerAPI::WiiuThemeSmall theme; + std::string transfer_name; + std::string error_message; bool set_current = true; - void show(const ThemezerAPI::WiiuThemeSmall &theme_data) { - state = State::confirmation; - popup_queued = true; - theme = theme_data; - utheme_path = ""; + void open(const ThemezerAPI::WiiuThemeSmall &theme_data) { + state = State::queued; + transfer_name = theme_data.name; + utheme_url = theme_data.downloadUrl; + utheme_filename = make_utheme_filename(theme_data.slug, theme_data.hexId); + error_message.clear(); } - void process_ui() { + void show_confirmation() { using namespace ImGui::RAII; - if (state == State::hidden) - return; - if (popup_queued) { - ImGui::OpenPopup(popup_id); - popup_queued = false; + const auto &style = ImGui::GetStyle(); + { + Font title_font{nullptr, 35}; + ImGui::Text("Download Confirmation"); } - auto viewport = ImGui::GetMainViewport(); - // WORKAROUND: setting an initial size helps with the initial position not jumping around. - ImGui::SetNextWindowSize(viewport->Size * 0.75f, ImGuiCond_Appearing); - auto center = viewport->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); - PopupModal popup{popup_id, nullptr, - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse | - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_None - }; - - if (!popup) { - state = State::hidden; - return; + ImGui::TextWrapped("Would you like to download the theme:\n%s ?", + transfer_name.c_str()); + + // Create two buttons with equal widths. + const ImVec2 available = ImGui::GetContentRegionAvail(); + + const std::string download_label = ICON_FA_DOWNLOAD " Download"; + const std::string cancel_label = ICON_FA_TIMES " Cancel"; + + const ImVec2 download_size = ImGui::CalcTextSize(download_label); + const ImVec2 cancel_size = ImGui::CalcTextSize(cancel_label); + + const ImVec2 button_size = + ImVec2{ std::fmax(download_size.x, cancel_size.x), + std::fmax(download_size.y, cancel_size.y) } + + 2 * style.FramePadding; + + // Place the buttons on the bottom. + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + available.y - button_size.y); + + const float total_width = 2 * button_size.x + style.ItemSpacing.x; + + const float start_x = (available.x - total_width) / 2; + + if (start_x > 0) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); + + if (ImGui::Button(download_label, button_size)) { + state = State::downloading; + if (!DownloadManager::add(utheme_url, + utheme_filename, + [](const DownloadManager::Info& info) + { + cout << "Finished " << info.filename << endl; + state = State::success; + }, + [](const std::exception& e) + { + state = State::error; + error_message = e.what(); + })) { + state = State::error; + error_message = "Failed to queue download transfer"; + } } + ImGui::SetItemDefaultFocus(); - const auto &style = ImGui::GetStyle(); + ImGui::SameLine(); - switch (state) { - case State::confirmation: { - { - Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); - ImGui::Text("Download Confirmation"); - } + if (ImGui::Button(cancel_label, button_size)) { + ImGui::CloseCurrentPopup(); + state = State::hidden; + } + } - ImGui::TextWrapped("Would you like to download the theme:\n%s ?", theme.name.c_str()); + void show_downloading() { + using namespace ImGui::RAII; + { + Font title_font{nullptr, 35}; + ImGui::Text("Downloading Theme..."); + } - ImGui::Spacing(); + ImGui::TextWrapped(transfer_name); - ImVec2 button_size{180.0f, 60.0f}; + ImGui::TextWrapped(utheme_url); - float spacing = style.ItemSpacing.x; - float total_width = button_size.x * 2.0f + spacing; + ImGui::TextWrapped("Saving to: %s", utheme_filename.filename().c_str()); - float start_x = (ImGui::GetContentRegionAvail().x - total_width) * 0.5f; + auto info = DownloadManager::get_info(utheme_url); - if (start_x > 0.0f) - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); + //auto speed = humanize::value_bin(info->speed) + "B/s"; + //ImGui::Text("DL speed: %s", speed.data()); - if (ImGui::Button("Download", button_size)) { - if (DownloadManager::add("Theme: " + theme.name, - theme.downloadUrl, - theme.collagePreview.thumbUrl, - THEMES_ROOT / (theme.slug + ".utheme"), - THEMIIFY_THUMBNAILS / ("Themezer" + theme.hexId + ".webp"), - {}, - {})) { - } + // Place the progress bar on the bottom. + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + + ImGui::GetContentRegionAvail().y + - ImGui::GetFrameHeight()); - state = State::downloading; - } - ImGui::SetItemDefaultFocus(); + ImGui::ProgressBar(info->progress); - ImGui::SameLine(); + // TODO: downloads should be cancelable, should have a cancel button here. + } - if (ImGui::Button("Cancel", button_size)) { - ImGui::CloseCurrentPopup(); - state = State::hidden; - } + void show_success() { + using namespace ImGui::RAII; - ImGui::Spacing(); + { + Font title_font{nullptr, 50}; + ImGui::Text("Download successful!"); + } - break; - } - case State::downloading: { - auto& infos = DownloadManager::get_infos(); - // Don't really wanna do a multiple downloads approach - // I think this is cleaner for actually installing themes - // Afterwards - auto& info = infos.at(0); - - utheme_path = info->utheme_output; - - { - Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); - ImGui::Text("Downloading Theme..."); - } + ImGui::TextWrapped("Would you like to install this theme for the StyleMiiU plugin?"); - ImGui::Text(info->label); + ImGui::Checkbox("Apply theme after install", &set_current); - ImGui::Text(info->utheme_url); + // Make two buttons of equal size. + const std::string install_label = ICON_FA_COGS " Install"; + const std::string cancel_label = ICON_FA_TIMES " Cancel"; + const ImVec2 install_size = ImGui::CalcTextSize(install_label); + const ImVec2 cancel_size = ImGui::CalcTextSize(cancel_label); - ImGui::TextWrapped("Saving to: %s", info->utheme_output.filename().c_str()); + const auto &style = ImGui::GetStyle(); + const ImVec2 button_size = + ImVec2{ std::fmax(install_size.x, cancel_size.x), + std::fmax(install_size.y, cancel_size.y) } + + 2 * style.FramePadding; - //auto speed = humanize::value_bin(info->speed) + "B/s"; - //ImGui::Text("DL speed: %s", speed.data()); + const ImVec2 available = ImGui::GetContentRegionAvail(); + // Place the buttons on the bottom. + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + available.y - button_size.y); - ImGui::ProgressBar(info->progress); + const float total_width = 2 * button_size.x + style.ItemSpacing.x; - if (info->progress >= 1.0f) { - DownloadManager::clear_finished(); - state = State::success; - } + const float start_x = (available.x - total_width) / 2; + if (start_x > 0.0f) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); - break; - } - case State::error: { - break; - } - case State::success: { - { - Font title_font{nullptr, 50}; - ImGui::AlignTextToFramePadding(); - ImGui::Text("Download successful!"); - } + if (ImGui::Button(install_label, button_size)) { + Installer::UThemeMetadata theme_data; + Installer::GetUThemeMetadata(utheme_filename, theme_data); - ImGui::TextWrapped("Would you like to install this theme for StyleMiiU plugin?"); + ImGui::CloseCurrentPopup(); + state = State::hidden; - ImGui::Checkbox("Set as current theme", &set_current); + InstallThemePopup::open(utheme_filename, theme_data, true, set_current); + } + ImGui::SetItemDefaultFocus(); - ImGui::Spacing(); + ImGui::SameLine(); - ImVec2 button_size{180.0f, 60.0f}; + if (ImGui::Button(cancel_label, button_size)) { + ImGui::CloseCurrentPopup(); + state = State::hidden; + } + } - float spacing = style.ItemSpacing.x; - float total_width = button_size.x * 2.0f + spacing; + void show_error() { + ImGui::Text("ERROR!"); + ImGui::TextWrapped(error_message); - float start_x = (ImGui::GetContentRegionAvail().x - total_width) * 0.5f; + if (ImGui::Button("Close")) { + DownloadManager::clear_all(); + state = State::hidden; + } + } - if (start_x > 0.0f) - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); + void process_ui() { + using namespace ImGui::RAII; + if (state == State::hidden) + return; - if (ImGui::Button("Install", button_size)) { - Installer::theme_data theme_data; - Installer::GetThemeMetadata(utheme_path, &theme_data); + if (state == State::queued) { + // NOTE: open popup before we create the popup. + ImGui::OpenPopup(popup_id); + state = State::confirmation; + } - ImGui::CloseCurrentPopup(); - state = State::hidden; + auto viewport = ImGui::GetMainViewport(); + // WORKAROUND: setting an initial size helps with the initial position not jumping around. + ImGui::SetNextWindowSize(viewport->Size * 0.7f, ImGuiCond_Appearing); + auto center = viewport->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); + PopupModal popup{popup_id, nullptr, + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize}; - InstallThemePopup::show(utheme_path, theme_data, true, set_current); - } - ImGui::SetItemDefaultFocus(); + if (!popup) { + state = State::hidden; + return; + } - ImGui::SameLine(); + switch (state) { + case State::confirmation: + show_confirmation(); + break; - if (ImGui::Button("Cancel", button_size)) { - ImGui::CloseCurrentPopup(); - state = State::hidden; - } + case State::downloading: + show_downloading(); + break; - ImGui::Spacing(); + case State::error: + show_error(); + break; + case State::success: + show_success(); break; - } + default: - break; + ; } } diff --git a/src/screens/DownloadThemePopup.h b/src/screens/DownloadThemePopup.h index 09a80cf..dbf82b9 100644 --- a/src/screens/DownloadThemePopup.h +++ b/src/screens/DownloadThemePopup.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -12,7 +12,7 @@ #include "../ThemezerAPI.h" namespace DownloadThemePopup { - void show(const ThemezerAPI::WiiuThemeSmall &theme_data); + void open(const ThemezerAPI::WiiuThemeSmall &theme_data); void process_ui(); -} \ No newline at end of file +} diff --git a/src/screens/HomeScreen.cpp b/src/screens/HomeScreen.cpp index c36b55b..484d5f8 100644 --- a/src/screens/HomeScreen.cpp +++ b/src/screens/HomeScreen.cpp @@ -13,11 +13,12 @@ #include "../NavBar.h" #include "../installer.h" #include "../IconsFontAwesome4.h" +#include "../ImageLoader.h" #include "../utils.h" #include -#include #include +#include #include #include @@ -35,17 +36,13 @@ using std::endl; // #define DEBUG_BG_COLOR namespace HomeScreen { + using Installer::InstalledThemeMetadata; + SDL_Renderer *home_renderer = nullptr; SDL_Texture *themiify_logo = nullptr; - std::string current_theme_str; - std::string current_theme_id_path; - std::string current_theme_json_path; - std::string current_theme_thumbnail_path; - - Installer::installed_theme_data current_theme_data; - SDL_Texture *current_theme_thumbnail = nullptr; + std::optional current_theme_data; bool current_theme_refresh = true; @@ -53,25 +50,6 @@ namespace HomeScreen { bool styleMiiUExists = true; bool queueStyleMiiUPrompt = false; - std::unordered_map thumbnail_cache; - SDL_Texture* placeholder_thumbnail = nullptr; - - SDL_Texture *getThumbnail(const std::filesystem::path& path) { - std::string key = path.string(); - - auto it = thumbnail_cache.find(key); - if (it != thumbnail_cache.end()) - return it->second; - - SDL_Texture* tex = IMG_LoadTexture(home_renderer, key.c_str()); - - if (!tex) - return placeholder_thumbnail; - - thumbnail_cache[key] = tex; - return tex; - } - std::string get_theme_id(const std::string& str) { auto open = str.rfind('('); auto close = str.rfind(')'); @@ -85,25 +63,7 @@ namespace HomeScreen { } void refresh_current_theme() { - current_theme_str = Installer::GetCurrentTheme(); - current_theme_id_path = get_theme_id(current_theme_str); - - current_theme_json_path = - THEMIIFY_INSTALLED_THEMES / (current_theme_id_path + ".json"); - - current_theme_thumbnail_path = - THEMIIFY_THUMBNAILS / (current_theme_id_path + ".webp"); - - int res = Installer::GetInstalledThemeMetadata( - current_theme_json_path, - ¤t_theme_data - ); - - if (res == 1) - current_theme_thumbnail = getThumbnail(current_theme_thumbnail_path); - else - current_theme_data.themeIDPath = ""; - + current_theme_data = Installer::GetCurrentTheme(); current_theme_refresh = false; } @@ -121,7 +81,7 @@ namespace HomeScreen { return false; } - if (std::filesystem::exists(std::string(environmentPathBuffer) + "/plugins/stylemiiu.wps")) + if (exists(std::filesystem::path{environmentPathBuffer} / "plugins/stylemiiu.wps")) return true; return false; @@ -133,7 +93,6 @@ namespace HomeScreen { home_renderer = renderer; themiify_logo = IMG_LoadTexture(renderer, "fs:/vol/content/ui/themiify-logo.png"); - placeholder_thumbnail = IMG_LoadTexture(renderer, "fs:/vol/content/ui/theme-placeholder-icon.png"); current_theme_refresh = true; @@ -149,18 +108,6 @@ namespace HomeScreen { void finalize() { cout << "Hello from HomeScreen finalize!" << endl; - for (auto& [path, tex] : thumbnail_cache) { - if (tex) - SDL_DestroyTexture(tex); - } - - thumbnail_cache.clear(); - - if (placeholder_thumbnail) { - SDL_DestroyTexture(placeholder_thumbnail); - placeholder_thumbnail = nullptr; - } - if (themiify_logo) { SDL_DestroyTexture(themiify_logo); themiify_logo = nullptr; @@ -188,14 +135,13 @@ namespace HomeScreen { ImGui::SameLine(); { - Font font_guard{nullptr, 25}; + Font font{nullptr, 25}; ImGui::Text("v%s", THEMIIFY_VERSION); } } { - Font font_guard{nullptr, 55}; - + Font font{nullptr, 55}; // Cute lil thing cause why not? isFirstBoot ? ImGui::Text("Welcome!") : ImGui::Text("Welcome back!"); } @@ -216,22 +162,18 @@ namespace HomeScreen { #ifdef DEBUG_BG_COLOR StyleColor brown_bg{ImGuiCol_ChildBg, {0.3, 0.3, 0.0, 1.0}}; #endif - Child scrollable_content{"scrollable_content", {0, 0}, - ImGuiChildFlags_AlwaysUseWindowPadding}; + Child scrollable_content{"scrollable_content"}; if (!scrollable_content) return; - - ImGui::Spacing(); - { - Font font_guard{nullptr, 35}; + Font font{nullptr, 35}; ImGui::Text("Your current theme:"); } ImGui::Spacing(); - if (current_theme_data.themeIDPath.empty()) { + if (!current_theme_data) { ImGui::Text("No current theme found."); ImGui::Spacing(); } @@ -240,24 +182,28 @@ namespace HomeScreen { { Child theme_frame{ - current_theme_data.themeIDPath, + "CurrentTheme", {800, 300}, ImGuiChildFlags_NavFlattened | ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_NoSavedSettings }; - ImGui::Image((ImTextureID)current_theme_thumbnail, {426, 240}); - - ImGui::SameLine(); + if (!current_theme_data->previewPaths.empty()) { + auto img = ImageLoader::get(current_theme_data->previewPaths.front()); + ImGui::Image((ImTextureID)img, {426, 240}); + ImGui::SameLine(); + } { Group right_group; { Font font_guard{nullptr, 30}; - ImGui::TextWrapped(current_theme_data.themeName); - ImGui::TextWrapped("by: %s", current_theme_data.themeAuthor.c_str()); + ImGui::TextWrapped(current_theme_data->uthemeMetadata.themeName); + if (current_theme_data->uthemeMetadata.themeAuthor) + ImGui::TextWrapped("by: " + + *current_theme_data->uthemeMetadata.themeAuthor); } } } @@ -281,7 +227,6 @@ namespace HomeScreen { NavBar::set_current_tab(NavBar::Tab::manage_themes); } - ImGui::Spacing(); ImGui::Separator(); { diff --git a/src/screens/HomeScreen.h b/src/screens/HomeScreen.h index cba0cec..1f74184 100644 --- a/src/screens/HomeScreen.h +++ b/src/screens/HomeScreen.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -13,10 +13,10 @@ namespace HomeScreen { void force_refresh(); - + void initialize(SDL_Renderer *renderer); void finalize(); void process_ui(); -} \ No newline at end of file +} diff --git a/src/screens/InstallThemePopup.cpp b/src/screens/InstallThemePopup.cpp index 3753d10..a4ef551 100644 --- a/src/screens/InstallThemePopup.cpp +++ b/src/screens/InstallThemePopup.cpp @@ -21,6 +21,7 @@ #include "../utils.h" #include "../installer.h" #include "../thread_safe.hpp" +#include "../IconsFontAwesome4.h" using std::cout; using std::cerr; @@ -46,7 +47,7 @@ namespace InstallThemePopup { const std::string popup_id = "Install Theme"s; std::filesystem::path utheme_path; - Installer::theme_data theme_data; + Installer::UThemeMetadata theme_data; bool set_current = true; std::jthread install_thread; @@ -85,9 +86,7 @@ namespace InstallThemePopup { using namespace ImGui::RAII; // NOTE: take up all available space except for a row of buttons at the bottom. - // ImVec2 size{0, -ImGui::GetFrameHeightWithSpacing()}; - const auto &style = ImGui::GetStyle(); - ImVec2 size{0.0f, -(style.ItemSpacing.y + 60.0f)}; + ImVec2 size{0.0f, -ImGui::GetFrameHeightWithSpacing()}; if (Child messages_box{"messages_box", size, ImGuiChildFlags_None, @@ -123,8 +122,8 @@ namespace InstallThemePopup { } // namespace - void show(const std::filesystem::path &uthemePath, - Installer::theme_data themeData, + void open(const std::filesystem::path &uthemePath, + const Installer::UThemeMetadata &themeData, bool confirmationCompleted, bool setCurrent) { create_directories(THEMES_ROOT); @@ -187,9 +186,7 @@ namespace InstallThemePopup { ImGui::Checkbox("Set as current theme after installation", &set_current); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float spacing = style.ItemSpacing.x; float total_width = button_size.x * 2.0f + spacing; @@ -199,20 +196,18 @@ namespace InstallThemePopup { if (start_x > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); - if (ImGui::Button("Install", button_size)) { + if (ImGui::Button(ICON_FA_COGS " Install", button_size)) { state = State::start_install; } ImGui::SetItemDefaultFocus(); ImGui::SameLine(); - if (ImGui::Button("Cancel", button_size)) { + if (ImGui::Button(ICON_FA_TIMES " Cancel", button_size)) { ImGui::CloseCurrentPopup(); state = State::hidden; } - ImGui::Spacing(); - break; } case State::start_install: { @@ -224,8 +219,12 @@ namespace InstallThemePopup { progress_handler, success_handler, error_handler); - if (state == State::success && set_current) - Installer::SetCurrentTheme(theme_data.themeName, theme_data.themeIDPath); + if (state == State::success && set_current) { + auto theme_path = Installer::GetThemePath(theme_data); + Installer::InstalledThemeMetadata imeta; + if (Installer::GetInstalledThemeMetadata(theme_path, imeta)) + Installer::SetCurrentTheme(imeta); + } }); break; @@ -236,7 +235,6 @@ namespace InstallThemePopup { ImGui::Dummy({0.0f, 0.0f}); { Font title_font{nullptr, 40}; - ImGui::AlignTextToFramePadding(); ImGui::TextWrapped("Installing %s...", theme_data.themeName.c_str()); } @@ -246,7 +244,7 @@ namespace InstallThemePopup { show_messages(); - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float button_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; if (button_x > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + button_x); @@ -258,13 +256,12 @@ namespace InstallThemePopup { case State::error: { { Font title_font{nullptr, 50}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Installation failed!"); } show_messages(); - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -282,7 +279,6 @@ namespace InstallThemePopup { case State::success: { { Font title_font{nullptr, 50}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Installation successful!"); } @@ -290,9 +286,7 @@ namespace InstallThemePopup { utheme_path.filename().string())); ImGui::TextWrapped("Would you like to delete it?"); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float spacing = style.ItemSpacing.x; float total_width = button_size.x * 2.0f + spacing; @@ -306,7 +300,7 @@ namespace InstallThemePopup { DeletePath(utheme_path); ImGui::CloseCurrentPopup(); state = State::hidden; - ManageThemesScreen::force_refresh(); + ManageThemesScreen::refresh_all(); } ImGui::SetItemDefaultFocus(); @@ -315,7 +309,7 @@ namespace InstallThemePopup { if (ImGui::Button("Keep", button_size)) { ImGui::CloseCurrentPopup(); state = State::hidden; - ManageThemesScreen::force_refresh(); + ManageThemesScreen::refresh_all(); } break; diff --git a/src/screens/InstallThemePopup.h b/src/screens/InstallThemePopup.h index 64998cc..a57020f 100644 --- a/src/screens/InstallThemePopup.h +++ b/src/screens/InstallThemePopup.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -13,7 +13,10 @@ #include "../installer.h" namespace InstallThemePopup { - void show(const std::filesystem::path &uthemePath, Installer::theme_data themeData, bool confirmationCompleted, bool setCurrent); + void open(const std::filesystem::path &uthemePath, + const Installer::UThemeMetadata &themeData, + bool confirmationCompleted, + bool setCurrent); void process_ui(); -} \ No newline at end of file +} diff --git a/src/screens/ManageThemesScreen.cpp b/src/screens/ManageThemesScreen.cpp index 3473b14..3f3f223 100644 --- a/src/screens/ManageThemesScreen.cpp +++ b/src/screens/ManageThemesScreen.cpp @@ -7,10 +7,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -#include -#include -#include #include +#include +#include +#include +#include +#include #include #include @@ -19,12 +21,16 @@ #include #include "ManageThemesScreen.h" +#include "HomeScreen.h" #include "InstallThemePopup.h" #include "ThemeDetailsPopup.h" #include "DeleteThemePopup.h" #include "../installer.h" #include "../utils.h" #include "../IconsFontAwesome4.h" +#include "../ImageLoader.h" +#include "../tracer.hpp" +#include "../thread_safe.hpp" // Define this to help seeing the padding and spacing values for windows. // #define DEBUG_BG_COLOR @@ -34,6 +40,9 @@ using std::endl; using namespace std::literals; namespace ManageThemesScreen { + + using Installer::InstalledThemeMetadata; + enum class Tab { manage_installed, install_local, @@ -41,48 +50,33 @@ namespace ManageThemesScreen { Tab current_tab = Tab::manage_installed; - std::vector local_themes; - std::vector json_files; + std::vector local_uthemes; + bool local_uthemes_need_refresh = true; - std::vector installed_themes; + std::jthread installed_themes_scanner; + thread_safe> safe_installed_themes; - bool local_themes_refresh = true; - bool is_current_theme = false; + enum class InstalledThemesScanState { + idle, + requested, + scanning, + }; + std::atomic installed_themes_scan_state{ + InstalledThemesScanState::requested + }; SDL_Renderer *manage_renderer; - SDL_Texture *thumbnail; - std::unordered_map thumbnail_cache; - SDL_Texture* placeholder_thumbnail = nullptr; + std::filesystem::path thumbnail_path; std::string search; - std::string current_theme; - SDL_Texture *getThumbnail(const std::filesystem::path& path) { - std::string key = path.string(); + std::string current_theme_name; + bool current_theme_need_refresh = true; - auto it = thumbnail_cache.find(key); - if (it != thumbnail_cache.end()) - return it->second; - - SDL_Texture* tex = IMG_LoadTexture(manage_renderer, key.c_str()); - - if (!tex) - return placeholder_thumbnail; - - thumbnail_cache[key] = tex; - return tex; - } - - std::string as_lower_case(std::string s) { - for (char &c : s) - c = std::tolower(static_cast(c)); - return s; - } - - int similarity_score(const std::string& haystack_, const std::string& needle_) { - std::string haystack = as_lower_case(haystack_); - std::string needle = as_lower_case(needle_); + int similarity_score(std::string haystack, std::string needle) { + haystack = as_lower_case(haystack); + needle = as_lower_case(needle); if (needle.empty()) return 0; @@ -111,364 +105,375 @@ namespace ManageThemesScreen { return score; } - void scan_local_themes() { - local_themes.clear(); + void scan_local_uthemes() { + local_uthemes.clear(); for (auto& entry : std::filesystem::directory_iterator(THEMES_ROOT)) { if (entry.is_regular_file() && entry.path().extension() == ".utheme") { - local_themes.push_back(entry.path()); + local_uthemes.push_back(entry.path()); } } + std::ranges::sort(local_uthemes, {}, as_lower_case); } void scan_installed_themes() { - json_files.clear(); - installed_themes.clear(); - - for (auto& entry : std::filesystem::directory_iterator(THEMIIFY_INSTALLED_THEMES)) { - if (!entry.is_regular_file() || entry.path().extension() != ".json") - continue; - - Installer::installed_theme_data data; - Installer::GetInstalledThemeMetadata(entry.path(), &data); - - if (!exists(data.installedThemePath)) { - DeletePath(entry.path()); - continue; + installed_themes_scan_state = InstalledThemesScanState::scanning; + installed_themes_scanner = std::jthread{ + [](std::stop_token stopper) + { + auto themes = Installer::GetInstalledThemes(stopper); + if (!stopper.stop_requested()) { + safe_installed_themes.store(std::move(themes)); + installed_themes_scan_state = InstalledThemesScanState::idle; + } } - - json_files.push_back(entry.path()); - installed_themes.push_back(data); - } + }; } void initialize(SDL_Renderer *renderer) { - cout << "Hello from InstalledScreen init!" << endl; + TRACE_FUNC; create_directories(THEMES_ROOT); create_directories(THEMIIFY_INSTALLED_THEMES); - manage_renderer = renderer; - - placeholder_thumbnail = IMG_LoadTexture(manage_renderer, "fs:/vol/content/ui/theme-placeholder-icon.png"); + installed_themes_scanner = {}; + safe_installed_themes.lock()->clear(); } void finalize() { - cout << "Hello from InstalledScreen finalize!" << endl; + TRACE_FUNC; + installed_themes_scanner = {}; + safe_installed_themes.lock()->clear(); + } - for (auto& [path, tex] : thumbnail_cache) { - if (tex) - SDL_DestroyTexture(tex); - } + void refresh_all() { + refresh_installed_themes(); + refresh_current_theme(); + refresh_local_uthemes(); + } - thumbnail_cache.clear(); + void refresh_installed_themes() { + installed_themes_scan_state = InstalledThemesScanState::requested; + } - if (placeholder_thumbnail) { - SDL_DestroyTexture(placeholder_thumbnail); - placeholder_thumbnail = nullptr; - } + void refresh_current_theme() { + current_theme_need_refresh = true; } - void force_refresh() { - local_themes_refresh = true; + void refresh_local_uthemes() + { + local_uthemes_need_refresh = true; } - void process_ui() { + static void text_limited(float width, const std::string& text) { + // WORKAROUND: prevent tooltip. + auto& io = ImGui::GetIO(); + auto old_mouse_pos = io.MousePos; + ImGui::TextAligned(0.0f, width, text); + io.MousePos = old_mouse_pos; + } + + void show_installed_theme(const Installer::InstalledThemeMetadata& theme_data, + bool is_active, + const ImVec2& inner_size, + const ImVec2& padding) { + // NOTE: to create a complex button, we create a button with no text, then overlap + // the contents. using namespace ImGui::RAII; -#ifdef DEBUG_BG_COLOR - StyleColor green_bg{ImGuiCol_ChildBg, {0.0, 0.5, 0.0, 1.0}}; -#endif + ID id{theme_data.themePath.string()}; - Child content{ - "ManageThemesContent", - {0, 0}, - ImGuiChildFlags_AlwaysUseWindowPadding - }; + const auto& style = ImGui::GetStyle(); + const ImVec2 outer_size = inner_size + 2 * padding; - if (!content) + // Put everything inside a child window so we can bail out when not visibile. + Child container{"container", outer_size, + ImGuiChildFlags_NavFlattened}; + if (!container) return; - const auto &style = ImGui::GetStyle(); + const ImVec2 start_pos = padding; - float tab_width = (ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) * 0.50f; + bool clicked = false; + ImGui::SetCursorPos({0, 0}); + if (ImGui::Button("##button", outer_size)) { + // NOTE: delay opening the popup, gotta check if the user clicked on the star. + clicked = true; + } + + // NOTE: when hovered or activated, make text light. + std::optional dark_text; + if (ImGui::IsItemHovered() || ImGui::IsItemActive()) { + const auto& colors = style.Colors; + auto dark_color = colors[ImGuiCol_WindowBg]; + dark_text.emplace(ImGuiCol_Text, dark_color); + } - constexpr float tab_height = 60.0f; - // Installer::installed_theme_data theme_data; + ImGui::SetCursorPos(start_pos); + Group grp; + if (!theme_data.previewPaths.empty()) { + StyleVar no_border{ImGuiStyleVar_ImageBorderSize, 0}; + auto img = ImageLoader::get(theme_data.previewPaths.front()); + ImVec2 img_size = {inner_size.x, inner_size.x * 9.0f / 16.0f}; + ImGui::Image((ImTextureID)img, img_size); + } - if (ImGui::Selectable( - "Manage Installed Themes", - current_tab == Tab::manage_installed, - 0, - {tab_width, tab_height})) + // NOTE: Measure size for the active star, but don't place it yet, to not mess + // with the cursor position. + const std::string star_label = is_active ? ICON_FA_STAR : ICON_FA_STAR_O; + const float star_font_size = 48; + ImVec2 star_size; { - current_tab = Tab::manage_installed; - force_refresh(); + Font star_font{nullptr, star_font_size}; + star_size = ImGui::CalcTextSize(star_label); } - ImGui::SameLine(); + { + Font font{nullptr, 24}; + // If there's an image, make sure to limit the width, so it doesn't get + // covered by the star. + float name_width = inner_size.x; + if (!theme_data.previewPaths.empty()) + name_width -= star_size.x + style.ItemSpacing.x; + text_limited(name_width, theme_data.uthemeMetadata.themeName); + } + + if (theme_data.uthemeMetadata.themeAuthor) { + Font font{nullptr, 18}; + float author_width = inner_size.x; + // If there's an image, make sure to limit the width, so it doesn't get + // covered by the star. + if (!theme_data.previewPaths.empty()) + author_width -= star_size.x + style.ItemSpacing.x; + text_limited(author_width, "by " + *theme_data.uthemeMetadata.themeAuthor); + } - if (ImGui::Selectable( - "Install Local Themes", - current_tab == Tab::install_local, - 0, - {tab_width, tab_height})) + // Put active star on bottom right. { - current_tab = Tab::install_local; - force_refresh(); + Font star_font{nullptr, star_font_size}; + StyleColor star_color{ImGuiCol_Text, {1.0f, 0.9f, 0.0f, 1.0f}}; + ImGui::SetCursorPos(inner_size - star_size); + ImGui::Text(star_label); + if (clicked && ImGui::IsItemHovered()) { + clicked = false; // cancel the click + // TODO: when shuffle is implemented, this would be a toggle. + Installer::SetCurrentTheme(theme_data); + HomeScreen::force_refresh(); + refresh_current_theme(); + } } - ImGui::Separator(); - ImGui::Spacing(); + if (clicked) + ThemeDetailsPopup::open_local(theme_data, is_active); + } -#ifdef DEBUG_BG_COLOR - StyleColor brown_bg{ImGuiCol_ChildBg, {0.3, 0.3, 0.0, 1.0}}; -#endif - if (Child contents{"contents", {0, 0}, ImGuiChildFlags_AlwaysUseWindowPadding}) { + void show_utheme(const std::filesystem::path& utheme_path) { + using namespace ImGui::RAII; - switch (current_tab) { - case Tab::manage_installed: { - ImGui::AlignTextToFramePadding(); - ImGui::Text("Search installed theme:"); + Child theme_frame{utheme_path.string(), + {0, 0}, + ImGuiChildFlags_NavFlattened | + ImGuiChildFlags_AutoResizeY | + ImGuiChildFlags_FrameStyle, + ImGuiWindowFlags_NoSavedSettings}; - ImGui::SameLine(); + if (!theme_frame) + return; - SDL_WiiUSetSWKBDKeyboardMode(SDL_WIIU_SWKBD_KEYBOARD_MODE_FULL); - SDL_WiiUSetSWKBDHintText("Iname of a theme to search for it..."); - SDL_WiiUSetSWKBDOKLabel("Search"); - SDL_WiiUSetSWKBDShowWordSuggestions(SDL_TRUE); - SDL_WiiUSetSWKBDHighlightInitialText(SDL_TRUE); + const auto &style = ImGui::GetStyle(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputTextWithHint("##local_search"s, "Search..."s, search); + const std::string install_label = ICON_FA_COGS " Install"; + const std::string remove_label = ICON_FA_TRASH; - if (ImGui::IsItemDeactivatedAfterEdit()) { - cout << "Searching: " << search << endl; - } + auto install_size = ImGui::CalcTextSize(install_label) + 2 * style.FramePadding; + auto remove_size = ImGui::CalcTextSize(remove_label) + 2 * style.FramePadding; - ImGui::Spacing(); + const float text_wrap_pos = + ImGui::GetCursorPosX() + + ImGui::GetContentRegionAvail().x + - style.ItemSpacing.x + - install_size.x + - style.ItemSpacing.x + - remove_size.x; + { + TextWrapPos wrap_at{text_wrap_pos}; + ImGui::AlignTextToFramePadding(); + ImGui::Text(utheme_path.filename().string()); + } - if (local_themes_refresh) { - scan_installed_themes(); - current_theme = Installer::GetCurrentTheme(); + ImGui::SameLine(); - auto current_it = std::find_if( - installed_themes.begin(), - installed_themes.end(), - [&](const auto& data) { - return sanitize_element(data.themeName + " (" + data.themeIDPath + ")") == current_theme; - } - ); + ImGui::SetCursorPosX(text_wrap_pos + style.ItemSpacing.x); - if (current_it != installed_themes.end()) { - auto index = std::distance(installed_themes.begin(), current_it); + if (ImGui::Button(install_label, install_size)) { + Installer::UThemeMetadata theme_data; + Installer::GetUThemeMetadata(utheme_path, theme_data); + InstallThemePopup::open(utheme_path, theme_data, false, true); + } - std::rotate(installed_themes.begin(), current_it, current_it + 1); - std::rotate(json_files.begin(), json_files.begin() + index, json_files.begin() + index + 1); - } + ImGui::SameLine(); - local_themes_refresh = false; - } + if (ImGui::Button(remove_label, remove_size)) { + DeletePath(utheme_path); + refresh_local_uthemes(); + } + } - std::vector visible_indexes; + void show_tab_manage_installed() { + using namespace ImGui::RAII; - for (std::size_t i = 0; i < installed_themes.size(); ++i) { - int score = similarity_score(installed_themes[i].themeName, search); + ImGui::AlignTextToFramePadding(); // align vertically with the InputText widget + ImGui::Text("Search installed theme:"); - if (search.empty() || score >= 0) - visible_indexes.push_back(i); - } + ImGui::SameLine(); - if (!search.empty()) { - std::ranges::sort( - visible_indexes, - [&](std::size_t a, std::size_t b) { - return similarity_score(installed_themes[a].themeName, search) - > similarity_score(installed_themes[b].themeName, search); - } - ); - } + SDL_WiiUSetSWKBDHintText("Write some search terms..."); + SDL_WiiUSetSWKBDOKLabel("Search"); + SDL_WiiUSetSWKBDHighlightInitialText(SDL_TRUE); - // To keep the search widget visible, put the search results inside another child. - if (Child search_results{"search_results"}) { - - for (std::size_t index : visible_indexes) { - auto& theme_data = installed_themes[index]; - auto& current_json_path = json_files[index]; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextWithHint("##local_search"s, "Search..."s, search); - bool is_current_theme = - sanitize_element(theme_data.themeName + " (" + theme_data.themeIDPath + ")") == current_theme; + // if (ImGui::IsItemDeactivatedAfterEdit()) { + // cout << "Searching: " << search << endl; + // } - Child theme_frame{ - theme_data.themeIDPath, - {0, 320}, - ImGuiChildFlags_NavFlattened | - ImGuiChildFlags_FrameStyle, - ImGuiWindowFlags_NoSavedSettings - }; - - if (!theme_frame) - continue; - - if (is_current_theme) { - auto* draw_list = ImGui::GetWindowDrawList(); - - ImVec2 min = ImGui::GetWindowPos(); - ImVec2 max = { - min.x + ImGui::GetWindowSize().x, - min.y + ImGui::GetWindowSize().y - }; - - constexpr float rounding = 16.0f; - - draw_list->AddRect( - min, - max, - IM_COL32(50, 220, 50, 255), - rounding, - ImDrawFlags_RoundCornersAll, - 6.0f - ); - - constexpr float radius = 18.0f; - - ImVec2 badge_center{ - max.x - radius - 12.0f, - min.y + radius + 12.0f - }; - - draw_list->AddCircleFilled( - badge_center, - radius, - IM_COL32(50, 220, 50, 255) - ); - - draw_list->AddCircle( - badge_center, - radius, - IM_COL32(255, 255, 255, 255), - 0, - 2.0f - ); - - const char* star = ICON_FA_STAR; - ImVec2 star_size = ImGui::CalcTextSize(star); - - draw_list->AddText( - { - badge_center.x - star_size.x * 0.5f, - badge_center.y - star_size.y * 0.5f - }, - IM_COL32(255, 255, 255, 255), - star - ); - } - - auto thumbnailPath = - THEMIIFY_THUMBNAILS / (theme_data.themeIDPath + ".webp"); - - SDL_Texture* thumbnail = getThumbnail(thumbnailPath); - - ImGui::Image((ImTextureID)thumbnail, {426, 240}); - - ImGui::SameLine(); - - { - Group right_group; - - ImGui::TextWrapped("%s", theme_data.themeName.c_str()); - ImGui::TextWrapped("by: %s", theme_data.themeAuthor.c_str()); - - if (ImGui::Button(ICON_FA_INFO_CIRCLE " Details")) { - ThemeDetailsPopup::show_local(theme_data, thumbnail, is_current_theme); - } - - ImGui::SameLine(); - - { - Disabled disable_when{is_current_theme}; - - if (ImGui::Button(ICON_FA_STAR " Make Default")) { - Installer::SetCurrentTheme(theme_data.themeName, theme_data.themeIDPath); - force_refresh(); - } - } - - ImGui::Spacing(); - - if (ImGui::Button(ICON_FA_TRASH " Delete")) { - DeleteThemePopup::show(theme_data, current_json_path); - } - } - } - } + if (installed_themes_scan_state == InstalledThemesScanState::requested) { + scan_installed_themes(); + } - break; - } - case Tab::install_local: - ImGui::Text("Install .utheme files from sd:/wiiu/themes here."); + if (current_theme_need_refresh) { + current_theme_name = Installer::GetCurrentThemeName(); + current_theme_need_refresh = false; + } - ImGui::Spacing(); + std::vector visible_indexes; - if (local_themes_refresh) { - scan_local_themes(); - local_themes_refresh = false; - } + auto installed_themes = safe_installed_themes.lock(); - for (const auto& utheme_path : local_themes) { - std::string id = utheme_path.string(); + for (std::size_t i = 0; i < installed_themes->size(); ++i) { + int score = similarity_score((*installed_themes)[i].uthemeMetadata.themeName, + search); - Child theme_frame{ - id.c_str(), - {0, 80}, - ImGuiChildFlags_NavFlattened | - ImGuiChildFlags_FrameStyle, - ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse - }; + if (search.empty() || score >= 0) + visible_indexes.push_back(i); + } - if (!theme_frame) - continue; + if (!search.empty()) { + std::ranges::sort( + visible_indexes, + [&](std::size_t a, std::size_t b) { + const auto& ta = (*installed_themes)[a]; + const auto& tb = (*installed_themes)[b]; + auto sa = similarity_score(ta.uthemeMetadata.themeName, search); + auto sb = similarity_score(tb.uthemeMetadata.themeName, search); + return sa > sb; + } + ); + } - ImGui::TextWrapped( - "%s", - utheme_path.filename().string().c_str() - ); + // To keep the search widget visible, put the search results inside + // another child. + if (Child search_results{"ThemeGrid"}) { + + if (installed_themes_scan_state == InstalledThemesScanState::idle) { + + const ImVec2 grid_start_pos = ImGui::GetCursorPos(); + const ImVec2 inner_size = {320, 260}; + const ImVec2 padding = {12, 12}; + const ImVec2 outer_size = inner_size + 2 * padding; + const ImVec2 spacing = {18, 18}; + + for (auto [counter, index] : visible_indexes | std::views::enumerate) { + auto& theme_data = (*installed_themes)[index]; + bool is_current_theme = current_theme_name == theme_data.themePath.filename(); + + ImVec2 grid_pos = { float(counter % 3), float(counter / 3) }; + ImVec2 pos = grid_pos * (outer_size + spacing); + ImGui::SetCursorPos(grid_start_pos + pos); + show_installed_theme(theme_data, + is_current_theme, + inner_size, + padding); + } + } else { + ImGui::Text("Scanning installed themes..."); + } + } + } - ImGui::SameLine(); + void show_tab_install_local() { + ImGui::Text("Install .utheme files from sd:/wiiu/themes here."); - ImVec2 install_button_size{150.0f, 50.0f}; - ImVec2 trash_button_size{50.0f, 50.0f}; + ImGui::Separator(); + + if (local_uthemes_need_refresh) { + scan_local_uthemes(); + local_uthemes_need_refresh = false; + } - float spacing = style.ItemSpacing.x; + for (const auto& utheme_path : local_uthemes) + show_utheme(utheme_path); + } - float total_width = - install_button_size.x + - spacing + - trash_button_size.x; + void process_ui() { + using namespace ImGui::RAII; - float start_x = - ImGui::GetWindowWidth() - - total_width - - style.WindowPadding.x; + { + // NOTE: use a scope to contain all the temporary style changes, so they don't + // leak into the popups at the bottom. +#ifdef DEBUG_BG_COLOR + StyleColor green_bg{ImGuiCol_ChildBg, {0.0, 0.5, 0.0, 1.0}}; +#endif + const auto &style = ImGui::GetStyle(); + // Remove horizontal padding. + StyleVar no_hori_padding{ImGuiStyleVar_WindowPadding, {0, style.WindowPadding.y}}; + if (Child manage_content{"ManageThemesContent", + {0, 0}, + ImGuiChildFlags_NavFlattened | + ImGuiChildFlags_AlwaysUseWindowPadding}) { + + float tab_width = (ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) * 0.50f; + const float tab_height = ImGui::GetFrameHeight(); + + + if (ImGui::Selectable("Manage Installed Themes", + current_tab == Tab::manage_installed, + 0, + {tab_width, tab_height})) { + current_tab = Tab::manage_installed; + refresh_installed_themes(); + refresh_current_theme(); + } - ImGui::SetCursorPosX(start_x); + ImGui::SameLine(); - if (ImGui::Button(ICON_FA_DOWNLOAD " Install", install_button_size)) { - Installer::theme_data theme_data; - Installer::GetThemeMetadata(utheme_path, &theme_data); - InstallThemePopup::show(utheme_path, theme_data, false, true); - } + if (ImGui::Selectable("Install Local Themes", + current_tab == Tab::install_local, + 0, + {tab_width, tab_height})) { + current_tab = Tab::install_local; + refresh_local_uthemes(); + } - ImGui::SameLine(); + ImGui::Separator(); - if (ImGui::Button(ICON_FA_TRASH, trash_button_size)) { - DeletePath(utheme_path); - force_refresh(); - } +#ifdef DEBUG_BG_COLOR + StyleColor brown_bg{ImGuiCol_ChildBg, {0.3, 0.3, 0.0, 1.0}}; +#endif + if (Child tab_contents{"tab_contents", + {0, 0}, + ImGuiChildFlags_NavFlattened}) { + + switch (current_tab) { + case Tab::manage_installed: + show_tab_manage_installed(); + break; + case Tab::install_local: + show_tab_install_local(); + break; } - break; + } } } diff --git a/src/screens/ManageThemesScreen.h b/src/screens/ManageThemesScreen.h index 66b7e67..ed54835 100644 --- a/src/screens/ManageThemesScreen.h +++ b/src/screens/ManageThemesScreen.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -16,7 +16,13 @@ namespace ManageThemesScreen { void finalize(); - void force_refresh(); + void refresh_all(); + + void refresh_installed_themes(); + + void refresh_current_theme(); + + void refresh_local_uthemes(); void process_ui(); -} \ No newline at end of file +} diff --git a/src/screens/SettingsPopup.cpp b/src/screens/SettingsPopup.cpp index f83cf49..754f880 100644 --- a/src/screens/SettingsPopup.cpp +++ b/src/screens/SettingsPopup.cpp @@ -9,10 +9,10 @@ #include "SettingsPopup.h" #include "../utils.h" +#include "../App.h" #include #include -#include #include #include @@ -330,9 +330,14 @@ namespace SettingsPopup { auto center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); - PopupModal popup{popup_id, nullptr, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar}; + PopupModal popup{popup_id, nullptr, + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoTitleBar}; if (!popup) { state = State::hidden; @@ -345,17 +350,17 @@ namespace SettingsPopup { case State::stylmiiu_error: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("StyleMiiU Not Found!"); } - ImGui::Text("The StyleMiiU aroma plugin could not be found!\n\nFor your installed themes to work in the Wii U Menu " \ - "you will need to install\nthis plugin from either the Homebrew App Store or at:\n\n" \ + ImGui::Text("The StyleMiiU aroma plugin could not be found!\n" + "\n" + "For your installed themes to work in the Wii U Menu you will need to install\n" + "this plugin from either the Homebrew App Store or at:\n" + "\n" "github.com/Themiify-hb/StyleMiiU-Plugin/releases/latest"); - ImGui::Spacing(); - - ImVec2 button_size{250.0f, 60.0f}; + ImVec2 button_size{250, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -364,28 +369,27 @@ namespace SettingsPopup { ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); if (ImGui::Button("Close Themiify", button_size)) { - SYSLaunchMenu(); + App::quit(); } - ImGui::Spacing(); - break; } case State::integrity_confirmation: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Check Menu Integrity Confirmation"); } - ImGui::Text("Would you like to check the integrity of your Wii U Menu's\nfiles on your NAND to verify " \ - "whether they have been modified?\n\nIf your files have been modified, Themiify will " \ - "always fail to install themes\nuntil you either restore clean files to your NAND, or place clean files" \ - "\nin sd:/themiify/cache.\n\nPlease check the Theme Café Docs for more info."); + ImGui::Text("Would you like to check the integrity of your Wii U Menu's\n" + "files on your NAND to verify whether they have been modified?\n" + "\n" + "If your files have been modified, Themiify will always fail to install themes\n" + "until you either restore clean files to your NAND, or place clean files\n" + "in sd:/themiify/cache.\n" + "\n" + "Please check the Theme Café Docs for more info."); - ImGui::Spacing(); - - ImVec2 button_size{250.0f, 60.0f}; + ImVec2 button_size{250, 0}; float spacing = style.ItemSpacing.x; float total_width = button_size.x * 2.0f + spacing; @@ -419,21 +423,16 @@ namespace SettingsPopup { state = State::checking_integrity; } - ImGui::SameLine(); - if (ImGui::Button("Close", button_size)) { ImGui::CloseCurrentPopup(); state = State::hidden; } - ImGui::Spacing(); - break; } case State::checking_integrity: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Checking..."); } @@ -451,7 +450,6 @@ namespace SettingsPopup { case State::integrity_checked: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Integrity Checked"); } @@ -461,20 +459,16 @@ namespace SettingsPopup { else { ImGui::Text("The following files appear to be modified: "); - ImGui::Spacing(); ImGui::Indent(); for (auto &file : modified_files) { ImGui::Text("%s", file.c_str()); } ImGui::Unindent(); - ImGui::Spacing(); ImGui::Text("Please consult the Theme Cafe docs for steps to restore your original files."); } - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -487,28 +481,26 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } case State::dump_confirmation: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Dump Menu Files Confirmation"); } - ImGui::Text("Would you like to dump the most common Wii U Menu files\nused in theme creation to your SD Card?" \ - "\n\nThe files dumped are: Men.pack, Men2.pack & cafe_barista_men.bfsar\n\nNote: By installing any " \ - "theme via Themiify, this will be done automatically."); - - ImGui::Spacing(); + ImGui::Text("Would you like to dump the most common Wii U Menu files\n" + "used in theme creation to your SD Card?\n" + "\n" + "The files dumped are: Men.pack, Men2.pack & cafe_barista_men.bfsar\n" + "\n" + "Note: By installing any theme via Themiify, this will be done automatically."); - ImGui::Checkbox("Dump AllMessage.szs for all languages.\nConsult the Theme Café docs for more info on these files.", &dump_allmessage); + ImGui::Checkbox("Dump AllMessage.szs for all languages.\n" + "Consult the Theme Café docs for more info on these files.", + &dump_allmessage); - ImGui::Spacing(); - - ImVec2 button_size{210.0f, 60.0f}; + ImVec2 button_size{210, 0}; float spacing = style.ItemSpacing.x; float total_width = button_size.x * 2.0f + spacing; @@ -560,20 +552,15 @@ namespace SettingsPopup { state = State::dumping; } - ImGui::SameLine(); - if (ImGui::Button("Close", button_size)) { ImGui::CloseCurrentPopup(); state = State::hidden; } - ImGui::Spacing(); - break; } case State::dumping: { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Dumping..."); ImGui::Text("Please wait. Do not turn off your Wii U."); @@ -590,21 +577,20 @@ namespace SettingsPopup { case State::dump_completed: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Dump Completed"); } - ImGui::Text("The dump has been sucessfully completed\n\nYou can find your dumped files at:\n" \ + ImGui::Text("The dump has been sucessfully completed\n" + "\n" + "You can find your dumped files at:\n" "sd:/themiify/cache"); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; - if (start_x > 0.0f) + if (start_x > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); if (ImGui::Button("Close", button_size)) { @@ -612,22 +598,17 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } case State::dump_error: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Dump Error"); } ImGui::Text("One or more files failed to dump correctly."); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -640,30 +621,29 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } case State::cache_confirmation: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Clear Cache Confirmation"); } - ImGui::Text("Would you like to delete your Themiify cache located at:\nsd:/themiify/cache ?" \ - "\n\nDoing so will delete all dumped Wii U Menu files"); + ImGui::Text("Would you like to delete your Themiify cache located at:\n" + "sd:/themiify/cache ?\n" + "\n" + "Doing so will delete all dumped Wii U Menu files"); ImGui::Checkbox("Delete theme thumbnails as well?", &delete_thumbnails); - ImVec2 button_size{210.0f, 60.0f}; + ImVec2 button_size{210, 0}; float spacing = style.ItemSpacing.x; float total_width = button_size.x * 2.0f + spacing; float start_x = (ImGui::GetContentRegionAvail().x - total_width) * 0.5f; - if (start_x > 0.0f) + if (start_x > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_x); if (ImGui::Button("Clear Cache", button_size)) { @@ -701,14 +681,13 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } case State::clearing_cache: { - Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); - ImGui::Text("Clearing Cache..."); + { + Font title_font{nullptr, 35}; + ImGui::Text("Clearing Cache..."); + } ImGui::Text("Please wait."); @@ -724,15 +703,12 @@ namespace SettingsPopup { case State::cache_cleared: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Cache Cleared"); } ImGui::Text("The Themiify cache has been succesfully cleared."); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -745,22 +721,17 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } case State::cache_error: { { Font title_font{nullptr, 35}; - ImGui::AlignTextToFramePadding(); ImGui::Text("Error Clearing Cache"); } ImGui::Text("One or more files could not be removed from the cache"); - ImGui::Spacing(); - - ImVec2 button_size{180.0f, 60.0f}; + ImVec2 button_size{180, 0}; float start_x = (ImGui::GetContentRegionAvail().x - button_size.x) * 0.5f; @@ -773,8 +744,6 @@ namespace SettingsPopup { state = State::hidden; } - ImGui::Spacing(); - break; } default: diff --git a/src/screens/SettingsScreen.cpp b/src/screens/SettingsScreen.cpp index 458f55f..a999559 100644 --- a/src/screens/SettingsScreen.cpp +++ b/src/screens/SettingsScreen.cpp @@ -105,7 +105,7 @@ namespace SettingsScreen { bootIntegrityCheckPending = false; } - void initialize(SDL_Renderer *renderer) { + void initialize(SDL_Renderer * /*renderer*/) { cout << "Hello from SettingsScreen init!" << endl; load_settings(); diff --git a/src/screens/ThemeDetailsPopup.cpp b/src/screens/ThemeDetailsPopup.cpp index 039c5a0..44d411a 100644 --- a/src/screens/ThemeDetailsPopup.cpp +++ b/src/screens/ThemeDetailsPopup.cpp @@ -16,10 +16,11 @@ #include #include "ThemeDetailsPopup.h" -#include "ThemePreviewPopup.h" -#include "DownloadThemePopup.h" #include "DeleteThemePopup.h" +#include "DownloadThemePopup.h" +#include "HomeScreen.h" #include "ManageThemesScreen.h" +#include "ThemePreviewPopup.h" #include "../App.h" #include "../installer.h" #include "../DownloadManager.h" @@ -39,7 +40,7 @@ using ThemezerAPI::WiiuThemeSmall; namespace ThemeDetailsPopup { enum class State { hidden, - waiting, + waiting_themezer, ready_themezer, ready_local, error, @@ -49,38 +50,253 @@ namespace ThemeDetailsPopup { State state; std::string hexId; std::string error; - WiiuThemeFull theme; + WiiuThemeFull fullTheme; WiiuThemeSmall smallTheme; - Installer::installed_theme_data installedThemeData; - SDL_Texture *localPreview; + Installer::InstalledThemeMetadata installedThemeData; const std::string popup_id = "ThemeDetailsPopup"s; bool isCurrent; - void show_themezer(const std::string& request_id, const WiiuThemeSmall &small_theme) { + void open_themezer(const WiiuThemeSmall &small_theme) { popup_queued = true; - hexId = request_id; + hexId = small_theme.hexId; state = State::hidden; error.clear(); - theme = {}; + fullTheme = {}; smallTheme = small_theme; ThemezerAPI::wiiu::theme(hexId, [](const WiiuThemeFull& t) { cout << "Got theme!" << endl; - theme = t; + fullTheme = t; state = State::ready_themezer; }); - state = State::waiting; + state = State::waiting_themezer; } - void show_local(Installer::installed_theme_data installed_theme_data, SDL_Texture *local_preview, bool is_current) { + void open_local(const Installer::InstalledThemeMetadata& installed_theme_data, + bool is_current) { popup_queued = true; installedThemeData = installed_theme_data; - localPreview = local_preview; state = State::ready_local; isCurrent = is_current; } + void show_label_text(const std::string& label, + const std::string& text) + { + const auto colors = ImGui::GetStyle().Colors; + { + ImGui::RAII::StyleColor label_color{ImGuiCol_Text, colors[ImGuiCol_HeaderActive]}; + ImGui::Text(label); + } + ImGui::SameLine(); + ImGui::TextWrapped(text); + } + + void show_local() { + using namespace ImGui::RAII; + const auto &style = ImGui::GetStyle(); + + // Split the window into two child windows: left and right + const float total_width = ImGui::GetContentRegionAvail().x; + float right_width = 300; + float left_width = total_width - right_width - style.ItemSpacing.x; + + if (Child left{"left", {left_width, 0}, ImGuiChildFlags_NavFlattened}) { + { + Font title_font{nullptr, 50}; + ImGui::TextWrapped(installedThemeData.uthemeMetadata.themeName); + } + if (installedThemeData.uthemeMetadata.themeAuthor) + ImGui::TextWrapped("by " + *installedThemeData.uthemeMetadata.themeAuthor); + + // Put content in a scrollable child window. + if (Child content{"content", {0, 0}, + ImGuiChildFlags_NavFlattened + | ImGuiChildFlags_AlwaysUseWindowPadding}) { + // Always scroll to the top when the popup first appears. + if (ImGui::IsWindowAppearing()) + ImGui::SetScrollY(0); + + if (!installedThemeData.previewPaths.empty()) { + ImVec2 preview_size {640, 360}; + // Center the image. + auto available = ImGui::GetContentRegionAvail(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (available.x - preview_size.x) + / 2); + auto img = ImageLoader::get(installedThemeData.previewPaths.front()); + StyleVar no_padding{ImGuiStyleVar_FramePadding, {0, 0}}; + if (ImGui::ImageButton("preview", (ImTextureID)img, preview_size)) + ThemePreviewPopup::open(installedThemeData.themePath, + installedThemeData.previewPaths); + } + + if (!installedThemeData.files.empty()) { + ImGui::SeparatorText("Files:"); + Font file_font{nullptr, 20}; + const std::string prefix = installedThemeData.themePath.string() + "/"; + for (const auto& filename : installedThemeData.files) { + std::string label = filename; + if (label.starts_with(prefix)) + label.erase(0, prefix.size()); + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::TextAligned(0, -1, label); + } + } + } + } + + ImGui::SameLine(); + + if (Child right{"right", {right_width, 0}, ImGuiChildFlags_NavFlattened}) { + { + // Calculate how much space is needed for the buttons at the bottom. + const int num_buttons = 4; + const float buttons_height = num_buttons * ImGui::GetFrameHeightWithSpacing(); + + // Put content in a scrollable child window. + if (Child content{"content", {0 , -buttons_height}, + ImGuiChildFlags_NavFlattened + | ImGuiChildFlags_AlwaysUseWindowPadding}) { + if (installedThemeData.uthemeMetadata.themeVersion) { + Font small{nullptr, 24}; + show_label_text(ICON_FA_CALENDAR_CHECK_O, + installedThemeData.uthemeMetadata.themeVersion->substr(0, 10)); + ImGui::SetItemTooltip("Theme version."); + } + } + + { + Disabled disabled_if{isCurrent}; + if (ImGui::Button(ICON_FA_STAR " Apply Theme", {-1, 0})) { + Installer::SetCurrentTheme(installedThemeData); + ImGui::CloseCurrentPopup(); + HomeScreen::force_refresh(); + ManageThemesScreen::refresh_current_theme(); + } + ImGui::SetItemDefaultFocus(); + } + + { + Disabled if_no_previews{installedThemeData.previewPaths.empty()}; + if (ImGui::Button(ICON_FA_EYE " Preview", {-1, 0})) + ThemePreviewPopup::open(installedThemeData.themePath, + installedThemeData.previewPaths); + } + + if (ImGui::Button(ICON_FA_TRASH " Delete", {-1, 0})) { + DeleteThemePopup::open(installedThemeData); + } + + if (ImGui::Button(ICON_FA_TIMES " Close", {-1, 0})) + ImGui::CloseCurrentPopup(); + } + } + } + + void show_themezer() { + using namespace ImGui::RAII; + const auto &style = ImGui::GetStyle(); + + // Split the window into two child windows: left and right + const float total_width = ImGui::GetContentRegionAvail().x; + float right_width = 300; + float left_width = total_width - right_width - style.ItemSpacing.x; + + if (Child left{"left", {left_width, 0}, + ImGuiChildFlags_NavFlattened}) { + { + Font title_font{nullptr, 50}; + ImGui::TextWrapped(fullTheme.name); + } + ImGui::TextWrapped("by %s", fullTheme.creator.username.data()); + + // Put content in a scrollable child window. + if (Child content{"content", + {0, 0}, + ImGuiChildFlags_NavFlattened + | ImGuiChildFlags_AlwaysUseWindowPadding}) { + // Always scroll to the top when the popup first appears. + if (ImGui::IsWindowAppearing()) + ImGui::SetScrollY(0); + + if (!fullTheme.collagePreview.hdUrl.empty()) { + ImVec2 preview_size = {640, 360}; + // Center the image. + auto available = ImGui::GetContentRegionAvail(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (available.x - preview_size.x) / 2); + auto img = ImageLoader::get(fullTheme.collagePreview.sdUrl); + StyleVar no_padding{ImGuiStyleVar_FramePadding, {0, 0}}; + if (ImGui::ImageButton("preview", + (ImTextureID)img, + preview_size)) + ThemePreviewPopup::open(fullTheme.hexId, + std::vector{ + fullTheme.collagePreview.hdUrl, + fullTheme.launcherScreenshot.hdUrl, + fullTheme.waraWaraPlazaScreenshot.hdUrl + }); + } + + if (fullTheme.description) { + Font small{nullptr, 24}; + ImGui::TextWrapped(*fullTheme.description); + } + } + + } + + ImGui::SameLine(); + + if (Child right{"right", {right_width, 0}, + ImGuiChildFlags_NavFlattened}) { + + // Calculate how much space is needed for the buttons at the bottom. + const int num_buttons = 3; + const float buttons_height = num_buttons * ImGui::GetFrameHeightWithSpacing(); + + // Put content in a scrollable child window. + if (Child content{"content", {0, -buttons_height}, + ImGuiChildFlags_NavFlattened | + ImGuiChildFlags_AlwaysUseWindowPadding}) { + // Always scroll to the top when the popup first appears. + if (ImGui::IsWindowAppearing()) + ImGui::SetScrollY(0); + + Font small{nullptr, 24}; + StyleVar spacing{ImGuiStyleVar_ItemSpacing, {12, 10}}; + show_label_text("QuickID:", fullTheme.quickId); + show_label_text(ICON_FA_CALENDAR_PLUS_O, fullTheme.createdAt.substr(0, 10)); + ImGui::SetItemTooltip("Creation date"); + show_label_text(ICON_FA_CALENDAR_CHECK_O, fullTheme.updatedAt.substr(0, 10)); + ImGui::SetItemTooltip("Update date"); + show_label_text(ICON_FA_DOWNLOAD, std::to_string(fullTheme.downloadCount)); + ImGui::SetItemTooltip("Number of downloads"); + if (!fullTheme.tags.empty()) { + ImGui::Separator(); + for (auto& tag : fullTheme.tags) + show_label_text(ICON_FA_TAG, tag.name); + } + } + + if (ImGui::Button(ICON_FA_DOWNLOAD " Download", {-1, 0})) + DownloadThemePopup::open(smallTheme); + ImGui::SetItemDefaultFocus(); + + if (ImGui::Button(ICON_FA_EYE " Preview", {-1, 0})) + ThemePreviewPopup::open(fullTheme.hexId, + std::vector{ + fullTheme.collagePreview.hdUrl, + fullTheme.launcherScreenshot.hdUrl, + fullTheme.waraWaraPlazaScreenshot.hdUrl + }); + + if (ImGui::Button(ICON_FA_TIMES " Close", {-1, 0})) + ImGui::CloseCurrentPopup(); + } + } + void process_ui() { using namespace ImGui::RAII; @@ -92,9 +308,9 @@ namespace ThemeDetailsPopup { popup_queued = false; } - auto center = ImGui::GetMainViewport()->GetCenter(); auto viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowSize(viewport->Size * 0.85f, ImGuiCond_Always); + auto center = viewport->GetCenter(); + ImGui::SetNextWindowSize({1140, 680}, ImGuiCond_Always); ImGui::SetNextWindowPos(center, ImGuiCond_Always, {0.5f, 0.5f}); Popup popup{popup_id, ImGuiWindowFlags_NoSavedSettings | @@ -106,7 +322,7 @@ namespace ThemeDetailsPopup { } switch (state) { - case State::waiting: + case State::waiting_themezer: ImGui::Text("Fetching theme details..."); break; @@ -114,147 +330,13 @@ namespace ThemeDetailsPopup { ImGui::TextWrapped("Error: %s", error.data()); break; - case State::ready_themezer: { - ImGui::Text("Theme details"); - ImGui::Separator(); - - ImGui::TextWrapped("Name: %s", theme.name.data()); - - float footer_height = ImGui::GetFrameHeightWithSpacing(); - if (Child contents{"contents", {0, -footer_height}}) { - // Always scroll to the top when the popup first appears. - if (ImGui::IsWindowAppearing()) - ImGui::SetScrollY(0); - - ImGui::TextWrapped("Created: %s", theme.createdAt.data()); - if (!theme.updatedAt.empty() && theme.updatedAt != theme.createdAt) - ImGui::TextWrapped("Updated: %s", theme.updatedAt.data()); - - ImGui::Text("Downloads: %u", theme.downloadCount); - - ImGui::TextWrapped("Author: %s", theme.creator.username.data()); - - if (theme.creator.avatarUrl) { - ImGui::SameLine(); - auto avatar = ImageLoader::get(*theme.creator.avatarUrl); - ImGui::Image((ImTextureID)avatar, {64, 64}); - ImGui::SetItemTooltip(*theme.creator.avatarUrl); - } - - ImGui::Text("Tags:"); - ImGui::Indent(); - for (auto& tag : theme.tags) - ImGui::Text(ICON_FA_TAG " %s", tag.name.data()); - ImGui::Unindent(); - - auto collageImg = ImageLoader::get(theme.collagePreview.sdUrl); - float collageWidth = 720; - ImGui::SetCursorPosX( - ImGui::GetCursorPosX() + - (ImGui::GetContentRegionAvail().x - collageWidth) * 0.5f - ); - - { - StyleVar no_padding{ImGuiStyleVar_FramePadding, {0.0f, 0.0f}}; - if (ImGui::ImageButton("collagePreviewSD", - (ImTextureID)collageImg, - {collageWidth, 405})) { - ThemePreviewPopup::show(theme.hexId, - { - theme.launcherScreenshot.hdUrl, - theme.waraWaraPlazaScreenshot.hdUrl, - theme.collagePreview.hdUrl - } - ); - } - } - - } - - // Footer area: action buttons, always visible. - - if (ImGui::Button("Download")) { - DownloadThemePopup::show(smallTheme); - } - - ImGui::SameLine(); - - { - // align button to the right - const auto &style = ImGui::GetStyle(); - std::string text = "Preview Theme"; - auto button_size = ImGui::CalcTextSize(text) + 2 * style.FramePadding; - auto available = ImGui::GetContentRegionAvail(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + available.x - button_size.x); - if (ImGui::Button(text)) { - ThemePreviewPopup::show(theme.hexId, - { - theme.launcherScreenshot.hdUrl, - theme.waraWaraPlazaScreenshot.hdUrl, - theme.collagePreview.hdUrl - } - ); - } - } - + case State::ready_themezer: + show_themezer(); break; - } - - case State::ready_local: { - ImGui::Text("Theme details"); - ImGui::Separator(); - ImGui::TextWrapped("Name: %s", installedThemeData.themeName.c_str()); - - float footer_height = ImGui::GetFrameHeightWithSpacing(); - if (Child contents{"contents", {0, -footer_height}}) { - // Always scroll to the top when the popup first appears. - if (ImGui::IsWindowAppearing()) - ImGui::SetScrollY(0); - - ImGui::TextWrapped("Author: %s", installedThemeData.themeAuthor.c_str()); - ImGui::TextWrapped("Theme Version: %s", installedThemeData.themeVersion.c_str()); - - float collageWidth = 720; - ImGui::SetCursorPosX( - ImGui::GetCursorPosX() + - (ImGui::GetContentRegionAvail().x - collageWidth) * 0.5f - ); - - { - StyleVar no_padding{ImGuiStyleVar_FramePadding, {0.0f, 0.0f}}; - ImGui::ImageButton("collagePreviewSD", - (ImTextureID)localPreview, - {collageWidth, 405}); - } - } - - // Footer area: action buttons, always visible. - { - Disabled disabled_if{isCurrent}; - if (ImGui::Button(ICON_FA_STAR " Make Default")) { - Installer::SetCurrentTheme(installedThemeData.themeName, installedThemeData.themeIDPath); - ImGui::CloseCurrentPopup(); - ManageThemesScreen::force_refresh(); - } - } - - ImGui::SameLine(); - - { - // align button to the right - const auto &style = ImGui::GetStyle(); - std::string text = ICON_FA_TRASH " Delete"; - auto button_size = ImGui::CalcTextSize(text) + 2 * style.FramePadding; - auto available = ImGui::GetContentRegionAvail(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + available.x - button_size.x); - if (ImGui::Button(text)) { - auto themeJsonPath = THEMIIFY_INSTALLED_THEMES / (installedThemeData.themeIDPath + ".json"); - DeleteThemePopup::show(installedThemeData, themeJsonPath); - } - } + case State::ready_local: + show_local(); break; - } default: ; diff --git a/src/screens/ThemeDetailsPopup.h b/src/screens/ThemeDetailsPopup.h index 4410d5f..f292020 100644 --- a/src/screens/ThemeDetailsPopup.h +++ b/src/screens/ThemeDetailsPopup.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -9,14 +9,17 @@ #pragma once +#include #include #include #include "../installer.h" #include "../ThemezerAPI.h" namespace ThemeDetailsPopup { - void show_themezer(const std::string& request_id, const ThemezerAPI::WiiuThemeSmall &small_theme); - void show_local(Installer::installed_theme_data installed_theme_data, SDL_Texture *local_preview, bool is_current); + void open_themezer(const ThemezerAPI::WiiuThemeSmall &small_theme); + + void open_local(const Installer::InstalledThemeMetadata& installed_theme_data, + bool is_current); void process_ui(); -} \ No newline at end of file +} diff --git a/src/screens/ThemePreviewPopup.cpp b/src/screens/ThemePreviewPopup.cpp index b0891f9..a20318c 100644 --- a/src/screens/ThemePreviewPopup.cpp +++ b/src/screens/ThemePreviewPopup.cpp @@ -39,8 +39,8 @@ namespace ThemePreviewPopup { bool popup_queued; const std::string popup_id = "ThemePreviewPopup"s; - std::string hex_id; - std::vector url_list; + std::string unique_id; + std::vector image_list; bool hide_ui; @@ -58,12 +58,22 @@ namespace ThemePreviewPopup { } // namespace - void show(const std::string& hexId, const std::vector& urlList) { + void open(const std::string& id, const std::vector& images) { state = State::visible; popup_queued = true; hide_ui = false; - hex_id = hexId; - url_list = urlList; + unique_id = id; + image_list = images; + } + + void open(const std::string& id, const std::vector& images) { + state = State::visible; + popup_queued = true; + hide_ui = false; + unique_id = id; + image_list.clear(); + for (auto& img : images) + image_list.push_back(img); } void process_ui() { @@ -95,7 +105,7 @@ namespace ThemePreviewPopup { // Push a unique ID to the ID stack, to avoid reusing the carousel state from a // different theme. - ID carousel_unique_id{hex_id}; + ID carousel_unique_id{unique_id}; ImVec2 page_size = viewport->Size; ImGuiCarouselSpecs specs{ @@ -107,10 +117,10 @@ namespace ThemePreviewPopup { StyleVar carousel_no_rounding {ImGuiStyleVar_ChildRounding, 0}; if (Carousel images_carousel{"images_carousel", page_size, specs}) { - for (auto [idx, url] : url_list | std::views::enumerate) { + for (auto [idx, location] : image_list | std::views::enumerate) { if (idx > 0) ImGui::SameLine(); - auto tex = ImageLoader::get(url); + auto tex = ImageLoader::get(location); ImGui::SetNextItemAllowOverlap(); ImGui::Image((ImTextureID)tex, page_size); } @@ -132,14 +142,14 @@ namespace ThemePreviewPopup { const bool is_first_page = page_x <= 0; const bool is_last_page = page_x >= columns - 1; - ImVec2 arrow_button_size{120.0f, 60.0f}; + ImVec2 arrow_button_size{120.0f, ImGui::GetFrameHeight()}; float middle_y = viewport->Pos.y + (viewport->Size.y - arrow_button_size.y) * 0.5f; float padding = 30.0f; - ImVec2 button_size{160.0f, 60.0f}; - ImVec2 text_box_size{220.0f, 60.0f}; + ImVec2 button_size{160.0f, ImGui::GetFrameHeight()}; + ImVec2 text_box_size{220.0f, ImGui::GetFrameHeight()}; - float spacing = 20.0f; + float spacing = style.ItemSpacing.x; float total_width = button_size.x + spacing + text_box_size.x + spacing + button_size.x; diff --git a/src/screens/ThemePreviewPopup.h b/src/screens/ThemePreviewPopup.h index 4fa2da3..d2c24ef 100644 --- a/src/screens/ThemePreviewPopup.h +++ b/src/screens/ThemePreviewPopup.h @@ -9,11 +9,13 @@ #pragma once +#include #include #include namespace ThemePreviewPopup { - void show(const std::string& hexId, const std::vector& urlList); + void open(const std::string& id, const std::vector& images); + void open(const std::string& id, const std::vector& images); void process_ui(); } diff --git a/src/screens/ThemezerScreen.cpp b/src/screens/ThemezerScreen.cpp index b15c969..25eeb53 100644 --- a/src/screens/ThemezerScreen.cpp +++ b/src/screens/ThemezerScreen.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ using std::endl; using namespace std::literals; namespace ThemezerScreen { + bool first_fetch = true; uint32_t page = 0; ItemSort sort = ItemSort::CREATED; @@ -93,6 +95,7 @@ namespace ThemezerScreen { small.name = full.name; small.slug = full.slug; small.creator.username = full.creator.username; + small.collagePreview.tinyUrl = full.collagePreview.tinyUrl; small.collagePreview.thumbUrl = full.collagePreview.thumbUrl; small.downloadCount = full.downloadCount; small.downloadUrl = full.downloadUrl; @@ -124,7 +127,7 @@ namespace ThemezerScreen { ThemezerAPI::wiiu::themes({ .paginationArgs = { - .limit = 20, + .limit = 21, .page = page, }, .sort = sort, @@ -148,7 +151,7 @@ namespace ThemezerScreen { themezer_logo = IMG_LoadTexture(renderer, "fs:/vol/content/ui/themezer-logo.png"); qr_sfx = Mix_LoadWAV("fs:/vol/content/sound/qr-scan.wav"); - fetch_page(1); + first_fetch = true; } void finalize() { @@ -160,103 +163,150 @@ namespace ThemezerScreen { cout << "Hello from ThemezerScreen finalize!" << endl; } - void show(const WiiuThemeSmall& theme) { + static void text_limited(float width, const std::string& text) { + // WORKAROUND: prevent tooltip. + auto& io = ImGui::GetIO(); + auto old_mouse_pos = io.MousePos; + ImGui::TextAligned(0.0f, width, text); + io.MousePos = old_mouse_pos; + } + + void show(const WiiuThemeSmall& theme, + const ImVec2& inner_size, + const ImVec2& padding) { + // NOTE: to create a complex button, we create a button with no text, then overlap + // the contents. using namespace ImGui::RAII; - Child theme_frame{ - theme.uuid, - {0, 320}, - ImGuiChildFlags_NavFlattened | ImGuiChildFlags_FrameStyle, - ImGuiWindowFlags_NoSavedSettings - }; + ID id{theme.hexId}; + + const auto& style = ImGui::GetStyle(); + const ImVec2 outer_size = inner_size + 2 * padding; - if (!theme_frame) + // Put everything inside a child window so we can bail out when not visibile. + Child container{"container", outer_size, + ImGuiChildFlags_NavFlattened}; + if (!container) return; - auto thumbnail = ImageLoader::get(theme.collagePreview.thumbUrl); - ImGui::Image((ImTextureID)thumbnail, {426, 240}); + ImVec2 start_pos = padding; - ImGui::SameLine(); + ImGui::SetCursorPos({0, 0}); + if (ImGui::Button("##button", outer_size)) { + ThemeDetailsPopup::open_themezer(theme); + } - { - Group right_group; + // NOTE: when hovered or activated, change the text color to the window bg color. + std::optional dark_text; + if (ImGui::IsItemHovered() || ImGui::IsItemActive()) { + const auto& colors = style.Colors; + dark_text.emplace(ImGuiCol_Text, colors[ImGuiCol_WindowBg]); + } - ImGui::TextWrapped("%s", theme.name.data()); - ImGui::TextWrapped("by: %s", theme.creator.username.data()); - ImGui::Text("Downloads: %u", theme.downloadCount); + ImGui::SetCursorPos(start_pos); + Group grp; - if (ImGui::Button(ICON_FA_INFO_CIRCLE " Details")) { - ThemeDetailsPopup::show_themezer(theme.hexId, theme); - } + { + StyleVar no_border{ImGuiStyleVar_ImageBorderSize, 0}; + auto img = ImageLoader::get(theme.collagePreview.tinyUrl); + ImVec2 img_size = {inner_size.x, inner_size.x * 9.0f / 16.0f}; + ImGui::Image((ImTextureID)img, img_size); + } - ImGui::SameLine(); + { + Font font{nullptr, 24}; + text_limited(inner_size.x, theme.name); + } - if (ImGui::Button(ICON_FA_DOWNLOAD " Download")) { - DownloadThemePopup::show(theme); - } + { + // Show author aligned to the left, downloads to the right. + Font font{nullptr, 18}; + std::string downloads_label = ICON_FA_DOWNLOAD " " + + std::to_string(theme.downloadCount); + float downloads_width = ImGui::CalcTextSize(downloads_label).x; + float author_width = inner_size.x - downloads_width - style.ItemSpacing.x; + text_limited(author_width, "by " + theme.creator.username); + ImGui::SameLine(); + ImGui::SetCursorPosX(inner_size.x - downloads_width); + ImGui::Text(downloads_label); } } void process_ui() { using namespace ImGui::RAII; + { + // NOTE: use a scope to contain all the temporary style changes, so they don't + // leak into the popups at the bottom. #ifdef DEBUG_BG_COLOR - StyleColor green_bg{ImGuiCol_ChildBg, {0.0, 0.5, 0.0, 1.0}}; + StyleColor green_bg{ImGuiCol_ChildBg, {0.0, 0.5, 0.0, 1.0}}; #endif - Child themezer_content{ - "ThemezerContent", - {0, 0}, - ImGuiChildFlags_NavFlattened | - ImGuiChildFlags_AlwaysUseWindowPadding - }; - - if (!themezer_content) - return; + auto &style = ImGui::GetStyle(); + // Remove horizontal padding. + StyleVar no_hori_padding{ImGuiStyleVar_WindowPadding, {0, style.WindowPadding.y}}; + if (Child themezer_content{"ThemezerContent", + {0, 0}, + ImGuiChildFlags_NavFlattened | + ImGuiChildFlags_AlwaysUseWindowPadding}) { + // Title + if (themezer_logo) { + ImGui::Image((ImTextureID)themezer_logo, {300, 86}); + } - // Title - if (themezer_logo) { - ImGui::Image((ImTextureID)themezer_logo, {300, 86}); - } + // Sort and Filter controls + { + Disabled disable_when{ThemezerAPI::is_busy()}; - // Sort and Filter controls - { - Disabled disable_when{ThemezerAPI::is_busy()}; + if (Child filter_order_search_box{ + "FilterOrderSearchBox", + {700.0f, 75.0f}, + ImGuiChildFlags_NavFlattened + }) { - if (Child filter_order_search_box{ - "FilterOrderSearchBox", - {700.0f, 75.0f}, - ImGuiChildFlags_NavFlattened - }) { + SDL_WiiUSetSWKBDKeyboardMode(SDL_WIIU_SWKBD_KEYBOARD_MODE_RESTRICTED); + SDL_WiiUSetSWKBDHintText("Input the name or ID (starting with 'T') of\n" + "a theme to search for it..."); + SDL_WiiUSetSWKBDOKLabel("Search"); + SDL_WiiUSetSWKBDHighlightInitialText(SDL_TRUE); - SDL_WiiUSetSWKBDKeyboardMode(SDL_WIIU_SWKBD_KEYBOARD_MODE_RESTRICTED); - SDL_WiiUSetSWKBDHintText("Input the name or ID (starting with 'T') of\na theme to search for it..."); - SDL_WiiUSetSWKBDOKLabel("Search"); - SDL_WiiUSetSWKBDHighlightInitialText(SDL_TRUE); + ImGui::SetNextItemWidth(280); + if (ImGui::InputTextWithHint("##network_search"s, "Search..."s, query)) { + cout << "Searching: " << query << endl; - ImGui::SetNextItemWidth(300.0f); - if (ImGui::InputTextWithHint("##network_search"s, "Search..."s, query)) { - cout << "Searching: " << query << endl; + exact_id_mode = false; + exact_theme.reset(); + fetching_theme_by_id = false; - exact_id_mode = false; - exact_theme.reset(); - fetching_theme_by_id = false; + fetch_page(1); + } - fetch_page(1); - } + ImGui::SameLine(); - ImGui::SameLine(); + if (ImGui::Button(ICON_FA_QRCODE)) { + QRCodePopup::show(qr_sfx); + } - if (ImGui::Button(ICON_FA_QRCODE)) { - QRCodePopup::show(qr_sfx); - } + ImGui::SameLine(); - ImGui::SameLine(); + ImGui::SetNextItemWidth(220); + if (Combo sort_combo{"##sort_combo"s, sort_to_label(sort)}) { + for (auto new_sort : ThemezerAPI::ItemSortList) { + if (ImGui::Selectable(sort_to_label(new_sort), new_sort == sort)) { + sort = new_sort; - ImGui::SetNextItemWidth(220); - if (Combo sort_combo{"##sort_combo"s, sort_to_label(sort)}) { - for (auto new_sort : ThemezerAPI::ItemSortList) { - if (ImGui::Selectable(sort_to_label(new_sort), new_sort == sort)) { - sort = new_sort; + exact_id_mode = false; + exact_theme.reset(); + fetching_theme_by_id = false; + + fetch_page(1); + } + } + } + + ImGui::SameLine(); + + if (ImGui::Button(order_to_label(order))) { + order = order == SortOrder::ASC ? SortOrder::DESC : SortOrder::ASC; exact_id_mode = false; exact_theme.reset(); @@ -269,120 +319,118 @@ namespace ThemezerScreen { ImGui::SameLine(); - if (ImGui::Button(order_to_label(order))) { - order = order == SortOrder::ASC ? SortOrder::DESC : SortOrder::ASC; - - exact_id_mode = false; - exact_theme.reset(); - fetching_theme_by_id = false; - - fetch_page(1); - } - } - } - - ImGui::SameLine(); - - // Navigation controls - { - Disabled disabled_when{ThemezerAPI::is_busy() || exact_id_mode}; + // Navigation controls + { + Disabled disabled_when{ThemezerAPI::is_busy() || exact_id_mode}; - auto& new_page_info = page_info; + auto& new_page_info = page_info; - { - bool first_page = true; - if (new_page_info) - first_page = new_page_info->page == 1; + { + bool first_page = true; + if (new_page_info) + first_page = new_page_info->page == 1; - Disabled disable_when{first_page}; + Disabled disable_when{first_page}; - if (ImGui::Button(ICON_FA_CHEVRON_LEFT)) - fetch_page(page - 1); - } + if (ImGui::Button(ICON_FA_CHEVRON_LEFT)) + fetch_page(page - 1); + } - ImGui::SameLine(); + ImGui::SameLine(); - if (exact_id_mode) { - ImGui::Text("ID Result"); - } - else if (new_page_info) { - ImGui::Text("Page %u/%u", new_page_info->page, new_page_info->pageCount); - } + if (exact_id_mode) { + ImGui::Text("ID Result"); + } + else if (new_page_info) { + ImGui::Text("Page %u/%u", new_page_info->page, new_page_info->pageCount); + } - ImGui::SameLine(); + ImGui::SameLine(); - { - bool last_page = true; - if (new_page_info) - last_page = new_page_info->page == new_page_info->pageCount; + { + bool last_page = true; + if (new_page_info) + last_page = new_page_info->page == new_page_info->pageCount; - Disabled disable_when{last_page}; + Disabled disable_when{last_page}; - if (ImGui::Button(ICON_FA_CHEVRON_RIGHT)) - fetch_page(page + 1); - } - } + if (ImGui::Button(ICON_FA_CHEVRON_RIGHT)) + fetch_page(page + 1); + } + } - // Trigger exact ID lookup after normal search returns 0 results - if (!ThemezerAPI::is_busy() && - is_item_count_zero && - query.starts_with('T') && - !fetching_theme_by_id && - !exact_id_mode) { + // Trigger exact ID lookup after normal search returns 0 results + if (!ThemezerAPI::is_busy() && + is_item_count_zero && + query.starts_with('T') && + !fetching_theme_by_id && + !exact_id_mode) { - std::string hex_id = query.substr(1); + std::string hex_id = query.substr(1); - cout << "Searching exact Themezer ID: " << hex_id << endl; + cout << "Searching exact Themezer ID: " << hex_id << endl; - fetch_theme_by_id(hex_id); - } + fetch_theme_by_id(hex_id); + } - // Themes List - { - Disabled disable_when{ThemezerAPI::is_busy()}; + // Themes List + { + Disabled disable_when{ThemezerAPI::is_busy()}; #ifdef DEBUG_BG_COLOR - StyleColor brown_bg{ImGuiCol_ChildBg, {0.3, 0.3, 0.0, 1.0}}; + StyleColor brown_bg{ImGuiCol_ChildBg, {0.3, 0.3, 0.0, 1.0}}; #endif - if (Child theme_list{"ThemeList", {0, 0}, ImGuiChildFlags_AlwaysUseWindowPadding}) { + if (Child theme_list{"ThemeGrid"}) { - if (scroll_to_top) { - scroll_to_top = false; - ImGui::SetScrollY(0); - } + if (scroll_to_top) { + scroll_to_top = false; + ImGui::SetScrollY(0); + } - if (exact_id_mode) { - if (fetching_theme_by_id) { - ImGui::Text("Searching by exact Themezer ID..."); - if (ImGui::Button("Cancel Search")) { - exact_id_mode = false; - query = ""; - fetch_page(1); + const ImVec2 inner_size = {320, 260}; + const ImVec2 padding = {12, 12}; + + if (exact_id_mode) { + if (fetching_theme_by_id) { + ImGui::Text("Searching by exact Themezer ID..."); + if (ImGui::Button("Cancel Search")) { + exact_id_mode = false; + query = ""; + fetch_page(1); + } + } + else if (exact_theme) { + show(*exact_theme, inner_size, padding); + ImGui::Spacing(); + if (ImGui::Button("Clear Search")) { + exact_id_mode = false; + query = ""; + fetch_page(1); + } + } + else { + ImGui::Text("No theme found for this Themezer ID."); + } } - } - else if (exact_theme) { - show(*exact_theme); - ImGui::Spacing(); - if (ImGui::Button("Clear Search")) { - exact_id_mode = false; - query = ""; - fetch_page(1); + else { + auto& new_themes = themes; + + if (!new_themes) { + ImGui::Text("Waiting for Themezer to respond..."); + } + else { + const ImVec2 grid_start_pos = ImGui::GetCursorPos(); + const ImVec2 outer_size = inner_size + 2 * padding; + const ImVec2 spacing = {18, 18}; + for (auto [idx, theme] : *new_themes | std::views::enumerate) { + ImVec2 grid_pos = { float(idx % 3), float(idx / 3) }; + ImVec2 pos = grid_pos * (outer_size + spacing); + ImGui::SetCursorPos(grid_start_pos + pos); + show(theme, inner_size, padding); + } + } } } - else { - ImGui::Text("No theme found for this Themezer ID."); - } - } - else { - auto& new_themes = themes; - - if (!new_themes) { - ImGui::Text("Waiting for Themezer to respond..."); - } - else { - for (auto& theme : *new_themes) - show(theme); - } } } } @@ -391,5 +439,10 @@ namespace ThemezerScreen { DownloadThemePopup::process_ui(); InstallThemePopup::process_ui(); QRCodePopup::process_ui(); + + if (first_fetch) { + fetch_page(1); + first_fetch = false; + } } } diff --git a/src/utils.cpp b/src/utils.cpp index 90be254..e57dd36 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -23,8 +24,12 @@ bool CreateParentDirectories(const std::filesystem::path& inputPath) { } void DeletePath(const std::filesystem::path& inputPath) { + if (inputPath.empty()) { + cerr << "attempting to delete empty path!" << endl; + return; + } if (!exists(inputPath)) { - cerr << inputPath << " could not be found!" << endl; + // cerr << inputPath << " could not be found!" << endl; return; } @@ -48,8 +53,9 @@ void DeletePath(const std::filesystem::path& inputPath) { } } -static std::u32string format_codepoint(char32_t c) -{ +static +std::u32string +format_codepoint(char32_t c) { char buffer[32]; std::snprintf(buffer, sizeof buffer, "U+%04X", c); std::u32string result; @@ -58,8 +64,8 @@ static std::u32string format_codepoint(char32_t c) return result; } -std::filesystem::path sanitize_element(const std::filesystem::path& input) -{ +std::filesystem::path +sanitize_element(const std::filesystem::path& input) { std::u32string output_str; std::u32string input_str = input.u32string(); for (auto c : input_str) { @@ -98,7 +104,8 @@ std::filesystem::path sanitize_element(const std::filesystem::path& input) return output_str; } -std::filesystem::path sanitize(const std::filesystem::path& input) { +std::filesystem::path +sanitize(const std::filesystem::path& input) { std::filesystem::path output; auto is_newlib_root = [](const std::filesystem::path& name) -> bool { @@ -113,3 +120,47 @@ std::filesystem::path sanitize(const std::filesystem::path& input) { } return output; } + +std::string +as_lower_case(const std::string& input) +{ + std::string output = input; + for (char &c : output) + c = std::tolower(static_cast(c)); + return output; +} + +std::filesystem::path +theme_id_to_cached_thumbnail_path(const std::string& themeID) +{ + return THEMIIFY_THUMBNAILS / (make_theme_id_filename(themeID) + ".webp"); +} + +std::filesystem::path +make_utheme_filename(const std::string& slug, + const std::string& hexID) { + return sanitize(THEMES_ROOT / (slug + "-" + hexID + ".utheme")); +} + +std::string +make_theme_id_filename(const std::string& themeID) +{ + std::string result = themeID; + // Simply delete the ':' from the ID. + std::erase(result, ':'); + return result; +} + +// TODO: this belongs to installer.cpp +std::filesystem::path +make_theme_folder_name(const std::string& name, + const std::optional& themeID) +{ + std::string result = name; + if (themeID) { + std::string clean_theme_id = *themeID; + std::erase(clean_theme_id, ':'); + result += " (" + clean_theme_id + ")"; + } + return sanitize_element(result); +} diff --git a/src/utils.h b/src/utils.h index 7aafab8..b695dc6 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,6 +1,6 @@ /* * Themiify - A theme manager for the Nintendo Wii U - * Copyright (C) 2026 Fangal-Airbag + * Copyright (C) 2026 Fangal-Airbag * Copyright (C) 2026 AlphaCraft9658 * Copyright (C) 2026 Daniel K. O. * @@ -11,6 +11,7 @@ #include #include +#include #include #ifndef THEMIIFY_VERSION @@ -38,5 +39,24 @@ bool CreateParentDirectories(const std::filesystem::path& inputPath); void DeletePath(const std::filesystem::path& inputPath); -std::filesystem::path sanitize_element(const std::filesystem::path& input); -std::filesystem::path sanitize(const std::filesystem::path& input); +std::filesystem::path +sanitize_element(const std::filesystem::path& input); + +std::filesystem::path +sanitize(const std::filesystem::path& input); + +std::string +as_lower_case(const std::string& input); + +std::filesystem::path +theme_id_to_cached_thumbnail_path(const std::string& themeID); + +std::filesystem::path +make_utheme_filename(const std::string& slug, const std::string& hexID); + +std::string +make_theme_id_filename(const std::string& themeID); + +std::filesystem::path +make_theme_folder_name(const std::string& name, + const std::optional& themeID);