From 40e461652089cb992d5726019bf2a6ad1f9a539d Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:21:07 +0100 Subject: [PATCH] themezer: prompt user to download ThemeInjector if not installed on launch. --- sphaira/include/ui/menus/ghdl.hpp | 16 +++++++- sphaira/include/ui/menus/themezer.hpp | 2 + sphaira/source/ui/menus/ghdl.cpp | 56 ++++++++++++++++++++------- sphaira/source/ui/menus/themezer.cpp | 41 ++++++++++++++++++++ 4 files changed, 98 insertions(+), 17 deletions(-) diff --git a/sphaira/include/ui/menus/ghdl.hpp b/sphaira/include/ui/menus/ghdl.hpp index 7bec1de..e42798c 100644 --- a/sphaira/include/ui/menus/ghdl.hpp +++ b/sphaira/include/ui/menus/ghdl.hpp @@ -69,12 +69,24 @@ private: void Sort(); void UpdateSubheading(); - void DownloadEntries(); - private: std::vector m_entries{}; s64 m_index{}; std::unique_ptr m_list{}; }; +// creates a popup box on another thread. +void DownloadEntries(const Entry& entry); + +// parses the params into entry struct and calls DonwloadEntries +bool Download(const std::string& url, const std::vector& assets = {}, const std::string& pre_install_message = {}, const std::string& post_install_message = {}); + +// calls the above function by pushing the asset to an array. +inline bool Download(const std::string& url, const AssetEntry& asset, const std::string& pre_install_message = {}, const std::string& post_install_message = {}) { + std::vector assets; + assets.emplace_back(asset); + + return Download(url, assets, pre_install_message, post_install_message); +} + } // namespace sphaira::ui::menu::gh diff --git a/sphaira/include/ui/menus/themezer.hpp b/sphaira/include/ui/menus/themezer.hpp index ece5f96..7ff6368 100644 --- a/sphaira/include/ui/menus/themezer.hpp +++ b/sphaira/include/ui/menus/themezer.hpp @@ -169,6 +169,8 @@ private: option::OptionLong m_sort{INI_SECTION, "sort", 0}; option::OptionLong m_order{INI_SECTION, "order", 0}; option::OptionBool m_nsfw{INI_SECTION, "nsfw", false}; + + bool m_checked_for_nro{}; }; } // namespace sphaira::ui::menu::themezer diff --git a/sphaira/source/ui/menus/ghdl.cpp b/sphaira/source/ui/menus/ghdl.cpp index e2b3c66..e31d4eb 100644 --- a/sphaira/source/ui/menus/ghdl.cpp +++ b/sphaira/source/ui/menus/ghdl.cpp @@ -176,7 +176,7 @@ Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} { return; } - DownloadEntries(); + DownloadEntries(GetEntry()); }}), std::make_pair(Button::B, Action{"Back"_i18n, [this](){ @@ -341,14 +341,14 @@ void Menu::UpdateSubheading() { this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size())); } -void Menu::DownloadEntries() { +void DownloadEntries(const Entry& entry) { // hack static std::vector gh_entries; gh_entries = {}; - App::Push(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox) -> Result { - return DownloadReleaseJsonJson(pbox, GenerateApiUrl(GetEntry()), gh_entries); - }, [this](Result rc){ + App::Push(0, "Downloading "_i18n, entry.repo, [entry](auto pbox) -> Result { + return DownloadReleaseJsonJson(pbox, GenerateApiUrl(entry), gh_entries); + }, [entry](Result rc){ App::PushErrorBox(rc, "Failed to download json"_i18n); if (R_FAILED(rc) || gh_entries.empty()) { return; @@ -370,13 +370,13 @@ void Menu::DownloadEntries() { entry_items.emplace_back(str); } - App::Push("Select release to download for "_i18n + GetEntry().repo, entry_items, [this](auto op_index){ + App::Push("Select release to download for "_i18n + entry.repo, entry_items, [entry](auto op_index){ if (!op_index) { return; } const auto& gh_entry = gh_entries[*op_index]; - const auto& assets = GetEntry().assets; + const auto& assets = entry.assets; PopupList::Items asset_items; std::vector asset_ptr; std::vector api_assets; @@ -406,7 +406,7 @@ void Menu::DownloadEntries() { } } - App::Push("Select asset to download for "_i18n + GetEntry().repo, asset_items, [this, api_assets, asset_ptr](auto op_index){ + App::Push("Select asset to download for "_i18n + entry.repo, asset_items, [entry, api_assets, asset_ptr](auto op_index){ if (!op_index) { return; } @@ -414,7 +414,7 @@ void Menu::DownloadEntries() { const auto index = *op_index; const auto& asset_entry = api_assets[index]; const AssetEntry* ptr{}; - auto pre_install_message = GetEntry().pre_install_message; + auto pre_install_message = entry.pre_install_message; if (asset_ptr.size()) { ptr = asset_ptr[index]; if (!ptr->pre_install_message.empty()) { @@ -422,16 +422,16 @@ void Menu::DownloadEntries() { } } - const auto func = [this, &asset_entry, ptr](){ - App::Push(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox) -> Result { + const auto func = [entry, &asset_entry, ptr](){ + App::Push(0, "Downloading "_i18n, entry.repo, [entry, &asset_entry, ptr](auto pbox) -> Result { return DownloadApp(pbox, asset_entry, ptr); - }, [this, ptr](Result rc){ + }, [entry, ptr](Result rc){ homebrew::SignalChange(); App::PushErrorBox(rc, "Failed to download app!"_i18n); if (R_SUCCEEDED(rc)) { - App::Notify("Downloaded "_i18n + GetEntry().repo); - auto post_install_message = GetEntry().post_install_message; + App::Notify("Downloaded "_i18n + entry.repo); + auto post_install_message = entry.post_install_message; if (ptr && !ptr->post_install_message.empty()) { post_install_message = ptr->post_install_message; } @@ -446,7 +446,7 @@ void Menu::DownloadEntries() { if (!pre_install_message.empty()) { App::Push( pre_install_message, - "Back"_i18n, "Download"_i18n, 1, [this, func](auto op_index){ + "Back"_i18n, "Download"_i18n, 1, [entry, func](auto op_index){ if (op_index && *op_index) { func(); } @@ -460,4 +460,30 @@ void Menu::DownloadEntries() { }); } +bool Download(const std::string& url, const std::vector& assets, const std::string& pre_install_message, const std::string& post_install_message) { + Entry entry{}; + entry.url = url; + entry.assets = assets; + entry.pre_install_message = pre_install_message; + entry.post_install_message = post_install_message; + + // parse owner and author from url (if needed). + if (!entry.url.empty()) { + const auto s = entry.url.substr(std::strlen("https://github.com/")); + const auto it = s.find('/'); + if (it != s.npos) { + entry.owner = s.substr(0, it); + entry.repo = s.substr(it + 1); + } + } + + // check that we have a owner and repo + if (entry.owner.empty() || entry.repo.empty()) { + return false; + } + + DownloadEntries(entry); + return true; +} + } // namespace sphaira::ui::menu::gh diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 17fc131..dd70ce5 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -1,4 +1,5 @@ #include "ui/menus/themezer.hpp" +#include "ui/menus/ghdl.hpp" #include "ui/progress_box.hpp" #include "ui/option_box.hpp" #include "ui/sidebar.hpp" @@ -29,6 +30,13 @@ constexpr fs::FsPath THEME_FOLDER{"/themes/sphaira/"}; constexpr auto CACHE_PATH = "/switch/sphaira/cache/themezer"; constexpr auto URL_BASE = "https://switch.cdn.fortheusers.org"; +constexpr const char* NRO_URL = "https://github.com/exelix11/SwitchThemeInjector"; + +constexpr const char* NRO_PATHS[]{ + "/switch/NXThemesInstaller.nro", + "/switch/Switch_themes_Installer/NXThemesInstaller.nro", +}; + constexpr const char* REQUEST_TARGET[]{ "ResidentMenu", "Entrance", @@ -54,6 +62,17 @@ constexpr const char* REQUEST_ORDER[]{ // https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]} // https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]} +auto HasNro() -> bool { + fs::FsNativeSd fs; + for (auto& path : NRO_PATHS) { + if (fs.FileExists(path)) { + return true; + } + } + + return false; +} + // i know, this is cursed // todo: send actual POST request rather than GET. auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string { @@ -549,6 +568,28 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { void Menu::OnFocusGained() { MenuBase::OnFocusGained(); + + if (!m_checked_for_nro) { + m_checked_for_nro = true; + + // check if we have the nro, if not, then prompt the user to download from the appstore. + if (!HasNro()) { + App::Push( + "NXthemes_Installer.nro not found, download now?"_i18n, + "Back"_i18n, "Download"_i18n, 1, [this](auto op_index){ + if (op_index && *op_index) { + const gh::AssetEntry asset{ + .name = "NXThemesInstaller.nro", + // same path as appstore + .path = "/switch/Switch_themes_Installer/NXThemesInstaller.nro", + }; + + gh::Download(NRO_URL, asset); + } + } + ); + } + } } void Menu::InvalidateAllPages() {