diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 4b22e03..2cdbc95 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -182,15 +182,37 @@ inline FsPath operator+(const std::string_view& v, const FsPath& fp) { return r += fp; } -static_assert(FsPath::Test("abc")); -static_assert(FsPath::Test(std::string_view{"abc"})); -static_assert(FsPath::Test(std::string{"abc"})); -static_assert(FsPath::Test(FsPath{"abc"})); +// Fs seems to be limted to file paths of 255 characters. +struct FsPathReal { + static constexpr inline size_t FS_REAL_MAX_LENGTH = 255; -static_assert(FsPath::TestFrom("abc")); -static_assert(FsPath::TestFrom(std::string_view{"abc"})); -static_assert(FsPath::TestFrom(std::string{"abc"})); -static_assert(FsPath::TestFrom(FsPath{"abc"})); + constexpr FsPathReal(const FsPath& str) : FsPathReal{str.s} { } + explicit constexpr FsPathReal(const char* str) { + size_t real = 0; + for (size_t i = 0; str[i]; i++) { + // skip multiple slashes. + if (i && str[i] == '/' && str[i - 1] == '/') { + continue; + } + + // save single char. + s[real++] = str[i]; + + // check if we have exceeded the path. + if (real >= FS_REAL_MAX_LENGTH) { + break; + } + } + + // null the end. + s[real] = '\0'; + } + + constexpr operator const char*() const { return s; } + constexpr operator std::string_view() const { return s; } + + char s[FS_MAX_PATH]; +}; // fwd struct Fs; @@ -227,44 +249,38 @@ struct Dir { FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path); -Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true); -Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); +Result CreateFile(FsFileSystem* fs, const FsPathReal& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true); +Result CreateDirectory(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only = true); Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); -Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); -Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); -Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true); -Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true); -Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true); -Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out); -Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out); -Result SetTimestamp(FsFileSystem* fs, const FsPath& path, const FsTimeStampRaw* ts); +Result DeleteFile(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only = true); +Result DeleteDirectory(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only = true); +Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only = true); +Result RenameFile(FsFileSystem* fs, const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only = true); +Result RenameDirectory(FsFileSystem* fs, const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only = true); +Result GetEntryType(FsFileSystem* fs, const FsPathReal& path, FsDirEntryType* out); +Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPathReal& path, FsTimeStampRaw *out); +Result SetTimestamp(FsFileSystem* fs, const FsPathReal& path, const FsTimeStampRaw* ts); bool FileExists(FsFileSystem* fs, const FsPath& path); bool DirExists(FsFileSystem* fs, const FsPath& path); -Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector& out); -Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector& in, bool ignore_read_only = true); -Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true); -Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true); -Result CreateDirectory(const FsPath& path, bool ignore_read_only = true); +Result CreateFile(const FsPathReal& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true); +Result CreateDirectory(const FsPathReal& path, bool ignore_read_only = true); Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = true); Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = true); -Result DeleteFile(const FsPath& path, bool ignore_read_only = true); -Result DeleteDirectory(const FsPath& path, bool ignore_read_only = true); +Result DeleteFile(const FsPathReal& path, bool ignore_read_only = true); +Result DeleteDirectory(const FsPathReal& path, bool ignore_read_only = true); Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = true); -Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = true); -Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = true); -Result GetEntryType(const FsPath& path, FsDirEntryType* out); -Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out); -Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts); +Result RenameFile(const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only = true); +Result RenameDirectory(const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only = true); +Result GetEntryType(const FsPathReal& path, FsDirEntryType* out); +Result GetFileTimeStampRaw(const FsPathReal& path, FsTimeStampRaw *out); +Result SetTimestamp(const FsPathReal& path, const FsTimeStampRaw* ts); bool FileExists(const FsPath& path); bool DirExists(const FsPath& path); -Result read_entire_file(const FsPath& path, std::vector& out); -Result write_entire_file(const FsPath& path, const std::vector& in, bool ignore_read_only = true); -Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true); -Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f); -Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d); +Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f); +Result OpenDirectory(fs::Fs* fs, const FsPathReal& path, u32 mode, Dir* d); // opens dir, fetches count for all entries. // NOTE: this function will be slow on non-native fs, due to multiple @@ -281,6 +297,11 @@ Result DirGetEntryCount(fs::Fs* fs, const fs::FsPath& path, s64* file_count, s64 Result FileGetSizeAndTimestamp(fs::Fs* fs, const FsPath& path, FsTimeStampRaw* ts, s64* size); Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out); +// helpers. +Result read_entire_file(Fs* fs, const FsPath& path, std::vector& out); +Result write_entire_file(Fs* fs, const FsPath& path, const std::vector& in, bool ignore_read_only = true); +Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true); + struct Fs { Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {} virtual ~Fs() = default; @@ -302,9 +323,6 @@ struct Fs { virtual bool DirExists(const FsPath& path) = 0; virtual bool IsNative() const = 0; virtual FsPath Root() const { return "/"; } - virtual Result read_entire_file(const FsPath& path, std::vector& out) = 0; - virtual Result write_entire_file(const FsPath& path, const std::vector& in) = 0; - virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0; Result OpenFile(const fs::FsPath& path, u32 mode, File* f) { return fs::OpenFile(this, path, mode, f); @@ -324,6 +342,15 @@ struct Fs { Result IsDirEmpty(const fs::FsPath& path, bool* out) { return fs::IsDirEmpty(this, path, out); } + Result read_entire_file(const FsPath& path, std::vector& out) { + return fs::read_entire_file(this, path, out); + } + Result write_entire_file(const FsPath& path, const std::vector& in) { + return fs::write_entire_file(this, path, in, m_ignore_read_only); + } + Result copy_entire_file(const FsPath& dst, const FsPath& src) { + return fs::copy_entire_file(this, dst, src, m_ignore_read_only); + } void SetIgnoreReadOnly(bool enable) { m_ignore_read_only = enable; @@ -388,15 +415,6 @@ struct FsStdio : Fs { FsPath Root() const override { return m_root; } - Result read_entire_file(const FsPath& path, std::vector& out) override { - return fs::read_entire_file(path, out); - } - Result write_entire_file(const FsPath& path, const std::vector& in) override { - return fs::write_entire_file(path, in, m_ignore_read_only); - } - Result copy_entire_file(const FsPath& dst, const FsPath& src) override { - return fs::copy_entire_file(dst, src, m_ignore_read_only); - } const FsPath m_root; }; @@ -475,15 +493,6 @@ struct FsNative : Fs { bool IsNative() const override { return true; } - Result read_entire_file(const FsPath& path, std::vector& out) override { - return fs::read_entire_file(&m_fs, path, out); - } - Result write_entire_file(const FsPath& path, const std::vector& in) override { - return fs::write_entire_file(&m_fs, path, in, m_ignore_read_only); - } - Result copy_entire_file(const FsPath& dst, const FsPath& src) override { - return fs::copy_entire_file(&m_fs, dst, src, m_ignore_read_only); - } FsFileSystem m_fs{}; Result m_open_result{}; diff --git a/sphaira/include/ui/menus/file_picker.hpp b/sphaira/include/ui/menus/file_picker.hpp index 66ed3c6..c5adead 100644 --- a/sphaira/include/ui/menus/file_picker.hpp +++ b/sphaira/include/ui/menus/file_picker.hpp @@ -1,143 +1,19 @@ #pragma once #include "ui/menus/filebrowser.hpp" -#include "ui/menus/menu_base.hpp" -#include "ui/scrolling_text.hpp" -#include "ui/list.hpp" -#include "fs.hpp" -#include "option.hpp" -#include -namespace sphaira::ui::menu::filepicker { - -enum FsEntryFlag { - FsEntryFlag_None, - // write protected. - FsEntryFlag_ReadOnly = 1 << 0, - // supports file assoc. - FsEntryFlag_Assoc = 1 << 1, -}; - -enum SortType { - SortType_Size, - SortType_Alphabetical, -}; - -enum OrderType { - OrderType_Descending, - OrderType_Ascending, -}; - -using FsType = filebrowser::FsType; -using FsEntry = filebrowser::FsEntry; -using FileEntry = filebrowser::FileEntry; -using LastFile = filebrowser::LastFile; +namespace sphaira::ui::menu::filebrowser::picker { using Callback = std::function; -struct Menu final : MenuBase { +struct Menu final : Base { explicit Menu(const Callback& cb, const std::vector& filter = {}, const fs::FsPath& path = {}); - ~Menu(); - - auto GetShortTitle() const -> const char* override { return "Picker"; }; - void Update(Controller* controller, TouchInfo* touch) override; - void Draw(NVGcontext* vg, Theme* theme) override; - void OnFocusGained() override; - - static auto GetNewPath(const fs::FsPath& root_path, const fs::FsPath& file_path) -> fs::FsPath { - return fs::AppendPath(root_path, file_path); - } private: - auto GetFs() { - return m_fs.get(); - } - - auto& GetFsEntry() const { - return m_fs_entry; - } - - void SetIndex(s64 index); - - auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result; - - auto GetNewPath(const FileEntry& entry) const -> fs::FsPath { - return GetNewPath(m_path, entry.name); - } - - auto GetNewPath(s64 index) const -> fs::FsPath { - return GetNewPath(m_path, GetEntry(index).name); - } - - auto GetNewPathCurrent() const -> fs::FsPath { - return GetNewPath(m_index); - } - - auto GetEntry(u32 index) -> FileEntry& { - return m_entries[m_entries_current[index]]; - } - - auto GetEntry(u32 index) const -> const FileEntry& { - return m_entries[m_entries_current[index]]; - } - - auto GetEntry() -> FileEntry& { - return GetEntry(m_index); - } - - auto GetEntry() const -> const FileEntry& { - return GetEntry(m_index); - } - - auto IsSd() const -> bool { - return m_fs_entry.type == FsType::Sd; - } - - void Sort(); - void SortAndFindLastFile(bool scan = false); - void SetIndexFromLastFile(const LastFile& last_file); - - void SetFs(const fs::FsPath& new_path, const FsEntry& new_entry); - - auto GetNative() -> fs::FsNative* { - return (fs::FsNative*)m_fs.get(); - } - - void DisplayOptions(); - - void UpdateSubheading(); - void PromptIfShouldExit(); + void OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path) override; private: - static constexpr inline const char* INI_SECTION = "filepicker"; - - Callback m_callback; - std::vector m_filter; - - std::unique_ptr m_fs{}; - FsEntry m_fs_entry{}; - fs::FsPath m_path{}; - std::vector m_entries{}; - std::vector m_entries_index{}; // files not including hidden - std::vector m_entries_index_hidden{}; // includes hidden files - std::span m_entries_current{}; - - std::unique_ptr m_list{}; - - // this keeps track of the highlighted file before opening a folder - // if the user presses B to go back to the previous dir - // this vector is popped, then, that entry is checked if it still exists - // if it does, the index becomes that file. - std::vector m_previous_highlighted_file{}; - s64 m_index{}; - ScrollingText m_scroll_name{}; - - option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical, false}; - option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending, false}; - option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false, false}; - option::OptionBool m_folders_first{INI_SECTION, "folders_first", true, false}; - option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false, false}; - option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false, false}; + const Callback m_callback; }; -} // namespace sphaira::ui::menu::filepicker +} // namespace sphaira::ui::menu::filebrowser::picker diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 7912356..005edac 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -11,12 +11,35 @@ namespace sphaira::ui::menu::filebrowser { +enum FsOption : u32 { + FsOption_NONE, + + // can split screen. + FsOption_CanSplit = BIT(0), + // can upload files. + FsOption_CanUpload = BIT(1), + // can selected multiple files. + FsOption_CanSelect = BIT(2), + // shows the option to install. + FsOption_CanInstall = BIT(3), + // loads file assoc. + FsOption_LoadAssoc = BIT(4), + // do not prompt on exit even if not tabbed. + FsOption_DoNotPrompt = BIT(5), + + FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanUpload | FsOption_CanSelect, + FsOption_All = FsOption_DoNotPrompt | FsOption_Normal, + FsOption_Picker = FsOption_NONE, +}; + enum FsEntryFlag { FsEntryFlag_None, // write protected. FsEntryFlag_ReadOnly = 1 << 0, // supports file assoc. FsEntryFlag_Assoc = 1 << 1, + // this is an sd card, files can be launched from here. + FsEntryFlag_IsSd = 1 << 2, }; enum class FsType { @@ -24,6 +47,7 @@ enum class FsType { ImageNand, ImageSd, Stdio, + Custom, }; enum class SelectedType { @@ -62,13 +86,17 @@ struct FsEntry { return flags & FsEntryFlag_Assoc; } + auto IsSd() const -> bool { + return flags & FsEntryFlag_IsSd; + } + auto IsSame(const FsEntry& e) const { return root == e.root && type == e.type; } }; // roughly 1kib in size per entry -struct FileEntry : FsDirectoryEntry { +struct FileEntry final : FsDirectoryEntry { std::string extension{}; // if any std::string internal_name{}; // if any std::string internal_extension{}; // if any @@ -161,13 +189,14 @@ using FsDirCollections = std::vector; void SignalChange(); -struct Menu; +struct Base; struct FsView final : Widget { - friend class Menu; + friend class Base; - FsView(Menu* menu, ViewSide side); - FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side); + FsView(FsView* view, ViewSide side); + FsView(Base* menu, ViewSide side); + FsView(Base* menu, const std::shared_ptr& fs, const fs::FsPath& path, const FsEntry& entry, ViewSide side); ~FsView(); void Update(Controller* controller, TouchInfo* touch) override; @@ -192,7 +221,9 @@ struct FsView final : Widget { static auto get_collection(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result; static auto get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result; -private: +// private: + void OnClick(); + void SetIndex(s64 index); void InstallForwarder(); @@ -201,7 +232,7 @@ private: void ZipFiles(fs::FsPath zip_path); void UploadFiles(); - auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result; + auto Scan(fs::FsPath new_path, bool is_walk_up = false) -> Result; auto GetNewPath(const FileEntry& entry) const -> fs::FsPath { return GetNewPath(m_path, entry.name); @@ -248,7 +279,7 @@ private: } auto IsSd() const -> bool { - return m_fs_entry.type == FsType::Sd; + return m_fs_entry.IsSd(); } void Sort(); @@ -263,7 +294,7 @@ private: auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result; auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result; - void SetFs(const fs::FsPath& new_path, const FsEntry& new_entry); + void SetFs(const std::shared_ptr& fs, const fs::FsPath& new_path, const FsEntry& new_entry); auto GetNative() -> fs::FsNative* { return (fs::FsNative*)m_fs.get(); @@ -274,11 +305,11 @@ private: void DisplayOptions(); void DisplayAdvancedOptions(); -private: - Menu* m_menu{}; +// private: + Base* m_menu{}; ViewSide m_side{}; - std::unique_ptr m_fs{}; + std::shared_ptr m_fs{}; FsEntry m_fs_entry{}; fs::FsPath m_path{}; std::vector m_entries{}; @@ -345,22 +376,28 @@ struct SelectedStash { SelectedType m_type{SelectedType::None}; }; -struct Menu final : MenuBase { +struct Base : MenuBase { friend class FsView; - Menu(u32 flags); - ~Menu(); + Base(u32 flags, u32 options); + Base(const std::shared_ptr& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options); + + void SetFilter(const std::vector& filter) { + m_filter = filter; + } auto GetShortTitle() const -> const char* override { return "Files"; }; void Update(Controller* controller, TouchInfo* touch) override; void Draw(NVGcontext* vg, Theme* theme) override; - void OnFocusGained() override; + virtual void OnFocusGained() override; static auto GetNewPath(const fs::FsPath& root_path, const fs::FsPath& file_path) -> fs::FsPath { return fs::AppendPath(root_path, file_path); } -private: + virtual void OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path); + +protected: auto IsSplitScreen() const { return m_split_screen; } @@ -390,9 +427,20 @@ private: void PromptIfShouldExit(); -private: + auto CanInstall() const { + return m_options & FsOption_CanInstall; + } + + auto CreateFs(const FsEntry& fs_entry) -> std::shared_ptr; + +protected: static constexpr inline const char* INI_SECTION = "filebrowser"; + const u32 m_options; + + std::shared_ptr m_custom_fs{}; + FsEntry m_custom_fs_entry{}; + FsView* view{}; std::unique_ptr view_left{}; std::unique_ptr view_right{}; @@ -400,6 +448,8 @@ private: std::vector m_assoc_entries{}; SelectedStash m_selected{}; + std::vector m_filter{}; + option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false}; @@ -411,4 +461,19 @@ private: bool m_split_screen{}; }; +struct Menu final : Base { + Menu(u32 flags, u32 options = FsOption_All) : Base{flags, options} { + } + + Menu(const std::shared_ptr& fs, const FsEntry& fs_entry, const fs::FsPath& path, u32 options = FsOption_All) + : Base{fs, fs_entry, path, true, MenuFlag_None, options} { + + } +}; + +// case insensitive check +auto IsSamePath(std::string_view a, std::string_view b) -> bool; +auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool; +auto IsExtension(std::string_view ext, std::span list) -> bool; + } // namespace sphaira::ui::menu::filebrowser diff --git a/sphaira/include/ui/menus/game_nca_menu.hpp b/sphaira/include/ui/menus/game_nca_menu.hpp index 199ca29..d66bbf8 100644 --- a/sphaira/include/ui/menus/game_nca_menu.hpp +++ b/sphaira/include/ui/menus/game_nca_menu.hpp @@ -77,6 +77,7 @@ private: } void DumpNcas(); + Result MountNcaFs(); private: Entry& m_entry; diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp index 04971d2..dd0e0fa 100644 --- a/sphaira/include/ui/menus/gc_menu.hpp +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -199,6 +199,8 @@ private: void OnChangeIndex(s64 new_index); Result DumpGames(u32 flags); + Result MountGcFs(); + private: FsDeviceOperator m_dev_op{}; FsGameCardHandle m_handle{}; diff --git a/sphaira/include/ui/menus/save_menu.hpp b/sphaira/include/ui/menus/save_menu.hpp index 7a71747..b157f8c 100644 --- a/sphaira/include/ui/menus/save_menu.hpp +++ b/sphaira/include/ui/menus/save_menu.hpp @@ -80,6 +80,8 @@ private: m_selected_count = 0; } + void DisplayOptions(); + void BackupSaves(std::vector>& entries); void RestoreSave(); @@ -87,6 +89,8 @@ private: Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const; Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const; + Result MountSaveFs(); + private: static constexpr inline const char* INI_SECTION = "saves"; diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp index fa091bb..474c403 100644 --- a/sphaira/include/yati/nx/ncm.hpp +++ b/sphaira/include/yati/nx/ncm.hpp @@ -80,6 +80,6 @@ static constexpr inline bool HasRequiredSystemVersion(const NcmContentMetaKey *k } // fills program id and out path of the control nca. -Result GetControlPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path); +Result GetFsPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path); } // namespace sphaira::ncm diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 347fff1..a6e8514 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1512,10 +1512,9 @@ App::App(const char* argv0) { plsrPlayerInit(); } - if (R_SUCCEEDED(romfsMountDataStorageFromProgram(0x0100000000001000, "qlaunch"))) { - ON_SCOPE_EXIT(romfsUnmount("qlaunch")); + if (R_SUCCEEDED(romfsMountDataStorageFromProgram(0x0100000000001000, "Qlaunch_romfs"))) { PLSR_BFSAR qlaunch_bfsar; - if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) { + if (R_SUCCEEDED(plsrBFSAROpen("Qlaunch_romfs:/sound/qlaunch.bfsar", &qlaunch_bfsar))) { ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar)); const auto load_sound = [&](const char* name, u32 id) { @@ -2075,6 +2074,7 @@ App::~App() { } fatfs::UnmountAll(); + romfsUnmount("Qlaunch_romfs"); log_write("\t[EXIT] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); diff --git a/sphaira/source/download.cpp b/sphaira/source/download.cpp index 8fd383f..5d81afe 100644 --- a/sphaira/source/download.cpp +++ b/sphaira/source/download.cpp @@ -313,8 +313,8 @@ struct ThreadQueueEntry { }; struct ThreadQueue { - std::deque m_entries; - Thread m_thread; + std::deque m_entries{}; + Thread m_thread{}; Mutex m_mutex{}; UEvent m_uevent{}; @@ -1066,7 +1066,6 @@ void ThreadQueue::ThreadFunc(void* p) { } // find the next avaliable thread - u32 pop_count{}; for (auto& entry : data->m_entries) { if (!g_running) { return; @@ -1080,13 +1079,14 @@ void ThreadQueue::ThreadFunc(void* p) { } if (!thread.InProgress()) { - thread.Setup(entry.api); - // log_write("[dl queue] starting download\n"); - // mark entry for deletion - entry.m_delete = true; - pop_count++; - keep_going = true; - break; + if (thread.Setup(entry.api)) { + // log_write("[dl queue] starting download\n"); + // mark entry for deletion + entry.m_delete = true; + // pop_count++; + keep_going = true; + break; + } } } @@ -1096,9 +1096,9 @@ void ThreadQueue::ThreadFunc(void* p) { } // delete all entries marked for deletion - for (u32 i = 0; i < pop_count; i++) { - data->m_entries.pop_front(); - } + std::erase_if(data->m_entries, [](auto& e){ + return e.m_delete; + }); } log_write("exited download thread queue\n"); diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index 041238d..68a089e 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -19,6 +19,16 @@ namespace fs { namespace { +static_assert(FsPath::Test("abc")); +static_assert(FsPath::Test(std::string_view{"abc"})); +static_assert(FsPath::Test(std::string{"abc"})); +static_assert(FsPath::Test(FsPath{"abc"})); + +static_assert(FsPath::TestFrom("abc")); +static_assert(FsPath::TestFrom(std::string_view{"abc"})); +static_assert(FsPath::TestFrom(std::string{"abc"})); +static_assert(FsPath::TestFrom(FsPath{"abc"})); + // these folders and internals cannot be modified constexpr std::string_view READONLY_ROOT_FOLDERS[]{ "/atmosphere/automatic_backups", @@ -109,19 +119,58 @@ FsPath AppendPath(const FsPath& root_path, const FsPath& _file_path) { return path; } -Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size, u32 option, bool ignore_read_only) { +Result read_entire_file(Fs* fs, const FsPath& path, std::vector& out) { + File f; + R_TRY(fs->OpenFile(path, FsOpenMode_Read, &f)); + + s64 size; + R_TRY(f.GetSize(&size)); + out.resize(size); + + u64 bytes_read; + R_TRY(f.Read(0, out.data(), out.size(), FsReadOption_None, &bytes_read)); + R_UNLESS(bytes_read == out.size(), 1); + + R_SUCCEED(); +} + +Result write_entire_file(Fs* fs, const FsPath& path, const std::vector& in, bool ignore_read_only) { + R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); + + if (auto rc = fs->CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) { + return rc; + } + + File f; + R_TRY(fs->OpenFile(path, FsOpenMode_Write, &f)); + R_TRY(f.SetSize(in.size())); + R_TRY(f.Write(0, in.data(), in.size(), FsWriteOption_None)); + + R_SUCCEED(); +} + +Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) { + R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); + + std::vector data; + R_TRY(read_entire_file(fs, src, data)); + return write_entire_file(fs, dst, data, ignore_read_only); +} + +Result CreateFile(FsFileSystem* fs, const FsPathReal& path, u64 size, u32 option, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly); if (size >= 1024ULL*1024ULL*1024ULL*4ULL) { option |= FsCreateOption_BigFile; } + log_write("trying to create path: %s\n", path.s); R_TRY(fsFsCreateFile(fs, path, size, option)); fsFsCommit(fs); R_SUCCEED(); } -Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) { +Result CreateDirectory(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly); R_TRY(fsFsCreateDirectory(fs, path)); @@ -192,14 +241,14 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path, R_SUCCEED(); } -Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) { +Result DeleteFile(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); R_TRY(fsFsDeleteFile(fs, path)); fsFsCommit(fs); R_SUCCEED(); } -Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) { +Result DeleteDirectory(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); R_TRY(fsFsDeleteDirectory(fs, path)); @@ -207,7 +256,7 @@ Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_on R_SUCCEED(); } -Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) { +Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); R_TRY(fsFsDeleteDirectoryRecursively(fs, path)); @@ -215,7 +264,7 @@ Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ign R_SUCCEED(); } -Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) { +Result RenameFile(FsFileSystem* fs, const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly); R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); @@ -224,7 +273,7 @@ Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool i R_SUCCEED(); } -Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) { +Result RenameDirectory(FsFileSystem* fs, const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly); R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); @@ -233,15 +282,15 @@ Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, b R_SUCCEED(); } -Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out) { +Result GetEntryType(FsFileSystem* fs, const FsPathReal& path, FsDirEntryType* out) { return fsFsGetEntryType(fs, path, out); } -Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out) { +Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPathReal& path, FsTimeStampRaw *out) { return fsFsGetFileTimeStampRaw(fs, path, out); } -Result SetTimestamp(FsFileSystem* fs, const FsPath& path, const FsTimeStampRaw* ts) { +Result SetTimestamp(FsFileSystem* fs, const FsPathReal& path, const FsTimeStampRaw* ts) { // unsuported. R_SUCCEED(); } @@ -258,51 +307,7 @@ bool DirExists(FsFileSystem* fs, const FsPath& path) { return type == FsDirEntryType_Dir; } -Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector& out) { - FsNative fs{_fs, false}; - R_TRY(fs.GetFsOpenResult()); - - File f; - R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f)); - - s64 size; - R_TRY(f.GetSize(&size)); - out.resize(size); - - u64 bytes_read; - R_TRY(f.Read(0, out.data(), out.size(), FsReadOption_None, &bytes_read)); - R_UNLESS(bytes_read == out.size(), 1); - - R_SUCCEED(); -} - -Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector& in, bool ignore_read_only) { - R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); - - FsNative fs{_fs, false, ignore_read_only}; - R_TRY(fs.GetFsOpenResult()); - - if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) { - return rc; - } - - File f; - R_TRY(fs.OpenFile(path, FsOpenMode_Write, &f)); - R_TRY(f.SetSize(in.size())); - R_TRY(f.Write(0, in.data(), in.size(), FsWriteOption_None)); - - R_SUCCEED(); -} - -Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) { - R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); - - std::vector data; - R_TRY(read_entire_file(fs, src, data)); - return write_entire_file(fs, dst, data, ignore_read_only); -} - -Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_only) { +Result CreateFile(const FsPathReal& path, u64 size, u32 option, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly); auto fd = open(path, O_WRONLY | O_CREAT, DEFFILEMODE); @@ -323,7 +328,7 @@ Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_onl R_SUCCEED(); } -Result CreateDirectory(const FsPath& path, bool ignore_read_only) { +Result CreateDirectory(const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly); if (mkdir(path, ACCESSPERMS)) { @@ -349,7 +354,7 @@ Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_o return CreateDirectoryRecursivelyWithPath(nullptr, path, ignore_read_only); } -Result DeleteFile(const FsPath& path, bool ignore_read_only) { +Result DeleteFile(const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); if (unlink(path)) { @@ -359,7 +364,7 @@ Result DeleteFile(const FsPath& path, bool ignore_read_only) { R_SUCCEED(); } -Result DeleteDirectory(const FsPath& path, bool ignore_read_only) { +Result DeleteDirectory(const FsPathReal& path, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); if (rmdir(path)) { @@ -390,7 +395,7 @@ Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) { #endif } -Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) { +Result RenameFile(const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly); R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); @@ -401,14 +406,14 @@ Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) { R_SUCCEED(); } -Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only) { +Result RenameDirectory(const FsPathReal& src, const FsPathReal& dst, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly); R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); return RenameFile(src, dst, ignore_read_only); } -Result GetEntryType(const FsPath& path, FsDirEntryType* out) { +Result GetEntryType(const FsPathReal& path, FsDirEntryType* out) { struct stat st; if (stat(path, &st)) { R_TRY(fsdevGetLastResult()); @@ -418,7 +423,7 @@ Result GetEntryType(const FsPath& path, FsDirEntryType* out) { R_SUCCEED(); } -Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) { +Result GetFileTimeStampRaw(const FsPathReal& path, FsTimeStampRaw *out) { struct stat st; if (stat(path, &st)) { R_TRY(fsdevGetLastResult()); @@ -432,7 +437,7 @@ Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) { R_SUCCEED(); } -Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts) { +Result SetTimestamp(const FsPathReal& path, const FsTimeStampRaw* ts) { if (ts->is_valid) { timeval val[2]{}; val[0].tv_sec = ts->accessed; @@ -458,47 +463,7 @@ bool DirExists(const FsPath& path) { return type == FsDirEntryType_Dir; } -Result read_entire_file(const FsPath& path, std::vector& out) { - auto f = std::fopen(path, "rb"); - if (!f) { - R_TRY(fsdevGetLastResult()); - return Result_FsUnknownStdioError; - } - ON_SCOPE_EXIT(std::fclose(f)); - - std::fseek(f, 0, SEEK_END); - const auto size = std::ftell(f); - std::rewind(f); - - out.resize(size); - - std::fread(out.data(), 1, out.size(), f); - R_SUCCEED(); -} - -Result write_entire_file(const FsPath& path, const std::vector& in, bool ignore_read_only) { - R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); - - auto f = std::fopen(path, "wb"); - if (!f) { - R_TRY(fsdevGetLastResult()); - return Result_FsUnknownStdioError; - } - ON_SCOPE_EXIT(std::fclose(f)); - - std::fwrite(in.data(), 1, in.size(), f); - R_SUCCEED(); -} - -Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only) { - R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly); - - std::vector data; - R_TRY(read_entire_file(src, data)); - return write_entire_file(dst, data, ignore_read_only); -} - -Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) { +Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f) { f->m_fs = fs; f->m_mode = mode; @@ -624,7 +589,7 @@ void File::Close() { } } -Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) { +Result OpenDirectory(fs::Fs* fs, const FsPathReal& path, u32 mode, Dir* d) { d->m_fs = fs; d->m_mode = mode; diff --git a/sphaira/source/location.cpp b/sphaira/source/location.cpp index 9b2fd56..eabd1b0 100644 --- a/sphaira/source/location.cpp +++ b/sphaira/source/location.cpp @@ -109,6 +109,9 @@ auto GetStdio(bool write) -> StdioEntries { auto GetFat() -> StdioEntries { StdioEntries out{}; + // todo: move this somewhere else. + out.emplace_back("Qlaunch_romfs:/", "Qlaunch RomFS (Read Only)", true); + for (auto& e : VolumeStr) { char path[64]; std::snprintf(path, sizeof(path), "%s:/", e); diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp index 071e52c..6befd23 100644 --- a/sphaira/source/title_info.cpp +++ b/sphaira/source/title_info.cpp @@ -486,7 +486,7 @@ Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u6 NcmContentId content_id; R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control)); - return ncm::GetControlPathFromContentId(&cs, key, content_id, out_program_id, out_path); + return ncm::GetFsPathFromContentId(&cs, key, content_id, out_program_id, out_path); } // taken from nxdumptool. diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 036f70f..d6f3b84 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -634,7 +634,7 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu) const auto path = BuildManifestCachePath(m_entry); std::vector data; - if (R_SUCCEEDED(fs::read_entire_file(path, data))) { + if (R_SUCCEEDED(fs::FsNativeSd().read_entire_file(path, data))) { m_file_list_state = ImageDownloadState::Done; data.push_back('\0'); m_manifest_list = std::make_unique((const char*)data.data(), 0, 374, 250, 768, 18); diff --git a/sphaira/source/ui/menus/file_picker.cpp b/sphaira/source/ui/menus/file_picker.cpp index 685d464..6b331ad 100644 --- a/sphaira/source/ui/menus/file_picker.cpp +++ b/sphaira/source/ui/menus/file_picker.cpp @@ -1,561 +1,28 @@ #include "ui/menus/file_picker.hpp" -#include "ui/sidebar.hpp" -#include "ui/option_box.hpp" -#include "ui/popup_list.hpp" -#include "ui/error_box.hpp" - -#include "log.hpp" -#include "app.hpp" -#include "ui/nvg_util.hpp" -#include "fs.hpp" -#include "defines.hpp" #include "i18n.hpp" -#include "location.hpp" -#include "minizip_helper.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace sphaira::ui::menu::filepicker { -namespace { - -constexpr FsEntry FS_ENTRY_DEFAULT{ - "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc, -}; - -constexpr FsEntry FS_ENTRIES[]{ - FS_ENTRY_DEFAULT, -}; - -constexpr std::string_view AUDIO_EXTENSIONS[] = { - "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", - "bfsar", "bfstm", -}; -constexpr std::string_view VIDEO_EXTENSIONS[] = { - "mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts", - "m2v", "m4a", "mov", "mpeg", "mpg", "mts", "swf", "ts", "vob", "wma", "wmv", -}; -constexpr std::string_view IMAGE_EXTENSIONS[] = { - "png", "jpg", "jpeg", "bmp", "gif", -}; -constexpr std::string_view INSTALL_EXTENSIONS[] = { - "nsp", "xci", "nsz", "xcz", -}; -constexpr std::string_view ZIP_EXTENSIONS[] = { - "zip", -}; - -// case insensitive check -auto IsSamePath(std::string_view a, std::string_view b) -> bool { - return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length()); -} - -auto IsExtension(std::string_view ext, std::span list) -> bool { - for (auto e : list) { - if (e.length() == ext.length() && !strncasecmp(ext.data(), e.data(), ext.length())) { - return true; - } - } - return false; -} - -auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool { - return ext1.length() == ext2.length() && !strncasecmp(ext1.data(), ext2.data(), ext1.length()); -} - -} // namespace - -void Menu::SetIndex(s64 index) { - m_index = index; - if (!m_index) { - m_list->SetYoff(); - } - - if (IsSd() && !m_entries_current.empty() && !GetEntry().checked_internal_extension && IsSamePath(GetEntry().extension, "zip")) { - GetEntry().checked_internal_extension = true; - - TimeStamp ts; - fs::FsPath filename_inzip{}; - if (R_SUCCEEDED(mz::PeekFirstFileName(GetFs(), GetNewPathCurrent(), filename_inzip))) { - if (auto ext = std::strrchr(filename_inzip, '.')) { - GetEntry().internal_name = filename_inzip.toString(); - GetEntry().internal_extension = ext+1; - } - log_write("\tzip, time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - } - } - - UpdateSubheading(); -} - -auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { - App::SetBoostMode(true); - ON_SCOPE_EXIT(App::SetBoostMode(false)); - - log_write("new scan path: %s\n", new_path.s); - if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) { - const LastFile f(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); - m_previous_highlighted_file.emplace_back(f); - } - - m_path = new_path; - m_entries.clear(); - m_entries_index.clear(); - m_entries_index_hidden.clear(); - m_entries_current = {}; - SetIndex(0); - SetTitleSubHeading(m_path); - - fs::Dir d; - R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d)); - - // we won't run out of memory here (tm) - std::vector dir_entries; - R_TRY(d.ReadAll(dir_entries)); - - const auto count = dir_entries.size(); - m_entries.reserve(count); - m_entries_index.reserve(count); - m_entries_index_hidden.reserve(count); - - u32 i = 0; - for (const auto& e : dir_entries) { - m_entries_index_hidden.emplace_back(i); - - bool hidden = false; - // check if we have a filter. - if (e.type == FsDirEntryType_File && !m_filter.empty()) { - hidden = true; - if (const auto ext = std::strrchr(e.name, '.')) { - for (const auto& filter : m_filter) { - if (IsExtension(ext, filter)) { - hidden = false; - break; - } - } - } - } - - if (!hidden) { - m_entries_index.emplace_back(i); - } - - m_entries.emplace_back(e); - i++; - } - - Sort(); - - // find previous entry - if (is_walk_up && !m_previous_highlighted_file.empty()) { - ON_SCOPE_EXIT(m_previous_highlighted_file.pop_back()); - SetIndexFromLastFile(m_previous_highlighted_file.back()); - } - - log_write("finished scan\n"); - R_SUCCEED(); -} - -void Menu::Sort() { - // returns true if lhs should be before rhs - const auto sort = m_sort.Get(); - const auto order = m_order.Get(); - const auto folders_first = m_folders_first.Get(); - const auto hidden_last = m_hidden_last.Get(); - - const auto sorter = [this, sort, order, folders_first, hidden_last](u32 _lhs, u32 _rhs) -> bool { - const auto& lhs = m_entries[_lhs]; - const auto& rhs = m_entries[_rhs]; - - if (hidden_last) { - if (lhs.IsHidden() && !rhs.IsHidden()) { - return false; - } else if (!lhs.IsHidden() && rhs.IsHidden()) { - return true; - } - } - - if (folders_first) { - if (lhs.type == FsDirEntryType_Dir && !(rhs.type == FsDirEntryType_Dir)) { // left is folder - return true; - } else if (!(lhs.type == FsDirEntryType_Dir) && rhs.type == FsDirEntryType_Dir) { // right is folder - return false; - } - } - - switch (sort) { - case SortType_Size: { - if (lhs.file_size == rhs.file_size) { - return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0; - } else if (order == OrderType_Descending) { - return lhs.file_size > rhs.file_size; - } else { - return lhs.file_size < rhs.file_size; - } - } break; - case SortType_Alphabetical: { - if (order == OrderType_Descending) { - return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0; - } else { - return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) > 0; - } - } break; - } - - std::unreachable(); - }; - - if (m_show_hidden.Get()) { - m_entries_current = m_entries_index_hidden; - } else { - m_entries_current = m_entries_index; - } - - std::sort(m_entries_current.begin(), m_entries_current.end(), sorter); -} - -void Menu::SortAndFindLastFile(bool scan) { - std::optional last_file; - if (!m_path.empty() && !m_entries_current.empty()) { - last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); - } - - if (scan) { - Scan(m_path); - } else { - Sort(); - } - - if (last_file.has_value()) { - SetIndexFromLastFile(*last_file); - } -} - -void Menu::SetIndexFromLastFile(const LastFile& last_file) { - SetIndex(0); - - s64 index = -1; - for (u64 i = 0; i < m_entries_current.size(); i++) { - if (last_file.name == GetEntry(i).name) { - index = i; - break; - } - } - if (index >= 0) { - if (index == last_file.index && m_entries_current.size() == last_file.entries_count) { - m_list->SetYoff(last_file.offset); - log_write("index is the same as last time\n"); - } else { - // file position changed! - log_write("file position changed\n"); - // guesstimate where the position is - if (index >= 8) { - m_list->SetYoff(((index - 8) + 1) * m_list->GetMaxY()); - } else { - m_list->SetYoff(0); - } - } - SetIndex(index); - } -} - -void Menu::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) { - if (m_fs && m_fs_entry.root == new_entry.root && m_fs_entry.type == new_entry.type) { - log_write("same fs, ignoring\n"); - return; - } - - // m_fs.reset(); - m_path = new_path; - m_entries.clear(); - m_entries_index.clear(); - m_entries_index_hidden.clear(); - m_entries_current = {}; - m_previous_highlighted_file.clear(); - m_fs_entry = new_entry; - - switch (new_entry.type) { - case FsType::Sd: - m_fs = std::make_unique(m_ignore_read_only.Get()); - break; - case FsType::ImageNand: - m_fs = std::make_unique(FsImageDirectoryId_Nand); - break; - case FsType::ImageSd: - m_fs = std::make_unique(FsImageDirectoryId_Sd); - break; - case FsType::Stdio: - m_fs = std::make_unique(true, new_entry.root); - break; - } - - if (HasFocus()) { - if (m_path.empty()) { - Scan(m_fs->Root()); - } else { - Scan(m_path); - } - } -} - -void Menu::DisplayOptions() { - auto options = std::make_unique("File Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - SidebarEntryArray::Items mount_items; - std::vector fs_entries; - - const auto stdio_locations = location::GetStdio(false); - for (const auto& e: stdio_locations) { - u32 flags{}; - if (e.write_protect) { - flags |= FsEntryFlag_ReadOnly; - } - - fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); - mount_items.push_back(e.name); - } - - for (const auto& e: FS_ENTRIES) { - fs_entries.emplace_back(e); - mount_items.push_back(i18n::get(e.name)); - } - - options->Add("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ - App::PopToMenu(); - SetFs(fs_entries[index_out].root, fs_entries[index_out]); - }, i18n::get(m_fs_entry.name)); -} +namespace sphaira::ui::menu::filebrowser::picker { Menu::Menu(const Callback& cb, const std::vector& filter, const fs::FsPath& path) -: MenuBase{"FilePicker"_i18n, MenuFlag_None} -, m_callback{cb} -, m_filter{filter} { - FsEntry entry = FS_ENTRY_DEFAULT; +: Base{MenuFlag_None, FsOption_Picker} +, m_callback{cb} { + SetFilter(filter); + SetTitle("File Picker"_i18n); +} - if (!IsTab()) { - SetAction(Button::SELECT, Action{"Close"_i18n, [this](){ - PromptIfShouldExit(); - }}); - } - - this->SetActions( - std::make_pair(Button::A, Action{"Open"_i18n, [this](){ - if (m_entries_current.empty()) { - return; - } - - const auto& entry = GetEntry(); - - if (entry.type == FsDirEntryType_Dir) { - // todo: add support for folder picker. - Scan(GetNewPathCurrent()); - } else { - if (m_callback(GetNewPathCurrent())) { +void Menu::OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path) { + if (entry.type == FsDirEntryType_Dir) { + view->Scan(path); + } else { + for (auto& e : m_filter) { + if (IsExtension(e, entry.GetExtension())) { + if (m_callback(path)) { SetPop(); } - } - }}), - - std::make_pair(Button::B, Action{"Back"_i18n, [this](){ - if (!IsTab() && App::GetApp()->m_controller.GotHeld(Button::R2)) { - PromptIfShouldExit(); - return; - } - - std::string_view view{m_path}; - if (view != m_fs->Root()) { - const auto end = view.find_last_of('/'); - assert(end != view.npos); - - if (end == 0) { - Scan(m_fs->Root(), true); - } else { - Scan(view.substr(0, end), true); - } - } else { - if (!IsTab()) { - PromptIfShouldExit(); - } - } - }}), - - std::make_pair(Button::X, Action{"Options"_i18n, [this](){ - DisplayOptions(); - }}) - ); - - const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60}; - m_list = std::make_unique(1, 8, m_pos, v); - - auto buf = path; - if (path.empty()) { - ini_gets(INI_SECTION, "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH); - } - - SetFs(buf, entry); -} - -Menu::~Menu() { - // don't store mount points for non-sd card paths. - if (IsSd()) { - ini_puts(INI_SECTION, "last_path", m_path, App::CONFIG_PATH); - - // save last selected file. - if (!m_entries.empty()) { - ini_puts(INI_SECTION, "last_file", GetEntry().name, App::CONFIG_PATH); - } - } -} - -void Menu::Update(Controller* controller, TouchInfo* touch) { - MenuBase::Update(controller, touch); - - m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) { - if (touch && m_index == i) { - FireAction(Button::A); - } else { - App::PlaySoundEffect(SoundEffect_Focus); - SetIndex(i); - } - }); -} - -void Menu::Draw(NVGcontext* vg, Theme* theme) { - MenuBase::Draw(vg, theme); - - const auto& text_col = theme->GetColour(ThemeEntryID_TEXT); - - if (m_entries_current.empty()) { - gfx::drawTextArgs(vg, GetX() + GetW() / 2.f, GetY() + GetH() / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str()); - return; - } - - constexpr float text_xoffset{15.f}; - bool got_dir_count = false; - - m_list->Draw(vg, theme, m_entries_current.size(), [this, text_col, &got_dir_count](auto* vg, auto* theme, auto& v, auto i) { - const auto& [x, y, w, h] = v; - auto& e = GetEntry(i); - - auto text_id = ThemeEntryID_TEXT; - const auto selected = m_index == i; - if (selected) { - text_id = ThemeEntryID_TEXT_SELECTED; - gfx::drawRectOutline(vg, theme, 4.f, v); - } else { - if (i != m_entries_current.size() - 1) { - gfx::drawRect(vg, Vec4{x, y + h, w, 1.f}, theme->GetColour(ThemeEntryID_LINE_SEPARATOR)); - } - } - - if (e.IsDir()) { - DrawElement(x + text_xoffset, y + 5, 50, 50, ThemeEntryID_ICON_FOLDER); - } else { - auto icon = ThemeEntryID_ICON_FILE; - const auto ext = e.GetExtension(); - if (IsExtension(ext, AUDIO_EXTENSIONS)) { - icon = ThemeEntryID_ICON_AUDIO; - } else if (IsExtension(ext, VIDEO_EXTENSIONS)) { - icon = ThemeEntryID_ICON_VIDEO; - } else if (IsExtension(ext, IMAGE_EXTENSIONS)) { - icon = ThemeEntryID_ICON_IMAGE; - } else if (IsExtension(ext, INSTALL_EXTENSIONS)) { - // todo: maybe replace this icon with something else? - icon = ThemeEntryID_ICON_NRO; - } else if (IsExtension(ext, ZIP_EXTENSIONS)) { - icon = ThemeEntryID_ICON_ZIP; - } else if (IsExtension(ext, "nro")) { - icon = ThemeEntryID_ICON_NRO; - } - - DrawElement(x + text_xoffset, y + 5, 50, 50, icon); - } - - m_scroll_name.Draw(vg, selected, x + text_xoffset+65, y + (h / 2.f), w-(75+text_xoffset+65+50), 20, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), e.name); - - // NOTE: make this native only if i disable dir scan from above. - if (e.IsDir()) { - // NOTE: this takes longer than 16ms when opening a new folder due to it - // checking all 9 folders at once. - if (!got_dir_count && e.file_count == -1 && e.dir_count == -1) { - got_dir_count = true; - m_fs->DirGetEntryCount(GetNewPath(e), &e.file_count, &e.dir_count); - } - - if (e.file_count != -1) { - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%zd files"_i18n.c_str(), e.file_count); - } - if (e.dir_count != -1) { - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count); - } - } else if (e.IsFile()) { - if (!e.time_stamp.is_valid) { - const auto path = GetNewPath(e); - if (m_fs->IsNative()) { - m_fs->GetFileTimeStampRaw(path, &e.time_stamp); - } else { - m_fs->FileGetSizeAndTimestamp(path, &e.time_stamp, &e.file_size); - } - } - - const auto t = (time_t)(e.time_stamp.modified); - struct tm tm{}; - localtime_r(&t, &tm); - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900); - if ((double)e.file_size / 1024.0 / 1024.0 <= 0.009) { - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.file_size / 1024.0); - } else { - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0); - } - } - }); -} - -void Menu::OnFocusGained() { - MenuBase::OnFocusGained(); - - if (m_entries.empty()) { - if (m_path.empty()) { - Scan(m_fs->Root()); - } else { - Scan(m_path); - } - - if (IsSd() && !m_entries.empty()) { - LastFile last_file{}; - if (ini_gets(INI_SECTION, "last_file", "", last_file.name, sizeof(last_file.name), App::CONFIG_PATH)) { - SetIndexFromLastFile(last_file); + break; } } } } -void Menu::UpdateSubheading() { - const auto index = m_entries_current.empty() ? 0 : m_index + 1; - this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries_current.size())); -} - -void Menu::PromptIfShouldExit() { - if (IsTab()) { - return; - } - - App::Push( - "Close File Picker?"_i18n, - "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ - if (op_index && *op_index) { - SetPop(); - } - } - ); -} - -} // namespace sphaira::ui::menu::filepicker +} // namespace sphaira::ui::menu::filebrowser::picker diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 63c76c4..e8358f6 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -38,7 +38,6 @@ #include #include #include -#include namespace sphaira::ui::menu::filebrowser { namespace { @@ -68,7 +67,7 @@ private: constinit UEvent g_change_uevent; constexpr FsEntry FS_ENTRY_DEFAULT{ - "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc, + "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd, }; constexpr FsEntry FS_ENTRIES[]{ @@ -77,11 +76,6 @@ constexpr FsEntry FS_ENTRIES[]{ { "Image microSD card", "/", FsType::ImageSd}, }; -struct ExtDbEntry { - std::string_view db_name; - std::span ext; -}; - constexpr std::string_view AUDIO_EXTENSIONS[] = { "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", "bfsar", "bfstm", @@ -105,11 +99,6 @@ constexpr std::string_view ZIP_EXTENSIONS[] = { "zip", }; -// case insensitive check -auto IsSamePath(std::string_view a, std::string_view b) -> bool { - return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length()); -} - struct RomDatabaseEntry { // uses the naming scheme from retropie. std::string_view folder{}; @@ -181,19 +170,6 @@ constexpr RomDatabaseEntry PATHS[]{ constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"}; -auto IsExtension(std::string_view ext, std::span list) -> bool { - for (auto e : list) { - if (e.length() == ext.length() && !strncasecmp(ext.data(), e.data(), ext.length())) { - return true; - } - } - return false; -} - -auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool { - return ext1.length() == ext2.length() && !strncasecmp(ext1.data(), ext2.data(), ext1.length()); -} - // tries to find database path using folder name // names are taken from retropie // retroarch database names can also be used @@ -325,7 +301,7 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex "Set the display version of the application"_i18n ); - const std::vector filters{".nro", ".png", ".jpg"}; + const std::vector filters{"nro", "png", "jpg"}; m_icon = this->Add( "Icon", icon, filters, "Set the path to the icon for the forwarder"_i18n @@ -396,11 +372,29 @@ auto ForwarderForm::LoadNroMeta() -> Result { } // namespace +// case insensitive check +auto IsSamePath(std::string_view a, std::string_view b) -> bool { + return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length()); +} + +auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool { + return ext1.length() == ext2.length() && !strncasecmp(ext1.data(), ext2.data(), ext1.length()); +} + +auto IsExtension(std::string_view ext, std::span list) -> bool { + for (auto e : list) { + if (IsExtension(e, ext)) { + return true; + } + } + return false; +} + void SignalChange() { ueventSignal(&g_change_uevent); } -FsView::FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} { +FsView::FsView(Base* menu, const std::shared_ptr& fs, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} { this->SetActions( std::make_pair(Button::L2, Action{[this](){ if (!m_menu->m_selected.Empty()) { @@ -436,61 +430,7 @@ FsView::FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSid return; } - if (IsSd() && m_is_update_folder && m_daybreak_path.has_value()) { - App::Push("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ - if (op_index && *op_index) { - // daybreak uses native fs so do not use nro_add_arg_file - // otherwise it'll fail to open the folder... - nro_launch(m_daybreak_path.value(), nro_add_arg(m_path)); - } - }); - return; - } - - const auto& entry = GetEntry(); - - if (entry.type == FsDirEntryType_Dir) { - Scan(GetNewPathCurrent()); - } else { - // special case for nro - if (IsSd() && IsSamePath(entry.GetExtension(), "nro")) { - App::Push("Launch "_i18n + entry.GetName() + '?', - "No"_i18n, "Launch"_i18n, 1, [this](auto op_index){ - if (op_index && *op_index) { - nro_launch(GetNewPathCurrent()); - } - }); - } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { - InstallFiles(); - } else if (IsSd()) { - const auto assoc_list = m_menu->FindFileAssocFor(); - if (!assoc_list.empty()) { - // for (auto&e : assoc_list) { - // log_write("assoc got: %s\n", e.path.c_str()); - // } - - PopupList::Items items; - for (const auto&p : assoc_list) { - items.emplace_back(p.name); - } - - const auto title = "Launch option for: "_i18n + GetEntry().name; - App::Push( - title, items, [this, assoc_list](auto op_index){ - if (op_index) { - log_write("selected: %s\n", assoc_list[*op_index].name.c_str()); - nro_launch(assoc_list[*op_index].path, nro_add_arg_file(GetNewPathCurrent())); - } else { - log_write("pressed B to skip launch...\n"); - } - } - - ); - } else { - log_write("assoc list is empty\n"); - } - } - } + m_menu->OnClick(this, m_fs_entry, GetEntry(), GetNewPathCurrent()); }}), std::make_pair(Button::B, Action{"Back"_i18n, [this](){ @@ -528,21 +468,31 @@ FsView::FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSid SetSide(m_side); auto buf = path; - if (path.empty()) { + if (path.empty() && entry.IsSd()) { ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH); } - SetFs(buf, entry); + // in case the above fails. + if (buf.empty()) { + buf = entry.root; + } + + SetFs(fs, buf, entry); } -FsView::FsView(Menu* menu, ViewSide side) : FsView{menu, "", FS_ENTRY_DEFAULT, side} { +FsView::FsView(FsView* view, ViewSide side) : FsView{view->m_menu, view->m_fs, view->m_path, view->m_fs_entry, side} { + +} + +FsView::FsView(Base* menu, ViewSide side) : FsView{menu, menu->CreateFs(FS_ENTRY_DEFAULT), {}, FS_ENTRY_DEFAULT, side} { } FsView::~FsView() { // don't store mount points for non-sd card paths. - if (IsSd()) { + if (IsSd() && !m_entries_current.empty()) { ini_puts("paths", "last_path", m_path, App::CONFIG_PATH); + ini_puts("paths", "last_file", GetEntry().name, App::CONFIG_PATH); } } @@ -658,6 +608,13 @@ void FsView::OnFocusGained() { } else { Scan(m_path); } + + if (!m_entries.empty()) { + LastFile last_file{}; + if (ini_gets("paths", "last_file", "", last_file.name, sizeof(last_file.name), App::CONFIG_PATH)) { + SetIndexFromLastFile(last_file); + } + } } } @@ -696,6 +653,64 @@ void FsView::SetSide(ViewSide side) { m_scroll_name.Reset(); } +void FsView::OnClick() { + if (IsSd() && m_is_update_folder && m_daybreak_path.has_value()) { + App::Push("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ + if (op_index && *op_index) { + // daybreak uses native fs so do not use nro_add_arg_file + // otherwise it'll fail to open the folder... + nro_launch(m_daybreak_path.value(), nro_add_arg(m_path)); + } + }); + return; + } + + const auto& entry = GetEntry(); + + if (entry.type == FsDirEntryType_Dir) { + Scan(GetNewPathCurrent()); + } else { + // special case for nro + if (IsSd() && IsSamePath(entry.GetExtension(), "nro")) { + App::Push("Launch "_i18n + entry.GetName() + '?', + "No"_i18n, "Launch"_i18n, 1, [this](auto op_index){ + if (op_index && *op_index) { + nro_launch(GetNewPathCurrent()); + } + }); + } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { + InstallFiles(); + } else if (IsSd()) { + const auto assoc_list = m_menu->FindFileAssocFor(); + if (!assoc_list.empty()) { + // for (auto&e : assoc_list) { + // log_write("assoc got: %s\n", e.path.c_str()); + // } + + PopupList::Items items; + for (const auto&p : assoc_list) { + items.emplace_back(p.name); + } + + const auto title = "Launch option for: "_i18n + GetEntry().name; + App::Push( + title, items, [this, assoc_list](auto op_index){ + if (op_index) { + log_write("selected: %s\n", assoc_list[*op_index].name.c_str()); + nro_launch(assoc_list[*op_index].path, nro_add_arg_file(GetNewPathCurrent())); + } else { + log_write("pressed B to skip launch...\n"); + } + } + + ); + } else { + log_write("assoc list is empty\n"); + } + } + } +} + void FsView::SetIndex(s64 index) { m_index = index; if (!m_index) { @@ -1028,10 +1043,15 @@ void FsView::UploadFiles() { ); } -auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { +auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result { App::SetBoostMode(true); ON_SCOPE_EXIT(App::SetBoostMode(false)); + // ensure that we have a slash as part of the file name. + if (!std::strchr(new_path, '/')) { + std::strcat(new_path, "/"); + } + log_write("new scan path: %s\n", new_path.s); if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) { const LastFile f(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); @@ -1045,6 +1065,7 @@ auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { m_entries_index_search.clear(); m_entries_current = {}; m_selected_count = 0; + m_is_update_folder = false; SetIndex(0); m_menu->SetTitleSubHeading(m_path); @@ -1062,19 +1083,40 @@ auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { u32 i = 0; for (const auto& e : dir_entries) { - m_entries_index_hidden.emplace_back(i); - if ('.' != e.name[0]) { + bool hidden = false; + if ('.' == e.name[0]) { + hidden = true; + } + // check if we have a filter. + else if (e.type == FsDirEntryType_File && !m_menu->m_filter.empty()) { + hidden = true; + if (const auto ext = std::strrchr(e.name, '.')) { + for (const auto& filter : m_menu->m_filter) { + if (IsExtension(ext + 1, filter)) { + hidden = false; + break; + } + } + } + } + + if (!hidden) { m_entries_index.emplace_back(i); } + m_entries_index_hidden.emplace_back(i); m_entries.emplace_back(e); i++; } Sort(); + SetIndex(0); // quick check to see if this is an update folder - m_is_update_folder = R_SUCCEEDED(CheckIfUpdateFolder()); + // todo: only check this on click. + if (m_menu->m_options & FsOption_LoadAssoc) { + m_is_update_folder = R_SUCCEEDED(CheckIfUpdateFolder()); + } // find previous entry if (is_walk_up && !m_previous_highlighted_file.empty()) { @@ -1533,7 +1575,7 @@ static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, co R_SUCCEED(); } -void FsView::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) { +void FsView::SetFs(const std::shared_ptr& fs, const fs::FsPath& new_path, const FsEntry& new_entry) { if (m_fs && m_fs_entry.root == new_entry.root && m_fs_entry.type == new_entry.type) { log_write("same fs, ignoring\n"); return; @@ -1550,21 +1592,7 @@ void FsView::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) { m_menu->m_selected.Reset(); m_selected_count = 0; m_fs_entry = new_entry; - - switch (new_entry.type) { - case FsType::Sd: - m_fs = std::make_unique(m_menu->m_ignore_read_only.Get()); - break; - case FsType::ImageNand: - m_fs = std::make_unique(FsImageDirectoryId_Nand); - break; - case FsType::ImageSd: - m_fs = std::make_unique(FsImageDirectoryId_Sd); - break; - case FsType::Stdio: - m_fs = std::make_unique(true, new_entry.root); - break; - } + m_fs = fs; if (HasFocus()) { if (m_path.empty()) { @@ -1602,6 +1630,46 @@ void FsView::DisplayOptions() { auto options = std::make_unique("File Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); + SidebarEntryArray::Items mount_items; + std::vector fs_entries; + + const auto stdio_locations = location::GetStdio(false); + for (const auto& e: stdio_locations) { + u32 flags{}; + if (e.write_protect) { + flags |= FsEntryFlag_ReadOnly; + } + + fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); + mount_items.push_back(e.name); + } + + for (const auto& e: FS_ENTRIES) { + fs_entries.emplace_back(e); + mount_items.push_back(i18n::get(e.name)); + } + + if (m_menu->m_custom_fs) { + fs_entries.emplace_back(m_menu->m_custom_fs_entry); + mount_items.push_back(m_menu->m_custom_fs_entry.name); + } + + const auto fat_entries = location::GetFat(); + for (const auto& e: fat_entries) { + u32 flags{}; + if (e.write_protect) { + flags |= FsEntryFlag_ReadOnly; + } + + fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); + mount_items.push_back(e.name); + } + + options->Add("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ + App::PopToMenu(); + SetFs(m_menu->CreateFs(fs_entries[index_out]), fs_entries[index_out].root, fs_entries[index_out]); + }, i18n::get(m_fs_entry.name)); + options->Add("Sort By"_i18n, [this](){ auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); @@ -1712,32 +1780,34 @@ void FsView::DisplayOptions() { } // returns true if all entries match the ext array. - const auto check_all_ext = [this](auto& exts){ + const auto check_all_ext = [this](const auto& exts){ const auto entries = GetSelectedEntries(); for (auto&e : entries) { if (!IsExtension(e.GetExtension(), exts)) { + log_write("not ext: %s\n", e.GetExtension().c_str()); return false; } } return true; }; - // if install is enabled, check if all currently selected files are installable. - if (m_entries_current.size()) { - if (check_all_ext(INSTALL_EXTENSIONS)) { - auto entry = options->Add("Install"_i18n, [this](){ - InstallFiles(); - }); - entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); + if (m_menu->CanInstall()) { + if (m_entries_current.size()) { + if (check_all_ext(INSTALL_EXTENSIONS)) { + auto entry = options->Add("Install"_i18n, [this](){ + InstallFiles(); + }); + entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); + } } - } - if (IsSd() && m_entries_current.size() && !m_selected_count) { - if (GetEntry().IsFile() && (IsSamePath(GetEntry().GetExtension(), "nro") || !m_menu->FindFileAssocFor().empty())) { - auto entry = options->Add("Install Forwarder"_i18n, [this](){; - InstallForwarder(); - }); - entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); + if (IsSd() && m_entries_current.size() && !m_selected_count) { + if (GetEntry().IsFile() && (IsSamePath(GetEntry().GetExtension(), "nro") || !m_menu->FindFileAssocFor().empty())) { + auto entry = options->Add("Install Forwarder"_i18n, [this](){; + InstallForwarder(); + }); + entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); + } } } @@ -1797,41 +1867,6 @@ void FsView::DisplayAdvancedOptions() { auto options = std::make_unique("Advanced Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); - SidebarEntryArray::Items mount_items; - std::vector fs_entries; - - const auto stdio_locations = location::GetStdio(false); - for (const auto& e: stdio_locations) { - u32 flags{}; - if (e.write_protect) { - flags |= FsEntryFlag_ReadOnly; - } - - fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); - mount_items.push_back(e.name); - } - - for (const auto& e: FS_ENTRIES) { - fs_entries.emplace_back(e); - mount_items.push_back(i18n::get(e.name)); - } - - const auto fat_entries = location::GetFat(); - for (const auto& e: fat_entries) { - u32 flags{}; - if (e.write_protect) { - flags |= FsEntryFlag_ReadOnly; - } - - fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); - mount_items.push_back(e.name); - } - - options->Add("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ - App::PopToMenu(); - SetFs(fs_entries[index_out].root, fs_entries[index_out]); - }, i18n::get(m_fs_entry.name)); - if (!m_fs_entry.IsReadOnly()) { options->Add("Create File"_i18n, [this](){ std::string out; @@ -1883,7 +1918,7 @@ void FsView::DisplayAdvancedOptions() { }); } - if (m_entries_current.size()) { + if (m_entries_current.size() && (m_menu->m_options & FsOption_CanUpload)) { options->Add("Upload"_i18n, [this](){ UploadFiles(); }); @@ -1915,10 +1950,18 @@ void FsView::DisplayAdvancedOptions() { }); } -Menu::Menu(u32 flags) : MenuBase{"FileBrowser"_i18n, flags} { - SetAction(Button::L3, Action{"Split"_i18n, [this](){ - SetSplitScreen(IsSplitScreen() ^ 1); - }}); +Base::Base(u32 flags, u32 options) +: Base{CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false, flags, options} { +} + +Base::Base(const std::shared_ptr& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options) +: MenuBase{"FileBrowser"_i18n, flags} +, m_options{options} { + if (m_options & FsOption_CanSplit) { + SetAction(Button::L3, Action{"Split"_i18n, [this](){ + SetSplitScreen(IsSplitScreen() ^ 1); + }}); + } if (!IsTab()) { SetAction(Button::SELECT, Action{"Close"_i18n, [this](){ @@ -1926,15 +1969,17 @@ Menu::Menu(u32 flags) : MenuBase{"FileBrowser"_i18n, flags} { }}); } - view_left = std::make_unique(this, ViewSide::Left); + if (is_custom) { + m_custom_fs = fs; + m_custom_fs_entry = fs_entry; + } + + view_left = std::make_unique(this, fs, path, fs_entry, ViewSide::Left); view = view_left.get(); ueventCreate(&g_change_uevent, true); } -Menu::~Menu() { -} - -void Menu::Update(Controller* controller, TouchInfo* touch) { +void Base::Update(Controller* controller, TouchInfo* touch) { if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) { if (IsSplitScreen()) { view_left->SortAndFindLastFile(true); @@ -1955,8 +2000,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { view->Update(controller, touch); } -void Menu::Draw(NVGcontext* vg, Theme* theme) { - // see Menu::Update(). +void Base::Draw(NVGcontext* vg, Theme* theme) { + // see Base::Update(). const auto view_actions = view->GetActions(); m_actions.insert_range(view_actions); ON_SCOPE_EXIT(RemoveActions(view_actions)); @@ -1979,7 +2024,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } } -void Menu::OnFocusGained() { +void Base::OnFocusGained() { MenuBase::OnFocusGained(); if (IsSplitScreen()) { @@ -1996,7 +2041,11 @@ void Menu::OnFocusGained() { } } -auto Menu::FindFileAssocFor() -> std::vector { +void Base::OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path) { + view->OnClick(); +} + +auto Base::FindFileAssocFor() -> std::vector { // only support roms in correctly named folders, sorry! const auto db_indexs = GetRomDatabaseFromPath(view->m_path); const auto& entry = view->GetEntry(); @@ -2045,7 +2094,7 @@ auto Menu::FindFileAssocFor() -> std::vector { return out_entries; } -void Menu::LoadAssocEntriesPath(const fs::FsPath& path) { +void Base::LoadAssocEntriesPath(const fs::FsPath& path) { auto dir = opendir(path); if (!dir) { return; @@ -2139,22 +2188,28 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) { } } -void Menu::LoadAssocEntries() { - // load from romfs first - if (R_SUCCEEDED(romfsInit())) { - LoadAssocEntriesPath("romfs:/assoc/"); - romfsExit(); +void Base::LoadAssocEntries() { + if (m_options & FsOption_LoadAssoc) { + // load from romfs first + if (R_SUCCEEDED(romfsInit())) { + LoadAssocEntriesPath("romfs:/assoc/"); + romfsExit(); + } + // then load custom entries + LoadAssocEntriesPath("/config/sphaira/assoc/"); } - // then load custom entries - LoadAssocEntriesPath("/config/sphaira/assoc/"); } -void Menu::UpdateSubheading() { +void Base::UpdateSubheading() { const auto index = view->m_entries_current.empty() ? 0 : view->m_index + 1; this->SetSubHeading(std::to_string(index) + " / " + std::to_string(view->m_entries_current.size())); } -void Menu::SetSplitScreen(bool enable) { +void Base::SetSplitScreen(bool enable) { + if (!(m_options & FsOption_CanSplit)) { + return; + } + if (m_split_screen != enable) { m_split_screen = enable; @@ -2171,7 +2226,7 @@ void Menu::SetSplitScreen(bool enable) { // load second screen as a copy of the left side. view->SetSide(ViewSide::Left); - view_right = std::make_unique(this, view->m_path, view->GetFsEntry(), ViewSide::Right); + view_right = std::make_unique(view, ViewSide::Right); change_view(view_right.get()); SetAction(Button::LEFT, Action{[this, change_view](){ @@ -2196,7 +2251,7 @@ void Menu::SetSplitScreen(bool enable) { } } -void Menu::RefreshViews() { +void Base::RefreshViews() { ResetSelection(); if (IsSplitScreen()) { @@ -2207,11 +2262,16 @@ void Menu::RefreshViews() { } } -void Menu::PromptIfShouldExit() { +void Base::PromptIfShouldExit() { if (IsTab()) { return; } + if (m_options & FsOption_DoNotPrompt) { + SetPop(); + return; + } + App::Push( "Close FileBrowser?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ @@ -2222,4 +2282,21 @@ void Menu::PromptIfShouldExit() { ); } +auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr { + switch (fs_entry.type) { + case FsType::Sd: + return std::make_shared(m_ignore_read_only.Get()); + case FsType::ImageNand: + return std::make_shared(FsImageDirectoryId_Nand); + case FsType::ImageSd: + return std::make_shared(FsImageDirectoryId_Sd); + case FsType::Stdio: + return std::make_shared(true, fs_entry.root); + case FsType::Custom: + return m_custom_fs; + } + + std::unreachable(); +} + } // namespace sphaira::ui::menu::filebrowser diff --git a/sphaira/source/ui/menus/game_meta_menu.cpp b/sphaira/source/ui/menus/game_meta_menu.cpp index b72f19f..88363d1 100644 --- a/sphaira/source/ui/menus/game_meta_menu.cpp +++ b/sphaira/source/ui/menus/game_meta_menu.cpp @@ -40,7 +40,7 @@ constexpr u64 MINI_NACP_OFFSET = offsetof(NacpStruct, display_version); Result GetMiniNacpFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, MiniNacp& out) { u64 program_id; fs::FsPath path; - R_TRY(ncm::GetControlPathFromContentId(cs, key, id, &program_id, &path)); + R_TRY(ncm::GetFsPathFromContentId(cs, key, id, &program_id, &path)); return nca::ParseControl(path, program_id, &out, sizeof(out), nullptr, MINI_NACP_OFFSET); } diff --git a/sphaira/source/ui/menus/game_nca_menu.cpp b/sphaira/source/ui/menus/game_nca_menu.cpp index f7d7563..20b776d 100644 --- a/sphaira/source/ui/menus/game_nca_menu.cpp +++ b/sphaira/source/ui/menus/game_nca_menu.cpp @@ -1,4 +1,6 @@ #include "ui/menus/game_nca_menu.hpp" +#include "ui/menus/filebrowser.hpp" + #include "ui/nvg_util.hpp" #include "ui/sidebar.hpp" #include "ui/option_box.hpp" @@ -116,6 +118,25 @@ private: bool m_is_file_based_emummc{}; }; +Result GetFsFileSystemType(u8 content_type, FsFileSystemType& out) { + switch (content_type) { + case nca::ContentType_Meta: + out = FsFileSystemType_ContentMeta; + R_SUCCEED(); + case nca::ContentType_Control: + out = FsFileSystemType_ContentControl; + R_SUCCEED(); + case nca::ContentType_Manual: + out = FsFileSystemType_ContentManual; + R_SUCCEED(); + case nca::ContentType_Data: + out = FsFileSystemType_ContentData; + R_SUCCEED(); + } + + R_THROW(0x1); +} + } // namespace Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry) @@ -148,6 +169,10 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry) } } }}), + std::make_pair(Button::A, Action{"Mount Fs"_i18n, [this](){ + // todo: handle error here. + MountNcaFs(); + }}), std::make_pair(Button::B, Action{"Back"_i18n, [this](){ SetPop(); }}), @@ -369,4 +394,30 @@ void Menu::DumpNcas() { dump::Dump(source, paths, [](Result){}, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S); } +Result Menu::MountNcaFs() { + const auto& e = GetEntry(); + + FsFileSystemType type; + R_TRY(GetFsFileSystemType(e.header.content_type, type)); + + // get fs path from ncm. + u64 program_id; + fs::FsPath path; + R_TRY(ncm::GetFsPathFromContentId(m_meta.cs, m_meta.key, e.content_id, &program_id, &path)); + + // ensure that mounting worked. + auto fs = std::make_shared(program_id, type, path); + R_TRY(fs->GetFsOpenResult()); + + const filebrowser::FsEntry fs_entry{ + .name = "NCA", + .root = "/", + .type = filebrowser::FsType::Custom, + .flags = filebrowser::FsEntryFlag_ReadOnly, + }; + + App::Push(fs, fs_entry, "/"); + R_SUCCEED(); +} + } // namespace sphaira::ui::menu::game::meta_nca diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index 01becb8..c998dc6 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -1,4 +1,6 @@ #include "ui/menus/gc_menu.hpp" +#include "ui/menus/filebrowser.hpp" + #include "ui/nvg_util.hpp" #include "ui/sidebar.hpp" #include "ui/popup_list.hpp" @@ -24,6 +26,7 @@ namespace { constexpr u32 XCI_MAGIC = std::byteswap(0x48454144); constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool. +constexpr const char* DUMP_BASE_PATH = "/dumps/Gamecard"; enum DumpFileType { DumpFileType_XCI, @@ -48,6 +51,7 @@ enum DumpFileFlag { const char *g_option_list[] = { "Install", "Export", + "Mount", "Exit", }; @@ -120,11 +124,11 @@ auto BuildFilePath(DumpFileType type, std::span entries) #endif // builds path suiteable for file dumps. -auto BuildFullDumpPath(DumpFileType type, std::span entries) -> fs::FsPath { +auto BuildFullDumpPath(DumpFileType type, std::span entries, bool use_folder) -> fs::FsPath { const auto base_path = BuildXciBasePath(entries); fs::FsPath out; - if (App::GetApp()->m_dump_app_folder.Get()) { + if (use_folder) { if (App::GetApp()->m_dump_append_folder_with_xci.Get()) { out = base_path + ".xci/" + base_path + GetDumpTypeStr(type); } else { @@ -134,7 +138,37 @@ auto BuildFullDumpPath(DumpFileType type, std::span entr out = base_path + GetDumpTypeStr(type); } - return fs::AppendPath("/dumps/Gamecard", out); + return fs::AppendPath(DUMP_BASE_PATH, out); +} + +auto BuildFullDumpPath(DumpFileType type, std::span entries) -> fs::FsPath { + // check if the base path is too long. + const auto max_len = fs::FsPathReal::FS_REAL_MAX_LENGTH - std::strlen(DUMP_BASE_PATH) - 30; + auto use_folder = App::GetApp()->m_dump_app_folder.Get(); + + for (;;) { + const auto mult = use_folder ? 2 : 1; + + for (size_t i = entries.size(); i > 0; i--) { + // see how many entries we can append to the file name. + const auto span = entries.subspan(0, i); + const auto base_path = BuildXciBasePath(span); + + if (std::strlen(base_path) * mult < max_len) { + return BuildFullDumpPath(type, span, use_folder); + } + } + + if (!use_folder) { + // if we get here, the game name is *really* long. Give up. + log_write("[GC] huge game name, giving up: %s\n", BuildXciBasePath(entries).s); + return {}; + } else { + // try again, but without the folder. + use_folder = false; + log_write("[GC] huge game name trying again without the folder: %s\n", BuildXciBasePath(entries).s); + } + } } // @Gc is the mount point, S is for secure partion, the remaining is the @@ -367,7 +401,7 @@ auto ApplicationEntry::GetSize() const -> s64 { Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { this->SetActions( std::make_pair(Button::A, Action{"OK"_i18n, [this](){ - if (m_option_index == 2) { + if (m_option_index == 3) { SetPop(); } else { if (!m_mounted) { @@ -390,7 +424,7 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { } }); } - } else { + } else if (m_option_index == 1) { auto options = std::make_unique("Select content to dump"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); @@ -408,6 +442,9 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { add("Export Card UID"_i18n, DumpFileFlag_UID); add("Export Certificate"_i18n, DumpFileFlag_Cert); add("Export Initial Data"_i18n, DumpFileFlag_Initial); + } else if (m_option_index == 2) { + const auto rc = MountGcFs(); + App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n); } } }}), @@ -429,8 +466,9 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { ); const Vec4 v{485, 275, 720, 70}; - const Vec2 pad{0, 125 - v.h}; - m_list = std::make_unique(1, 3, m_pos, v, pad); + const Vec2 pad{0, 23.75}; + + m_list = std::make_unique(1, 4, m_pos, v, pad); fsOpenDeviceOperator(std::addressof(m_dev_op)); fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier)); @@ -513,7 +551,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED)); colour = ThemeEntryID_TEXT_SELECTED; } - if (i != 2 && !m_mounted) { + if (i != 3 && !m_mounted) { colour = ThemeEntryID_TEXT_INFO; } @@ -1066,4 +1104,21 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) { R_THROW(Result_GcFailedToGetSecurityInfo); } +Result Menu::MountGcFs() { + const auto& e = m_entries[m_entry_index]; + + auto fs = std::make_shared(&m_fs->m_fs, false); + R_TRY(m_fs->GetFsOpenResult()); + + const filebrowser::FsEntry fs_entry{ + .name = e.lang_entry.name, + .root = "/", + .type = filebrowser::FsType::Custom, + .flags = filebrowser::FsEntryFlag_ReadOnly, + }; + + App::Push(fs, fs_entry, "/"); + R_SUCCEED(); +} + } // namespace sphaira::ui::menu::gc diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index 3218c32..92b4993 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -329,102 +329,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { SetPop(); }}), std::make_pair(Button::X, Action{"Options"_i18n, [this](){ - auto options = std::make_unique("Save Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - SidebarEntryArray::Items account_items; - for (const auto& e : m_accounts) { - account_items.emplace_back(e.nickname); - } - - PopupList::Items data_type_items; - data_type_items.emplace_back("System"_i18n); - data_type_items.emplace_back("Account"_i18n); - data_type_items.emplace_back("BCAT"_i18n); - data_type_items.emplace_back("Device"_i18n); - data_type_items.emplace_back("Temporary"_i18n); - data_type_items.emplace_back("Cache"_i18n); - data_type_items.emplace_back("System BCAT"_i18n); - - options->Add("Sort By"_i18n, [this](){ - auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - SidebarEntryArray::Items sort_items; - sort_items.push_back("Updated"_i18n); - - SidebarEntryArray::Items order_items; - order_items.push_back("Descending"_i18n); - order_items.push_back("Ascending"_i18n); - - SidebarEntryArray::Items layout_items; - layout_items.push_back("List"_i18n); - layout_items.push_back("Icon"_i18n); - layout_items.push_back("Grid"_i18n); - - options->Add("Sort"_i18n, sort_items, [this](s64& index_out){ - m_sort.Set(index_out); - SortAndFindLastFile(false); - }, m_sort.Get()); - - options->Add("Order"_i18n, order_items, [this](s64& index_out){ - m_order.Set(index_out); - SortAndFindLastFile(false); - }, m_order.Get()); - - options->Add("Layout"_i18n, layout_items, [this](s64& index_out){ - m_layout.Set(index_out); - OnLayoutChange(); - }, m_layout.Get()); - }); - - options->Add("Account"_i18n, account_items, [this](s64& index_out){ - m_account_index = index_out; - m_dirty = true; - App::PopToMenu(); - }, m_account_index); - - options->Add("Data Type"_i18n, data_type_items, [this](s64& index_out){ - m_data_type = index_out; - m_dirty = true; - App::PopToMenu(); - }, m_data_type); - - if (m_entries.size()) { - options->Add("Backup"_i18n, [this](){ - std::vector> entries; - if (m_selected_count) { - for (auto& e : m_entries) { - if (e.selected) { - entries.emplace_back(e); - } - } - } else { - entries.emplace_back(m_entries[m_index]); - } - - BackupSaves(entries); - }, true); - - if (m_entries[m_index].save_data_type == FsSaveDataType_Account || m_entries[m_index].save_data_type == FsSaveDataType_Bcat) { - options->Add("Restore"_i18n, [this](){ - RestoreSave(); - }, true); - } - } - - options->Add("Advanced"_i18n, [this](){ - auto options = std::make_unique("Advanced Options"_i18n, Sidebar::Side::RIGHT); - ON_SCOPE_EXIT(App::Push(std::move(options))); - - options->Add("Auto backup on restore"_i18n, m_auto_backup_on_restore.Get(), [this](bool& v_out){ - m_auto_backup_on_restore.Set(v_out); - }); - - options->Add("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){ - m_compress_save_backup.Set(v_out); - }); - }); + DisplayOptions(); }}) ); @@ -678,6 +583,110 @@ void Menu::OnLayoutChange() { grid::Menu::OnLayoutChange(m_list, m_layout.Get()); } +void Menu::DisplayOptions() { + auto options = std::make_unique("Save Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + SidebarEntryArray::Items account_items; + for (const auto& e : m_accounts) { + account_items.emplace_back(e.nickname); + } + + PopupList::Items data_type_items; + data_type_items.emplace_back("System"_i18n); + data_type_items.emplace_back("Account"_i18n); + data_type_items.emplace_back("BCAT"_i18n); + data_type_items.emplace_back("Device"_i18n); + data_type_items.emplace_back("Temporary"_i18n); + data_type_items.emplace_back("Cache"_i18n); + data_type_items.emplace_back("System BCAT"_i18n); + + options->Add("Sort By"_i18n, [this](){ + auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + SidebarEntryArray::Items sort_items; + sort_items.push_back("Updated"_i18n); + + SidebarEntryArray::Items order_items; + order_items.push_back("Descending"_i18n); + order_items.push_back("Ascending"_i18n); + + SidebarEntryArray::Items layout_items; + layout_items.push_back("List"_i18n); + layout_items.push_back("Icon"_i18n); + layout_items.push_back("Grid"_i18n); + + options->Add("Sort"_i18n, sort_items, [this](s64& index_out){ + m_sort.Set(index_out); + SortAndFindLastFile(false); + }, m_sort.Get()); + + options->Add("Order"_i18n, order_items, [this](s64& index_out){ + m_order.Set(index_out); + SortAndFindLastFile(false); + }, m_order.Get()); + + options->Add("Layout"_i18n, layout_items, [this](s64& index_out){ + m_layout.Set(index_out); + OnLayoutChange(); + }, m_layout.Get()); + }); + + options->Add("Account"_i18n, account_items, [this](s64& index_out){ + m_account_index = index_out; + m_dirty = true; + App::PopToMenu(); + }, m_account_index); + + options->Add("Data Type"_i18n, data_type_items, [this](s64& index_out){ + m_data_type = index_out; + m_dirty = true; + App::PopToMenu(); + }, m_data_type); + + if (m_entries.size()) { + options->Add("Backup"_i18n, [this](){ + std::vector> entries; + if (m_selected_count) { + for (auto& e : m_entries) { + if (e.selected) { + entries.emplace_back(e); + } + } + } else { + entries.emplace_back(m_entries[m_index]); + } + + BackupSaves(entries); + }, true); + + if (m_entries[m_index].save_data_type == FsSaveDataType_Account || m_entries[m_index].save_data_type == FsSaveDataType_Bcat) { + options->Add("Restore"_i18n, [this](){ + RestoreSave(); + }, true); + } + + options->Add("Mount Fs"_i18n, [this](){ + const auto rc = MountSaveFs(); + App::PushErrorBox(rc, "Failed to mount save filesystem"_i18n); + }); + } + + options->Add("Advanced"_i18n, [this](){ + auto options = std::make_unique("Advanced Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->Add("Auto backup on restore"_i18n, m_auto_backup_on_restore.Get(), [this](bool& v_out){ + m_auto_backup_on_restore.Set(v_out); + }); + + options->Add("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){ + m_compress_save_backup.Set(v_out); + }); + }); +} + void Menu::BackupSaves(std::vector>& entries) { dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this, entries](const dump::DumpLocation& location){ App::Push(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result { @@ -1092,4 +1101,30 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc R_SUCCEED(); } +Result Menu::MountSaveFs() { + const auto& e = m_entries[m_index]; + const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id; + + FsSaveDataAttribute attr{}; + attr.application_id = e.application_id; + attr.uid = e.uid; + attr.system_save_data_id = e.system_save_data_id; + attr.save_data_type = e.save_data_type; + attr.save_data_rank = e.save_data_rank; + attr.save_data_index = e.save_data_index; + + auto fs = std::make_shared((FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true); + R_TRY(fs->GetFsOpenResult()); + + const filebrowser::FsEntry fs_entry{ + .name = e.GetName(), + .root = "/", + .type = filebrowser::FsType::Custom, + .flags = filebrowser::FsEntryFlag_ReadOnly, + }; + + App::Push(fs, fs_entry, "/"); + R_SUCCEED(); +} + } // namespace sphaira::ui::menu::save diff --git a/sphaira/source/ui/sidebar.cpp b/sphaira/source/ui/sidebar.cpp index 0f9f150..e5dc620 100644 --- a/sphaira/source/ui/sidebar.cpp +++ b/sphaira/source/ui/sidebar.cpp @@ -272,7 +272,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s : SidebarEntryTextBase{title, value, {}, info}, m_filter{filter} { SetCallback([this](){ - App::Push( + App::Push( [this](const fs::FsPath& path) { SetValue(path); return true; diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp index dea46ab..b04cf95 100644 --- a/sphaira/source/yati/nx/ncm.cpp +++ b/sphaira/source/yati/nx/ncm.cpp @@ -205,7 +205,7 @@ Result SetRequiredSystemVersion(NcmContentMetaDatabase *db, const NcmContentMeta return ncmContentMetaDatabaseSet(db, key, data.data(), data.size()); } -Result GetControlPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path) { +Result GetFsPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path) { if (out_program_id) { *out_program_id = key.id; // todo: verify. if (hosversionAtLeast(17,0,0)) {