#include "ui/menus/filebrowser.hpp" #include "ui/menus/homebrew.hpp" #include "ui/menus/file_viewer.hpp" #include "ui/menus/image_viewer.hpp" #include "ui/sidebar.hpp" #include "ui/option_box.hpp" #include "ui/popup_list.hpp" #include "ui/progress_box.hpp" #include "ui/error_box.hpp" #include "ui/music_player.hpp" #include "utils/devoptab.hpp" #include "log.hpp" #include "app.hpp" #include "ui/nvg_util.hpp" #include "fs.hpp" #include "nro.hpp" #include "defines.hpp" #include "image.hpp" #include "download.hpp" #include "owo.hpp" #include "swkbd.hpp" #include "i18n.hpp" #include "hasher.hpp" #include "location.hpp" #include "threaded_file_transfer.hpp" #include "minizip_helper.hpp" #include "yati/yati.hpp" #include "yati/source/file.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sphaira::ui::menu::filebrowser { namespace { using RomDatabaseIndexs = std::vector; struct ForwarderForm final : public Sidebar { explicit ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path); private: auto LoadNroMeta() -> Result; private: const FileAssocEntry m_assoc; const RomDatabaseIndexs m_db_indexs; const fs::FsPath m_arg_path; NroEntry m_nro{}; NacpStruct m_nacp{}; SidebarEntryTextInput* m_name{}; SidebarEntryTextInput* m_author{}; SidebarEntryTextInput* m_version{}; SidebarEntryFilePicker* m_icon{}; }; constinit UEvent g_change_uevent; constexpr FsEntry FS_ENTRY_DEFAULT{ "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd, }; constexpr FsEntry FS_ENTRIES[]{ FS_ENTRY_DEFAULT, { "Image System memory", "/", FsType::ImageNand }, { "Image microSD card", "/", FsType::ImageSd}, }; constexpr std::string_view AUDIO_EXTENSIONS[] = { "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", "bfsar", "bfstm", "bwav", }; constexpr std::string_view VIDEO_EXTENSIONS[] = { "mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts", "webm", "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 NSP_EXTENSIONS[] = { "nsp", "nsz", }; constexpr std::string_view XCI_EXTENSIONS[] = { "xci", "xcz", }; constexpr std::string_view NCA_EXTENSIONS[] = { "nca", "ncz", }; // these are files that are already compressed or encrypted and should // be stored raw in a zip file. constexpr std::string_view COMPRESSED_EXTENSIONS[] = { "zip", "xz", "7z", "rar", "tar", "nca", "nsp", "xci", "nsz", "xcz" }; constexpr std::string_view ZIP_EXTENSIONS[] = { "zip", }; // supported music playback extensions. constexpr std::string_view MUSIC_EXTENSIONS[] = { "bfstm", "bfwav", "wav", "mp3", "ogg", "adf", }; // supported theme music playback extensions. constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_EXTENSIONS; constexpr std::string_view CDDVD_EXTENSIONS[] = { "iso", "cue", }; struct RomDatabaseEntry { // uses the naming scheme from retropie. std::string_view folder{}; // uses the naming scheme from Retroarch. std::string_view database{}; // custom alias, to make everyone else happy. std::array alias{}; // compares against all of the above strings. auto IsDatabase(std::string_view name) const { if (IsSamePath(name, folder) || IsSamePath(name, database)) { return true; } for (const auto& str : alias) { if (!str.empty() && IsSamePath(name, str)) { return true; } } return false; } }; constexpr RomDatabaseEntry PATHS[]{ { "3do", "The 3DO Company - 3DO"}, { "atari800", "Atari - 8-bit"}, { "atari2600", "Atari - 2600"}, { "atari5200", "Atari - 5200"}, { "atari7800", "Atari - 7800"}, { "atarilynx", "Atari - Lynx"}, { "atarijaguar", "Atari - Jaguar"}, { "atarijaguarcd", ""}, { "n3ds", "Nintendo - Nintendo 3DS"}, { "n64", "Nintendo - Nintendo 64"}, { "nds", "Nintendo - Nintendo DS"}, { "fds", "Nintendo - Famicom Disk System"}, { "nes", "Nintendo - Nintendo Entertainment System"}, { "pokemini", "Nintendo - Pokemon Mini"}, { "gb", "Nintendo - Game Boy"}, { "gba", "Nintendo - Game Boy Advance"}, { "gbc", "Nintendo - Game Boy Color"}, { "virtualboy", "Nintendo - Virtual Boy"}, { "gameandwatch", ""}, { "sega32x", "Sega - 32X"}, { "segacd", "Sega - Mega CD - Sega CD"}, { "dreamcast", "Sega - Dreamcast"}, { "gamegear", "Sega - Game Gear"}, { "genesis", "Sega - Mega Drive - Genesis"}, { "mastersystem", "Sega - Master System - Mark III"}, { "megadrive", "Sega - Mega Drive - Genesis"}, { "saturn", "Sega - Saturn"}, { "sg-1000", "Sega - SG-1000"}, { "psx", "Sony - PlayStation"}, { "psp", "Sony - PlayStation Portable"}, { "snes", "Nintendo - Super Nintendo Entertainment System"}, { "pico8", "Sega - PICO"}, { "wonderswan", "Bandai - WonderSwan"}, { "wonderswancolor", "Bandai - WonderSwan Color"}, { "mame", "MAME 2000", { "MAME", "mame-libretro", } }, { "mame", "MAME 2003", { "MAME", "mame-libretro", } }, { "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } }, { "neogeo", "SNK - Neo Geo Pocket" }, { "neogeo", "SNK - Neo Geo Pocket Color" }, { "neogeo", "SNK - Neo Geo CD" }, }; constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"}; // tries to find database path using folder name // names are taken from retropie // retroarch database names can also be used auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs { if (path.length() <= 1) { return {}; } // this won't fail :) RomDatabaseIndexs indexs; const auto db_name = path.substr(path.find_last_of('/') + 1); // log_write("new path: %s\n", db_name.c_str()); for (int i = 0; i < std::size(PATHS); i++) { const auto& p = PATHS[i]; if (p.IsDatabase(db_name)) { log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data()); indexs.emplace_back(i); } } // if we failed, try again but with the folder about // "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx if (indexs.empty()) { const auto last_off = path.substr(0, path.find_last_of('/')); if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) { const auto db_name2 = last_off.substr(off + 1); // printf("got db: %s\n", db_name2.c_str()); for (int i = 0; i < std::size(PATHS); i++) { const auto& p = PATHS[i]; if (p.IsDatabase(db_name2)) { log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data()); indexs.emplace_back(i); } } } } return indexs; } // auto GetRomIcon(std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) { // if no db entries, use nro icon if (db_indexs.empty()) { log_write("using nro image\n"); return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset); } // fix path to be url friendly constexpr std::string_view bad_chars{"&*/:`<>?\\|\""}; for (auto& c : filename) { for (auto bad_c : bad_chars) { if (c == bad_c) { c = '_'; break; } } } #define RA_BOXART_NAME "/Named_Boxarts/" #define RA_THUMBNAIL_PATH "/retroarch/thumbnails/" #define RA_BOXART_EXT ".png" for (auto db_idx : db_indexs) { const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension); auto system_name_gh = system_name + "/master"; for (auto& c : system_name_gh) { if (c == ' ') { c = '_'; } } const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT; const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path; log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str()); // try and find icon locally std::vector image_file; if (R_SUCCEEDED(fs::FsNativeSd().read_entire_file(ra_thumbnail_path, image_file))) { return image_file; } } // use nro icon log_write("using nro image\n"); return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset); } ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path) : Sidebar{"Forwarder Creation", Side::RIGHT} , m_assoc{assoc} , m_db_indexs{db_indexs} , m_arg_path{arg_path} { log_write("parsing nro\n"); if (R_FAILED(LoadNroMeta())) { App::Notify("Failed to parse nro"_i18n); SetPop(); return; } log_write("got nro data\n"); auto file_name = m_assoc.use_base_name ? entry.GetName() : entry.GetInternalName(); if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) { log_write("got filename\n"); file_name = file_name.substr(0, pos); log_write("got filename2: %s\n\n", file_name.c_str()); } const auto name = m_nro.nacp.lang.name + std::string{" | "} + file_name; const auto author = m_nacp.lang[0].author; const auto version = m_nacp.display_version; const auto icon = m_assoc.path; m_name = this->Add( "Name", name, "", -1, sizeof(NacpLanguageEntry::name) - 1, "Set the name of the application"_i18n ); m_author = this->Add( "Author", author, "", -1, sizeof(NacpLanguageEntry::author) - 1, "Set the author of the application"_i18n ); m_version = this->Add( "Version", version, "", -1, sizeof(NacpStruct::display_version) - 1, "Set the display version of the application"_i18n ); const std::vector filters{"nro", "png", "jpg"}; m_icon = this->Add( "Icon", icon, filters, "Set the path to the icon for the forwarder"_i18n ); auto callback = this->Add("Create", [this, file_name](){ OwoConfig config{}; config.nro_path = m_assoc.path.toString(); config.args = nro_add_arg_file(m_arg_path); config.nacp = m_nacp; // patch the name. config.name = m_name->GetValue(); // patch the author. config.author = m_author->GetValue(); // patch the display version. std::snprintf(config.nacp.display_version, sizeof(config.nacp.display_version), "%s", m_version->GetValue().c_str()); // load icon fron nro or image. if (m_icon->GetValue().ends_with(".nro")) { // if path was left as the default, try and load the icon from rom db. if (config.nro_path == m_icon->GetValue()) { config.icon = GetRomIcon(file_name, m_db_indexs, m_nro); } else { config.icon = nro_get_icon(m_icon->GetValue()); } } else { // try and read icon file into memory, bail if this fails. const auto rc = fs::FsStdio().read_entire_file(m_icon->GetValue(), config.icon); if (R_FAILED(rc)) { App::PushErrorBox(rc, "Failed to load icon"); return; } } // if this is a rom, load intro logo. if (!m_db_indexs.empty()) { fs::FsNativeSd().read_entire_file("/config/sphaira/logo/rom/NintendoLogo.png", config.logo); fs::FsNativeSd().read_entire_file("/config/sphaira/logo/rom/StartupMovie.gif", config.gif); } // try and install. if (R_FAILED(App::Install(config))) { App::Notify("Failed to install forwarder"_i18n); } else { SetPop(); } }, "Create the forwarder."_i18n); // ensure that all fields are valid. callback->Depends([this](){ return !m_name->GetValue().empty() && !m_author->GetValue().empty() && !m_version->GetValue().empty() && !m_icon->GetValue().empty(); }, "All fields must be non-empty!"_i18n); } auto ForwarderForm::LoadNroMeta() -> Result { // try and load nro meta data. R_TRY(nro_parse(m_assoc.path, m_nro)); R_TRY(nro_get_nacp(m_assoc.path, m_nacp)); R_SUCCEED(); } } // namespace // case insensitive check auto IsSamePath(std::string_view a, std::string_view b) -> bool { return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length()); } auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool { return ext1.length() == ext2.length() && !strncasecmp(ext1.data(), ext2.data(), ext1.length()); } auto IsExtension(std::string_view ext, std::span list) -> bool { for (auto e : list) { if (IsExtension(e, ext)) { return true; } } return false; } void SignalChange() { ueventSignal(&g_change_uevent); } FsView::FsView(Base* menu, const std::shared_ptr& fs, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} { this->SetActions( std::make_pair(Button::L2, Action{[this](){ if (!m_menu->m_selected.Empty()) { m_menu->ResetSelection(); } // if both set, select all. if (App::GetApp()->m_controller.GotHeld(Button::R2)) { const auto set = m_selected_count != m_entries_current.size(); for (u32 i = 0; i < m_entries_current.size(); i++) { auto& e = GetEntry(i); if (e.selected != set) { e.selected = set; if (set) { m_selected_count++; } else { m_selected_count--; } } } } else { GetEntry().selected ^= 1; if (GetEntry().selected) { m_selected_count++; } else { m_selected_count--; } } }}), std::make_pair(Button::A, Action{"Open"_i18n, [this](){ if (m_entries_current.empty()) { return; } m_menu->OnClick(this, m_fs_entry, GetEntry(), GetNewPathCurrent()); }}), std::make_pair(Button::B, Action{"Back"_i18n, [this](){ if (!m_menu->IsTab() && App::GetApp()->m_controller.GotHeld(Button::R2)) { m_menu->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 (!m_menu->IsTab()) { m_menu->PromptIfShouldExit(); } } }}), std::make_pair(Button::X, Action{"Options"_i18n, [this](){ if (App::GetApp()->m_controller.GotHeld(Button::R2)) { DisplayAdvancedOptions(); } else { DisplayOptions(); } }}) ); log_write("setting side\n"); SetSide(m_side); log_write("getting path\n"); auto buf = path; if (path.empty() && entry.IsSd()) { ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH); } // in case the above fails. if (buf.empty()) { buf = entry.root; } log_write("setting fs\n"); SetFs(fs, buf, entry); log_write("set fs\n"); } 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() && !m_entries_current.empty()) { ini_puts("paths", "last_path", m_path, App::CONFIG_PATH); ini_puts("paths", "last_file", GetEntry().name, App::CONFIG_PATH); } } void FsView::Update(Controller* controller, TouchInfo* 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 FsView::Draw(NVGcontext* vg, Theme* 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); } 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); // 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.done_stat && e.file_count == -1 && e.dir_count == -1) { got_dir_count = true; e.done_stat = 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 && !e.done_stat) { e.done_stat = true; 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 if ((double)e.file_size / 1024.0 / 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 MiB", (double)e.file_size / 1024.0 / 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 GiB", (double)e.file_size / 1024.0 / 1024.0 / 1024.0); } } }); } void FsView::OnFocusGained() { Widget::OnFocusGained(); if (m_entries.empty()) { if (m_path.empty()) { Scan(m_fs->Root()); } 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); } } } } 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 + 20.f); } 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 + 20.f; } 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::OnClick() { if (IsSd() && m_is_update_folder && m_daybreak_path.has_value()) { App::Push("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { // daybreak uses native fs so do not use nro_add_arg_file // otherwise it'll fail to open the folder... nro_launch(m_daybreak_path.value(), nro_add_arg(m_path)); } }); return; } const auto& entry = GetEntry(); if (entry.type == FsDirEntryType_Dir) { Scan(GetNewPathCurrent()); } else { // special case for nro if (IsSd() && IsSamePath(entry.GetExtension(), "nro")) { App::Push("Launch "_i18n + entry.GetName() + '?', "No"_i18n, "Launch"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { nro_launch(GetNewPathCurrent()); } }); } else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) { MountFileFs(devoptab::MountNca, devoptab::UmountNca); } else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) { MountFileFs(devoptab::MountNsp, devoptab::UmountNsp); } else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) { MountFileFs(devoptab::MountXci, devoptab::UmountXci); } else if (IsExtension(entry.GetExtension(), "zip")) { MountFileFs(devoptab::MountZip, devoptab::UmountZip); } else if (IsExtension(entry.GetExtension(), "bfsar")) { MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar); } else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) { App::Push(GetFs(), GetNewPathCurrent()); } else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) { App::Push(GetFs(), GetNewPathCurrent()); } else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) { std::shared_ptr usbdvd; if (entry.GetExtension() == "cue") { const auto cue_path = GetNewPathCurrent(); fs::FsPath bin_path = cue_path; std::strcpy(std::strstr(bin_path, ".cue"), ".bin"); if (m_fs->FileExists(bin_path)) { usbdvd = std::make_shared(cue_path, bin_path); } } else { usbdvd = std::make_shared(GetNewPathCurrent()); } if (usbdvd && usbdvd->usbdvd_drive_ctx.fs.mounted) { auto fs = std::make_shared(usbdvd->usbdvd_drive_ctx.fs.mountpoint, [usbdvd](){ // dummy func to keep shared_ptr alive until fs is closed. }); MountFsHelper(fs, usbdvd->usbdvd_drive_ctx.fs.disc_fstype); log_write("[USBDVD] mounted\n"); } else { log_write("[USBDVD] failed to mount\n"); } } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { InstallFiles(); } else if (IsSd()) { const auto assoc_list = m_menu->FindFileAssocFor(); if (!assoc_list.empty()) { // for (auto&e : assoc_list) { // log_write("assoc got: %s\n", e.path.c_str()); // } PopupList::Items items; for (const auto&p : assoc_list) { items.emplace_back(p.name); } const auto title = "Launch option for: "_i18n + GetEntry().name; App::Push( title, items, [this, assoc_list](auto op_index){ if (op_index) { log_write("selected: %s\n", assoc_list[*op_index].name.c_str()); nro_launch(assoc_list[*op_index].path, nro_add_arg_file(GetNewPathCurrent())); } else { log_write("pressed B to skip launch...\n"); } } ); } else { log_write("assoc list is empty\n"); } } } } void FsView::SetIndex(s64 index) { m_index = index; if (!m_index) { m_list->SetYoff(); } if (IsSd() && !m_entries_current.empty() && !GetEntry().checked_internal_extension && IsSamePath(GetEntry().GetExtension(), "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()); } } m_menu->UpdateSubheading(); } void FsView::InstallForwarder() { if (IsSamePath(GetEntry().GetExtension(), "nro")) { if (R_FAILED(homebrew::Menu::InstallHomebrewFromPath(GetNewPathCurrent()))) { log_write("failed to create forwarder\n"); } return; } 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().GetExtension().c_str()); return; } PopupList::Items items; for (const auto&p : assoc_list) { items.emplace_back(p.name); } const auto title = std::string{"Select launcher for: "_i18n} + GetEntry().name; App::Push( title, items, [this, assoc_list](auto op_index){ if (op_index) { const auto assoc = assoc_list[*op_index]; App::Push(assoc, GetRomDatabaseFromPath(m_path), GetEntry(), GetNewPathCurrent()); } else { log_write("pressed B to skip launch...\n"); } } ); } void FsView::InstallFiles() { if (!App::GetInstallEnable()) { App::ShowEnableInstallPrompt(); return; } const auto targets = GetSelectedEntries(); App::Push("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){ if (op_index && *op_index) { App::PopToMenu(); App::Push(0, "Installing "_i18n, "", [this, targets](auto pbox) -> Result { for (auto& e : targets) { R_TRY(yati::InstallFromFile(pbox, m_fs.get(), GetNewPath(e))); App::Notify("Installed "_i18n + e.GetName()); } R_SUCCEED(); }, [this](Result rc){ App::PushErrorBox(rc, "File install failed!"_i18n); }); } }); } void FsView::UnzipFiles(fs::FsPath dir_path) { const auto targets = GetSelectedEntries(); // set to current path. if (dir_path.empty()) { dir_path = m_path; } App::Push(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) -> Result { const auto is_hdd_fs = m_fs->Root().starts_with("ums"); for (auto& e : targets) { pbox->SetTitle(e.GetName()); const auto zip_out = GetNewPath(e); R_TRY(thread::TransferUnzipAll(pbox, zip_out, m_fs.get(), dir_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller)); } R_SUCCEED(); }, [this](Result rc){ App::PushErrorBox(rc, "Extract failed!"_i18n); if (R_SUCCEEDED(rc)) { App::Notify("Extract success!"_i18n); } Scan(m_path); log_write("did extract\n"); }); } void FsView::ZipFiles(fs::FsPath zip_out) { const auto targets = GetSelectedEntries(); // set to current path. if (zip_out.empty()) { if (std::size(targets) == 1) { const auto name = targets[0].name; const auto ext = std::strrchr(targets[0].name, '.'); fs::FsPath file_path; if (!ext) { std::snprintf(file_path, sizeof(file_path), "%s.zip", name); } else { std::snprintf(file_path, sizeof(file_path), "%.*s.zip", (int)(ext - name), name); } zip_out = fs::AppendPath(m_path, file_path); log_write("zip out: %s name: %s file_path: %s\n", zip_out.s, name, file_path.s); } else { // loop until we find an unused file name. for (u64 i = 0; ; i++) { fs::FsPath file_path = "Archive.zip"; if (i) { std::snprintf(file_path, sizeof(file_path), "Archive (%zu).zip", i); } zip_out = fs::AppendPath(m_path, file_path); if (!m_fs->FileExists(zip_out)) { break; } } } } else { if (!std::string_view(zip_out).ends_with(".zip")) { zip_out += ".zip"; } } App::Push(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) -> Result { const auto t = std::time(NULL); const auto tm = std::localtime(&t); const auto is_hdd_fs = m_fs->Root().starts_with("ums"); // pre-calculate the time rather than calculate it in the loop. zip_fileinfo zip_info{}; zip_info.tmz_date.tm_sec = tm->tm_sec; zip_info.tmz_date.tm_min = tm->tm_min; zip_info.tmz_date.tm_hour = tm->tm_hour; zip_info.tmz_date.tm_mday = tm->tm_mday; zip_info.tmz_date.tm_mon = tm->tm_mon; zip_info.tmz_date.tm_year = tm->tm_year; zlib_filefunc64_def file_func; mz::FileFuncStdio(&file_func); auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func); R_UNLESS(zfile, Result_ZipOpen2_64); ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH)); const auto zip_add = [&](const fs::FsPath& file_path) -> Result { // the file name needs to be relative to the current directory. const char* file_name_in_zip = file_path.s + std::strlen(m_path); // strip root path (/ or ums0:) if (!std::strncmp(file_name_in_zip, m_fs->Root(), std::strlen(m_fs->Root()))) { file_name_in_zip += std::strlen(m_fs->Root()); } // root paths are banned in zips, they will warn when extracting otherwise. while (file_name_in_zip[0] == '/') { file_name_in_zip++; } pbox->NewTransfer(file_name_in_zip); if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION)) { log_write("failed to add zip for %s\n", file_path.s); R_THROW(Result_ZipOpenNewFileInZip); } ON_SCOPE_EXIT(zipCloseFileInZip(zfile)); return thread::TransferZip(pbox, zfile, m_fs.get(), file_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller); }; for (auto& e : targets) { pbox->SetTitle(e.GetName()); if (e.IsFile()) { const auto file_path = GetNewPath(e); R_TRY(zip_add(file_path)); } else { FsDirCollections collections; get_collections(GetNewPath(e), e.name, collections); for (const auto& collection : collections) { for (const auto& file : collection.files) { const auto file_path = fs::AppendPath(collection.path, file.name); R_TRY(zip_add(file_path)); } } } } R_SUCCEED(); }, [this](Result rc){ App::PushErrorBox(rc, "Compress failed!"_i18n); if (R_SUCCEEDED(rc)) { App::Notify("Compress success!"_i18n); } Scan(m_path); log_write("did compress\n"); }); } void FsView::UploadFiles() { const auto targets = GetSelectedEntries(); const auto network_locations = location::Load(); if (network_locations.empty()) { App::Notify("No upload locations set!"_i18n); return; } PopupList::Items items; for (const auto&p : network_locations) { items.emplace_back(p.name); } App::Push( "Select upload location"_i18n, items, [this, network_locations](auto op_index){ if (!op_index) { return; } const auto loc = network_locations[*op_index]; App::Push(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> Result { auto targets = GetSelectedEntries(); const auto is_file_based_emummc = App::IsFileBaseEmummc(); const auto file_add = [&](s64 file_size, const fs::FsPath& file_path, const char* name) -> Result { // the file name needs to be relative to the current directory. const auto relative_file_name = file_path.s + std::strlen(m_path); pbox->SetTitle(name); pbox->NewTransfer(relative_file_name); fs::File f; R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f)); return thread::TransferPull(pbox, file_size, [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { const auto rc = f.Read(off, data, size, FsReadOption_None, bytes_read); if (m_fs->IsNative() && is_file_based_emummc) { svcSleepThread(2e+6); // 2ms } return rc; }, [&](thread::PullCallback pull) -> Result { s64 offset{}; const auto result = curl::Api().FromMemory( CURL_LOCATION_TO_API(loc), curl::OnProgress{pbox->OnDownloadProgressCallback()}, curl::UploadInfo{ relative_file_name, file_size, [&](void *ptr, size_t size) -> size_t { // curl will request past the size of the file, causing an error. if (offset >= file_size) { log_write("finished file upload\n"); return 0; } u64 bytes_read{}; if (R_FAILED(pull(ptr, size, &bytes_read))) { log_write("failed to read in custom callback: %zd size: %zd\n", offset, size); return 0; } offset += bytes_read; return bytes_read; } } ); R_UNLESS(result.success, Result_FileBrowserFailedUpload); R_SUCCEED(); } ); }; for (auto& e : targets) { if (e.IsFile()) { const auto file_path = GetNewPath(e); R_TRY(file_add(e.file_size, file_path, e.GetName().c_str())); } else { FsDirCollections collections; get_collections(GetNewPath(e), e.name, collections, true); for (const auto& collection : collections) { for (const auto& file : collection.files) { const auto file_path = fs::AppendPath(collection.path, file.name); R_TRY(file_add(file.file_size, file_path, file.name)); } } } } R_SUCCEED(); }, [this](Result rc){ App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); m_menu->ResetSelection(); if (R_SUCCEEDED(rc)) { App::Notify("Upload successfull!"_i18n); log_write("Upload successfull!!!\n"); } else { App::Notify("Upload failed!"_i18n); log_write("Upload failed!!!\n"); } }); } ); } 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()); 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_index_search.clear(); m_entries_current = {}; m_selected_count = 0; m_is_update_folder = false; SetIndex(0); m_menu->SetTitleSubHeading(m_path); fs::Dir d; R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d)); // we won't run out of memory here (tm) std::vector dir_entries; R_TRY(d.ReadAll(dir_entries)); const auto count = dir_entries.size(); m_entries.reserve(count); m_entries_index.reserve(count); m_entries_index_hidden.reserve(count); u32 i = 0; for (const auto& e : dir_entries) { 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 // 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()) { ON_SCOPE_EXIT(m_previous_highlighted_file.pop_back()); SetIndexFromLastFile(m_previous_highlighted_file.back()); } 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(bool scan) { std::optional last_file; if (!m_path.empty() && !m_entries_current.empty()) { last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size()); } if (scan) { Scan(m_path); } else { Sort(); } if (last_file.has_value()) { SetIndexFromLastFile(*last_file); } } void 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) { if (auto rc = m_fs->DeleteDirectory(full_path); R_FAILED(rc)) { App::PushErrorBox(rc, "Failed to delete directory"_i18n); } use_progress_box = false; } } else { if (auto rc = m_fs->DeleteFile(full_path); R_FAILED(rc)) { App::PushErrorBox(rc, "Failed to delete file"_i18n); } use_progress_box = false; } } if (!use_progress_box) { m_menu->RefreshViews(); log_write("did delete\n"); } else { App::Push(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 DeleteAllCollectionsWithSelected(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(0, "Pasting"_i18n, "", [this](auto pbox) -> Result { auto& selected = m_menu->m_selected; auto src_fs = selected.m_view->GetFs(); const auto is_same_fs = selected.SameFs(this); 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; const auto on_paste_file = [&](auto& src_path, auto& dst_path) -> Result { if (selected.m_type == SelectedType::Cut) { // update timestamp if possible. if (!m_fs->IsNative()) { FsTimeStampRaw ts; if (R_SUCCEEDED(src_fs->GetFileTimeStampRaw(src_path, &ts))) { m_fs->SetTimestamp(dst_path, &ts); } } // delete src file. folders are removed after. R_TRY(src_fs->DeleteFile(src_path)); } R_SUCCEED(); }; // 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, is_same_fs)); R_TRY(on_paste_file(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); 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, is_same_fs)); R_TRY(on_paste_file(src_path, dst_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(DeleteAllCollectionsWithSelected(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(), Result_FileBrowserDirNotDaybreak); R_UNLESS(m_fs->IsNative(), Result_FileBrowserDirNotDaybreak); // 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(homebrew::GetNroEntries(), "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); } // check that we have enough ncas and not too many R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, Result_FileBrowserDirNotDaybreak); // check that all entries end in .nca for (auto& e : m_entries) { // check that we are at the bottom level R_UNLESS(e.type == FsDirEntryType_File, Result_FileBrowserDirNotDaybreak); const auto ext = std::strrchr(e.name, '.'); R_UNLESS(ext && IsSamePath(ext, ".nca"), Result_FileBrowserDirNotDaybreak); } 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)); return d.ReadAll(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); } Result FsView::DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCollections& collections, u32 mode) { // 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.toString()); if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) { log_write("deleting dir: %s\n", full_path.s); R_TRY(fs->DeleteDirectory(full_path)); svcSleepThread(1e+5); } else if ((mode & FsDirOpenMode_ReadFiles) && p.type == FsDirEntryType_File) { log_write("deleting file: %s\n", full_path.s); R_TRY(fs->DeleteFile(full_path)); svcSleepThread(1e+5); } } R_SUCCEED(); }; R_TRY(delete_func(c.files)); R_TRY(delete_func(c.dirs)); } R_SUCCEED(); } static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) { R_TRY(FsView::DeleteAllCollections(pbox, fs, collections, mode)); 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.toString()); 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 std::shared_ptr& fs, const fs::FsPath& new_path, const FsEntry& new_entry) { if (m_fs && m_fs_entry.root == new_entry.root && m_fs_entry.type == new_entry.type) { log_write("same fs, ignoring\n"); return; } // 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; m_fs = fs; 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(0, "Hashing"_i18n, GetEntry().name, [this, type](auto pbox) -> Result { const auto full_path = GetNewPathCurrent(); pbox->NewTransfer(full_path); R_TRY(hash::Hash(pbox, type, m_fs.get(), full_path, 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(buf, "OK"_i18n); } }); } void FsView::DisplayOptions() { auto options = std::make_unique("File Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); SidebarEntryArray::Items mount_items; std::vector fs_entries; const auto stdio_locations = location::GetStdio(false); for (const auto& e: stdio_locations) { u32 flags{}; if (e.write_protect) { flags |= FsEntryFlag_ReadOnly; } fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); mount_items.push_back(e.name); } for (const auto& e: FS_ENTRIES) { fs_entries.emplace_back(e); mount_items.push_back(i18n::get(e.name)); } if (m_menu->m_custom_fs) { fs_entries.emplace_back(m_menu->m_custom_fs_entry); mount_items.push_back(m_menu->m_custom_fs_entry.name); } const auto fat_entries = location::GetFat(); for (const auto& e: fat_entries) { u32 flags{}; if (e.write_protect) { flags |= FsEntryFlag_ReadOnly; } fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); mount_items.push_back(e.name); } options->Add("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ App::PopToMenu(); SetFs(m_menu->CreateFs(fs_entries[index_out]), fs_entries[index_out].root, fs_entries[index_out]); }, i18n::get(m_fs_entry.name)); options->Add("Sort By"_i18n, [this](){ auto options = std::make_unique("Sort Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); SidebarEntryArray::Items sort_items; sort_items.push_back("Size"_i18n); sort_items.push_back("Alphabetical"_i18n); SidebarEntryArray::Items order_items; order_items.push_back("Descending"_i18n); order_items.push_back("Ascending"_i18n); options->Add("Sort"_i18n, sort_items, [this](s64& index_out){ m_menu->m_sort.Set(index_out); SortAndFindLastFile(); }, m_menu->m_sort.Get()); options->Add("Order"_i18n, order_items, [this](s64& index_out){ m_menu->m_order.Set(index_out); SortAndFindLastFile(); }, m_menu->m_order.Get()); options->Add("Show Hidden"_i18n, m_menu->m_show_hidden.Get(), [this](bool& v_out){ m_menu->m_show_hidden.Set(v_out); SortAndFindLastFile(); }); options->Add("Folders First"_i18n, m_menu->m_folders_first.Get(), [this](bool& v_out){ m_menu->m_folders_first.Set(v_out); SortAndFindLastFile(); }); options->Add("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()) { if (!m_fs_entry.IsReadOnly()) { options->Add("Cut"_i18n, [this](){ m_menu->AddSelectedEntries(SelectedType::Cut); }, true); } options->Add("Copy"_i18n, [this](){ m_menu->AddSelectedEntries(SelectedType::Copy); }, true); } if (!m_menu->m_selected.Empty() && !m_fs_entry.IsReadOnly() && (m_menu->m_selected.Type() == SelectedType::Cut || m_menu->m_selected.Type() == SelectedType::Copy)) { options->Add("Paste"_i18n, [this](){ const std::string buf = "Paste file(s)?"_i18n; App::Push( buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { App::PopToMenu(); OnPasteCallback(); } }); }); } // can't rename more than 1 file if (m_entries_current.size() && !m_selected_count && !m_fs_entry.IsReadOnly()) { options->Add("Rename"_i18n, [this](){ std::string out; const auto& entry = GetEntry(); const auto name = entry.GetName(); if (R_SUCCEEDED(swkbd::ShowText(out, "Set New File Name"_i18n.c_str(), name.c_str())) && !out.empty() && out != name) { App::PopToMenu(); const auto src_path = GetNewPath(entry); const auto dst_path = GetNewPath(m_path, out); Result rc; if (entry.IsFile()) { rc = m_fs->RenameFile(src_path, dst_path); } else { rc = m_fs->RenameDirectory(src_path, dst_path); } if (R_SUCCEEDED(rc)) { Scan(m_path); } else { const auto msg = std::string("Failed to rename file: ") + entry.name; App::PushErrorBox(rc, msg); } } }); } if (m_entries_current.size() && !m_fs_entry.IsReadOnly()) { options->Add("Delete"_i18n, [this](){ m_menu->AddSelectedEntries(SelectedType::Delete); log_write("clicked on delete\n"); App::Push( "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](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 (m_menu->CanInstall()) { if (m_entries_current.size()) { if (check_all_ext(INSTALL_EXTENSIONS)) { auto entry = options->Add("Install"_i18n, [this](){ InstallFiles(); }); entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); } } if (IsSd() && m_entries_current.size() && !m_selected_count) { if (GetEntry().IsFile() && (IsSamePath(GetEntry().GetExtension(), "nro") || !m_menu->FindFileAssocFor().empty())) { auto entry = options->Add("Install Forwarder"_i18n, [this](){; InstallForwarder(); }); entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); } } } if (m_entries_current.size()) { if (check_all_ext(ZIP_EXTENSIONS) && !m_fs_entry.IsReadOnly()) { options->Add("Extract zip"_i18n, [this](){ auto options = std::make_unique("Extract Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); options->Add("Extract here"_i18n, [this](){ UnzipFiles(""); }); options->Add("Extract to root"_i18n, [this](){ App::Push("Are you sure you want to extract to root?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { UnzipFiles(m_fs->Root()); } }); }); options->Add("Extract to..."_i18n, [this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) { UnzipFiles(out); } }); }); } if ((!check_all_ext(ZIP_EXTENSIONS) || m_selected_count) && !m_fs_entry.IsReadOnly()) { options->Add("Compress to zip"_i18n, [this](){ auto options = std::make_unique("Compress Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); options->Add("Compress"_i18n, [this](){ ZipFiles(""); }); options->Add("Compress to..."_i18n, [this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", m_path)) && !out.empty()) { ZipFiles(out); } }); }); } } options->Add("Advanced"_i18n, [this](){ DisplayAdvancedOptions(); }); } void FsView::DisplayAdvancedOptions() { auto options = std::make_unique("Advanced Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); if (!m_fs_entry.IsReadOnly()) { options->Add("Create File"_i18n, [this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) { App::PopToMenu(); fs::FsPath full_path; if (out.starts_with(m_fs_entry.root.s)) { full_path = out; } else { full_path = fs::AppendPath(m_path, out); } m_fs->CreateDirectoryRecursivelyWithPath(full_path); if (R_SUCCEEDED(m_fs->CreateFile(full_path, 0, 0))) { log_write("created file: %s\n", full_path.s); Scan(m_path); } else { log_write("failed to create file: %s\n", full_path.s); } } }); options->Add("Create Folder"_i18n, [this](){ std::string out; if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) { App::PopToMenu(); fs::FsPath full_path; if (out.starts_with(m_fs_entry.root.s)) { full_path = out; } else { full_path = fs::AppendPath(m_path, out); } if (R_SUCCEEDED(m_fs->CreateDirectoryRecursively(full_path))) { log_write("created dir: %s\n", full_path.s); Scan(m_path); } else { log_write("failed to create dir: %s\n", full_path.s); } } }); } if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) { options->Add("View as text (unfinished)"_i18n, [this](){ App::Push(GetFs(), GetNewPathCurrent()); }); } if (m_entries_current.size() && (m_menu->m_options & FsOption_CanUpload)) { options->Add("Upload"_i18n, [this](){ UploadFiles(); }); } if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) { options->Add("Set as background music"_i18n, [this](){ const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent()); App::PushErrorBox(rc, "Failed to set default music path"_i18n); }); } if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile()) { options->Add("Hash"_i18n, [this](){ auto options = std::make_unique("Hash Options"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); options->Add("CRC32"_i18n, [this](){ DisplayHash(hash::Type::Crc32); }); options->Add("MD5"_i18n, [this](){ DisplayHash(hash::Type::Md5); }); options->Add("SHA1"_i18n, [this](){ DisplayHash(hash::Type::Sha1); }); options->Add("SHA256"_i18n, [this](){ DisplayHash(hash::Type::Sha256); }); options->Add("/dev/null (Speed Test)"_i18n, [this](){ DisplayHash(hash::Type::Null); }); options->Add("Deflate (Speed Test)"_i18n, [this](){ DisplayHash(hash::Type::Deflate); }); options->Add("ZSTD (Speed Test)"_i18n, [this](){ DisplayHash(hash::Type::Zstd); }); }); } options->Add("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); }); } void FsView::MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func) { fs::FsPath mount; const auto rc = mount_func(GetFs(), GetNewPathCurrent(), mount); App::PushErrorBox(rc, "Failed to mount FS."_i18n); if (R_SUCCEEDED(rc)) { auto fs = std::make_shared(mount, [mount, umount_func](){ umount_func(mount); }); MountFsHelper(fs, GetEntry().GetName()); } } Base::Base(u32 flags, u32 options) : MenuBase{"FileBrowser"_i18n, flags} , m_options{options} { Init(CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false); } Base::Base(const std::shared_ptr& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options) : MenuBase{"FileBrowser"_i18n, flags} , m_options{options} { Init(fs, fs_entry, path, is_custom); } void Base::Update(Controller* controller, TouchInfo* touch) { if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) { if (IsSplitScreen()) { view_left->SortAndFindLastFile(true); view_right->SortAndFindLastFile(true); } else { view->SortAndFindLastFile(true); } } // workaround the buttons not being display properly. // basically, inherit all actions from the view, draw them, // then restore state after. const auto view_actions = view->GetActions(); m_actions.insert_range(view_actions); ON_SCOPE_EXIT(RemoveActions(view_actions)); MenuBase::Update(controller, touch); view->Update(controller, touch); } 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)); MenuBase::Draw(vg, theme); if (IsSplitScreen()) { view_left->Draw(vg, theme); view_right->Draw(vg, theme); if (view == view_left.get()) { gfx::drawRect(vg, view_right->GetPos(), theme->GetColour(ThemeEntryID_FOCUS), 5); } else { gfx::drawRect(vg, view_left->GetPos(), theme->GetColour(ThemeEntryID_FOCUS), 5); } gfx::drawRect(vg, SCREEN_WIDTH/2, GetY(), 1, GetH(), theme->GetColour(ThemeEntryID_LINE)); } else { view->Draw(vg, theme); } } void Base::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(); } } void Base::OnClick(FsView* view, const FsEntry& fs_entry, const FileEntry& entry, const fs::FsPath& path) { view->OnClick(); } auto Base::FindFileAssocFor() -> std::vector { // only support roms in correctly named folders, sorry! const auto db_indexs = GetRomDatabaseFromPath(view->m_path); const auto& entry = view->GetEntry(); const auto extension = entry.GetExtension(); const auto internal_extension = entry.GetInternalExtension(); if (extension.empty() && internal_extension.empty()) { // log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path); return {}; } std::vector out_entries; if (!db_indexs.empty()) { // if database isn't empty, then we are in a valid folder // search for an entry that matches the db and ext for (const auto& assoc : m_assoc_entries) { for (const auto& assoc_db : assoc.database) { // if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) { for (auto db_idx : db_indexs) { if (PATHS[db_idx].IsDatabase(assoc_db)) { if (assoc.IsExtension(extension, internal_extension)) { out_entries.emplace_back(assoc); goto jump; } } } } jump: } } else { // otherwise, if not in a valid folder, find an entry that doesn't // use a database, ie, not a emulator. // this is because media players and hbmenu can launch from anywhere // and the extension is enough info to know what type of file it is. // whereas with roms, a .iso can be used for multiple systems, so it needs // to be in the correct folder, ie psx, to know what system that .iso is for. for (const auto& assoc : m_assoc_entries) { if (assoc.database.empty()) { if (assoc.IsExtension(extension, internal_extension)) { log_write("found ext: %s\n", assoc.path.s); out_entries.emplace_back(assoc); } } } } return out_entries; } void Base::LoadAssocEntriesPath(const fs::FsPath& path) { auto dir = opendir(path); if (!dir) { return; } ON_SCOPE_EXIT(closedir(dir)); while (auto d = readdir(dir)) { if (d->d_name[0] == '.') { continue; } if (d->d_type != DT_REG) { continue; } const auto ext = std::strrchr(d->d_name, '.'); if (!ext || strcasecmp(ext, ".ini")) { continue; } const auto full_path = GetNewPath(path, d->d_name); FileAssocEntry assoc{}; ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) { auto assoc = static_cast(UserData); if (!std::strcmp(Key, "path")) { assoc->path = Value; } else if (!std::strcmp(Key, "supported_extensions")) { for (const auto& p : std::views::split(std::string_view{Value}, '|')) { if (p.empty()) { continue; } assoc->ext.emplace_back(p.data(), p.size()); } } else if (!std::strcmp(Key, "database")) { for (const auto& p : std::views::split(std::string_view{Value}, '|')) { if (p.empty()) { continue; } assoc->database.emplace_back(p.data(), p.size()); } } else if (!std::strcmp(Key, "use_base_name")) { if (!std::strcmp(Value, "true") || !std::strcmp(Value, "1")) { assoc->use_base_name = true; } } return 1; }, &assoc, full_path); if (assoc.ext.empty()) { continue; } assoc.name.assign(d->d_name, ext - d->d_name); // if path isn't empty, check if the file exists bool file_exists{}; if (!assoc.path.empty()) { file_exists = view->m_fs->FileExists(assoc.path); } else { const auto nro_name = assoc.name + ".nro"; for (const auto& nro : homebrew::GetNroEntries()) { const auto len = std::strlen(nro.path); if (len < nro_name.length()) { continue; } if (!strcasecmp(nro.path + len - nro_name.length(), nro_name.c_str())) { assoc.path = nro.path; file_exists = true; break; } } } // after all of that, the file doesn't exist :( if (!file_exists) { // log_write("removing: %s\n", assoc.name.c_str()); continue; } // log_write("\tpath: %s\n", assoc.path.s); // log_write("\tname: %s\n", assoc.name.c_str()); // for (const auto& ext : assoc.ext) { // log_write("\t\text: %s\n", ext.c_str()); // } // for (const auto& db : assoc.database) { // log_write("\t\tdb: %s\n", db.c_str()); // } m_assoc_entries.emplace_back(assoc); } } 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/"); } } 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 Base::SetSplitScreen(bool enable) { if (!(m_options & FsOption_CanSplit)) { return; } if (m_split_screen != enable) { m_split_screen = enable; if (m_split_screen) { const auto change_view = [this](FsView* new_view){ if (view != new_view) { view->OnFocusLost(); view = new_view; view->OnFocusGained(); SetTitleSubHeading(view->m_path); UpdateSubheading(); } }; // load second screen as a copy of the left side. view->SetSide(ViewSide::Left); view_right = std::make_unique(view, ViewSide::Right); change_view(view_right.get()); SetAction(Button::LEFT, Action{[this, change_view](){ change_view(view_left.get()); }}); SetAction(Button::RIGHT, Action{[this, change_view](){ change_view(view_right.get()); }}); } else { if (view == view_right.get()) { view_left = std::move(view_right); } view_right = {}; view = view_left.get(); view->SetSide(ViewSide::Left); RemoveAction(Button::LEFT); RemoveAction(Button::RIGHT); ResetSelection(); } } } void Base::RefreshViews() { ResetSelection(); if (IsSplitScreen()) { view_left->Scan(view_left->m_path); view_right->Scan(view_right->m_path); } else { view->Scan(view->m_path); } } void Base::PromptIfShouldExit() { if (IsTab()) { return; } if (m_options & FsOption_DoNotPrompt) { SetPop(); return; } App::Push( "Close FileBrowser?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { SetPop(); } } ); } auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr { switch (fs_entry.type) { case FsType::Sd: return std::make_shared(m_ignore_read_only.Get()); case FsType::ImageNand: return std::make_shared(FsImageDirectoryId_Nand); case FsType::ImageSd: return std::make_shared(FsImageDirectoryId_Sd); case FsType::Stdio: return std::make_shared(true, fs_entry.root); case FsType::Custom: return m_custom_fs; } std::unreachable(); } void Base::Init(const std::shared_ptr& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom) { if (m_options & FsOption_CanSplit) { SetAction(Button::L3, Action{"Split"_i18n, [this](){ SetSplitScreen(IsSplitScreen() ^ 1); }}); } if (!IsTab()) { SetAction(Button::SELECT, Action{"Close"_i18n, [this](){ PromptIfShouldExit(); }}); } if (is_custom) { m_custom_fs = fs; m_custom_fs_entry = fs_entry; } log_write("creating view\n"); view_left = std::make_unique(this, fs, path, fs_entry, ViewSide::Left); view = view_left.get(); ueventCreate(&g_change_uevent, true); } void MountFsHelper(const std::shared_ptr& fs, const fs::FsPath& name) { const filebrowser::FsEntry fs_entry{ .name = name, .root = fs->Root(), .type = filebrowser::FsType::Custom, .flags = filebrowser::FsEntryFlag_ReadOnly, }; const auto options = FsOption_All &~ FsOption_LoadAssoc; App::Push(fs, fs_entry, fs->Root(), options); } } // namespace sphaira::ui::menu::filebrowser