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:
@@ -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{};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -77,6 +77,7 @@ private:
|
||||
}
|
||||
|
||||
void DumpNcas();
|
||||
Result MountNcaFs();
|
||||
|
||||
private:
|
||||
Entry& m_entry;
|
||||
|
||||
@@ -199,6 +199,8 @@ private:
|
||||
void OnChangeIndex(s64 new_index);
|
||||
Result DumpGames(u32 flags);
|
||||
|
||||
Result MountGcFs();
|
||||
|
||||
private:
|
||||
FsDeviceOperator m_dev_op{};
|
||||
FsGameCardHandle m_handle{};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user