From 54a2215e0446c972f20d3f33c6c66b6318070259 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:56:36 +0100 Subject: [PATCH] support for filtering mtp/ftp mount options. use builtin config for ftp port,user,pass. --- sphaira/CMakeLists.txt | 13 +- sphaira/include/app.hpp | 36 +++- sphaira/include/ui/sidebar.hpp | 31 +++- sphaira/source/app.cpp | 258 ++++++++++++++++++++++++++ sphaira/source/ftpsrv_helper.cpp | 169 +++++++++-------- sphaira/source/haze_helper.cpp | 38 +++- sphaira/source/ui/menus/main_menu.cpp | 28 ++- sphaira/source/ui/sidebar.cpp | 42 ++++- 8 files changed, 496 insertions(+), 119 deletions(-) diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index dd78280..8211d66 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -341,7 +341,7 @@ endif() if (ENABLE_FTPSRV) FetchContent_Declare(ftpsrv GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git - GIT_TAG a7c2283 + GIT_TAG 7770dab SOURCE_SUBDIR NONE ) @@ -358,7 +358,7 @@ if (ENABLE_FTPSRV) USE_VFS_STORAGE=$ # disabled as it may conflict with the gamecard menu. USE_VFS_GC=$ - USE_VFS_USBHSFS=$ + USE_VFS_USBHSFS=$ VFS_NX_BUFFER_IO=$ # let sphaira handle init / closing of the hdd. USE_VFS_USBHSFS_INIT=$ @@ -378,14 +378,7 @@ if (ENABLE_FTPSRV) ${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c ) - if (ENABLE_LIBUSBHSFS) - target_sources(ftpsrv_helper PRIVATE - ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_stdio.c - ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_hdd.c - ) - endif() - - target_link_libraries(ftpsrv_helper PUBLIC ftpsrv libusbhsfs) + target_link_libraries(ftpsrv_helper PUBLIC ftpsrv) target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform) target_compile_definitions(sphaira PRIVATE ENABLE_FTPSRV) diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index a99721b..efd584c 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -131,6 +131,9 @@ public: static void DisplayAdvancedOptions(bool left_side = true); static void DisplayInstallOptions(bool left_side = true); static void DisplayDumpOptions(bool left_side = true); + static void DisplayFtpOptions(bool left_side = true); + static void DisplayMtpOptions(bool left_side = true); + static void DisplayHddOptions(bool left_side = true); // helper for sidebar options to toggle install on/off static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable); @@ -342,9 +345,38 @@ public: // todo: move this into it's own menu option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal + // ftp options. + option::OptionLong m_ftp_port{"ftp", "port", 5000}; + option::OptionBool m_ftp_anon{"ftp", "anon", true}; + option::OptionString m_ftp_user{"ftp", "user", ""}; + option::OptionString m_ftp_pass{"ftp", "pass", ""}; + option::OptionBool m_ftp_show_album{"ftp", "show_album", true}; + option::OptionBool m_ftp_show_ams_contents{"ftp", "show_ams_contents", false}; + option::OptionBool m_ftp_show_bis_storage{"ftp", "show_bis_storage", false}; + option::OptionBool m_ftp_show_bis_fs{"ftp", "show_bis_fs", false}; + option::OptionBool m_ftp_show_content_system{"ftp", "show_content_system", false}; + option::OptionBool m_ftp_show_content_user{"ftp", "show_content_user", false}; + option::OptionBool m_ftp_show_content_sd{"ftp", "show_content_sd", false}; + // option::OptionBool m_ftp_show_content_sd0{"ftp", "show_content_sd0", false}; + // option::OptionBool m_ftp_show_custom_system{"ftp", "show_custom_system", false}; + // option::OptionBool m_ftp_show_custom_sd{"ftp", "show_custom_sd", false}; + option::OptionBool m_ftp_show_games{"ftp", "show_games", true}; + option::OptionBool m_ftp_show_install{"ftp", "show_install", true}; + option::OptionBool m_ftp_show_mounts{"ftp", "show_mounts", false}; + option::OptionBool m_ftp_show_switch{"ftp", "show_switch", false}; + // mtp options. - option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo - option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch + option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo (hidden from ui) + option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch (hidden from ui) + option::OptionBool m_mtp_allocate_file{"mtp", "allocate_file", true}; + option::OptionBool m_mtp_show_album{"mtp", "show_album", true}; + option::OptionBool m_mtp_show_content_sd{"mtp", "show_content_sd", false}; + option::OptionBool m_mtp_show_content_system{"mtp", "show_content_system", false}; + option::OptionBool m_mtp_show_content_user{"mtp", "show_content_user", false}; + option::OptionBool m_mtp_show_games{"mtp", "show_games", true}; + option::OptionBool m_mtp_show_install{"mtp", "show_install", true}; + option::OptionBool m_mtp_show_mounts{"mtp", "show_mounts", false}; + option::OptionBool m_mtp_show_speedtest{"mtp", "show_speedtest", false}; std::shared_ptr m_fs{}; audio::SongID m_background_music{}; diff --git a/sphaira/include/ui/sidebar.hpp b/sphaira/include/ui/sidebar.hpp index c846cec..cdd1911 100644 --- a/sphaira/include/ui/sidebar.hpp +++ b/sphaira/include/ui/sidebar.hpp @@ -44,6 +44,14 @@ public: m_depends_click = depends_click; } + void SetDirty(bool dirty = true) { + m_dirty = dirty; + } + + auto IsDirty() const -> bool { + return m_dirty; + } + protected: auto IsEnabled() const -> bool { if (m_depends_callback) { @@ -69,6 +77,7 @@ private: DependsClickCallback m_depends_click{}; ScrollingText m_scolling_title{}; ScrollingText m_scolling_value{}; + bool m_dirty{}; }; template @@ -174,11 +183,14 @@ private: }; class SidebarEntryTextInput final : public SidebarEntryTextBase { +public: + using Callback = std::function; + public: // uses normal keyboard. - explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = ""); + explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr); // uses numpad. - explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = ""); + explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr); auto GetNumValue() const -> s64 { return std::stoul(GetValue()); @@ -191,6 +203,7 @@ private: const std::string m_guide; const s64 m_len_min; const s64 m_len_max; + const Callback m_callback; }; class SidebarEntryFilePicker final : public SidebarEntryTextBase { @@ -210,10 +223,12 @@ class Sidebar : public Widget { public: enum class Side { LEFT, RIGHT }; using Items = std::vector>; + using OnExitWhenChangedCallback = std::function; public: explicit Sidebar(const std::string& title, Side side, float width = 450.f); explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f); + ~Sidebar(); auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override; @@ -227,6 +242,12 @@ public: return (T*)Add(std::make_unique(std::forward(args)...)); } + // sets a callback that is called on exit when the any options were changed. + // the change detection isn't perfect, it just checks if the A button was pressed... + void SetOnExitWhenChanged(const OnExitWhenChangedCallback& cb) { + m_on_exit_when_changed = cb; + } + private: void SetIndex(s64 index); void SetupButtons(); @@ -235,16 +256,18 @@ private: const std::string m_title; const std::string m_sub; const Side m_side; - Items m_items; + Items m_items{}; s64 m_index{}; - std::unique_ptr m_list; + std::unique_ptr m_list{}; Vec4 m_top_bar{}; Vec4 m_bottom_bar{}; Vec2 m_title_pos{}; Vec4 m_base_pos{}; + OnExitWhenChangedCallback m_on_exit_when_changed{}; + static constexpr float m_title_size{28.f}; // static constexpr Vec2 box_size{380.f, 70.f}; static constexpr Vec2 m_box_size{400.f, 70.f}; diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index eb5df01..b435968 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1549,9 +1549,37 @@ App::App(const char* argv0) { else if (app->m_nsz_compress_ldm.LoadFrom(Key, Value)) {} else if (app->m_nsz_compress_block.LoadFrom(Key, Value)) {} else if (app->m_nsz_compress_block_exponent.LoadFrom(Key, Value)) {} + } else if (!std::strcmp(Section, "ftp")) { + if (app->m_ftp_port.LoadFrom(Key, Value)) {} + else if (app->m_ftp_anon.LoadFrom(Key, Value)) {} + else if (app->m_ftp_user.LoadFrom(Key, Value)) {} + else if (app->m_ftp_pass.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_album.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_ams_contents.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_bis_storage.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_bis_fs.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_content_system.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_content_user.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_content_sd.LoadFrom(Key, Value)) {} + // else if (app->m_ftp_show_content_sd0.LoadFrom(Key, Value)) {} + // else if (app->m_ftp_show_custom_system.LoadFrom(Key, Value)) {} + // else if (app->m_ftp_show_custom_sd.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_games.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_install.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_mounts.LoadFrom(Key, Value)) {} + else if (app->m_ftp_show_switch.LoadFrom(Key, Value)) {} } else if (!std::strcmp(Section, "mtp")) { if (app->m_mtp_vid.LoadFrom(Key, Value)) {} else if (app->m_mtp_pid.LoadFrom(Key, Value)) {} + else if (app->m_mtp_allocate_file.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_album.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_content_sd.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_content_system.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_content_user.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_games.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_install.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_mounts.LoadFrom(Key, Value)) {} + else if (app->m_mtp_show_speedtest.LoadFrom(Key, Value)) {} } return 1; @@ -2271,6 +2299,236 @@ void App::DisplayDumpOptions(bool left_side) { block_size_option->Depends(App::GetApp()->m_nsz_compress_block, "NSZ block compression is disabled."_i18n); } +void App::DisplayFtpOptions(bool left_side) { + // todo: prompt on exit to restart ftp server if options were changed. + auto options = std::make_unique("FTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->SetOnExitWhenChanged([](){ + if (App::GetFtpEnable()) { + App::Notify("Restarting FTP server..."_i18n); + App::SetFtpEnable(false); + App::SetFtpEnable(true); + } + }); + + options->Add("Enable"_i18n, App::GetFtpEnable(), [](bool& enable){ + App::SetFtpEnable(enable); + }, "Enable FTP server to run in the background."_i18n); + + options->Add( + "Port", App::GetApp()->m_ftp_port.Get(), "Port number", 1, 5, + "Opens the FTP server on this port."_i18n, + [](auto* input){ + App::GetApp()->m_ftp_port.Set(input->GetNumValue()); + } + ); + + options->Add( + "Anon"_i18n, App::GetApp()->m_ftp_anon, + "Allows you to login without setting a username and password.\n" + "If disabled, you must set a user name and password below!"_i18n + ); + + options->Add( + "User", App::GetApp()->m_ftp_user.Get(), "Username", -1, 64, + "Sets the username, must be set if anon is disabled."_i18n, + [](auto* input){ + App::GetApp()->m_ftp_user.Set(input->GetValue()); + } + ); + + options->Add( + "Pass", App::GetApp()->m_ftp_pass.Get(), "Password", -1, 64, + "Sets the password, must be set if anon is disabled."_i18n, + [](auto* input){ + App::GetApp()->m_ftp_pass.Set(input->GetValue()); + } + ); + + options->Add( + "Show album"_i18n, App::GetApp()->m_ftp_show_album, + "Shows the microSD card album folder."_i18n + ); + + options->Add( + "Show Atmosphere contents"_i18n, App::GetApp()->m_ftp_show_ams_contents, + "Shows the shortcut for the /atmosphere/contents folder."_i18n + ); + + options->Add( + "Show bis storage"_i18n, App::GetApp()->m_ftp_show_bis_storage, + "Shows the bis folder which contains the following:\n" + "- BootPartition1Root.bin\n" + "- BootPartition2Root.bin\n" + "- UserDataRoot.bin\n" + "- BootConfigAndPackage2Part1.bin\n" + "- BootConfigAndPackage2Part2.bin\n" + "- BootConfigAndPackage2Part3.bin\n" + "- BootConfigAndPackage2Part4.bin\n" + "- BootConfigAndPackage2Part5.bin\n" + "- BootConfigAndPackage2Part6.bin\n" + "- CalibrationFile.bin\n" + "- SafeMode.bin\n" + "- User.bin\n" + "- System.bin\n" + "- SystemProperEncryption.bin"_i18n + ); + + options->Add( + "Show bis file systems"_i18n, App::GetApp()->m_ftp_show_bis_fs, + "Shows the following bis file systems:\n" + "- bis_calibration_file\n" + "- bis_safe_mode\n" + "- bis_user\n" + "- bis_system"_i18n + ); + + options->Add( + "Show system contents"_i18n, App::GetApp()->m_ftp_show_content_system, + "Shows the system contents folder."_i18n + ); + + options->Add( + "Show user contents"_i18n, App::GetApp()->m_ftp_show_content_user, + "Shows the user contents folder."_i18n + ); + + options->Add( + "Show microSD contents"_i18n, App::GetApp()->m_ftp_show_content_sd, + "Shows the microSD contents folder.\n\n" + "NOTE: This is not the normal microSD card storage, it is instead " + "the location where NCA's are stored. The normal microSD card is always mounted."_i18n + ); + + options->Add( + "Show games"_i18n, App::GetApp()->m_ftp_show_games, + "Shows the games folder.\n\n" + "This folder contains all of your installed games, allowing you to create " + "backups over FTP!\n\n" + "NOTE: This folder is read-only. You cannot delete games over FTP."_i18n + ); + + options->Add( + "Show install"_i18n, App::GetApp()->m_ftp_show_install, + "Shows the install folder.\n\n" + "This folder is used for installing games via FTP.\n\n" + "NOTE: You must open the \"FTP Install\" menu when trying to install a game!"_i18n + ); + + options->Add( + "Show mounts"_i18n, App::GetApp()->m_ftp_show_mounts, + "Shows the mounts folder.\n\n" + "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over FTP!\n" + "For example, you can access your SMB, WebDav or other FTP mounts over FTP."_i18n + ); + + options->Add( + "Show switch"_i18n, App::GetApp()->m_ftp_show_switch, + "Shows the shortcut for the /switch folder." + "This is the folder that contains all your homebrew (NRO's)."_i18n + ); +} + +void App::DisplayMtpOptions(bool left_side) { + // todo: prompt on exit to restart ftp server if options were changed. + auto options = std::make_unique("MTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->SetOnExitWhenChanged([](){ + if (App::GetMtpEnable()) { + App::Notify("Restarting MTP server..."_i18n); + App::SetMtpEnable(false); + App::SetMtpEnable(true); + } + }); + + options->Add("Enable"_i18n, App::GetMtpEnable(), [](bool& enable){ + App::SetMtpEnable(enable); + }, "Enable MTP server to run in the background."_i18n); + + // not sure if i want to expose this to users yet. + // its also stubbed currently. + #if 0 + options->Add( + "Pre-allocate file"_i18n, App::GetApp()->m_mtp_allocate_file, + "Enables pre-allocating the file size before writing.\n" + "This speeds up file writes, however, this can cause timeouts if all these conditions are met:\n" + "- using Windows\n" + "- using emuMMC\n" + "- transferring a large file (>1GB)\n\n" + "This option should be left enabled, however if you use the above and experience timeouts, " + "then try again with this option disabled."_i18n + ); + #endif + + options->Add( + "Show album"_i18n, App::GetApp()->m_mtp_show_album, + "Shows the microSD card album folder."_i18n + ); + + options->Add( + "Show microSD contents"_i18n, App::GetApp()->m_mtp_show_content_sd, + "Shows the microSD contents folder.\n\n" + "NOTE: This is not the normal microSD card storage, it is instead " + "the location where NCA's are stored. The normal microSD card is always mounted."_i18n + ); + + options->Add( + "Show system contents"_i18n, App::GetApp()->m_mtp_show_content_system, + "Shows the system contents folder."_i18n + ); + + options->Add( + "Show user contents"_i18n, App::GetApp()->m_mtp_show_content_user, + "Shows the user contents folder."_i18n + ); + + options->Add( + "Show games"_i18n, App::GetApp()->m_mtp_show_games, + "Shows the games folder.\n\n" + "This folder contains all of your installed games, allowing you to create " + "backups over MTP!\n\n" + "NOTE: This folder is read-only. You cannot delete games over MTP."_i18n + ); + + options->Add( + "Show install"_i18n, App::GetApp()->m_mtp_show_install, + "Shows the install folder.\n\n" + "This folder is used for installing games via MTP.\n\n" + "NOTE: You must open the \"MTP Install\" menu when trying to install a game!"_i18n + ); + + options->Add( + "Show mounts"_i18n, App::GetApp()->m_mtp_show_mounts, + "Shows the mounts folder.\n\n" + "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over MTP!\n" + "For example, you can access your SMB, WebDav and FTP mounts over MTP."_i18n + ); + + options->Add( + "Show DevNull"_i18n, App::GetApp()->m_mtp_show_speedtest, + "Shows the DevNull (Speed Test) folder.\n\n" + "This folder is used for benchmarking USB uploads. " + "This ia virtual folder, nothing is actally written to disk."_i18n + ); +} + +void App::DisplayHddOptions(bool left_side) { + auto options = std::make_unique("HDD Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->Add("Enable"_i18n, App::GetHddEnable(), [](bool& enable){ + App::SetHddEnable(enable); + }, "Enable mounting of connected USB/HDD devices. " + "Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n + ); + + options->Add("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){ + App::SetWriteProtect(enable); + }, "Makes the connected HDD read-only."_i18n); +} + void App::ShowEnableInstallPrompt() { // warn the user the dangers of installing. App::Push( diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index a3e606f..e126155 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -6,7 +6,6 @@ #include "utils/thread.hpp" #include -#include #include #include #include @@ -30,11 +29,12 @@ struct InstallSharedData { bool enabled; }; -const char* INI_PATH = "/config/ftpsrv/config.ini"; -FtpSrvConfig g_ftpsrv_config = {0}; -std::atomic_bool g_should_exit = false; -bool g_is_running{false}; -Thread g_thread; +FtpSrvConfig g_ftpsrv_config{}; +int g_ftpsrv_mount_flags{}; +std::vector g_custom_vfs{}; +std::atomic_bool g_should_exit{}; +bool g_is_running{}; +Thread g_thread{}; Mutex g_mutex{}; void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { @@ -468,74 +468,12 @@ FtpVfs g_vfs_stdio = { void loop(void* arg) { log_write("[FTP] loop entered\n"); - // load config. { SCOPED_MUTEX(&g_mutex); - - g_ftpsrv_config.log_callback = ftp_log_callback; - g_ftpsrv_config.progress_callback = ftp_progress_callback; - g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH); - int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH); - int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH); - g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira - g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH); - g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH); - bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH); - - // get nx config - bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH); - bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH); - bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH); - g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat - - // get Nx-App overrides - g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH); - user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH); - pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH); - g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH); - g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH); - g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH); - log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH); - mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH); - mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH); - save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH); - g_should_exit = false; - mount_devices = true; - g_ftpsrv_config.timeout = 0; - - if (!g_ftpsrv_config.port) { - g_ftpsrv_config.port = 5000; - log_write("[FTP] no port config, defaulting to 5000\n"); - } - - // keep compat with older sphaira - if (!user_len && !pass_len) { - g_ftpsrv_config.anon = true; - log_write("[FTP] no user pass, defaulting to anon\n"); - } fsdev_wrapMountSdmc(); - - static const VfsNxCustomPath custom_vfs[] = { - { - .name = "games", - .user = NULL, - .func = &g_vfs_stdio, - }, - { - .name = "mounts", - .user = NULL, - .func = &g_vfs_stdio, - }, - { - .name = "install", - .user = NULL, - .func = &g_vfs_install, - }, - }; - - vfs_nx_init(custom_vfs, std::size(custom_vfs), mount_devices, save_writable, mount_bis, false); + vfs_nx_init(g_custom_vfs.data(), std::size(g_custom_vfs), g_ftpsrv_mount_flags, false); } ON_SCOPE_EXIT( @@ -566,13 +504,90 @@ bool Init() { return false; } - // if (R_FAILED(fsdev_wrapMountSdmc())) { - // log_write("[FTP] cannot mount sdmc\n"); - // return false; - // } + g_ftpsrv_mount_flags = 0; + g_ftpsrv_config = {}; + g_custom_vfs.clear(); - // todo: replace everything with ini_browse for faster loading. - // or load everything in the init thread. + auto app = App::GetApp(); + g_ftpsrv_config.log_callback = ftp_log_callback; + g_ftpsrv_config.progress_callback = ftp_progress_callback; + g_ftpsrv_config.anon = app->m_ftp_anon.Get(); + std::strncpy(g_ftpsrv_config.user, app->m_ftp_user.Get().c_str(), sizeof(g_ftpsrv_config.user) - 1); + std::strncpy(g_ftpsrv_config.pass, app->m_ftp_pass.Get().c_str(), sizeof(g_ftpsrv_config.pass) - 1); + g_ftpsrv_config.port = app->m_ftp_port.Get(); + + if (app->m_ftp_show_album.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_ALBUM; + } + if (app->m_ftp_show_ams_contents.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_AMS_CONTENTS; + } + if (app->m_ftp_show_bis_storage.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_STORAGE; + } + if (app->m_ftp_show_bis_fs.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_FS; + } + if (app->m_ftp_show_content_system.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SYSTEM; + } + if (app->m_ftp_show_content_user.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_USER; + } + if (app->m_ftp_show_content_sd.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD; + } + #if 0 + if (app->m_ftp_show_content_sd0.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD0; + } + if (app->m_ftp_show_custom_system.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SYSTEM; + } + if (app->m_ftp_show_custom_sd.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SDCARD; + } + #endif + if (app->m_ftp_show_switch.Get()) { + g_ftpsrv_mount_flags |= VfsNxMountFlag_SWITCH; + } + + if (app->m_ftp_show_install.Get()) { + g_custom_vfs.push_back({ + .name = "install", + .user = &g_shared_data, + .func = &g_vfs_install, + }); + } + + if (app->m_ftp_show_games.Get()) { + g_custom_vfs.push_back({ + .name = "games", + .user = NULL, + .func = &g_vfs_stdio, + }); + } + + if (app->m_ftp_show_mounts.Get()) { + g_custom_vfs.push_back({ + .name = "mounts", + .user = NULL, + .func = &g_vfs_stdio, + }); + } + + g_ftpsrv_config.timeout = 0; + + if (!g_ftpsrv_config.port) { + g_ftpsrv_config.port = 5000; + log_write("[FTP] no port config, defaulting to 5000\n"); + } + + // keep compat with older sphaira + if (!std::strlen(g_ftpsrv_config.user) && !std::strlen(g_ftpsrv_config.pass)) { + g_ftpsrv_config.anon = true; + log_write("[FTP] no user pass, defaulting to anon\n"); + } Result rc; if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr))) { @@ -602,7 +617,9 @@ void Exit() { threadWaitForExit(&g_thread); threadClose(&g_thread); - memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config)); + std::memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config)); + g_custom_vfs.clear(); + g_ftpsrv_mount_flags = 0; log_write("[FTP] exitied\n"); } diff --git a/sphaira/source/haze_helper.cpp b/sphaira/source/haze_helper.cpp index 3f53979..bc29703 100644 --- a/sphaira/source/haze_helper.cpp +++ b/sphaira/source/haze_helper.cpp @@ -669,12 +669,40 @@ bool Init() { return false; } + // add default mount of the sd card. g_fs_entries.emplace_back(std::make_shared(std::make_unique(), "", "microSD card")); - g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Sd), "Album", "Album (Image SD)")); - g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "games:/"), "Games", "Games")); - g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "mounts:/"), "Mounts", "Mounts")); - g_fs_entries.emplace_back(std::make_shared("DevNull", "DevNull (Speed Test)")); - g_fs_entries.emplace_back(std::make_shared("install", "Install (NSP, XCI, NSZ, XCZ)")); + + if (App::GetApp()->m_mtp_show_album.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Sd), "Album", "Album (Image SD)")); + } + + if (App::GetApp()->m_mtp_show_content_sd.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsContentStorageId_SdCard), "ContentsM", "Contents (microSD card)")); + } + + if (App::GetApp()->m_mtp_show_content_system.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsContentStorageId_System), "ContentsS", "Contents (System)")); + } + + if (App::GetApp()->m_mtp_show_content_user.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsContentStorageId_User), "ContentsU", "Contents (User)")); + } + + if (App::GetApp()->m_mtp_show_games.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "games:/"), "Games", "Games")); + } + + if (App::GetApp()->m_mtp_show_install.Get()) { + g_fs_entries.emplace_back(std::make_shared("install", "Install (NSP, XCI, NSZ, XCZ)")); + } + + if (App::GetApp()->m_mtp_show_mounts.Get()) { + g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "mounts:/"), "Mounts", "Mounts")); + } + + if (App::GetApp()->m_mtp_show_speedtest.Get()) { + g_fs_entries.emplace_back(std::make_shared("DevNull", "DevNull (Speed Test)")); + } g_should_exit = false; if (!haze::Initialize(haze_callback, g_fs_entries, App::GetApp()->m_mtp_vid.Get(), App::GetApp()->m_mtp_pid.Get())) { diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 4b0af57..7e69b46 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -337,15 +337,19 @@ MainMenu::MainMenu() { }); } - options->Add("FTP"_i18n, App::GetFtpEnable(), [](bool& enable){ - App::SetFtpEnable(enable); - }, "Enable FTP server to run in the background.\n\n" - "The default port is 5000 with no user/pass set. " - "You can change this behaviour in /config/ftpsrv/config.ini"_i18n); + options->Add("FTP"_i18n, [](){ App::DisplayFtpOptions(); }, + "Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n" + "NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu."_i18n + ); - options->Add("MTP"_i18n, App::GetMtpEnable(), [](bool& enable){ - App::SetMtpEnable(enable); - }, "Enable MTP server to run in the background."_i18n); + options->Add("MTP"_i18n, [](){ App::DisplayMtpOptions(); }, + "Enable / modify the MTP responder settings such as the folders that are shown.\n\n" + "NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu."_i18n + ); + + options->Add("HDD"_i18n, [](){ + App::DisplayHddOptions(); + }, "Enable / modify the HDD mount options."_i18n); options->Add("NXlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){ App::SetNxlinkEnable(enable); @@ -353,14 +357,6 @@ MainMenu::MainMenu() { "NXlink is used to send .nro's from PC to the switch\n\n" "If you are not a developer, you can disable this option."_i18n); - options->Add("HDD"_i18n, App::GetHddEnable(), [](bool& enable){ - App::SetHddEnable(enable); - }, "Enable mounting of connected USB/HDD devices. " - "Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n); - - options->Add("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){ - App::SetWriteProtect(enable); - }, "Makes the connected HDD read-only."_i18n); }, "Toggle FTP, MTP, HDD and NXlink\n\n" "If Sphaira has a update available, you can download it from this menu"_i18n); diff --git a/sphaira/source/ui/sidebar.cpp b/sphaira/source/ui/sidebar.cpp index 0e433f3..36557a8 100644 --- a/sphaira/source/ui/sidebar.cpp +++ b/sphaira/source/ui/sidebar.cpp @@ -124,24 +124,27 @@ SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, const } else { m_option ^= 1; m_callback(m_option); + SetDirty(); } } }); } SidebarEntryBool::SidebarEntryBool(const std::string& title, bool& option, const std::string& info, const std::string& true_str, const std::string& false_str) : SidebarEntryBool{title, option, Callback{}, info, true_str, false_str} { - m_callback = [&option](bool&){ + m_callback = [this, &option](bool&){ option ^= 1; + SetDirty(); }; } SidebarEntryBool::SidebarEntryBool(const std::string& title, option::OptionBool& option, const Callback& cb, const std::string& info, const std::string& true_str, const std::string& false_str) : SidebarEntryBool{title, option.Get(), Callback{}, info, true_str, false_str} { - m_callback = [&option, cb](bool& v_out){ + m_callback = [this, &option, cb](bool& v_out){ if (cb) { cb(v_out); } option.Set(v_out); + SetDirty(); }; } @@ -166,6 +169,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl DependsClick(); } else { m_value = std::clamp(m_value - m_inc, m_min, m_max); + SetDirty(); // m_callback(m_option); } } }); @@ -174,6 +178,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl DependsClick(); } else { m_value = std::clamp(m_value + m_inc, m_min, m_max); + SetDirty(); // m_callback(m_option); } } }); @@ -240,6 +245,8 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item App::Push( m_title, m_items, index, m_index ); + + SetDirty(); }; } @@ -275,6 +282,7 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item } else { // m_callback(m_index); m_list_callback(); + SetDirty(); }} }); } @@ -291,6 +299,7 @@ SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std:: SetAction(Button::A, Action{"OK"_i18n, [this](){ if (m_callback) { m_callback(); + SetDirty(); } }}); } @@ -300,26 +309,35 @@ void SidebarEntryTextBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_p SidebarEntryBase::DrawEntry(vg, theme, m_title, m_value, true); } -SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info) +SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback) : SidebarEntryTextBase{title, value, {}, info} , m_guide{guide} , m_len_min{len_min} -, m_len_max{len_max} { +, m_len_max{len_max} +, m_callback{callback} { SetCallback([this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) { SetValue(out); + + if (m_callback) { + m_callback(this); + } } }); } -SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info) -: SidebarEntryTextInput{title, std::to_string(value), guide, len_min, len_max, info} { +SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback) +: SidebarEntryTextInput{title, std::to_string(value), guide, len_min, len_max, info, callback} { SetCallback([this](){ s64 out = std::stoul(GetValue()); if (R_SUCCEEDED(swkbd::ShowNumPad(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) { SetValue(std::to_string(out)); + + if (m_callback) { + m_callback(this); + } } }); } @@ -331,6 +349,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s App::Push( [this](const fs::FsPath& path) { SetValue(path); + SetDirty(); return true; }, m_filter @@ -370,6 +389,17 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, fl m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48); } +Sidebar::~Sidebar() { + if (m_on_exit_when_changed) { + for (const auto& item : m_items) { + if (item->IsDirty()) { + m_on_exit_when_changed(); + break; + } + } + } +} + auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void { Widget::Update(controller, touch);