themezer: prompt user to download ThemeInjector if not installed on launch.

This commit is contained in:
ITotalJustice
2025-08-02 17:21:07 +01:00
parent 3ebb3bd055
commit 40e4616520
4 changed files with 98 additions and 17 deletions

View File

@@ -69,12 +69,24 @@ private:
void Sort();
void UpdateSubheading();
void DownloadEntries();
private:
std::vector<Entry> m_entries{};
s64 m_index{};
std::unique_ptr<List> 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<AssetEntry>& 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<AssetEntry> assets;
assets.emplace_back(asset);
return Download(url, assets, pre_install_message, post_install_message);
}
} // namespace sphaira::ui::menu::gh

View File

@@ -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

View File

@@ -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<GhApiEntry> gh_entries;
gh_entries = {};
App::Push<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox) -> Result {
return DownloadReleaseJsonJson(pbox, GenerateApiUrl(GetEntry()), gh_entries);
}, [this](Result rc){
App::Push<ProgressBox>(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<PopupList>("Select release to download for "_i18n + GetEntry().repo, entry_items, [this](auto op_index){
App::Push<PopupList>("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<const AssetEntry*> asset_ptr;
std::vector<GhApiAsset> api_assets;
@@ -406,7 +406,7 @@ void Menu::DownloadEntries() {
}
}
App::Push<PopupList>("Select asset to download for "_i18n + GetEntry().repo, asset_items, [this, api_assets, asset_ptr](auto op_index){
App::Push<PopupList>("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<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox) -> Result {
const auto func = [entry, &asset_entry, ptr](){
App::Push<ProgressBox>(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<OptionBox>(
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<AssetEntry>& 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

View File

@@ -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<OptionBox>(
"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() {