fs: add support for mounting nca, save and gamecard fs. file picker inherits from browser. fix bugs (see below).

- fixed fs real path length not actually being 0x301, but instead 255. fixes #204
- file picker inherits from file browser now so there's a lot less duplicated code.
- file browser now saves the last highlighted file.
- fix bug in file browser where the new file path could be empty (ie not containing a /).
- added support for viewing qlaunch romfs.
- moved fs mount options to the top of the list (may revert).
This commit is contained in:
ITotalJustice
2025-08-09 11:34:35 +01:00
parent 8a16188996
commit 44e1584461
21 changed files with 775 additions and 1165 deletions

View File

@@ -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<u8>& out);
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& 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<u8>& out);
Result write_entire_file(const FsPath& path, const std::vector<u8>& 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<u8>& out);
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& 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<u8>& out) = 0;
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& 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<u8>& out) {
return fs::read_entire_file(this, path, out);
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& 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<u8>& out) override {
return fs::read_entire_file(path, out);
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& 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<u8>& out) override {
return fs::read_entire_file(&m_fs, path, out);
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& 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{};

View File

@@ -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 <span>
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<bool(const fs::FsPath& path)>;
struct Menu final : MenuBase {
struct Menu final : Base {
explicit Menu(const Callback& cb, const std::vector<std::string>& 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<std::string> m_filter;
std::unique_ptr<fs::Fs> m_fs{};
FsEntry m_fs_entry{};
fs::FsPath m_path{};
std::vector<FileEntry> m_entries{};
std::vector<u32> m_entries_index{}; // files not including hidden
std::vector<u32> m_entries_index_hidden{}; // includes hidden files
std::span<u32> m_entries_current{};
std::unique_ptr<List> 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<LastFile> 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

View File

@@ -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<FsDirCollection>;
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::Fs>& 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::Fs>& 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<fs::Fs> m_fs{};
std::shared_ptr<fs::Fs> m_fs{};
FsEntry m_fs_entry{};
fs::FsPath m_path{};
std::vector<FileEntry> 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::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options);
void SetFilter(const std::vector<std::string>& 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<fs::Fs>;
protected:
static constexpr inline const char* INI_SECTION = "filebrowser";
const u32 m_options;
std::shared_ptr<fs::Fs> m_custom_fs{};
FsEntry m_custom_fs_entry{};
FsView* view{};
std::unique_ptr<FsView> view_left{};
std::unique_ptr<FsView> view_right{};
@@ -400,6 +448,8 @@ private:
std::vector<FileAssocEntry> m_assoc_entries{};
SelectedStash m_selected{};
std::vector<std::string> 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::Fs>& 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<const std::string_view> list) -> bool;
} // namespace sphaira::ui::menu::filebrowser

View File

@@ -77,6 +77,7 @@ private:
}
void DumpNcas();
Result MountNcaFs();
private:
Entry& m_entry;

View File

@@ -199,6 +199,8 @@ private:
void OnChangeIndex(s64 new_index);
Result DumpGames(u32 flags);
Result MountGcFs();
private:
FsDeviceOperator m_dev_op{};
FsGameCardHandle m_handle{};

View File

@@ -80,6 +80,8 @@ private:
m_selected_count = 0;
}
void DisplayOptions();
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& 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";

View File

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

View File

@@ -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());

View File

