diff --git a/CMakeLists.txt b/CMakeLists.txt index 3748e5d..5bcc77c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(themiify src/humanize.cpp src/installer.cpp src/ThemezerAPI.cpp + src/timer.cpp src/ImageLoader.cpp src/DownloadManager.cpp src/Camera.cpp @@ -68,7 +69,7 @@ target_link_libraries(themiify PRIVATE quirc hips PkgConfig::zlib - PkgConfig::freetype2 + PkgConfig::freetype2 PkgConfig::harfbuzz PkgConfig::libcurl PkgConfig::libpng @@ -78,7 +79,7 @@ target_link_libraries(themiify PRIVATE PkgConfig::SDL2_mixer ) -target_compile_definitions(themiify PRIVATE THEMIIFY_VERSION="1.1") +target_compile_definitions(themiify PRIVATE THEMIIFY_VERSION="1.1-dev") # run program using wiiload diff --git a/content/ui/theme-placeholder-icon.png b/content/ui/theme-placeholder-icon.png deleted file mode 100644 index fae3979..0000000 Binary files a/content/ui/theme-placeholder-icon.png and /dev/null differ diff --git a/content/ui/theme-placeholder-loading.png b/content/ui/theme-placeholder-loading.png new file mode 100644 index 0000000..507f0b4 Binary files /dev/null and b/content/ui/theme-placeholder-loading.png differ diff --git a/content/ui/theme-placeholder-no-preview.png b/content/ui/theme-placeholder-no-preview.png new file mode 100644 index 0000000..eafdfb5 Binary files /dev/null and b/content/ui/theme-placeholder-no-preview.png differ diff --git a/content/ui/theme-placeholder-no-theme.png b/content/ui/theme-placeholder-no-theme.png new file mode 100644 index 0000000..032a940 Binary files /dev/null and b/content/ui/theme-placeholder-no-theme.png differ diff --git a/content/ui/theme-placeholder-random.png b/content/ui/theme-placeholder-random.png new file mode 100644 index 0000000..b3aafe8 Binary files /dev/null and b/content/ui/theme-placeholder-random.png differ diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 1826e7c..77d7a6b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -19,14 +19,13 @@ add_library(imgui STATIC ) target_include_directories(imgui PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/imgui ${CMAKE_CURRENT_SOURCE_DIR}/imgui/backends ${CMAKE_CURRENT_SOURCE_DIR}/imgui/misc/carousel ${CMAKE_CURRENT_SOURCE_DIR}/imgui/misc/cpp ${CMAKE_CURRENT_SOURCE_DIR}/imgui/misc/freetype ) -target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG="custom_imconfig.h") +target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/custom_imconfig.h") target_link_libraries(imgui PUBLIC PkgConfig::sdl2 PkgConfig::freetype2) diff --git a/external/glaze b/external/glaze index 9618ef8..0a21fd9 160000 --- a/external/glaze +++ b/external/glaze @@ -1 +1 @@ -Subproject commit 9618ef8ef503c72b10e7b2769839784f09d24d2c +Subproject commit 0a21fd9dbd36963790b6f26b9539050e13d78649 diff --git a/src/App.cpp b/src/App.cpp index d3a58b8..d016ce1 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -15,7 +15,10 @@ #include "DownloadManager.h" #include "Camera.h" #include "utils.h" +#include "installer.h" +#include "timer.hpp" +#include #include #include #include @@ -53,6 +56,7 @@ using std::cout; using std::cerr; using std::endl; +using namespace std::literals; namespace App { SDL_Window *window; @@ -238,6 +242,8 @@ namespace App { Camera::initialize(renderer); Camera::open(); + Installer::initialize(); + DownloadManager::initialize(user_agent); ImageLoader::initialize(renderer); NavBar::initialize(renderer); @@ -277,6 +283,7 @@ namespace App { NavBar::finalize(); ContentPanel::finalize(); + Installer::finalize(); ImageLoader::finalize(); DownloadManager::finalize(); @@ -348,6 +355,8 @@ namespace App { } } + ImageLoader::process(); + ImGui_ImplSDLRenderer2_NewFrame(); ImGui_ImplSDL2_NewFrame(); @@ -378,8 +387,13 @@ namespace App { StyleVar restore_border{ImGuiStyleVar_WindowBorderSize, orig_border}; StyleVar restore_rounding{ImGuiStyleVar_WindowRounding, orig_rounding}; NavBar::process_ui(); - ImGui::SameLine(0, 0); // NOTE: ignore ItemSpacing - ContentPanel::process_ui(NavBar::get_current_tab()); + ImGui::SameLine(0, 9); // NOTE: override ItemSpacing + { + TimerReporter slow_content{std::cout, + "ContentPanel::process_ui()", + 10ms}; + ContentPanel::process_ui(NavBar::get_current_tab()); + } } } diff --git a/src/ImageLoader.cpp b/src/ImageLoader.cpp index 5445ecb..7264b17 100644 --- a/src/ImageLoader.cpp +++ b/src/ImageLoader.cpp @@ -16,14 +16,16 @@ #include #include #include -#include +#include #include #include #include #include #include #include +#include #include +#include #include #include @@ -36,22 +38,43 @@ #include "App.h" #include "async_queue.hpp" -#include "thread_safe.hpp" +#include "timer.hpp" #include "tracer.hpp" -using std::cout; -using std::cerr; using std::endl; using namespace std::literals; +#define COUT (resources->sync_cout) +#define CERR (resources->sync_cerr) + +#define TCOUT (resources->sync_tcout) +#define TCERR (resources->sync_tcerr) + +// #define MEASURE_IMG_LOAD +// #define MEASURE_CREATE_TEXTURE +#define MEASURE_GET + namespace ImageLoader { + + struct Resources { + std::osyncstream sync_cout{std::cout}; + std::osyncstream sync_cerr{std::cerr}; + + std::osyncstream sync_tcout{std::cout}; + std::osyncstream sync_tcerr{std::cerr}; + + }; + + std::optional resources; + std::string user_agent = App::user_agent; - std::filesystem::path content_prefix = "fs:/vol/content"; + const std::filesystem::path content_prefix = "fs:/vol/content"; const std::size_t max_cache_size = 64; - std::uint64_t use_counter = 0; + using Timestamp = std::uint64_t; + Timestamp timestamp = 0; SDL_Renderer *renderer = nullptr; SDL_Texture *load_error_image = nullptr; @@ -64,57 +87,77 @@ namespace ImageLoader { requested, loading, loaded, + converted, error, }; std::string to_string(LoadState st); - struct CacheEntry { - std::atomic state{LoadState::unloaded}; - std::uint64_t last_use = 0; + struct CacheEntry : std::enable_shared_from_this { + std::atomic state{LoadState::requested}; + Timestamp last_use = 0; SDL_Surface *img = nullptr; SDL_Texture *tex = nullptr; + std::string location; + CURL *easy = nullptr; curl_slist *headers = nullptr; std::optional> raw_buf; - std::string location; - }; - using cache_t = std::unordered_map; - thread_safe safe_cache; + CacheEntry(Timestamp now, + const std::string& location); - async_queue requests_queue; + ~CacheEntry() noexcept; + + void prepare_download(); + void cleanup_download(); + + static std::shared_ptr get_entry(CURL* handle); + }; // struct CacheEntry + + using CacheEntryPtr = std::shared_ptr; + using Cache = std::unordered_map; + Cache cache; + + async_queue requests_queue; std::jthread worker_thread; - static void destroy_entry(CacheEntry& entry) + async_queue convert_queue; + + CacheEntry::CacheEntry(Timestamp now, + const std::string& location) : + last_use{now}, + location{location} + {} + + CacheEntry::~CacheEntry() + noexcept { - if (multi && entry.easy) - curl_multi_remove_handle(multi, entry.easy); + if (multi && easy) + curl_multi_remove_handle(multi, easy); - if (entry.headers) { - curl_slist_free_all(entry.headers); - entry.headers = nullptr; + if (headers) { + curl_slist_free_all(headers); + headers = nullptr; } - if (entry.easy) { - curl_easy_cleanup(entry.easy); - entry.easy = nullptr; + if (easy) { + curl_easy_cleanup(easy); + easy = nullptr; } - if (entry.tex) { - SDL_DestroyTexture(entry.tex); - entry.tex = nullptr; + if (tex) { + SDL_DestroyTexture(tex); + tex = nullptr; } - if (entry.img) { - SDL_FreeSurface(entry.img); - entry.img = nullptr; + if (img) { + SDL_FreeSurface(img); + img = nullptr; } - - entry.raw_buf.reset(); } static size_t write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) @@ -130,17 +173,82 @@ namespace ImageLoader { return total; } + void + CacheEntry::prepare_download() + { + easy = curl_easy_init(); + if (!easy) + throw std::runtime_error{"curl_easy_init() failed"}; + + curl_easy_setopt(easy, CURLOPT_PRIVATE, this); + + headers = curl_slist_append(headers, "Accept: image/*"); + curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(easy, CURLOPT_URL, location.data()); + curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(easy, CURLOPT_AUTOREFERER, 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_WRITEFUNCTION, write_cb); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, this); + + if (!user_agent.empty()) + curl_easy_setopt(easy, CURLOPT_USERAGENT, user_agent.data()); + + curl_multi_add_handle(multi, easy); + } + + void + CacheEntry::cleanup_download() + { + curl_multi_remove_handle(multi, easy); + + if (headers) { + curl_slist_free_all(headers); + headers = nullptr; + } + + if (easy) { + curl_easy_cleanup(easy); + easy = nullptr; + } + raw_buf.reset(); + } + + CacheEntryPtr + CacheEntry::get_entry(CURL *handle) + { + if (!handle) + return {}; + CacheEntry* self = nullptr; + auto e = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &self); + if (e != CURLE_OK) + return {}; + return self->shared_from_this(); + } + void worker_func(std::stop_token token); void initialize(SDL_Renderer* rend) { + resources.emplace(); + COUT.rdbuf()->set_emit_on_sync(true); + CERR.rdbuf()->set_emit_on_sync(true); + TCOUT.rdbuf()->set_emit_on_sync(true); + TCERR.rdbuf()->set_emit_on_sync(true); + curl_global_init(CURL_GLOBAL_DEFAULT); renderer = rend; loading_image = IMG_LoadTexture( renderer, - "fs:/vol/content/ui/theme-placeholder-icon.png"); + "fs:/vol/content/ui/theme-placeholder-loading.png"); load_error_image = IMG_LoadTexture( renderer, @@ -154,7 +262,7 @@ namespace ImageLoader { requests_queue.reset(); - cout << "ImageLoader: launching worker thread." << endl; + COUT << "ImageLoader: launching worker thread." << endl; worker_thread = std::jthread{worker_func}; assert((std::atomic{}.is_lock_free())); @@ -162,24 +270,23 @@ namespace ImageLoader { void finalize() { - cout << "Stopping requests_queue" << endl; + COUT << "Stopping convert_queue" << endl; + convert_queue.stop(); + + COUT << "Stopping requests_queue" << endl; requests_queue.stop(); - cout << "Destroying thread" << endl; + COUT << "Destroying thread" << endl; worker_thread = {}; - cout << "Thread destroyed" << endl; + COUT << "Thread destroyed" << endl; - { - cout << "Clearing safe_cache" << endl; - auto cache = safe_cache.lock(); + convert_queue.clear(); + requests_queue.clear(); - for (auto& [location, entry] : *cache) - destroy_entry(entry); - - cache->clear(); - } + COUT << "Clearing cache" << endl; + cache.clear(); - cout << "Destroying predefined icons" << endl; + COUT << "Destroying predefined icons" << endl; if (loading_image) { SDL_DestroyTexture(loading_image); @@ -192,152 +299,144 @@ namespace ImageLoader { } curl_global_cleanup(); + + resources.reset(); } SDL_Texture* get(const std::string& location) { - ++use_counter; +#ifdef MEASURE_GET + TimerReporter get_timer{COUT, "ImageLoader::get()", 5ms}; +#endif - auto cache = safe_cache.lock(); - auto it = cache->find(location); + CacheEntryPtr entry; + auto it = cache.find(location); + if (it != cache.end()) + entry = it->second; try { - if (it != cache->end()) { - auto& entry = it->second; - entry.last_use = use_counter; - - switch (entry.state.load()) { - case LoadState::loaded: - if (!entry.tex && entry.img) { - entry.tex = SDL_CreateTextureFromSurface(renderer, entry.img); - if (entry.tex) - SDL_SetTextureBlendMode(entry.tex, SDL_BLENDMODE_BLEND); - - SDL_FreeSurface(entry.img); - entry.img = nullptr; - } - - if (entry.tex) - return entry.tex; + if (entry) { + entry->last_use = timestamp; - return load_error_image; + switch (entry->state.load()) { + case LoadState::converted: + return entry->tex; case LoadState::error: return load_error_image; case LoadState::requested: case LoadState::loading: + case LoadState::loaded: return loading_image; case LoadState::unloaded: - entry.state = LoadState::requested; - requests_queue.push(location); + entry->state = LoadState::requested; + requests_queue.push(entry); return loading_image; default: throw std::logic_error{ - "invalid entry state: " + to_string(entry.state.load()) + "invalid entry state: " + to_string(entry->state.load()) }; } + } else { + // entry not found, so prepare a request + entry = std::make_shared(timestamp, location); + cache.emplace(location, entry); + requests_queue.push(std::move(entry)); + return loading_image; } - - auto& entry = (*cache)[location]; - entry.state = LoadState::requested; - entry.last_use = use_counter; - - requests_queue.push(location); - - return loading_image; } catch (std::exception& e) { - cerr << "ERROR: ImageLoader::get(): " << e.what() << endl; + CERR << "ERROR: ImageLoader::get(): " << e.what() << endl; return load_error_image; } } - CacheEntry* find(thread_safe::guard& cache, CURL* easy) + /* + * NOTE: GX2 only supports a few texture formats, so this allows converting the image + * to a supported format early. + */ + static SDL_Surface* optimized(SDL_Surface* input) { - for (auto& [location, entry] : *cache) { - if (entry.easy == easy) - return &entry; + if (!input) + return nullptr; + switch (input->format->format) { + // Supported formats, see SDL_render_wiiu.h from the SDL2 port. + case SDL_PIXELFORMAT_ARGB4444: + case SDL_PIXELFORMAT_RGBA4444: + case SDL_PIXELFORMAT_ABGR4444: + case SDL_PIXELFORMAT_BGRA4444: + case SDL_PIXELFORMAT_ARGB1555: + case SDL_PIXELFORMAT_ABGR1555: + case SDL_PIXELFORMAT_RGBA5551: + case SDL_PIXELFORMAT_BGRA5551: + case SDL_PIXELFORMAT_RGB565: + case SDL_PIXELFORMAT_BGR565: + case SDL_PIXELFORMAT_RGBX8888: + case SDL_PIXELFORMAT_RGBA8888: + case SDL_PIXELFORMAT_ARGB8888: + case SDL_PIXELFORMAT_BGRX8888: + case SDL_PIXELFORMAT_BGRA8888: + case SDL_PIXELFORMAT_ABGR8888: + case SDL_PIXELFORMAT_ARGB2101010: + return input; + default: + auto output = SDL_ConvertSurfaceFormat(input, SDL_PIXELFORMAT_RGBA32, 0); + SDL_FreeSurface(input); + return output; } - - return nullptr; } - void process_one_request(const std::string& location) + void process_one_request(CacheEntryPtr& entry) { - auto cache = safe_cache.lock(); - auto it = cache->find(location); - - if (it == cache->end()) + if (!entry) return; - - auto& entry = it->second; - LoadState expected = LoadState::requested; - if (!entry.state.compare_exchange_strong(expected, LoadState::loading)) { - cerr << "ERROR: ImageLoader::process_one_request() wrong cache entry state: " - << to_string(expected) - << endl; + if (!entry->state.compare_exchange_strong(expected, LoadState::loading)) { + TCERR << "ERROR: ImageLoader::process_one_request() wrong cache entry state: " + << to_string(expected) + << endl; return; } - entry.location = location; - try { - if (location.starts_with("http://") || location.starts_with("https://")) { - entry.easy = curl_easy_init(); - if (!entry.easy) - throw std::runtime_error{"curl_easy_init() failed"}; - - 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, 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, ""); - curl_easy_setopt(entry.easy, CURLOPT_TRANSFER_ENCODING, 1L); - curl_easy_setopt(entry.easy, CURLOPT_BUFFERSIZE, 65536L); - curl_easy_setopt(entry.easy, CURLOPT_TCP_NODELAY, 0L); - curl_easy_setopt(entry.easy, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(entry.easy, CURLOPT_WRITEFUNCTION, write_cb); - curl_easy_setopt(entry.easy, CURLOPT_WRITEDATA, &entry); - - if (!user_agent.empty()) - curl_easy_setopt(entry.easy, CURLOPT_USERAGENT, user_agent.c_str()); - - curl_multi_add_handle(multi, entry.easy); + if (entry->location.starts_with("http://") || + entry->location.starts_with("https://")) { + entry->prepare_download(); } - else if (location.starts_with("ui/")) { - const auto path = content_prefix / location; - - entry.img = IMG_Load(path.c_str()); - if (!entry.img) + else { + // Assume everything else is a local path. + std::filesystem::path img_path; + if (entry->location.starts_with("ui/")) + img_path = content_prefix / entry->location; + else + img_path = entry->location; + +#ifdef MEASURE_IMG_LOAD + Timer timer{"IMG_Load()"}; + timer.start(); +#endif + entry->img = optimized(IMG_Load(img_path.c_str())); +#ifdef MEASURE_IMG_LOAD + timer.stop(); + timer.print(TCOUT) << endl; +#endif + 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"}; + entry->state = LoadState::loaded; + convert_queue.push(entry); } } catch (std::exception& e) { - std::println(cerr, + std::println(TCERR, "ERROR: ImageLoader::process_one_request(): location=\"{}\", exception={}", - location, + entry->location, e.what()); - entry.state = LoadState::error; + entry->state = LoadState::error; } } @@ -380,24 +479,32 @@ namespace ImageLoader { } } - std::uint64_t& by_last_use(cache_t::iterator it) noexcept + Timestamp& by_last_use(const Cache::iterator& it) noexcept { - return it->second.last_use; + return it->second->last_use; } + struct CompareLastUse { + bool + operator ()(const Cache::iterator& a, const Cache::iterator& b) + const noexcept + { + return by_last_use(a) < by_last_use(b); + } + }; + void trim_cache() { - auto cache = safe_cache.lock(); - - if (cache->size() <= max_cache_size) + if (cache.size() <= max_cache_size) return; - std::size_t excess = cache->size() - max_cache_size; + std::size_t excess = cache.size() - max_cache_size; - std::vector to_remove(excess); +#if 1 + std::vector to_remove(excess); auto to_remove_end = to_remove.begin(); - for (auto it = cache->begin(); it != cache->end(); ++it) { + for (auto it = cache.begin(); it != cache.end(); ++it) { if (to_remove_end != to_remove.end()) { *to_remove_end++ = it; std::ranges::push_heap(to_remove.begin(), @@ -412,10 +519,48 @@ namespace ImageLoader { } } } + for (auto it : to_remove) + cache.erase(it); +#else + std::priority_queue, + CompareLastUse> to_remove; + for (auto it = cache.begin(); it != cache.end(); ++it) { + to_remove.push(it); + if (to_remove.size() > excess) + to_remove.pop(); + } + while (!to_remove.empty()) { + cache.erase(to_remove.top()); + to_remove.pop(); + } +#endif + } - for (auto it : to_remove) { - destroy_entry(it->second); - cache->erase(it); + void process() + { + trim_cache(); + ++timestamp; + + // Convert exactly one surface into a texture. + if (auto entry = convert_queue.try_pop()) { + if ((*entry)->state == LoadState::loaded) { +#ifdef MEASURE_CREATE_TEXTURE + Timer timer{"SDL_CreateTextureFromSurface()"}; + timer.start(); +#endif + (*entry)->tex = SDL_CreateTextureFromSurface(renderer, (*entry)->img); +#ifdef MEASURE_CREATE_TEXTURE + timer.stop(); + timer.print(COUT) << endl; +#endif + if ((*entry)->tex) + SDL_SetTextureBlendMode((*entry)->tex, SDL_BLENDMODE_BLEND); + + SDL_FreeSurface((*entry)->img); + (*entry)->img = nullptr; + (*entry)->state = LoadState::converted; + } } } @@ -429,11 +574,9 @@ namespace ImageLoader { CURL* easy = msg->easy_handle; - auto cache = safe_cache.lock(); - auto* entry = find(cache, easy); - + auto entry = CacheEntry::get_entry(easy); if (!entry) { - cerr << "ERROR: ImageLoader::handle_finished_downloads(): failed to find entry" << endl; + TCERR << "ERROR: ImageLoader::handle_finished_downloads(): no entry" << endl; curl_multi_remove_handle(multi, easy); curl_easy_cleanup(easy); continue; @@ -465,30 +608,27 @@ namespace ImageLoader { if (!rw) throw std::runtime_error{SDL_GetError()}; - entry->img = IMG_Load_RW(rw, 1); +#ifdef MEASURE_IMG_LOAD + Timer timer{"IMG_Load_RW()"}; + timer.start(); +#endif + entry->img = optimized(IMG_Load_RW(rw, 1)); +#ifdef MEASURE_IMG_LOAD + timer.stop(); + timer.print(TCOUT) << endl; +#endif if (!entry->img) throw std::runtime_error{IMG_GetError()}; entry->state = LoadState::loaded; + convert_queue.push(entry); } catch (std::exception& e) { - cerr << "ERROR: ImageLoader::handle_finished_downloads(): " << e.what() << endl; + TCERR << "ERROR: ImageLoader::handle_finished_downloads(): " << e.what() << endl; entry->state = LoadState::error; } - curl_multi_remove_handle(multi, entry->easy); - - if (entry->headers) { - curl_slist_free_all(entry->headers); - entry->headers = nullptr; - } - - if (entry->easy) { - curl_easy_cleanup(entry->easy); - entry->easy = nullptr; - } - - entry->raw_buf.reset(); + entry->cleanup_download(); } } @@ -503,29 +643,28 @@ namespace ImageLoader { curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, 10L); while (!token.stop_requested()) { - auto location = requests_queue.try_pop_for(50ms); + auto entry = requests_queue.try_pop_for(50ms); - if (location) { - process_one_request(*location); + if (entry) { + process_one_request(*entry); } - else if (location.error() == async_queue_error::stop) { + else if (entry.error() == async_queue_error::stop) { break; } - else if (location.error() == async_queue_error::locked) { - cout << "WARNING: requests_queue was locked" << endl; + else if (entry.error() == async_queue_error::locked) { + TCOUT << "WARNING: requests_queue was locked" << endl; } int running = 0; curl_multi_perform(multi, &running); handle_finished_downloads(); - trim_cache(); - std::this_thread::sleep_for(50ms); + // std::this_thread::sleep_for(50ms); } } catch (std::exception& e) { - cerr << "ERROR: ImageLoader::worker_func(): " << e.what() << endl; + TCERR << "ERROR: ImageLoader::worker_func(): " << e.what() << endl; } if (multi) { diff --git a/src/ImageLoader.h b/src/ImageLoader.h index 6806b0d..9a1543f 100644 --- a/src/ImageLoader.h +++ b/src/ImageLoader.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. * @@ -18,5 +18,7 @@ namespace ImageLoader { void finalize(); + void process(); + SDL_Texture *get(const std::string& location); -} \ No newline at end of file +} diff --git a/src/async_queue.hpp b/src/async_queue.hpp index e2ad388..2c42991 100644 --- a/src/async_queue.hpp +++ b/src/async_queue.hpp @@ -151,6 +151,14 @@ class async_queue { return result; } + + void + clear() + { + std::lock_guard guard{mutex}; + queue = {}; + } + }; // class async_queue #endif diff --git a/src/installer.cpp b/src/installer.cpp index 3b256e8..5891858 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,8 @@ #include "installer.h" #include "utils.h" +#include "tracer.hpp" +#include "thread_safe.hpp" using std::cout; using std::cerr; @@ -41,7 +44,7 @@ using namespace std::literals; namespace Installer { - std::unordered_map regionLangMap = { + std::unordered_map regionLangMap = { {"UsEn", "UsEnglish/Message/AllMessage.szs"}, {"UsFr", "UsFrench/Message/AllMessage.szs"}, {"UsPt", "UsPortuguese/Message/AllMessage.szs"}, @@ -57,23 +60,133 @@ namespace Installer { {"JpJa", "JpJapanese/Message/AllMessage.szs"} }; - static std::string ReadWholeFile(std::ifstream& file) - { - std::ostringstream ss; - ss << file.rdbuf(); - return ss.str(); - } + struct StyleMiiUCfg { + std::string enabledThemes = {}; + bool mashupThemes = false; + bool showNotification = false; + bool suffleThemes = false; // NOTE the typo! + bool themeManagerEnabled = false; + }; + + struct StyleMiiUJson { + StyleMiiUCfg storageitems; + }; + + std::optional currentStyleMiiUCfg; + + std::unordered_set enabled_themes; - std::filesystem::path GetMenuContentPath() { - uint64_t menuTitleID = _SYSGetSystemApplicationTitleId(SYSTEM_APP_ID_WII_U_MENU); + using InstalledThemesList = std::vector; + + thread_safe safe_themes_list; + + namespace { + + std::filesystem::path + realGetStyleMiiUConfigPath() + { + char buffer[256]; + auto res = Mocha_GetEnvironmentPath(buffer, sizeof buffer); + if (res) { + std::runtime_error e{"Could not get Aroma environment path: "s + + Mocha_GetStatusStr(res)}; + cerr << "ERROR: " << e.what() << endl; + throw e; + } + return std::filesystem::path{buffer} / "plugins/config/style-mii-u.json"; + } - uint32_t menuIDParentDir = menuTitleID >> 32; - uint32_t menuIDChildDir = menuTitleID; + std::filesystem::path + GetStyleMiiUConfigPath() + { + static const std::filesystem::path result = realGetStyleMiiUConfigPath(); + return result; + } + + static void LoadStyleMiiUCfg() { + currentStyleMiiUCfg.reset(); + enabled_themes.clear(); + auto stylemiiu_cfg_path = GetStyleMiiUConfigPath(); + if (!exists(stylemiiu_cfg_path)) { + // If config doesn't exist yet, it's not an error. + currentStyleMiiUCfg.emplace(); + return; + } + StyleMiiUJson json; + glz::ex::read_file_json(json, stylemiiu_cfg_path.string(), std::string{}); + currentStyleMiiUCfg = std::move(json.storageitems); + if (currentStyleMiiUCfg->suffleThemes) { + auto themes = split(currentStyleMiiUCfg->enabledThemes, "|", true); + for (const auto& theme : themes) + if (!theme.empty()) + enabled_themes.insert(theme); + } else { + enabled_themes.insert(currentStyleMiiUCfg->enabledThemes); + } + } + + static void SaveStyleMiiUCfg() { + if (!currentStyleMiiUCfg) + throw std::runtime_error{"No valid StyleMiiU config state to store."}; + StyleMiiUJson json; + json.storageitems = *currentStyleMiiUCfg; + glz::ex::write_file_json( + json, + GetStyleMiiUConfigPath().string(), + std::string{} + ); + } + + StyleMiiUCfg& + GetStyleMiiUCfg() + { + try { + if (!currentStyleMiiUCfg) + LoadStyleMiiUCfg(); + return *currentStyleMiiUCfg; + } + catch (std::exception& e) { + cerr << "ERROR in GetStyleMiiUCfg(): " << e.what() << endl; + throw; + } + } - char splitMenuID[18]; - snprintf(splitMenuID, sizeof splitMenuID, "%08x/%08x", menuIDParentDir, menuIDChildDir); + std::filesystem::path GetMenuContentPath() { + uint64_t menuTitleID = _SYSGetSystemApplicationTitleId(SYSTEM_APP_ID_WII_U_MENU); - return "storage_mlc:/sys/title" / std::filesystem::path{splitMenuID} / "content"; + uint32_t menuIDParentDir = menuTitleID >> 32; + uint32_t menuIDChildDir = menuTitleID; + + char splitMenuID[18]; + snprintf(splitMenuID, sizeof splitMenuID, "%08x/%08x", menuIDParentDir, menuIDChildDir); + + return "storage_mlc:/sys/title" / std::filesystem::path{splitMenuID} / "content"; + } + + } // namespace + + + void initialize() + { + TRACE_FUNC; + + try { + LoadStyleMiiUCfg(); + } + catch (std::exception &e) { + cerr << "ERROR: failed to load StyleMiiU config: " << e.what() << endl; + } + } + + void finalize() + { + TRACE_FUNC; + try { + SaveStyleMiiUCfg(); + } + catch (std::exception &e) { + cerr << "ERROR: failed to save StyleMiiU config: " << e.what() << endl; + } } void CreateCacheFile(std::ifstream &sourceFile, const std::filesystem::path &outputPath) { @@ -586,101 +699,29 @@ namespace Installer { DeletePath(meta.themePath); } - std::string GetStyleMiiUConfigPath() { - char environmentPathBuffer[0x100]; - - MochaUtilsStatus res; - if ((res = Mocha_GetEnvironmentPath(environmentPathBuffer, sizeof(environmentPathBuffer))) != MOCHA_RESULT_SUCCESS) { - cerr << "Failed to get environment path. Are you running on Aroma? Result: " - << Mocha_GetStatusStr(res) << endl; - return ""; // TOOD: should we use the default aroma path as fallback? - } - - return std::string(environmentPathBuffer) + "/plugins/config/style-mii-u.json"; - } - - 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::string jsonStr = ReadWholeFile(configFile); - configFile.close(); - - 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; - } - - configJson["storageitems"]["enabledThemes"] = meta.themePath.filename().string(); - - auto outputJson = glz::write(configJson); - if (!outputJson) { - cerr << "Failed to serialize config json" << 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(); - - std::println("Succesfully set {} as current StyleMiiU theme!", - meta.themePath.string()); - - return true; - } - catch (std::exception& e) { - cerr << "ERROR in SetCurrentTheme(): " << e.what() << endl; - return false; - } - } - - std::string GetCurrentThemeName() { - std::string styleMiiUConfigPath = GetStyleMiiUConfigPath(); - - std::ifstream configFile(styleMiiUConfigPath); - if (!configFile.is_open()) { - cerr << "Failed to open config file: " << styleMiiUConfigPath << endl; - return ""; - } - - 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::format_error(err, jsonStr) << endl; - return ""; - } - - return configJson.at("storageitems").at("enabledThemes").get(); + std::string + GetCurrentThemeName() + { + auto& cfg = GetStyleMiiUCfg(); + if (enabled_themes.size() == 1) + return *enabled_themes.begin(); + else + return {}; } std::optional - GetCurrentTheme() { - // TODO: handle shuffle mode + GetCurrentTheme() + { auto themeName = GetCurrentThemeName(); + if (themeName.empty()) + return {}; auto themePath = THEMES_ROOT / themeName; if (!exists(themePath)) return {}; InstalledThemeMetadata result; if (!GetInstalledThemeMetadata(themePath, result)) return {}; - return {std::move(result)}; + return { std::move(result) }; } std::filesystem::path @@ -689,4 +730,51 @@ namespace Installer { return THEMES_ROOT / make_theme_folder_name(meta.themeName, meta.themeID); } + + bool IsShuffling() { + return GetStyleMiiUCfg().suffleThemes; + } + + void ToggleShuffling() { + auto &cfg = GetStyleMiiUCfg(); + cfg.suffleThemes = !cfg.suffleThemes; + if (!cfg.suffleThemes) { + enabled_themes.clear(); + cfg.enabledThemes.clear(); + } + } + + bool IsEnabled(const InstalledThemeMetadata& meta) { + return enabled_themes.contains(meta.themePath.filename()); + } + + void Enable(const InstalledThemeMetadata& meta) { + auto& cfg = GetStyleMiiUCfg(); + if (cfg.suffleThemes) { + enabled_themes.insert(meta.themePath.filename()); + cfg.enabledThemes.clear(); + for (const auto& theme : enabled_themes) + cfg.enabledThemes += theme + "|"; + } else { + enabled_themes.clear(); + auto theme_str = meta.themePath.filename().string(); + enabled_themes.insert(theme_str); + cfg.enabledThemes = theme_str; + } + } + + void Disable(const InstalledThemeMetadata& meta) { + auto& cfg = GetStyleMiiUCfg(); + auto theme_str = meta.themePath.filename().string(); + if (cfg.suffleThemes) { + enabled_themes.erase(theme_str); + cfg.enabledThemes.clear(); + for (const auto& theme : enabled_themes) + cfg.enabledThemes += theme + "|"; + } else { + if (enabled_themes.erase(theme_str)) + cfg.enabledThemes.clear(); + } + } + } // namespace Installer diff --git a/src/installer.h b/src/installer.h index 7b28499..fb542dc 100644 --- a/src/installer.h +++ b/src/installer.h @@ -32,14 +32,6 @@ namespace Installer { std::vector files; }; - 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; @@ -49,6 +41,19 @@ namespace Installer { using error_function_sig = void (const std::exception &e); using error_function_t = std::function; + void initialize(); + void finalize(); + + void process(); + + 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); + void InstallTheme(std::stop_token &stopper, const std::filesystem::path &themePath, UThemeMetadata themeMetadata, @@ -56,9 +61,19 @@ namespace Installer { success_function_t successCallback, error_function_t errorCallback); void DeleteTheme(const InstalledThemeMetadata& meta); - bool SetCurrentTheme(const InstalledThemeMetadata &meta); + std::string GetCurrentThemeName(); std::optional GetCurrentTheme(); std::filesystem::path GetThemePath(const UThemeMetadata& meta); + + bool IsShuffling(); + void ToggleShuffling(); + + bool IsEnabled(const InstalledThemeMetadata& meta); + void Enable(const InstalledThemeMetadata& meta); + void Disable(const InstalledThemeMetadata& meta); + + void ReloadStyleMiiUCfg(); + } // namespace Installer diff --git a/src/screens/HomeScreen.cpp b/src/screens/HomeScreen.cpp index bcf0b31..844e4f4 100644 --- a/src/screens/HomeScreen.cpp +++ b/src/screens/HomeScreen.cpp @@ -42,7 +42,7 @@ namespace HomeScreen { SDL_Texture *themiify_logo = nullptr; - std::optional current_theme_data; + std::optional current_theme; bool current_theme_refresh = true; @@ -63,7 +63,7 @@ namespace HomeScreen { } void refresh_current_theme() { - current_theme_data = Installer::GetCurrentTheme(); + current_theme = Installer::GetCurrentTheme(); current_theme_refresh = false; } @@ -114,6 +114,65 @@ namespace HomeScreen { } } + void show_current_theme() { + using namespace ImGui::RAII; + { + Font font{nullptr, 35}; + ImGui::Text("Your current theme:"); + } + + // const ImVec2 img_size = {640, 360}; + // const ImVec2 img_size = {560, 315}; + const ImVec2 img_size = {512, 288}; + + if (Child theme_frame{"theme_frame", + {0, 0}, + ImGuiChildFlags_NavFlattened | + ImGuiChildFlags_FrameStyle | + ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoSavedSettings}) { + + StyleVar no_border{ImGuiStyleVar_ImageBorderSize, 0}; + + if (!current_theme) { + if (Installer::IsShuffling()) { + auto img = ImageLoader::get("ui/theme-placeholder-random.png"); + ImGui::Image((ImTextureID)img, img_size); + ImGui::SameLine(); + ImGui::TextWrapped("StyleMiiU is shuffling themes."); + } else { + auto img = ImageLoader::get("ui/theme-placeholder-no-theme.png"); + ImGui::Image((ImTextureID)img, img_size); + ImGui::SameLine(); + ImGui::TextWrapped("No theme set."); + } + } + else { + if (!current_theme->previewPaths.empty()) { + auto img = ImageLoader::get(current_theme->previewPaths.front()); + ImGui::Image((ImTextureID)img, img_size); + } else { + auto img = ImageLoader::get("ui/theme-placeholder-no-preview.png"); + ImGui::Image((ImTextureID)img, img_size); + } + + ImGui::SameLine(); + + { + Group right_group; + + { + Font font_guard{nullptr, 30}; + ImGui::TextWrapped(current_theme->uthemeMetadata.themeName); + } + if (current_theme->uthemeMetadata.themeAuthor) + ImGui::TextWrapped("by: " + + *current_theme->uthemeMetadata.themeAuthor); + } + } + } + } + void process_ui() { using namespace ImGui::RAII; @@ -166,51 +225,7 @@ namespace HomeScreen { if (!scrollable_content) return; - { - Font font{nullptr, 35}; - ImGui::Text("Your current theme:"); - } - - ImGui::Spacing(); - - if (!current_theme_data) { - ImGui::Text("No current theme found."); - ImGui::Spacing(); - } - else { - ImGui::Indent(140); - - { - Child theme_frame{ - "CurrentTheme", - {800, 300}, - ImGuiChildFlags_NavFlattened | - ImGuiChildFlags_FrameStyle, - ImGuiWindowFlags_NoSavedSettings - }; - - 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->uthemeMetadata.themeName); - if (current_theme_data->uthemeMetadata.themeAuthor) - ImGui::TextWrapped("by: " + - *current_theme_data->uthemeMetadata.themeAuthor); - } - } - } - - ImGui::Unindent(140); - } - + show_current_theme(); if (ImGui::Button("Download Themes")) NavBar::set_current_tab(NavBar::Tab::themezer); diff --git a/src/screens/InstallThemePopup.cpp b/src/screens/InstallThemePopup.cpp index 5768547..493b46b 100644 --- a/src/screens/InstallThemePopup.cpp +++ b/src/screens/InstallThemePopup.cpp @@ -224,7 +224,7 @@ namespace InstallThemePopup { auto theme_path = Installer::GetThemePath(theme_data); Installer::InstalledThemeMetadata imeta; if (Installer::GetInstalledThemeMetadata(theme_path, imeta)) - Installer::SetCurrentTheme(imeta); + Installer::Enable(imeta); } }); diff --git a/src/screens/ManageThemesScreen.cpp b/src/screens/ManageThemesScreen.cpp index 3f3f223..d4207ed 100644 --- a/src/screens/ManageThemesScreen.cpp +++ b/src/screens/ManageThemesScreen.cpp @@ -51,7 +51,7 @@ namespace ManageThemesScreen { Tab current_tab = Tab::manage_installed; std::vector local_uthemes; - bool local_uthemes_need_refresh = true; + bool local_uthemes_needs_refresh = true; std::jthread installed_themes_scanner; thread_safe> safe_installed_themes; @@ -71,9 +71,6 @@ namespace ManageThemesScreen { std::string search; - std::string current_theme_name; - bool current_theme_need_refresh = true; - int similarity_score(std::string haystack, std::string needle) { haystack = as_lower_case(haystack); needle = as_lower_case(needle); @@ -147,7 +144,6 @@ namespace ManageThemesScreen { void refresh_all() { refresh_installed_themes(); - refresh_current_theme(); refresh_local_uthemes(); } @@ -155,13 +151,9 @@ namespace ManageThemesScreen { installed_themes_scan_state = InstalledThemesScanState::requested; } - void refresh_current_theme() { - current_theme_need_refresh = true; - } - void refresh_local_uthemes() { - local_uthemes_need_refresh = true; + local_uthemes_needs_refresh = true; } static void text_limited(float width, const std::string& text) { @@ -173,7 +165,6 @@ namespace ManageThemesScreen { } 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 @@ -211,60 +202,66 @@ namespace ManageThemesScreen { ImGui::SetCursorPos(start_pos); Group grp; - if (!theme_data.previewPaths.empty()) { + { + const ImVec2 img_size = {inner_size.x, inner_size.x * 9.0f / 16.0f}; 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}; + auto img = !theme_data.previewPaths.empty() + ? ImageLoader::get(theme_data.previewPaths.front()) + : ImageLoader::get("ui/theme-placeholder-no-preview.png"); ImGui::Image((ImTextureID)img, img_size); } - // NOTE: Measure size for the active star, but don't place it yet, to not mess + bool is_shuffling = Installer::IsShuffling(); + bool is_enabled = Installer::IsEnabled(theme_data); + + // NOTE: Measure size for the active marker, 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; + + const std::string enabled_label = is_shuffling + ? (is_enabled ? ICON_FA_CHECK_CIRCLE_O : ICON_FA_CIRCLE_O) + : (is_enabled ? ICON_FA_STAR : ICON_FA_STAR_O); + + const float enabled_font_size = 48; + ImVec2 enabled_size; { - Font star_font{nullptr, star_font_size}; - star_size = ImGui::CalcTextSize(star_label); + Font enabled_font{nullptr, enabled_font_size}; + enabled_size = ImGui::CalcTextSize(enabled_label); } { 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; + // Make sure to limit the name width, so it doesn't get covered by the enabled + // icon. + float name_width = inner_size.x - enabled_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; + // Make sure to limit the author width, so it doesn't get + // covered by the enabled icon. + float author_width = inner_size.x - enabled_size.x - style.ItemSpacing.x; text_limited(author_width, "by " + *theme_data.uthemeMetadata.themeAuthor); } - // Put active star on bottom right. + // Put active marker on bottom right. { - 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); + Font enabled_font{nullptr, enabled_font_size}; + StyleColor enabled_color{ImGuiCol_Text, {1.0f, 0.9f, 0.0f, 1.0f}}; + ImGui::SetCursorPos(inner_size - enabled_size); + ImGui::Text(enabled_label); if (clicked && ImGui::IsItemHovered()) { clicked = false; // cancel the click - // TODO: when shuffle is implemented, this would be a toggle. - Installer::SetCurrentTheme(theme_data); + if (is_enabled) + Installer::Disable(theme_data); + else + Installer::Enable(theme_data); HomeScreen::force_refresh(); - refresh_current_theme(); } } if (clicked) - ThemeDetailsPopup::open_local(theme_data, is_active); + ThemeDetailsPopup::open_local(theme_data); } void show_utheme(const std::filesystem::path& utheme_path) { @@ -322,31 +319,45 @@ namespace ManageThemesScreen { void show_tab_manage_installed() { using namespace ImGui::RAII; - ImGui::AlignTextToFramePadding(); // align vertically with the InputText widget - ImGui::Text("Search installed theme:"); - - ImGui::SameLine(); + const auto &style = ImGui::GetStyle(); SDL_WiiUSetSWKBDHintText("Write some search terms..."); SDL_WiiUSetSWKBDOKLabel("Search"); SDL_WiiUSetSWKBDHighlightInitialText(SDL_TRUE); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + // Let search widget expand, leaving space for the buttons. + const std::string shuffle_label = "Shuffle"; + const float shuffle_width = + ImGui::CalcTextSize(shuffle_label).x + + style.ItemInnerSpacing.x + + ImGui::GetTextLineHeight() + // defines the size of the box + 2 * style.FramePadding.x; + + const std::string enable_all_label = ICON_FA_CHECK_SQUARE_O " All"; + const float enable_all_width = + ImGui::CalcTextSize(enable_all_label).x + + 2 * style.FramePadding.x; + + const std::string disable_all_label = ICON_FA_SQUARE_O " All"; + const float disable_all_width = + ImGui::CalcTextSize(disable_all_label).x + + 2 * style.FramePadding.x; + + const float buttons_width = + shuffle_width + + style.ItemSpacing.x + + enable_all_width + + style.ItemSpacing.x + + disable_all_width; + + auto available = ImGui::GetContentRegionAvail(); + ImGui::SetNextItemWidth(available.x - style.ItemSpacing.x - buttons_width); ImGui::InputTextWithHint("##local_search"s, "Search..."s, search); - // if (ImGui::IsItemDeactivatedAfterEdit()) { - // cout << "Searching: " << search << endl; - // } - if (installed_themes_scan_state == InstalledThemesScanState::requested) { scan_installed_themes(); } - if (current_theme_need_refresh) { - current_theme_name = Installer::GetCurrentThemeName(); - current_theme_need_refresh = false; - } - std::vector visible_indexes; auto installed_themes = safe_installed_themes.lock(); @@ -372,6 +383,28 @@ namespace ManageThemesScreen { ); } + ImGui::SameLine(); + + bool is_shuffling = Installer::IsShuffling(); + if (ImGui::Checkbox("Shuffle", is_shuffling)) { + Installer::ToggleShuffling(); + } + + if (is_shuffling) { + ImGui::SameLine(); + if (ImGui::Button(enable_all_label)) { + auto installed_themes = safe_installed_themes.lock(); + for (auto& theme : *installed_themes) + Installer::Enable(theme); + } + ImGui::SameLine(); + if (ImGui::Button(disable_all_label)) { + auto installed_themes = safe_installed_themes.lock(); + for (auto& theme : *installed_themes) + Installer::Disable(theme); + } + } + // To keep the search widget visible, put the search results inside // another child. if (Child search_results{"ThemeGrid"}) { @@ -386,13 +419,10 @@ namespace ManageThemesScreen { 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); } @@ -407,9 +437,9 @@ namespace ManageThemesScreen { ImGui::Separator(); - if (local_uthemes_need_refresh) { + if (local_uthemes_needs_refresh) { scan_local_uthemes(); - local_uthemes_need_refresh = false; + local_uthemes_needs_refresh = false; } for (const auto& utheme_path : local_uthemes) @@ -443,7 +473,6 @@ namespace ManageThemesScreen { {tab_width, tab_height})) { current_tab = Tab::manage_installed; refresh_installed_themes(); - refresh_current_theme(); } ImGui::SameLine(); diff --git a/src/screens/ManageThemesScreen.h b/src/screens/ManageThemesScreen.h index ed54835..34a0a75 100644 --- a/src/screens/ManageThemesScreen.h +++ b/src/screens/ManageThemesScreen.h @@ -20,8 +20,6 @@ namespace ManageThemesScreen { void refresh_installed_themes(); - void refresh_current_theme(); - void refresh_local_uthemes(); void process_ui(); diff --git a/src/screens/ThemeDetailsPopup.cpp b/src/screens/ThemeDetailsPopup.cpp index 44d411a..bb2852e 100644 --- a/src/screens/ThemeDetailsPopup.cpp +++ b/src/screens/ThemeDetailsPopup.cpp @@ -22,9 +22,9 @@ #include "ManageThemesScreen.h" #include "ThemePreviewPopup.h" #include "../App.h" -#include "../installer.h" #include "../DownloadManager.h" #include "../IconsFontAwesome4.h" +#include "../installer.h" #include "../ImageLoader.h" #include "../NavBar.h" #include "../ThemezerAPI.h" @@ -54,7 +54,6 @@ namespace ThemeDetailsPopup { WiiuThemeSmall smallTheme; Installer::InstalledThemeMetadata installedThemeData; const std::string popup_id = "ThemeDetailsPopup"s; - bool isCurrent; void open_themezer(const WiiuThemeSmall &small_theme) { popup_queued = true; @@ -73,12 +72,10 @@ namespace ThemeDetailsPopup { state = State::waiting_themezer; } - void open_local(const Installer::InstalledThemeMetadata& installed_theme_data, - bool is_current) { + void open_local(const Installer::InstalledThemeMetadata& installed_theme_data) { popup_queued = true; installedThemeData = installed_theme_data; state = State::ready_local; - isCurrent = is_current; } void show_label_text(const std::string& label, @@ -167,16 +164,26 @@ namespace ThemeDetailsPopup { } } - { - Disabled disabled_if{isCurrent}; - if (ImGui::Button(ICON_FA_STAR " Apply Theme", {-1, 0})) { - Installer::SetCurrentTheme(installedThemeData); + bool is_shuffling = Installer::IsShuffling(); + bool is_enabled = Installer::IsEnabled(installedThemeData); + + if (is_shuffling) { + if (ImGui::Checkbox("Enabled", is_enabled)) { + if (is_enabled) + Installer::Enable(installedThemeData); + else + Installer::Disable(installedThemeData); + HomeScreen::force_refresh(); + } + } else { + Disabled disabled_if{is_enabled}; + if (ImGui::Button(ICON_FA_STAR " Apply", {-1, 0})) { + Installer::Enable(installedThemeData); ImGui::CloseCurrentPopup(); HomeScreen::force_refresh(); - ManageThemesScreen::refresh_current_theme(); } - ImGui::SetItemDefaultFocus(); } + ImGui::SetItemDefaultFocus(); { Disabled if_no_previews{installedThemeData.previewPaths.empty()}; diff --git a/src/screens/ThemeDetailsPopup.h b/src/screens/ThemeDetailsPopup.h index f292020..6d2371c 100644 --- a/src/screens/ThemeDetailsPopup.h +++ b/src/screens/ThemeDetailsPopup.h @@ -18,8 +18,7 @@ namespace ThemeDetailsPopup { void open_themezer(const ThemezerAPI::WiiuThemeSmall &small_theme); - void open_local(const Installer::InstalledThemeMetadata& installed_theme_data, - bool is_current); + void open_local(const Installer::InstalledThemeMetadata& installed_theme_data); void process_ui(); } diff --git a/src/timer.cpp b/src/timer.cpp new file mode 100644 index 0000000..71cf60f --- /dev/null +++ b/src/timer.cpp @@ -0,0 +1,27 @@ +#include + +#include "timer.hpp" + + +Timer::Timer(const std::string& name) : + name{name} +{} + + +std::ostream& +Timer::print(std::ostream& out) +{ + out << "Timer"; + if (!name.empty()) + out << " \"" << name << "\""; + out << ": " << duration_cast(elapsed()); + return out; +} + + +TimerReporter::~TimerReporter() +{ + auto diff = timer.stop(); + if (diff > threshold) + timer.print(out) << std::endl; +} diff --git a/src/timer.hpp b/src/timer.hpp new file mode 100644 index 0000000..1fa87ef --- /dev/null +++ b/src/timer.hpp @@ -0,0 +1,91 @@ +#ifndef TIMER_HPP +#define TIMER_HPP + +#include +#include +#include + +struct Timer { + + using clock_type = std::chrono::steady_clock; + using time_point = clock_type::time_point; + using duration = clock_type::duration; + + + std::string name = {}; + time_point start_time = {}; + time_point finish_time = {}; + + + constexpr + Timer() + noexcept = default; + + Timer(const std::string& name); + + inline + void + start() + noexcept + { + start_time = clock_type::now(); + } + + inline + duration + stop() + noexcept + { + finish_time = clock_type::now(); + return elapsed(); + } + + [[nodiscard]] + inline + duration + elapsed() + const noexcept + { + return finish_time - start_time; + } + + std::ostream& + print(std::ostream& out); + +}; // struct Timer + + +struct TimerReporter { + + std::ostream& out; + Timer timer; + Timer::duration threshold; + + + TimerReporter(std::ostream& out, + Timer::duration threshold = Timer::duration{0}) + noexcept : + out(out), + threshold{threshold} + { + timer.start(); + } + + + TimerReporter(std::ostream& out, + const std::string& name, + Timer::duration threshold = Timer::duration{0}) + noexcept: + out(out), + timer{name}, + threshold{threshold} + { + timer.start(); + } + + + ~TimerReporter(); + +}; // struct TimerReporter + +#endif diff --git a/src/utils.cpp b/src/utils.cpp index e57dd36..eef8502 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -164,3 +165,67 @@ make_theme_folder_name(const std::string& name, } return sanitize_element(result); } + + +// Used by the split() functions. +namespace { + + std::tuple::size_type> + find_first_of(const std::string& haystack, + const std::vector& needles, + std::string::size_type start = 0) + { + std::string::size_type result_pos = std::string::npos; + std::vector::size_type result_index = 0; + for (std::vector::size_type i = 0; i < needles.size(); ++i) { + auto pos = haystack.find(needles[i], start); + if (pos < result_pos) { + result_pos = pos; + result_index = i; + } + } + return { result_pos, result_index }; + } + +} // namespace + +std::vector +split(const std::string& input, + const std::vector& separators, + bool compress, + std::size_t max_tokens) +{ + std::vector result; + + using size_type = std::string::size_type; + auto [sep_start, sep_index] = find_first_of(input, separators); + size_type tok_start = 0; + + // Loop until no more separators are found. + while (sep_start != std::string::npos) { + if (!compress || sep_start > tok_start) { + // If this token reaches the maximum allowed, stop the looop + if (max_tokens && result.size() + 1 == max_tokens) + break; + result.push_back(input.substr(tok_start, sep_start - tok_start)); + } + tok_start = sep_start + separators[sep_index].size(); + if (tok_start >= input.size()) + break; + std::tie(sep_start, sep_index) = find_first_of(input, separators, tok_start); + } + // The remainder of the string is the last token, unless (compress && empty) + if (!compress || tok_start < input.size()) + result.push_back(input.substr(tok_start)); + return result; +} + +std::vector +split(const std::string& input, + const std::string& separator, + bool compress, + std::size_t max_tokens) +{ + return split(input, std::vector{separator}, compress, max_tokens); +} diff --git a/src/utils.h b/src/utils.h index b695dc6..c9d81e9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -13,6 +13,7 @@ #include #include #include +#include #ifndef THEMIIFY_VERSION #define THEMIIFY_VERSION "?.?" @@ -60,3 +61,19 @@ make_theme_id_filename(const std::string& themeID); std::filesystem::path make_theme_folder_name(const std::string& name, const std::optional& themeID); + + + +[[nodiscard]] +std::vector +split(const std::string& input, + const std::vector& separators = {","}, + bool compress = true, + std::size_t max_tokens = 0); + +[[nodiscard]] +std::vector +split(const std::string& input, + const std::string& separator = ",", + bool compress = true, + std::size_t max_tokens = 0);