diff --git a/sphaira/include/dumper.hpp b/sphaira/include/dumper.hpp index 5645dfe..801e7bc 100644 --- a/sphaira/include/dumper.hpp +++ b/sphaira/include/dumper.hpp @@ -35,6 +35,7 @@ struct BaseSource { virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0; virtual auto GetName(const std::string& path) const -> std::string = 0; virtual auto GetSize(const std::string& path) const -> s64 = 0; + virtual auto GetIcon(const std::string& path) const -> int { return 0; } }; // called after dump has finished. diff --git a/sphaira/include/ui/list.hpp b/sphaira/include/ui/list.hpp index 66c11a6..3de3621 100644 --- a/sphaira/include/ui/list.hpp +++ b/sphaira/include/ui/list.hpp @@ -60,6 +60,14 @@ struct List final : Object { return m_page; } + auto SetPageJump(bool enable) { + m_page_jump = enable; + } + + auto GetPageJump() const { + return m_page_jump; + } + private: auto Draw(NVGcontext* vg, Theme* theme) -> void override {} auto ClampX(float x, s64 count) const -> float; @@ -85,6 +93,7 @@ private: float m_y_prog{}; Layout m_layout{Layout::GRID}; + bool m_page_jump{true}; }; } // namespace sphaira::ui diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index e1f2070..902913f 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -1,6 +1,7 @@ #pragma once #include "ui/menus/menu_base.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include "nro.hpp" #include "fs.hpp" @@ -33,6 +34,11 @@ enum class SelectedType { Delete, }; +enum class ViewSide { + Left, + Right, +}; + enum SortType { SortType_Size, SortType_Alphabetical, @@ -56,6 +62,10 @@ struct FsEntry { auto IsAssoc() const -> bool { return flags & FsEntryFlag_Assoc; } + + auto IsSame(const FsEntry& e) const { + return root == e.root && type == e.type; + } }; // roughly 1kib in size per entry @@ -145,11 +155,15 @@ struct FsDirCollection { using FsDirCollections = std::vector; -struct Menu final : MenuBase { - Menu(const std::vector& nro_entries); - ~Menu(); +struct Menu; + +struct FsView final : Widget { + friend class Menu; + + FsView(Menu* menu, ViewSide side); + FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side); + ~FsView(); - 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; @@ -158,6 +172,16 @@ struct Menu final : MenuBase { return fs::AppendPath(root_path, file_path); } + auto GetFs() { + return m_fs.get(); + } + + auto& GetFsEntry() const { + return m_fs_entry; + } + + void SetSide(ViewSide side); + private: void SetIndex(s64 index); void InstallForwarder(); @@ -169,10 +193,6 @@ private: auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result; - void LoadAssocEntriesPath(const fs::FsPath& path); - void LoadAssocEntries(); - auto FindFileAssocFor() -> std::vector; - auto GetNewPath(const FileEntry& entry) const -> fs::FsPath { return GetNewPath(m_path, entry.name); } @@ -201,39 +221,6 @@ private: return out; } - void AddSelectedEntries(SelectedType type) { - auto entries = GetSelectedEntries(); - if (entries.empty()) { - // log_write("%s with no selected files\n", __PRETTY_FUNCTION__); - return; - } - - m_selected_type = type; - m_selected_files = entries; - m_selected_path = m_path; - } - - void ResetSelection() { - m_selected_files.clear(); - m_selected_count = 0; - m_selected_type = SelectedType::None; - m_selected_path = {}; - } - - auto HasTypeInSelectedEntries(FsDirEntryType type) const -> bool { - if (!m_selected_count) { - return GetEntry().type == type; - } else { - for (auto&p : m_selected_files) { - if (p.type == type) { - return true; - } - } - - return false; - } - } - auto GetEntry(u32 index) -> FileEntry& { return m_entries[m_entries_current[index]]; } @@ -257,13 +244,15 @@ private: void Sort(); void SortAndFindLastFile(); void SetIndexFromLastFile(const LastFile& last_file); - void UpdateSubheading(); void OnDeleteCallback(); void OnPasteCallback(); void OnRenameCallback(); auto CheckIfUpdateFolder() -> Result; + 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; + 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; @@ -276,9 +265,9 @@ private: void DisplayHash(hash::Type type); private: - static constexpr inline const char* INI_SECTION = "filebrowser"; + Menu* m_menu{}; + ViewSide m_side{}; - const std::vector& m_nro_entries; std::unique_ptr m_fs{}; FsEntry m_fs_entry{}; fs::FsPath m_path{}; @@ -291,23 +280,122 @@ private: std::unique_ptr m_list{}; std::optional m_daybreak_path{}; - // search options - // show files [X] - // show folders [X] - // recursive (slow) [ ] + // this keeps track of the highlighted file before opening a folder + // if the user presses B to go back to the previous dir + // this vector is popped, then, that entry is checked if it still exists + // if it does, the index becomes that file. + std::vector m_previous_highlighted_file{}; + s64 m_index{}; + s64 m_selected_count{}; + ScrollingText m_scroll_name{}; + + bool m_is_update_folder{}; +}; + +// contains all selected files for a command, such as copy, delete, cut etc. +struct SelectedStash { + void Add(std::shared_ptr view, SelectedType type, const std::vector& files, const fs::FsPath& path) { + if (files.empty()) { + Reset(); + } else { + m_view = view; + m_type = type; + m_files = files; + m_path = path; + } + } + + auto SameFs(FsView* view) -> bool { + if (m_view && view->GetFsEntry().IsSame(m_view->GetFsEntry())) { + return true; + } else { + return false; + } + } + + auto Type() const -> SelectedType { + return m_type; + } + + auto Empty() const -> bool { + return m_files.empty(); + } + + void Reset() { + m_view = {}; + m_type = {}; + m_files = {}; + m_path = {}; + } + +// private: + std::shared_ptr m_view{}; + std::vector m_files{}; + fs::FsPath m_path{}; + SelectedType m_type{SelectedType::None}; +}; + +struct Menu final : MenuBase { + friend class FsView; + + Menu(const std::vector& nro_entries); + ~Menu(); + + 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; + + static auto GetNewPath(const fs::FsPath& root_path, const fs::FsPath& file_path) -> fs::FsPath { + return fs::AppendPath(root_path, file_path); + } + +private: + auto IsSplitScreen() const { + return m_split_screen; + } + + void SetSplitScreen(bool enable); + + void RefreshViews(); + + void LoadAssocEntriesPath(const fs::FsPath& path); + void LoadAssocEntries(); + auto FindFileAssocFor() -> std::vector; + + void AddSelectedEntries(SelectedType type) { + auto entries = view->GetSelectedEntries(); + if (entries.empty()) { + return; + } + + m_selected.Add(view, type, entries, view->m_path); + } + + void ResetSelection() { + m_selected.Reset(); + } + + void UpdateSubheading(); + +private: + static constexpr inline const char* INI_SECTION = "filebrowser"; + + const std::vector& m_nro_entries; + std::shared_ptr view{}; + std::shared_ptr view_left{}; + std::shared_ptr view_right{}; std::vector m_assoc_entries{}; - std::vector m_selected_files{}; + SelectedStash m_selected{}; // this keeps track of the highlighted file before opening a folder // if the user presses B to go back to the previous dir // this vector is popped, then, that entry is checked if it still exists // if it does, the index becomes that file. std::vector m_previous_highlighted_file{}; - fs::FsPath m_selected_path{}; s64 m_index{}; s64 m_selected_count{}; - SelectedType m_selected_type{SelectedType::None}; option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; @@ -318,6 +406,7 @@ private: bool m_loaded_assoc_entries{}; bool m_is_update_folder{}; + bool m_split_screen{}; }; } // namespace sphaira::ui::menu::filebrowser diff --git a/sphaira/include/ui/menus/menu_base.hpp b/sphaira/include/ui/menus/menu_base.hpp index d56f91e..cf56c95 100644 --- a/sphaira/include/ui/menus/menu_base.hpp +++ b/sphaira/include/ui/menus/menu_base.hpp @@ -1,6 +1,7 @@ #pragma once #include "ui/widget.hpp" +#include "ui/scrolling_text.hpp" #include namespace sphaira::ui::menu { @@ -33,6 +34,9 @@ private: std::string m_title_sub_heading{}; std::string m_sub_heading{}; + ScrollingText m_scroll_title_sub_heading{}; + ScrollingText m_scroll_sub_heading{}; + protected: struct tm m_tm{}; TimeStamp m_poll_timestamp{}; diff --git a/sphaira/include/ui/progress_box.hpp b/sphaira/include/ui/progress_box.hpp index cf7c69e..371fae8 100644 --- a/sphaira/include/ui/progress_box.hpp +++ b/sphaira/include/ui/progress_box.hpp @@ -1,6 +1,7 @@ #pragma once -#include "widget.hpp" +#include "ui/widget.hpp" +#include "ui/scrolling_text.hpp" #include "fs.hpp" #include #include @@ -28,6 +29,7 @@ struct ProgressBox final : Widget { auto NewTransfer(const std::string& transfer) -> ProgressBox&; auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&; // not const in order to avoid copy by using std::swap + auto SetImage(int image) -> ProgressBox&; auto SetImageData(std::vector& data) -> ProgressBox&; auto SetImageDataConst(std::span data) -> ProgressBox&; @@ -36,6 +38,7 @@ struct ProgressBox final : Widget { auto ShouldExitResult() -> Result; // helper functions + auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst) -> Result; auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst) -> Result; auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result; void Yield(); @@ -86,8 +89,13 @@ private: s64 m_speed{}; TimeStamp m_timestamp{}; std::vector m_image_data{}; + int m_image_pending{}; + bool m_is_image_pending{}; // shared data end. + ScrollingText m_scroll_title{}; + ScrollingText m_scroll_transfer{}; + int m_cpuid{}; int m_image{}; bool m_own_image{}; diff --git a/sphaira/include/ui/types.hpp b/sphaira/include/ui/types.hpp index af6fc0e..929bb2e 100644 --- a/sphaira/include/ui/types.hpp +++ b/sphaira/include/ui/types.hpp @@ -299,6 +299,7 @@ enum class Button : u64 { UP = static_cast(HidNpadButton_AnyUp), DOWN = static_cast(HidNpadButton_AnyDown), + NONE = 0, ANY_BUTTON = A | B | X | Y | L | R | L2 | R2 | L3 | R3 | START | SELECT, ANY_HORIZONTAL = LEFT | RIGHT, ANY_VERTICAL = UP | DOWN, diff --git a/sphaira/source/dumper.cpp b/sphaira/source/dumper.cpp index 9be4f5e..5a22a9d 100644 --- a/sphaira/source/dumper.cpp +++ b/sphaira/source/dumper.cpp @@ -66,6 +66,7 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream { m_pull_offset = 0; Stream::Reset(); m_size = m_source->GetSize(path); + m_pbox->SetImage(m_source->GetIcon(path)); m_pbox->SetTitle(m_source->GetName(path)); m_pbox->NewTransfer(m_path); } @@ -107,6 +108,7 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas for (auto path : paths) { const auto file_size = source->GetSize(path); + pbox->SetImage(source->GetIcon(path)); pbox->SetTitle(source->GetName(path)); pbox->NewTransfer(path); @@ -246,6 +248,7 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::spanShouldExitResult()); const auto file_size = source->GetSize(path); + pbox->SetImage(source->GetIcon(path)); pbox->SetTitle(source->GetName(path)); pbox->NewTransfer(path); @@ -267,6 +270,7 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour R_TRY(pbox->ShouldExitResult()); const auto file_size = source->GetSize(path); + pbox->SetImage(source->GetIcon(path)); pbox->SetTitle(source->GetName(path)); pbox->NewTransfer(path); @@ -375,8 +379,4 @@ void Dump(std::shared_ptr source, const std::vector& pat )); } -void DumpSingle(std::shared_ptr source, const fs::FsPath& path, OnExit on_exit, DumpLocationType type) { - -} - } // namespace sphaira::dump diff --git a/sphaira/source/ui/list.cpp b/sphaira/source/ui/list.cpp index 3548772..aa8683d 100644 --- a/sphaira/source/ui/list.cpp +++ b/sphaira/source/ui/list.cpp @@ -12,7 +12,8 @@ List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad) , m_v{v} , m_pad{pad} { m_pos = pos; - SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200); + // SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200); + SetScrollBarPos(pos.x + pos.w, 100, SCREEN_HEIGHT-200); } auto List::ClampX(float x, s64 count) const -> float { @@ -165,8 +166,8 @@ void List::OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 } void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) { - const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2; - const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2; + const auto page_up_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_LEFT : Button::L2) : (Button::NONE); + const auto page_down_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_RIGHT : Button::R2) : (Button::NONE); if (controller->GotDown(Button::DOWN)) { if (ScrollDown(index, m_row, count)) { @@ -277,7 +278,11 @@ void List::DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const auto x = v.x; - for (; i < count; i++, v.x += v.w + m_pad.x) { + for (s64 row = 0; i < count; row++, i++, v.x += v.w + m_pad.x) { + if (row >= m_row) { + break; + } + // only draw if full x is in bounds if (v.x + v.w > GetX() + GetW()) { break; diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 38159f6..52042a1 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -288,11 +288,11 @@ auto GetRomIcon(fs::Fs* fs, ProgressBox* pbox, std::string filename, const RomDa } // namespace -Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} { +FsView::FsView(Menu* menu, 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_selected_files.empty()) { - ResetSelection(); + if (!m_menu->m_selected.Empty()) { + m_menu->ResetSelection(); } const auto set = m_selected_count != m_entries_current.size(); @@ -310,8 +310,8 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } }}), std::make_pair(Button::R2, Action{[this](){ - if (!m_selected_files.empty()) { - ResetSelection(); + if (!m_menu->m_selected.Empty()) { + m_menu->ResetSelection(); } GetEntry().selected ^= 1; @@ -353,7 +353,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { InstallFiles(); } else if (IsSd()) { - const auto assoc_list = FindFileAssocFor(); + 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()); @@ -414,59 +414,44 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 order_items.push_back("Ascending"_i18n); options->Add(std::make_shared("Sort"_i18n, sort_items, [this](s64& index_out){ - m_sort.Set(index_out); + m_menu->m_sort.Set(index_out); SortAndFindLastFile(); - }, m_sort.Get())); + }, m_menu->m_sort.Get())); options->Add(std::make_shared("Order"_i18n, order_items, [this](s64& index_out){ - m_order.Set(index_out); + m_menu->m_order.Set(index_out); SortAndFindLastFile(); - }, m_order.Get())); + }, m_menu->m_order.Get())); - options->Add(std::make_shared("Show Hidden"_i18n, m_show_hidden.Get(), [this](bool& v_out){ - m_show_hidden.Set(v_out); + options->Add(std::make_shared("Show Hidden"_i18n, m_menu->m_show_hidden.Get(), [this](bool& v_out){ + m_menu->m_show_hidden.Set(v_out); SortAndFindLastFile(); })); - options->Add(std::make_shared("Folders First"_i18n, m_folders_first.Get(), [this](bool& v_out){ - m_folders_first.Set(v_out); + options->Add(std::make_shared("Folders First"_i18n, m_menu->m_folders_first.Get(), [this](bool& v_out){ + m_menu->m_folders_first.Set(v_out); SortAndFindLastFile(); })); - options->Add(std::make_shared("Hidden Last"_i18n, m_hidden_last.Get(), [this](bool& v_out){ - m_hidden_last.Set(v_out); + options->Add(std::make_shared("Hidden Last"_i18n, m_menu->m_hidden_last.Get(), [this](bool& v_out){ + m_menu->m_hidden_last.Set(v_out); SortAndFindLastFile(); })); })); if (m_entries_current.size()) { options->Add(std::make_shared("Cut"_i18n, [this](){ - AddSelectedEntries(SelectedType::Cut); + m_menu->AddSelectedEntries(SelectedType::Cut); }, true)); options->Add(std::make_shared("Copy"_i18n, [this](){ - AddSelectedEntries(SelectedType::Copy); + m_menu->AddSelectedEntries(SelectedType::Copy); }, true)); - - options->Add(std::make_shared("Delete"_i18n, [this](){ - AddSelectedEntries(SelectedType::Delete); - - log_write("clicked on delete\n"); - App::Push(std::make_shared( - "Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ - if (op_index && *op_index) { - App::PopToMenu(); - OnDeleteCallback(); - } - } - )); - log_write("pushed delete\n"); - })); } - if (!m_selected_files.empty() && (m_selected_type == SelectedType::Cut || m_selected_type == SelectedType::Copy)) { + if (!m_menu->m_selected.Empty() && (m_menu->m_selected.Type() == SelectedType::Cut || m_menu->m_selected.Type() == SelectedType::Copy)) { options->Add(std::make_shared("Paste"_i18n, [this](){ - const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n; + const std::string buf = "Paste file(s)?"_i18n; App::Push(std::make_shared( buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { @@ -506,16 +491,29 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); } + if (m_entries_current.size()) { + options->Add(std::make_shared("Delete"_i18n, [this](){ + m_menu->AddSelectedEntries(SelectedType::Delete); + + log_write("clicked on delete\n"); + App::Push(std::make_shared( + "Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ + if (op_index && *op_index) { + App::PopToMenu(); + OnDeleteCallback(); + } + } + )); + log_write("pushed delete\n"); + })); + } + // returns true if all entries match the ext array. const auto check_all_ext = [this](auto& exts){ - if (!m_selected_count) { - return IsExtension(GetEntry().GetExtension(), exts); - } else { - const auto entries = GetSelectedEntries(); - for (auto&e : entries) { - if (!IsExtension(e.GetExtension(), exts)) { - return false; - } + const auto entries = GetSelectedEntries(); + for (auto&e : entries) { + if (!IsExtension(e.GetExtension(), exts)) { + return false; } } return true; @@ -530,8 +528,8 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } } - if (IsSd() && m_entries_current.size()) { - if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) { + if (IsSd() && m_entries_current.size() && !m_selected_count) { + if (App::GetInstallEnable() && GetEntry().IsFile() && (GetEntry().GetExtension() == "nro" || !m_menu->FindFileAssocFor().empty())) { options->Add(std::make_shared("Install Forwarder"_i18n, [this](){; if (App::GetInstallPrompt()) { App::Push(std::make_shared( @@ -696,31 +694,37 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); })); - options->Add(std::make_shared("Ignore read only"_i18n, m_ignore_read_only.Get(), [this](bool& v_out){ - m_ignore_read_only.Set(v_out); + options->Add(std::make_shared("Ignore read only"_i18n, m_menu->m_ignore_read_only.Get(), [this](bool& v_out){ + m_menu->m_ignore_read_only.Set(v_out); m_fs->SetIgnoreReadOnly(v_out); })); })); }}) ); - const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60}; - m_list = std::make_unique(1, 8, m_pos, v); + SetSide(m_side); - fs::FsPath buf; - ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH); - SetFs(buf, FS_ENTRY_DEFAULT); + auto buf = path; + if (path.empty()) { + ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH); + } + + SetFs(buf, entry); } -Menu::~Menu() { +FsView::FsView(Menu* menu, ViewSide side) : FsView{menu, "", FS_ENTRY_DEFAULT, side} { + +} + +FsView::~FsView() { // don't store mount points for non-sd card paths. if (IsSd()) { ini_puts("paths", "last_path", m_path, App::CONFIG_PATH); } } -void Menu::Update(Controller* controller, TouchInfo* touch) { - MenuBase::Update(controller, touch); +void FsView::Update(Controller* controller, TouchInfo* touch) { + Widget::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); @@ -731,13 +735,11 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { }); } -void Menu::Draw(NVGcontext* vg, Theme* theme) { - MenuBase::Draw(vg, theme); - +void FsView::Draw(NVGcontext* vg, Theme* theme) { const auto& text_col = theme->GetColour(ThemeEntryID_TEXT); if (m_entries_current.empty()) { - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str()); + 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; } @@ -760,10 +762,6 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } } - if (e.IsSelected()) { - gfx::drawText(vg, Vec2{x - 10.f, y + (h / 2.f) - (24.f / 2)}, 24.f, "\uE14B", nullptr, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED)); - } - auto text_id = ThemeEntryID_TEXT; const auto selected = m_index == i; if (selected) { @@ -798,10 +796,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { DrawElement(x + text_xoffset, y + 5, 50, 50, icon); } - nvgSave(vg); - nvgIntersectScissor(vg, x + text_xoffset+65, y, w-(x+text_xoffset+65+50), h); - gfx::drawText(vg, x + text_xoffset+65, y + (h / 2.f), 20.f, e.name, NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id)); - nvgRestore(vg); + if (e.IsSelected()) { + gfx::drawText(vg, x + text_xoffset + 50 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED)); + } + + 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); if (m_fs->IsNative() && e.IsDir()) { 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); @@ -815,6 +814,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { 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); @@ -828,8 +828,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { }); } -void Menu::OnFocusGained() { - MenuBase::OnFocusGained(); +void FsView::OnFocusGained() { + Widget::OnFocusGained(); if (m_entries.empty()) { if (m_path.empty()) { Scan(m_fs->Root()); @@ -837,15 +837,44 @@ void Menu::OnFocusGained() { Scan(m_path); } } - - if (!m_loaded_assoc_entries) { - m_loaded_assoc_entries = true; - log_write("loading assoc entries\n"); - LoadAssocEntries(); - } } -void Menu::SetIndex(s64 index) { +void FsView::SetSide(ViewSide side) { + m_side = side; + + const auto pos = m_menu->GetPos(); + this->SetPos(pos); + Vec4 v{75, GetY() + 1.f + 42.f, 1220.f - 45.f * 2, 60}; + + if (m_menu->IsSplitScreen()) { + if (m_side == ViewSide::Left) { + this->SetW(pos.w / 2 - pos.x / 2); + this->SetX(pos.x / 2); + } else if (m_side == ViewSide::Right) { + this->SetW(pos.w / 2 - pos.x / 2); + this->SetX(pos.x / 2 + SCREEN_WIDTH / 2); + } + + v.w /= 2; + v.w -= v.x / 2; + + if (m_side == ViewSide::Left) { + v.x = v.x / 2; + } else if (m_side == ViewSide::Right) { + v.x = v.x / 2 + SCREEN_WIDTH / 2; + } + } + + m_list = std::make_unique(1, 8, m_pos, v); + if (m_menu->IsSplitScreen()) { + m_list->SetPageJump(false); + } + + // reset scroll position. + m_scroll_name.Reset(); +} + +void FsView::SetIndex(s64 index) { m_index = index; if (!m_index) { m_list->SetYoff(); @@ -874,10 +903,10 @@ void Menu::SetIndex(s64 index) { } } - UpdateSubheading(); + m_menu->UpdateSubheading(); } -void Menu::InstallForwarder() { +void FsView::InstallForwarder() { if (GetEntry().GetExtension() == "nro") { if (R_FAILED(homebrew::Menu::InstallHomebrewFromPath(GetNewPathCurrent()))) { log_write("failed to create forwarder\n"); @@ -885,7 +914,7 @@ void Menu::InstallForwarder() { return; } - const auto assoc_list = FindFileAssocFor(); + const auto assoc_list = m_menu->FindFileAssocFor(); if (assoc_list.empty()) { log_write("failed to find assoc for: %s ext: %s\n", GetEntry().name, GetEntry().extension.c_str()); return; @@ -938,7 +967,7 @@ void Menu::InstallForwarder() { )); } -void Menu::InstallFiles() { +void FsView::InstallFiles() { const auto targets = GetSelectedEntries(); App::Push(std::make_shared("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){ @@ -961,7 +990,7 @@ void Menu::InstallFiles() { })); } -void Menu::UnzipFiles(fs::FsPath dir_path) { +void FsView::UnzipFiles(fs::FsPath dir_path) { const auto targets = GetSelectedEntries(); // set to current path. @@ -1056,7 +1085,7 @@ void Menu::UnzipFiles(fs::FsPath dir_path) { })); } -void Menu::ZipFiles(fs::FsPath zip_out) { +void FsView::ZipFiles(fs::FsPath zip_out) { const auto targets = GetSelectedEntries(); // set to current path. @@ -1188,7 +1217,7 @@ void Menu::ZipFiles(fs::FsPath zip_out) { })); } -void Menu::UploadFiles() { +void FsView::UploadFiles() { const auto targets = GetSelectedEntries(); const auto network_locations = location::Load(); @@ -1278,7 +1307,7 @@ void Menu::UploadFiles() { R_SUCCEED(); }, [this](Result rc){ App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); - ResetSelection(); + m_menu->ResetSelection(); if (R_SUCCEEDED(rc)) { App::Notify("Upload successfull!"); @@ -1289,7 +1318,7 @@ void Menu::UploadFiles() { )); } -auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { +auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { 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()); @@ -1300,11 +1329,13 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { m_entries.clear(); m_index = 0; m_list->SetYoff(0); - SetTitleSubHeading(m_path); + m_menu->SetTitleSubHeading(m_path); - if (m_selected_type == SelectedType::None) { - ResetSelection(); - } + // todo: verify this works as expected. + m_selected_count = 0; + // if (m_selected_type == SelectedType::None) { + // ResetSelection(); + // } fs::Dir d; R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d)); @@ -1355,10 +1386,558 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { R_SUCCEED(); } +void FsView::Sort() { + // returns true if lhs should be before rhs + const auto sort = m_menu->m_sort.Get(); + const auto order = m_menu->m_order.Get(); + const auto folders_first = m_menu->m_folders_first.Get(); + const auto hidden_last = m_menu->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_menu->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 FsView::SortAndFindLastFile() { + std::optional last_file; + if (!m_path.empty() && !m_entries_current.empty()) { + last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); + } + + Sort(); + + if (last_file.has_value()) { + SetIndexFromLastFile(*last_file); + } +} + +void FsView::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 FsView::OnDeleteCallback() { + bool use_progress_box{true}; + + // check if we only have 1 file / folder + if (m_menu->m_selected.m_files.size() == 1) { + const auto& entry = m_menu->m_selected.m_files[0]; + const auto full_path = GetNewPath(m_menu->m_selected.m_path, entry.name); + + if (entry.IsDir()) { + bool empty{}; + m_fs->IsDirEmpty(full_path, &empty); + if (empty) { + m_fs->DeleteDirectory(full_path); + use_progress_box = false; + } + } else { + m_fs->DeleteFile(full_path); + use_progress_box = false; + } + } + + if (!use_progress_box) { + m_menu->RefreshViews(); + log_write("did delete\n"); + } else { + App::Push(std::make_shared(0, "Deleting"_i18n, "", [this](auto pbox) -> Result { + FsDirCollections collections; + auto& selected = m_menu->m_selected; + auto src_fs = selected.m_view->GetFs(); + + // build list of dirs / files + for (const auto&p : selected.m_files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto full_path = GetNewPath(selected.m_path, p.name); + if (p.IsDir()) { + pbox->NewTransfer("Scanning "_i18n + full_path); + R_TRY(get_collections(src_fs, full_path, p.name, collections)); + } + } + + return DeleteAllCollections(pbox, src_fs, selected, collections); + }, [this](Result rc){ + App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); + + m_menu->RefreshViews(); + log_write("did delete\n"); + })); + } +} + +void FsView::OnPasteCallback() { + // check if we only have 1 file / folder and is cut (rename) + if (m_menu->m_selected.SameFs(this) && m_menu->m_selected.m_files.size() == 1 && m_menu->m_selected.m_type == SelectedType::Cut) { + const auto& entry = m_menu->m_selected.m_files[0]; + const auto full_path = GetNewPath(m_menu->m_selected.m_path, entry.name); + + if (entry.IsDir()) { + m_fs->RenameDirectory(full_path, GetNewPath(entry)); + } else { + m_fs->RenameFile(full_path, GetNewPath(entry)); + } + + m_menu->RefreshViews(); + } else { + App::Push(std::make_shared(0, "Pasting"_i18n, "", [this](auto pbox) -> Result { + auto& selected = m_menu->m_selected; + auto src_fs = selected.m_view->GetFs(); + + if (selected.SameFs(this) && selected.m_type == SelectedType::Cut) { + for (const auto& p : selected.m_files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto src_path = GetNewPath(selected.m_path, p.name); + const auto dst_path = GetNewPath(m_path, p.name); + + pbox->SetTitle(p.name); + pbox->NewTransfer("Pasting "_i18n + src_path); + + if (p.IsDir()) { + m_fs->RenameDirectory(src_path, dst_path); + } else { + m_fs->RenameFile(src_path, dst_path); + } + } + } else { + FsDirCollections collections; + + // build list of dirs / files + for (const auto&p : selected.m_files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto full_path = GetNewPath(selected.m_path, p.name); + if (p.IsDir()) { + pbox->NewTransfer("Scanning "_i18n + full_path); + R_TRY(get_collections(src_fs, full_path, p.name, collections)); + } + } + + for (const auto& p : selected.m_files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto src_path = GetNewPath(selected.m_path, p.name); + const auto dst_path = GetNewPath(p); + + if (p.IsDir()) { + pbox->SetTitle(p.name); + pbox->NewTransfer("Creating "_i18n + dst_path); + m_fs->CreateDirectory(dst_path); + } else { + pbox->SetTitle(p.name); + pbox->NewTransfer("Copying "_i18n + src_path); + R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path)); + + // delete src file. folders are removed after. + if (selected.m_type == SelectedType::Cut) { + R_TRY(src_fs->DeleteFile(src_path)); + } + } + } + + // copy everything in collections + for (const auto& c : collections) { + const auto base_dst_path = GetNewPath(m_path, c.parent_name); + + for (const auto& p : c.dirs) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + // const auto src_path = GetNewPath(c.path, p.name); + const auto dst_path = GetNewPath(base_dst_path, p.name); + + pbox->SetTitle(p.name); + pbox->NewTransfer("Creating "_i18n + dst_path); + m_fs->CreateDirectory(dst_path); + } + + for (const auto& p : c.files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto src_path = GetNewPath(c.path, p.name); + const auto dst_path = GetNewPath(base_dst_path, p.name); + + pbox->SetTitle(p.name); + pbox->NewTransfer("Copying "_i18n + src_path); + R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path)); + + // delete src file. folders are removed after. + if (selected.m_type == SelectedType::Cut) { + R_TRY(src_fs->DeleteFile(src_path)); + } + } + } + + // moving accross fs is not possible, thus files have to be copied. + // this leaves the files on the src_fs. + // the files are deleted one by one after a successfull copy (see above) + // however this leaves the folders. + // the folders cannot be deleted until the end as they have to be removed in + // reverse order so that the folder can be deleted (it must be empty). + if (selected.m_type == SelectedType::Cut) { + R_TRY(DeleteAllCollections(pbox, src_fs, selected, collections, FsDirOpenMode_ReadDirs)); + } + } + + R_SUCCEED(); + }, [this](Result rc){ + App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); + + m_menu->RefreshViews(); + log_write("did paste\n"); + })); + } +} + +void FsView::OnRenameCallback() { + +} + +auto FsView::CheckIfUpdateFolder() -> Result { + R_UNLESS(IsSd(), FsError_InvalidMountName); + R_UNLESS(m_fs->IsNative(), 0x1); + + // check if we have already tried to find daybreak + if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) { + return FsError_FileNotFound; + } + + // check that we have daybreak installed + if (!m_daybreak_path.has_value()) { + auto daybreak_path = DAYBREAK_PATH; + if (!m_fs->FileExists(DAYBREAK_PATH)) { + if (auto e = nro_find(m_menu->m_nro_entries, "Daybreak", "Atmosphere-NX", {}); e.has_value()) { + daybreak_path = e.value().path; + } else { + log_write("failed to find daybreak\n"); + m_daybreak_path = ""; + return FsError_FileNotFound; + } + } + m_daybreak_path = daybreak_path; + log_write("found daybreak in: %s\n", m_daybreak_path.value().s); + } + + s64 count; + R_TRY(GetNative()->DirGetEntryCount(m_path, FsDirOpenMode_ReadDirs, &count)); + + // check that we are at the bottom level + R_UNLESS(count == 0, 0x1); + // check that we have enough ncas and not too many + R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, 0x1); + + // check that all entries end in .nca + const auto nca_ext = std::string_view{".nca"}; + for (auto& e : m_entries) { + const auto ext = std::strrchr(e.name, '.'); + R_UNLESS(ext && ext == nca_ext, 0x1); + } + + R_SUCCEED(); +} + +auto FsView::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 { + out.path = path; + out.parent_name = parent_name; + + const auto fetch = [fs, &path](std::vector& out, u32 flags) -> Result { + fs::Dir d; + R_TRY(fs->OpenDirectory(path, flags, &d)); + ON_SCOPE_EXIT(fs->DirClose(&d)); + return fs->DirReadAll(&d, out); + }; + + if (inc_file) { + u32 flags = FsDirOpenMode_ReadFiles; + if (!inc_size) { + flags |= FsDirOpenMode_NoFileSize; + } + R_TRY(fetch(out.files, flags)); + } + + if (inc_dir) { + R_TRY(fetch(out.dirs, FsDirOpenMode_ReadDirs)); + } + + R_SUCCEED(); +} + +auto FsView::get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size) -> Result { + // get a list of all the files / dirs + FsDirCollection collection; + R_TRY(get_collection(fs, path, parent_name, collection, true, true, inc_size)); + log_write("got collection: %s parent_name: %s files: %zu dirs: %zu\n", path.s, parent_name.s, collection.files.size(), collection.dirs.size()); + out.emplace_back(collection); + + // for (size_t i = 0; i < collection.dirs.size(); i++) { + for (const auto&p : collection.dirs) { + // use heap as to not explode the stack + const auto new_path = std::make_unique(FsView::GetNewPath(path, p.name)); + const auto new_parent_name = std::make_unique(FsView::GetNewPath(parent_name, p.name)); + log_write("trying to get nested collection: %s parent_name: %s\n", new_path->s, new_parent_name->s); + R_TRY(get_collections(fs, *new_path, *new_parent_name, out, inc_size)); + } + + R_SUCCEED(); +} + +auto FsView::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result { + return get_collection(m_fs.get(), path, parent_name, out, true, true, inc_size); +} + +auto FsView::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size) -> Result { + return get_collections(m_fs.get(), path, parent_name, out, inc_size); +} + +static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) { + // delete everything in collections, reversed + for (const auto& c : std::views::reverse(collections)) { + const auto delete_func = [&](auto& array) -> Result { + for (const auto& p : array) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto full_path = FsView::GetNewPath(c.path, p.name); + pbox->SetTitle(p.name); + pbox->NewTransfer("Deleting "_i18n + full_path); + if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) { + log_write("deleting dir: %s\n", full_path.s); + R_TRY(fs->DeleteDirectory(full_path)); + } else if ((mode & FsDirOpenMode_ReadFiles) && p.type == FsDirEntryType_File) { + log_write("deleting file: %s\n", full_path.s); + R_TRY(fs->DeleteFile(full_path)); + } + } + + R_SUCCEED(); + }; + + R_TRY(delete_func(c.files)); + R_TRY(delete_func(c.dirs)); + } + + for (const auto& p : selected.m_files) { + pbox->Yield(); + R_TRY(pbox->ShouldExitResult()); + + const auto full_path = FsView::GetNewPath(selected.m_path, p.name); + pbox->SetTitle(p.name); + pbox->NewTransfer("Deleting "_i18n + full_path); + + if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) { + log_write("deleting dir: %s\n", full_path.s); + R_TRY(fs->DeleteDirectory(full_path)); + } else if ((mode & FsDirOpenMode_ReadFiles) && p.type == FsDirEntryType_File) { + log_write("deleting file: %s\n", full_path.s); + R_TRY(fs->DeleteFile(full_path)); + } + } + + R_SUCCEED(); +} + +void FsView::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_index_search.clear(); + m_entries_current = {}; + m_previous_highlighted_file.clear(); + m_menu->m_selected.Reset(); + m_selected_count = 0; + m_fs_entry = new_entry; + + switch (new_entry.type) { + case FsType::Sd: + m_fs = std::make_unique(m_menu->m_ignore_read_only.Get()); + break; + case FsType::ImageNand: + m_fs = std::make_unique(FsImageDirectoryId_Nand); + break; + case FsType::ImageSd: + m_fs = std::make_unique(FsImageDirectoryId_Sd); + break; + case FsType::Stdio: + m_fs = std::make_unique(true, new_entry.root); + break; + } + + if (HasFocus()) { + if (m_path.empty()) { + Scan(m_fs->Root()); + } else { + Scan(m_path); + } + } +} + +void FsView::DisplayHash(hash::Type type) { + // hack because we cannot share output between threaded calls... + static std::string hash_out; + hash_out.clear(); + + App::Push(std::make_shared(0, "Hashing"_i18n, GetEntry().name, [this, type](auto pbox) -> Result { + R_TRY(hash::Hash(pbox, type, m_fs.get(), GetNewPathCurrent(), hash_out)); + + R_SUCCEED(); + }, [this, type](Result rc){ + App::PushErrorBox(rc, "Failed to hash file..."_i18n); + + if (R_SUCCEEDED(rc)) { + char buf[0x100]; + // std::snprintf(buf, sizeof(buf), "%s\n%s\n%s", hash::GetTypeStr(type), hash_out.c_str(), GetEntry().GetName()); + std::snprintf(buf, sizeof(buf), "%s\n%s", hash::GetTypeStr(type), hash_out.c_str()); + App::Push(std::make_shared(buf, "OK"_i18n)); + } + })); +} + +Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} { + SetAction(Button::L3, Action{"Split"_i18n, [this](){ + SetSplitScreen(IsSplitScreen() ^ 1); + }}); + + view = view_left = std::make_shared(this, ViewSide::Left); +} + +Menu::~Menu() { +} + +void Menu::Update(Controller* controller, TouchInfo* touch) { + MenuBase::Update(controller, touch); + view->Update(controller, touch); +} + +void Menu::Draw(NVGcontext* vg, Theme* theme) { + // workaround the buttons not being display properly. + // basically, inherit all actions from the view, draw them, + // then restore state after. + const auto actions_copy = GetActions(); + ON_SCOPE_EXIT(m_actions = actions_copy); + m_actions.insert_range(view->GetActions()); + + MenuBase::Draw(vg, theme); + + if (IsSplitScreen()) { + view_left->Draw(vg, theme); + view_right->Draw(vg, theme); + + if (view == view_left) { + gfx::drawRect(vg, view_right->GetPos(), nvgRGBA(0, 0, 0, 180), 5); + } else { + gfx::drawRect(vg, view_left->GetPos(), nvgRGBA(0, 0, 0, 180), 5); + } + + gfx::drawRect(vg, SCREEN_WIDTH/2, GetY(), 1, GetH(), theme->GetColour(ThemeEntryID_LINE)); + } else { + view->Draw(vg, theme); + } +} + +void Menu::OnFocusGained() { + MenuBase::OnFocusGained(); + + if (IsSplitScreen()) { + view_left->OnFocusGained(); + view_right->OnFocusGained(); + } else { + view->OnFocusGained(); + } + + if (!m_loaded_assoc_entries) { + m_loaded_assoc_entries = true; + log_write("loading assoc entries\n"); + LoadAssocEntries(); + } +} + auto Menu::FindFileAssocFor() -> std::vector { // only support roms in correctly named folders, sorry! - const auto db_indexs = GetRomDatabaseFromPath(m_path); - const auto& entry = GetEntry(); + const auto db_indexs = GetRomDatabaseFromPath(view->m_path); + const auto& entry = view->GetEntry(); const auto extension = entry.extension; const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension; if (extension.empty() && internal_extension.empty()) { @@ -1469,7 +2048,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) { // if path isn't empty, check if the file exists bool file_exists{}; if (!assoc.path.empty()) { - file_exists = m_fs->FileExists(assoc.path); + file_exists = view->m_fs->FileExists(assoc.path); } else { const auto nro_name = assoc.name + ".nro"; for (const auto& nro : m_nro_entries) { @@ -1514,469 +2093,59 @@ void Menu::LoadAssocEntries() { LoadAssocEntriesPath("/config/sphaira/assoc/"); } -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() { - std::optional last_file; - if (!m_path.empty() && !m_entries_current.empty()) { - last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); - } - - 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::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())); + 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::OnDeleteCallback() { - bool use_progress_box{true}; +void Menu::SetSplitScreen(bool enable) { + if (m_split_screen != enable) { + m_split_screen = enable; - // check if we only have 1 file / folder - if (m_selected_files.size() == 1) { - const auto& entry = m_selected_files[0]; - const auto full_path = GetNewPath(m_selected_path, entry.name); + if (m_split_screen) { + // load second screen as a copy of the left side. + view->SetSide(ViewSide::Left); + view_right = std::make_shared(this, view->m_path, view->GetFsEntry(), ViewSide::Right); + view_right->OnFocusGained(); - if (entry.IsDir()) { - bool empty{}; - m_fs->IsDirEmpty(full_path, &empty); - if (empty) { - m_fs->DeleteDirectory(full_path); - use_progress_box = false; - } + static const auto change_view = [this](auto& new_view){ + if (view != new_view) { + view->OnFocusLost(); + view = new_view; + view->OnFocusGained(); + SetTitleSubHeading(view->m_path); + UpdateSubheading(); + } + }; + + SetAction(Button::LEFT, Action{[this](){ + change_view(view_left); + }}); + SetAction(Button::RIGHT, Action{[this](){ + change_view(view_right); + }}); } else { - m_fs->DeleteFile(full_path); - use_progress_box = false; - } - } - - if (!use_progress_box) { - ResetSelection(); - Scan(m_path); - log_write("did delete\n"); - } else { - App::Push(std::make_shared(0, "Deleting"_i18n, "", [this](auto pbox) -> Result { - FsDirCollections collections; - - // build list of dirs / files - for (const auto&p : m_selected_files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto full_path = GetNewPath(m_selected_path, p.name); - if (p.IsDir()) { - pbox->NewTransfer("Scanning "_i18n + full_path); - R_TRY(get_collections(full_path, p.name, collections)); - } - } - - // delete everything in collections, reversed - for (const auto& c : std::views::reverse(collections)) { - const auto delete_func = [&](auto& array) -> Result { - for (const auto& p : array) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto full_path = GetNewPath(c.path, p.name); - pbox->NewTransfer("Deleting "_i18n + full_path); - if (p.type == FsDirEntryType_Dir) { - log_write("deleting dir: %s\n", full_path.s); - R_TRY(m_fs->DeleteDirectory(full_path)); - } else { - log_write("deleting file: %s\n", full_path.s); - R_TRY(m_fs->DeleteFile(full_path)); - } - } - - R_SUCCEED(); - }; - - R_TRY(delete_func(c.files)); - R_TRY(delete_func(c.dirs)); - } - - for (const auto& p : m_selected_files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto full_path = GetNewPath(m_selected_path, p.name); - pbox->NewTransfer("Deleting "_i18n + full_path); - - if (p.IsDir()) { - log_write("deleting dir: %s\n", full_path.s); - R_TRY(m_fs->DeleteDirectory(full_path)); - } else { - log_write("deleting file: %s\n", full_path.s); - R_TRY(m_fs->DeleteFile(full_path)); - } - } - - R_SUCCEED(); - }, [this](Result rc){ - App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); + view_left = {}; + view_right = {}; + view_left = view; + view->SetSide(ViewSide::Left); + RemoveAction(Button::LEFT); + RemoveAction(Button::RIGHT); ResetSelection(); - Scan(m_path); - log_write("did delete\n"); - })); + } } } -void Menu::OnPasteCallback() { - // check if we only have 1 file / folder and is cut (rename) - if (m_selected_files.size() == 1 && m_selected_type == SelectedType::Cut) { - const auto& entry = m_selected_files[0]; - const auto full_path = GetNewPath(m_selected_path, entry.name); +void Menu::RefreshViews() { + ResetSelection(); - if (entry.IsDir()) { - m_fs->RenameDirectory(full_path, GetNewPath(entry)); - } else { - m_fs->RenameFile(full_path, GetNewPath(entry)); - } - - ResetSelection(); - Scan(m_path); - log_write("did paste\n"); + if (IsSplitScreen()) { + view_left->Scan(view_left->m_path); + view_right->Scan(view_right->m_path); } else { - App::Push(std::make_shared(0, "Pasting"_i18n, "", [this](auto pbox) -> Result { - - if (m_selected_type == SelectedType::Cut) { - for (const auto& p : m_selected_files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto src_path = GetNewPath(m_selected_path, p.name); - const auto dst_path = GetNewPath(m_path, p.name); - - pbox->NewTransfer("Pasting "_i18n + src_path); - - if (p.IsDir()) { - m_fs->RenameDirectory(src_path, dst_path); - } else { - m_fs->RenameFile(src_path, dst_path); - } - } - } else { - FsDirCollections collections; - - // build list of dirs / files - for (const auto&p : m_selected_files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto full_path = GetNewPath(m_selected_path, p.name); - if (p.IsDir()) { - pbox->NewTransfer("Scanning "_i18n + full_path); - R_TRY(get_collections(full_path, p.name, collections)); - } - } - - for (const auto& p : m_selected_files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto src_path = GetNewPath(m_selected_path, p.name); - const auto dst_path = GetNewPath(p); - - if (p.IsDir()) { - pbox->NewTransfer("Creating "_i18n + dst_path); - m_fs->CreateDirectory(dst_path); - } else { - pbox->NewTransfer("Copying "_i18n + src_path); - R_TRY(pbox->CopyFile(m_fs.get(), src_path, dst_path)); - } - } - - // copy everything in collections - for (const auto& c : collections) { - const auto base_dst_path = GetNewPath(m_path, c.parent_name); - - for (const auto& p : c.dirs) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto src_path = GetNewPath(c.path, p.name); - const auto dst_path = GetNewPath(base_dst_path, p.name); - - log_write("creating: %s to %s\n", src_path.s, dst_path.s); - pbox->NewTransfer("Creating "_i18n + dst_path); - m_fs->CreateDirectory(dst_path); - } - - for (const auto& p : c.files) { - pbox->Yield(); - R_TRY(pbox->ShouldExitResult()); - - const auto src_path = GetNewPath(c.path, p.name); - const auto dst_path = GetNewPath(base_dst_path, p.name); - - pbox->NewTransfer("Copying "_i18n + src_path); - log_write("copying: %s to %s\n", src_path.s, dst_path.s); - R_TRY(pbox->CopyFile(m_fs.get(), src_path, dst_path)); - } - } - } - - R_SUCCEED(); - }, [this](Result rc){ - App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); - - ResetSelection(); - Scan(m_path); - log_write("did paste\n"); - })); + view->Scan(view->m_path); } } -void Menu::OnRenameCallback() { - -} - -auto Menu::CheckIfUpdateFolder() -> Result { - R_UNLESS(IsSd(), FsError_InvalidMountName); - R_UNLESS(m_fs->IsNative(), 0x1); - - // check if we have already tried to find daybreak - if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) { - return FsError_FileNotFound; - } - - // check that we have daybreak installed - if (!m_daybreak_path.has_value()) { - auto daybreak_path = DAYBREAK_PATH; - if (!m_fs->FileExists(DAYBREAK_PATH)) { - if (auto e = nro_find(m_nro_entries, "Daybreak", "Atmosphere-NX", {}); e.has_value()) { - daybreak_path = e.value().path; - } else { - log_write("failed to find daybreak\n"); - m_daybreak_path = ""; - return FsError_FileNotFound; - } - } - m_daybreak_path = daybreak_path; - log_write("found daybreak in: %s\n", m_daybreak_path.value().s); - } - - s64 count; - R_TRY(GetNative()->DirGetEntryCount(m_path, FsDirOpenMode_ReadDirs, &count)); - - // check that we are at the bottom level - R_UNLESS(count == 0, 0x1); - // check that we have enough ncas and not too many - R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, 0x1); - - // check that all entries end in .nca - const auto nca_ext = std::string_view{".nca"}; - for (auto& e : m_entries) { - const auto ext = std::strrchr(e.name, '.'); - R_UNLESS(ext && ext == nca_ext, 0x1); - } - - R_SUCCEED(); -} - -auto Menu::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result { - out.path = path; - out.parent_name = parent_name; - - const auto fetch = [this, &path](std::vector& out, u32 flags) -> Result { - fs::Dir d; - R_TRY(m_fs->OpenDirectory(path, flags, &d)); - ON_SCOPE_EXIT(m_fs->DirClose(&d)); - return m_fs->DirReadAll(&d, out); - }; - - if (inc_file) { - u32 flags = FsDirOpenMode_ReadFiles; - if (!inc_size) { - flags |= FsDirOpenMode_NoFileSize; - } - R_TRY(fetch(out.files, flags)); - } - - if (inc_dir) { - R_TRY(fetch(out.dirs, FsDirOpenMode_ReadDirs)); - } - - R_SUCCEED(); -} - -auto Menu::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size) -> Result { - // get a list of all the files / dirs - FsDirCollection collection; - R_TRY(get_collection(path, parent_name, collection, true, true, inc_size)); - log_write("got collection: %s parent_name: %s files: %zu dirs: %zu\n", path.s, parent_name.s, collection.files.size(), collection.dirs.size()); - out.emplace_back(collection); - - // for (size_t i = 0; i < collection.dirs.size(); i++) { - for (const auto&p : collection.dirs) { - // use heap as to not explode the stack - const auto new_path = std::make_unique(Menu::GetNewPath(path, p.name)); - const auto new_parent_name = std::make_unique(Menu::GetNewPath(parent_name, p.name)); - log_write("trying to get nested collection: %s parent_name: %s\n", new_path->s, new_parent_name->s); - R_TRY(get_collections(*new_path, *new_parent_name, out, inc_size)); - } - - R_SUCCEED(); -} - -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_index_search.clear(); - m_entries_current = {}; - m_previous_highlighted_file.clear(); - m_selected_path.clear(); - m_selected_count = 0; - m_selected_type = SelectedType::None; - m_fs_entry = new_entry; - - switch (new_entry.type) { - case FsType::Sd: - m_fs = std::make_unique(m_ignore_read_only.Get()); - break; - case FsType::ImageNand: - m_fs = std::make_unique(FsImageDirectoryId_Nand); - break; - case FsType::ImageSd: - m_fs = std::make_unique(FsImageDirectoryId_Sd); - break; - case FsType::Stdio: - m_fs = std::make_unique(true, new_entry.root); - break; - } - - if (HasFocus()) { - if (m_path.empty()) { - Scan(m_fs->Root()); - } else { - Scan(m_path); - } - } -} - -void Menu::DisplayHash(hash::Type type) { - // hack because we cannot share output between threaded calls... - static std::string hash_out; - hash_out.clear(); - - App::Push(std::make_shared(0, "Hashing"_i18n, GetEntry().name, [this, type](auto pbox) -> Result { - R_TRY(hash::Hash(pbox, type, m_fs.get(), GetNewPathCurrent(), hash_out)); - - R_SUCCEED(); - }, [this, type](Result rc){ - App::PushErrorBox(rc, "Failed to hash file..."_i18n); - - if (R_SUCCEEDED(rc)) { - char buf[0x100]; - // std::snprintf(buf, sizeof(buf), "%s\n%s\n%s", hash::GetTypeStr(type), hash_out.c_str(), GetEntry().GetName()); - std::snprintf(buf, sizeof(buf), "%s\n%s", hash::GetTypeStr(type), hash_out.c_str()); - App::Push(std::make_shared(buf, "OK"_i18n)); - } - })); -} - } // namespace sphaira::ui::menu::filebrowser - -// options -// Cancel -// Skip -// Rename -// Overwrite diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 8ca618d..296cdc0 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -129,6 +129,8 @@ struct NspEntry { s64 nsp_size{}; // copy of ncm cs, it is not closed. NcmContentStorage cs{}; + // copy of the icon, if invalid, it will use the default icon. + int icon{}; // todo: benchmark manual sdcard read and decryption vs ncm. Result Read(void* buf, s64 off, s64 size, u64* bytes_read) { @@ -185,7 +187,7 @@ struct NspSource final : dump::BaseSource { Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override { const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ - return path == e.path; + return path.find(e.path.s) != path.npos; }); R_UNLESS(it != m_entries.end(), 0x1); @@ -194,7 +196,7 @@ struct NspSource final : dump::BaseSource { auto GetName(const std::string& path) const -> std::string { const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ - return path == e.path; + return path.find(e.path.s) != path.npos; }); if (it != m_entries.end()) { @@ -206,7 +208,7 @@ struct NspSource final : dump::BaseSource { auto GetSize(const std::string& path) const -> s64 { const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ - return path == e.path; + return path.find(e.path.s) != path.npos; }); if (it != m_entries.end()) { @@ -216,6 +218,18 @@ struct NspSource final : dump::BaseSource { return 0; } + auto GetIcon(const std::string& path) const -> int override { + const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ + return path.find(e.path.s) != path.npos; + }); + + if (it != m_entries.end()) { + return it->icon; + } + + return App::GetDefaultImage(); + } + private: std::vector m_entries{}; }; @@ -568,7 +582,7 @@ Result BuildNspEntries(Entry& e, u32 flags, std::vector& out) { NspEntry nsp; R_TRY(BuildNspEntry(e, info, nsp)); - out.emplace_back(nsp); + out.emplace_back(nsp).icon = e.image; } R_UNLESS(!out.empty(), 0x1); diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index f13e376..e89a416 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -171,6 +171,7 @@ struct XciSource final : dump::BaseSource { // size of the entire xci. s64 xci_size{}; Menu* menu{}; + int icon{}; Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override { if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) { @@ -218,6 +219,10 @@ struct XciSource final : dump::BaseSource { return 0; } + auto GetIcon(const std::string& path) const -> int override { + return icon; + } + private: static auto InRange(s64 off, s64 offset, s64 size) -> bool { return off < offset + size && off >= offset; @@ -944,6 +949,7 @@ Result Menu::DumpGames(u32 flags) { auto source = std::make_shared(); source->menu = this; source->application_name = m_entries[m_entry_index].lang_entry.name; + source->icon = m_icon; std::vector paths; if (flags & DumpFileFlag_XCI) { diff --git a/sphaira/source/ui/menus/menu_base.cpp b/sphaira/source/ui/menus/menu_base.cpp index 4d9d8fd..1adc651 100644 --- a/sphaira/source/ui/menus/menu_base.cpp +++ b/sphaira/source/ui/menus/menu_base.cpp @@ -70,10 +70,13 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) { nvgFontSize(vg, 28); gfx::textBounds(vg, 0, 0, bounds, m_title.c_str()); - gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str()); - gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str()); - gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), "%s", m_sub_heading.c_str()); + const auto text_w = SCREEN_WIDTH / 2 - 30; + const auto title_sub_x = 80 + (bounds[2] - bounds[0]) + 10; + + gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str()); + m_scroll_title_sub_heading.Draw(vg, true, title_sub_x, start_y, text_w - title_sub_x, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str()); + m_scroll_sub_heading.Draw(vg, true, 80, 685, text_w - 80, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str()); } void MenuBase::SetTitle(std::string title) { diff --git a/sphaira/source/ui/progress_box.cpp b/sphaira/source/ui/progress_box.cpp index 74afb71..937f345 100644 --- a/sphaira/source/ui/progress_box.cpp +++ b/sphaira/source/ui/progress_box.cpp @@ -87,11 +87,19 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { const auto offset = m_offset; const auto speed = m_speed; const auto last_offset = m_last_offset; + auto image = m_image; + + if (m_is_image_pending) { + FreeImage(); + image = m_image = m_image_pending; + m_image_pending = 0; + m_is_image_pending = false; + } mutexUnlock(&m_mutex); if (!image_data.empty()) { FreeImage(); - m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size()); + image = m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size()); m_own_image = true; } @@ -108,8 +116,8 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { nvgSave(vg); nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH()); - if (m_image) { - gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 5); + if (image) { + gfx::drawImage(vg, GetX() + 25, GetY() + 25, 120, 120, image, 5); } // shapes. @@ -152,9 +160,21 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { } gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str()); - gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str()); + + const auto draw_text = [&](ScrollingText& scroll, const std::string& txt, float y, float size, float pad, ThemeEntryID id){ + float bounds[4]; + nvgFontSize(vg, size); + gfx::textBounds(vg, 0, 0, bounds, txt.c_str()); + + const auto min_x = GetX() + pad; + const auto title_x = std::max(min_x, center_x - (bounds[2] - bounds[0]) / 2); + + scroll.Draw(vg, true, title_x, y, GetW() - pad * 2, size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(id), txt.c_str()); + }; + + draw_text(m_scroll_title, title, m_pos.y + 100, 22, 160, ThemeEntryID_TEXT); if (!transfer.empty()) { - gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str()); + draw_text(m_scroll_transfer, transfer, m_pos.y + 160, 18, 30, ThemeEntryID_TEXT_INFO); } nvgRestore(vg); @@ -189,6 +209,14 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& { return *this; } +auto ProgressBox::SetImage(int image) -> ProgressBox& { + mutexLock(&m_mutex); + m_image_pending = image; + m_is_image_pending = true; + mutexUnlock(&m_mutex); + return *this; +} + auto ProgressBox::SetImageData(std::vector& data) -> ProgressBox& { mutexLock(&m_mutex); std::swap(m_image_data, data); @@ -219,24 +247,24 @@ auto ProgressBox::ShouldExitResult() -> Result { R_SUCCEED(); } -auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { +auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { fs::File src_file; - R_TRY(fs->OpenFile(src_path, FsOpenMode_Read, &src_file)); - ON_SCOPE_EXIT(fs->FileClose(&src_file)); + R_TRY(fs_src->OpenFile(src_path, FsOpenMode_Read, &src_file)); + ON_SCOPE_EXIT(fs_src->FileClose(&src_file)); s64 src_size; - R_TRY(fs->FileGetSize(&src_file, &src_size)); + R_TRY(fs_src->FileGetSize(&src_file, &src_size)); // this can fail if it already exists so we ignore the result. // if the file actually failed to be created, the result is implicitly // handled when we try and open it for writing. - fs->CreateFile(dst_path, src_size, 0); + fs_dst->CreateFile(dst_path, src_size, 0); fs::File dst_file; - R_TRY(fs->OpenFile(dst_path, FsOpenMode_Write, &dst_file)); - ON_SCOPE_EXIT(fs->FileClose(&dst_file)); + R_TRY(fs_dst->OpenFile(dst_path, FsOpenMode_Write, &dst_file)); + ON_SCOPE_EXIT(fs_dst->FileClose(&dst_file)); - R_TRY(fs->FileSetSize(&dst_file, src_size)); + R_TRY(fs_dst->FileSetSize(&dst_file, src_size)); s64 offset{}; std::vector buf(1024*1024*4); // 4MiB @@ -245,10 +273,10 @@ auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsP R_TRY(ShouldExitResult()); u64 bytes_read; - R_TRY(fs->FileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read)); + R_TRY(fs_src->FileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read)); Yield(); - R_TRY(fs->FileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None)); + R_TRY(fs_dst->FileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None)); Yield(); UpdateTransfer(offset, src_size); @@ -258,6 +286,10 @@ auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsP R_SUCCEED(); } +auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { + return CopyFile(fs, fs, src_path, dst_path); +} + auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { fs::FsNativeSd fs; R_TRY(fs.GetFsOpenResult());