@@ -313,8 +313,8 @@ struct ThreadQueueEntry {
};
struct ThreadQueue {
std::deque<ThreadQueueEntry> m_entries;
Thread m_thread;
std::deque<ThreadQueueEntry> 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");

View File

@@ -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<u8>& 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<u8>& 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<u8> 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<u8>& 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<u8>& 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<u8> 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<u8>& 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<u8>& 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<u8> 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;

View File

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

View File

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

View File

@@ -634,7 +634,7 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
const auto path = BuildManifestCachePath(m_entry);
std::vector<u8> 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<ScrollableText>((const char*)data.data(), 0, 374, 250, 768, 18);

View File

@@ -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 <minIni.h>
#include <cstring>
#include <cassert>
#include <string>
#include <string_view>
#include <ctime>
#include <span>
#include <utility>
#include <ranges>
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<const std::string_view> 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<FsDirectoryEntry> 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<LastFile> 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<fs::FsNativeSd>(m_ignore_read_only.Get());
break;
case FsType::ImageNand:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand);
break;
case FsType::ImageSd:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd);
break;
case FsType::Stdio:
m_fs = std::make_unique<fs::FsStdio>(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<Sidebar>("File Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
SidebarEntryArray::Items mount_items;
std::vector<FsEntry> 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<SidebarEntryArray>("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<std::string>& 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<List>(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<ui::OptionBox>(
"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

View File

@@ -38,7 +38,6 @@
#include <span>
#include <utility>
#include <ranges>
#include <expected>
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<const std::string_view> 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<const std::string_view> 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<std::string> filters{".nro", ".png", ".jpg"};
const std::vector<std::string> filters{"nro", "png", "jpg"};
m_icon = this->Add<SidebarEntryFilePicker>(
"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<const std::string_view> 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::Fs>& 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<OptionBox>("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<OptionBox>("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<PopupList>(
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<OptionBox>("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<OptionBox>("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<PopupList>(
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::Fs>& 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<fs::FsNativeSd>(m_menu->m_ignore_read_only.Get());
break;
case FsType::ImageNand:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand);
break;
case FsType::ImageSd:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd);
break;
case FsType::Stdio:
m_fs = std::make_unique<fs::FsStdio>(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<Sidebar>("File Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
SidebarEntryArray::Items mount_items;
std::vector<FsEntry> 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<SidebarEntryArray>("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<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_unique<Sidebar>("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<SidebarEntryCallback>("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<SidebarEntryCallback>("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<SidebarEntryCallback>("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<SidebarEntryCallback>("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<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
SidebarEntryArray::Items mount_items;
std::vector<FsEntry> 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<SidebarEntryArray>("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<SidebarEntryCallback>("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<SidebarEntryCallback>("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::Fs>& 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<FsView>(this, ViewSide::Left);
if (is_custom) {
m_custom_fs = fs;
m_custom_fs_entry = fs_entry;
}
view_left = std::make_unique<FsView>(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<FileAssocEntry> {
void Base::OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path) {
view->OnClick();
}
auto Base::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// 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<FileAssocEntry> {
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<FsView>(this, view->m_path, view->GetFsEntry(), ViewSide::Right);
view_right = std::make_unique<FsView>(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<ui::OptionBox>(
"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<fs::Fs> {
switch (fs_entry.type) {
case FsType::Sd:
return std::make_shared<fs::FsNativeSd>(m_ignore_read_only.Get());
case FsType::ImageNand:
return std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Nand);
case FsType::ImageSd:
return std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Sd);
case FsType::Stdio:
return std::make_shared<fs::FsStdio>(true, fs_entry.root);
case FsType::Custom:
return m_custom_fs;
}
std::unreachable();
}
} // namespace sphaira::ui::menu::filebrowser

View File

@@ -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);
}

View File

@@ -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<fs::FsNativeId>(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<filebrowser::Menu>(fs, fs_entry, "/");
R_SUCCEED();
}
} // namespace sphaira::ui::menu::game::meta_nca

View File

@@ -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<const ApplicationEntry> entries)
#endif
// builds path suiteable for file dumps.
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> 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<const ApplicationEntry> 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<const ApplicationEntry> 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<Sidebar>("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<List>(1, 3, m_pos, v, pad);
const Vec2 pad{0, 23.75};
m_list = std::make_unique<List>(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<fs::FsNative>(&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<filebrowser::Menu>(fs, fs_entry, "/");
R_SUCCEED();
}
} // namespace sphaira::ui::menu::gc

View File

@@ -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<Sidebar>("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<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_unique<Sidebar>("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<SidebarEntryArray>("Sort"_i18n, sort_items, [this](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile(false);
}, m_sort.Get());
options->Add<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
m_order.Set(index_out);
SortAndFindLastFile(false);
}, m_order.Get());
options->Add<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
m_layout.Set(index_out);
OnLayoutChange();
}, m_layout.Get());
});
options->Add<SidebarEntryArray>("Account"_i18n, account_items, [this](s64& index_out){
m_account_index = index_out;
m_dirty = true;
App::PopToMenu();
}, m_account_index);
options->Add<SidebarEntryArray>("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<SidebarEntryCallback>("Backup"_i18n, [this](){
std::vector<std::reference_wrapper<Entry>> 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<SidebarEntryCallback>("Restore"_i18n, [this](){
RestoreSave();
}, true);
}
}
options->Add<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryBool>("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<SidebarEntryBool>("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<Sidebar>("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<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_unique<Sidebar>("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<SidebarEntryArray>("Sort"_i18n, sort_items, [this](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile(false);
}, m_sort.Get());
options->Add<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
m_order.Set(index_out);
SortAndFindLastFile(false);
}, m_order.Get());
options->Add<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
m_layout.Set(index_out);
OnLayoutChange();
}, m_layout.Get());
});
options->Add<SidebarEntryArray>("Account"_i18n, account_items, [this](s64& index_out){
m_account_index = index_out;
m_dirty = true;
App::PopToMenu();
}, m_account_index);
options->Add<SidebarEntryArray>("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<SidebarEntryCallback>("Backup"_i18n, [this](){
std::vector<std::reference_wrapper<Entry>> 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<SidebarEntryCallback>("Restore"_i18n, [this](){
RestoreSave();
}, true);
}
options->Add<SidebarEntryCallback>("Mount Fs"_i18n, [this](){
const auto rc = MountSaveFs();
App::PushErrorBox(rc, "Failed to mount save filesystem"_i18n);
});
}
options->Add<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryBool>("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<SidebarEntryBool>("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){
m_compress_save_backup.Set(v_out);
});
});
}
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this, entries](const dump::DumpLocation& location){
App::Push<ProgressBox>(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<fs::FsNativeSave>((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<filebrowser::Menu>(fs, fs_entry, "/");
R_SUCCEED();
}
} // namespace sphaira::ui::menu::save

View File

@@ -272,7 +272,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
: SidebarEntryTextBase{title, value, {}, info}, m_filter{filter} {
SetCallback([this](){
App::Push<menu::filepicker::Menu>(
App::Push<menu::filebrowser::picker::Menu>(
[this](const fs::FsPath& path) {
SetValue(path);
return true;

View File

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