add mtp custom mount support (image sd, image nand, install, speed test).
This commit is contained in:
@@ -45,6 +45,7 @@ add_executable(sphaira
|
||||
source/ui/menus/ghdl.cpp
|
||||
source/ui/menus/usb_menu.cpp
|
||||
source/ui/menus/ftp_menu.cpp
|
||||
source/ui/menus/mtp_menu.cpp
|
||||
source/ui/menus/gc_menu.cpp
|
||||
source/ui/menus/game_menu.cpp
|
||||
source/ui/menus/grid_menu_base.cpp
|
||||
@@ -79,6 +80,7 @@ add_executable(sphaira
|
||||
source/hasher.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
source/haze_helper.cpp
|
||||
source/threaded_file_transfer.cpp
|
||||
source/minizip_helper.cpp
|
||||
|
||||
@@ -160,7 +162,7 @@ FetchContent_Declare(ftpsrv
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 8e16df2
|
||||
GIT_TAG d318432
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <nxlink.h>
|
||||
#include <haze.h>
|
||||
#include "download.hpp"
|
||||
|
||||
namespace sphaira::evman {
|
||||
@@ -24,7 +23,6 @@ struct ExitEventData {
|
||||
using EventData = std::variant<
|
||||
LaunchNroEventData,
|
||||
ExitEventData,
|
||||
HazeCallbackData,
|
||||
NxlinkCallbackData,
|
||||
curl::DownloadEventData
|
||||
>;
|
||||
|
||||
@@ -195,6 +195,7 @@ struct Dir {
|
||||
~Dir();
|
||||
|
||||
Result GetEntryCount(s64* out);
|
||||
Result Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf);
|
||||
Result ReadAll(std::vector<FsDirectoryEntry>& buf);
|
||||
void Close();
|
||||
|
||||
@@ -398,36 +399,6 @@ struct FsNative : Fs {
|
||||
return fsFsGetTotalSpace(&m_fs, path, out);
|
||||
}
|
||||
|
||||
// Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
|
||||
// return fsFsOpenDirectory(&m_fs, path, mode, out);
|
||||
// }
|
||||
|
||||
// void DirClose(FsDir *d) {
|
||||
// fsDirClose(d);
|
||||
// }
|
||||
|
||||
// Result DirGetEntryCount(FsDir *d, s64* out) {
|
||||
// return fsDirGetEntryCount(d, out);
|
||||
// }
|
||||
|
||||
// Result DirGetEntryCount(const FsPath& path, u32 mode, s64* out) {
|
||||
// FsDir d;
|
||||
// R_TRY(OpenDirectory(path, mode, &d));
|
||||
// ON_SCOPE_EXIT(DirClose(&d));
|
||||
// return DirGetEntryCount(&d, out);
|
||||
// }
|
||||
|
||||
// Result DirRead(FsDir *d, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
// return fsDirRead(d, total_entries, max_entries, buf);
|
||||
// }
|
||||
|
||||
// Result DirRead(const FsPath& path, u32 mode, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
// FsDir d;
|
||||
// R_TRY(OpenDirectory(path, mode, &d));
|
||||
// ON_SCOPE_EXIT(DirClose(&d));
|
||||
// return DirRead(&d, total_entries, max_entries, buf);
|
||||
// }
|
||||
|
||||
virtual bool IsFsActive() {
|
||||
return serviceIsActive(&m_fs.s);
|
||||
}
|
||||
|
||||
17
sphaira/include/haze_helper.hpp
Normal file
17
sphaira/include/haze_helper.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::haze {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(void* user, const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void(void* user)>;
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
} // namespace sphaira::haze
|
||||
57
sphaira/include/ui/menus/mtp_menu.hpp
Normal file
57
sphaira/include/ui/menus/mtp_menu.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/stream.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct StreamFtp final : yati::source::Stream {
|
||||
StreamFtp(const fs::FsPath& path, std::stop_token token);
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
bool Push(const void* buf, s64 size);
|
||||
void Disable();
|
||||
|
||||
// private:
|
||||
fs::FsPath m_path{};
|
||||
std::stop_token m_token{};
|
||||
std::vector<u8> m_buffer{};
|
||||
Mutex m_mutex{};
|
||||
bool m_active{};
|
||||
// bool m_push_exit{};
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "MTP"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<StreamFtp> m_source{};
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
|
||||
bool m_was_mtp_enabled{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
// TODO: remove these when libnx pr is merged.
|
||||
enum { UsbDeviceSpeed_None = 0x0 };
|
||||
enum { UsbDeviceSpeed_Low = 0x1 };
|
||||
Result usbDsGetSpeed(UsbDeviceSpeed *out);
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
// Device Host
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
#include "web.hpp"
|
||||
#include "swkbd.hpp"
|
||||
|
||||
#include <nanovg_dk.h>
|
||||
#include <minIni.h>
|
||||
#include <pulsar.h>
|
||||
#include <haze.h>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <cassert>
|
||||
@@ -400,11 +400,6 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
|
||||
}
|
||||
}
|
||||
|
||||
void haze_callback(const HazeCallbackData *data) {
|
||||
App::NotifyFlashLed();
|
||||
evman::push(*data, false);
|
||||
}
|
||||
|
||||
void nxlink_callback(const NxlinkCallbackData *data) {
|
||||
App::NotifyFlashLed();
|
||||
evman::push(*data, false);
|
||||
@@ -460,9 +455,6 @@ void App::Loop() {
|
||||
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
|
||||
log_write("[ExitEventData] got event\n");
|
||||
m_quit = true;
|
||||
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
|
||||
// log_write("[ExitEventData] got event\n");
|
||||
// m_quit = true;
|
||||
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
|
||||
switch (arg.type) {
|
||||
case NxlinkCallbackType_Connected:
|
||||
@@ -857,9 +849,9 @@ void App::SetMtpEnable(bool enable) {
|
||||
if (App::GetMtpEnable() != enable) {
|
||||
g_app->m_mtp_enabled.Set(enable);
|
||||
if (enable) {
|
||||
hazeInitialize(haze_callback, 0x2C, 2);
|
||||
haze::Init();
|
||||
} else {
|
||||
hazeExit();
|
||||
haze::Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1379,7 +1371,7 @@ App::App(const char* argv0) {
|
||||
}
|
||||
|
||||
if (App::GetMtpEnable()) {
|
||||
hazeInitialize(haze_callback, PRIO_PREEMPTIVE, 2);
|
||||
haze::Init();
|
||||
}
|
||||
|
||||
if (App::GetFtpEnable()) {
|
||||
@@ -1948,7 +1940,7 @@ App::~App() {
|
||||
|
||||
if (App::GetMtpEnable()) {
|
||||
log_write("closing mtp\n");
|
||||
hazeExit();
|
||||
haze::Exit();
|
||||
}
|
||||
|
||||
if (App::GetFtpEnable()) {
|
||||
|
||||
@@ -702,6 +702,47 @@ Result Dir::GetEntryCount(s64* out) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Dir::Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
*total_entries = 0;
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsDirRead(&m_native, total_entries, max_entries, buf));
|
||||
} else {
|
||||
while (auto d = readdir(m_stdio)) {
|
||||
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FsDirectoryEntry entry{};
|
||||
|
||||
if (d->d_type == DT_DIR) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
|
||||
continue;
|
||||
}
|
||||
entry.type = FsDirEntryType_Dir;
|
||||
} else if (d->d_type == DT_REG) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
|
||||
continue;
|
||||
}
|
||||
entry.type = FsDirEntryType_File;
|
||||
} else {
|
||||
log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::strcpy(entry.name, d->d_name);
|
||||
std::memcpy(&buf[*total_entries], &entry, sizeof(*buf));
|
||||
*total_entries = *total_entries + 1;
|
||||
if (*total_entries >= max_entries) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
|
||||
buf.clear();
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
520
sphaira/source/haze_helper.cpp
Normal file
520
sphaira/source/haze_helper.cpp
Normal file
@@ -0,0 +1,520 @@
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "evman.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <haze.h>
|
||||
|
||||
namespace sphaira::haze {
|
||||
namespace {
|
||||
|
||||
struct InstallSharedData {
|
||||
std::mutex mutex;
|
||||
std::deque<std::string> queued_files;
|
||||
|
||||
void* user;
|
||||
OnInstallStart on_start;
|
||||
OnInstallWrite on_write;
|
||||
OnInstallClose on_close;
|
||||
|
||||
bool in_progress;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
std::mutex g_mutex{};
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
const char* SUPPORTED_EXT[] = {
|
||||
".nsp", ".xci", ".nsz", ".xcz",
|
||||
};
|
||||
|
||||
// ive given up with good names.
|
||||
void on_thing() {
|
||||
log_write("[MTP] doing on_thing\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
log_write("[MTP] locked on_thing\n");
|
||||
|
||||
if (!g_shared_data.in_progress) {
|
||||
if (!g_shared_data.queued_files.empty()) {
|
||||
log_write("[MTP] pushing new file data\n");
|
||||
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
|
||||
g_shared_data.queued_files.clear();
|
||||
} else {
|
||||
log_write("[MTP] success on new file push\n");
|
||||
g_shared_data.in_progress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FsProxyBase : ::haze::FileSystemProxyImpl {
|
||||
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
|
||||
|
||||
}
|
||||
|
||||
auto FixPath(const char* path) const {
|
||||
fs::FsPath buf;
|
||||
const auto len = std::strlen(GetName());
|
||||
|
||||
if (len && !strncasecmp(path + 1, GetName(), len)) {
|
||||
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
|
||||
} else {
|
||||
std::strcpy(buf, path);
|
||||
}
|
||||
|
||||
log_write("[FixPath] %s -> %s\n", path, buf.s);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char* GetName() const override {
|
||||
return m_name.c_str();
|
||||
}
|
||||
const char* GetDisplayName() const override {
|
||||
return m_display_name.c_str();
|
||||
}
|
||||
|
||||
protected:
|
||||
const std::string m_name;
|
||||
const std::string m_display_name;
|
||||
};
|
||||
|
||||
struct FsProxy final : FsProxyBase {
|
||||
FsProxy(std::shared_ptr<fs::Fs> fs, const char* name, const char* display_name) : FsProxyBase{name, display_name} {
|
||||
m_fs = fs;
|
||||
}
|
||||
|
||||
~FsProxy() {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
fsFsCommit(&fs->m_fs);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: impl this for stdio
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
|
||||
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
log_write("[HAZE] CreateFile(%s)\n", path);
|
||||
return m_fs->CreateFile(FixPath(path), size, option);
|
||||
}
|
||||
Result DeleteFile(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->DeleteFile(FixPath(path));
|
||||
}
|
||||
Result RenameFile(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
log_write("[HAZE] OpenFile(%s)\n", path);
|
||||
auto fptr = new fs::File();
|
||||
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
Result GetFileSize(FsFile *file, s64 *out_size) override {
|
||||
log_write("[HAZE] GetFileSize()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->GetSize(out_size);
|
||||
}
|
||||
Result SetFileSize(FsFile *file, s64 size) override {
|
||||
log_write("[HAZE] SetFileSize()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->SetSize(size);
|
||||
}
|
||||
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
|
||||
log_write("[HAZE] ReadFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Read(off, buf, read_size, option, out_bytes_read);
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
log_write("[HAZE] WriteFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Write(off, buf, write_size, option);
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
log_write("[HAZE] CloseFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
if (f) {
|
||||
delete f;
|
||||
}
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->CreateDirectory(FixPath(path));
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
|
||||
return m_fs->DeleteDirectoryRecursively(FixPath(path));
|
||||
}
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
auto fptr = new fs::Dir();
|
||||
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
}
|
||||
|
||||
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->Read(out_total_entries, max_entries, buf);
|
||||
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
|
||||
return rc;
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->GetEntryCount(out_count);
|
||||
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
|
||||
return rc;
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
log_write("[HAZE] CloseDirectory()\n");
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
if (f) {
|
||||
delete f;
|
||||
}
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<fs::Fs> m_fs{};
|
||||
};
|
||||
|
||||
struct FsDevNullProxy final : FsProxyBase {
|
||||
using FsProxyBase::FsProxyBase;
|
||||
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
if (FixPath(path) == "/") {
|
||||
*out_entry_type = FsDirEntryType_Dir;
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
*out_entry_type = FsDirEntryType_File;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result DeleteFile(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result RenameFile(const char *old_path, const char *new_path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFileSize(FsFile *file, s64 *out_size) override {
|
||||
*out_size = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result SetFileSize(FsFile *file, s64 size) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
|
||||
*out_bytes_read = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
*out_total_entries = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
*out_count = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
}
|
||||
};
|
||||
|
||||
struct FsInstallProxy final : FsProxyBase {
|
||||
using FsProxyBase::FsProxyBase;
|
||||
|
||||
Result FailedIfNotEnabled() {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
if (!g_shared_data.enabled) {
|
||||
App::Notify("Please launch MTP install menu before trying to install"_i18n);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// TODO: impl this.
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
if (App::GetApp()->m_install_sd.Get()) {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetTotalSpace("/", out);
|
||||
} else {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
|
||||
}
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (App::GetApp()->m_install_sd.Get()) {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
|
||||
} else {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_User).GetFreeSpace("/", out);
|
||||
}
|
||||
}
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
if (FixPath(path) == "/") {
|
||||
*out_entry_type = FsDirEntryType_Dir;
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
*out_entry_type = FsDirEntryType_File;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
return FailedIfNotEnabled();
|
||||
}
|
||||
Result DeleteFile(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result RenameFile(const char *old_path, const char *new_path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
if (mode & FsOpenMode_Read) {
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
if (!g_shared_data.enabled) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
const char* ext = std::strrchr(path, '.');
|
||||
if (!ext) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
|
||||
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
// check if we already have this file queued.
|
||||
auto it = std::ranges::find(g_shared_data.queued_files, path);
|
||||
if (it != g_shared_data.queued_files.cend()) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
g_shared_data.queued_files.push_back(path);
|
||||
}
|
||||
|
||||
on_thing();
|
||||
log_write("[MTP] got file: %s\n", path);
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFileSize(FsFile *file, s64 *out_size) override {
|
||||
*out_size = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result SetFileSize(FsFile *file, s64 size) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
|
||||
*out_bytes_read = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
if (!g_shared_data.enabled) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, write_size)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
{
|
||||
log_write("[MTP] closing file\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
log_write("[MTP] closing valid file\n");
|
||||
|
||||
log_write("[MTP] closing current file\n");
|
||||
if (g_shared_data.on_close) {
|
||||
g_shared_data.on_close(g_shared_data.user);
|
||||
}
|
||||
|
||||
g_shared_data.in_progress = false;
|
||||
g_shared_data.queued_files.clear();
|
||||
}
|
||||
|
||||
on_thing();
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
*out_total_entries = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
*out_count = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
}
|
||||
};
|
||||
|
||||
::haze::FsEntries g_fs_entries{};
|
||||
|
||||
void haze_callback(const ::haze::CallbackData *data) {
|
||||
App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Init() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
if (g_is_running) {
|
||||
log_write("[MTP] already enabled, cannot open\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeSd>(), "", "microSD card"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||
|
||||
g_should_exit = false;
|
||||
if (!::haze::Initialize(haze_callback, PRIO_PREEMPTIVE, 2, g_fs_entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_write("[MTP] started\n");
|
||||
return g_is_running = true;
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
if (!g_is_running) {
|
||||
return;
|
||||
}
|
||||
|
||||
::haze::Exit();
|
||||
g_is_running = false;
|
||||
g_should_exit = true;
|
||||
g_fs_entries.clear();
|
||||
|
||||
log_write("[MTP] exitied\n");
|
||||
}
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
g_shared_data.user = user;
|
||||
g_shared_data.on_start = on_start;
|
||||
g_shared_data.on_write = on_write;
|
||||
g_shared_data.on_close = on_close;
|
||||
g_shared_data.enabled = true;
|
||||
}
|
||||
|
||||
void DisableInstallMode() {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::haze
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "ui/menus/mtp_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
#include "ui/menus/game_menu.hpp"
|
||||
#include "ui/menus/save_menu.hpp"
|
||||
@@ -54,6 +55,7 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
|
||||
290
sphaira/source/ui/menus/mtp_menu.cpp
Normal file
290
sphaira/source/ui/menus/mtp_menu.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
#include "ui/menus/mtp_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
namespace {
|
||||
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
|
||||
constexpr u64 SLEEPNS = 1000;
|
||||
volatile bool IN_PUSH_THREAD{};
|
||||
|
||||
auto GetUsbStateStr(UsbState state) -> const char* {
|
||||
switch (state) {
|
||||
case UsbState_Detached: return "Detached";
|
||||
case UsbState_Attached: return "Attached";
|
||||
case UsbState_Powered: return "Powered";
|
||||
case UsbState_Default: return "Default";
|
||||
case UsbState_Address: return "Address";
|
||||
case UsbState_Configured: return "Configured";
|
||||
case UsbState_Suspended: return "Suspended";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
auto GetUsbSpeedStr(UsbDeviceSpeed speed) -> const char* {
|
||||
// todo: remove this cast when libnx pr is merged.
|
||||
switch ((u32)speed) {
|
||||
case UsbDeviceSpeed_None: return "None";
|
||||
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
|
||||
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
|
||||
case UsbDeviceSpeed_High: return "USB 2.0 High Speed";
|
||||
case UsbDeviceSpeed_Super: return "USB 3.0 Super Speed";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool OnInstallStart(void* user, const char* path) {
|
||||
auto menu = (Menu*)user;
|
||||
log_write("[INSTALL] inside OnInstallStart()\n");
|
||||
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
|
||||
if (menu->m_state != State::Progress) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
|
||||
|
||||
if (menu->m_source) {
|
||||
log_write("[INSTALL] OnInstallStart() we have source\n");
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_source->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
|
||||
|
||||
if (!IN_PUSH_THREAD) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
|
||||
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
|
||||
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
menu->m_state = State::Connected;
|
||||
log_write("[INSTALL] OnInstallStart() done make shared\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OnInstallWrite(void* user, const void* buf, size_t size) {
|
||||
auto menu = (Menu*)user;
|
||||
|
||||
return menu->m_source->Push(buf, size);
|
||||
}
|
||||
|
||||
void OnInstallClose(void* user) {
|
||||
auto menu = (Menu*)user;
|
||||
menu->m_source->Disable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
|
||||
m_path = path;
|
||||
m_token = token;
|
||||
m_buffer.reserve(MAX_BUFFER_SIZE);
|
||||
m_active = true;
|
||||
}
|
||||
|
||||
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_buffer.empty()) {
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
size = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), size);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
bool StreamFtp::Push(const void* buf, s64 size) {
|
||||
IN_PUSH_THREAD = true;
|
||||
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
|
||||
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
const auto offset = m_buffer.size();
|
||||
m_buffer.resize(offset + size);
|
||||
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void StreamFtp::Disable() {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
Menu::Menu(u32 flags) : MenuBase{"MTP Install"_i18n, flags} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
App::SetAutoSleepDisabled(true);
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] wasn't enabled, forcefully enabling\n");
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
|
||||
haze::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
haze::DisableInstallMode();
|
||||
m_stop_source.request_stop();
|
||||
|
||||
if (m_source) {
|
||||
m_source->Disable();
|
||||
}
|
||||
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] disabling on exit\n");
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
App::SetAutoSleepDisabled(false);
|
||||
log_write("closing data!!!!\n");
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
static TimeStamp poll_ts;
|
||||
if (poll_ts.GetSeconds() >= 1) {
|
||||
poll_ts.Update();
|
||||
|
||||
UsbState state{UsbState_Detached};
|
||||
usbDsGetState(&state);
|
||||
|
||||
UsbDeviceSpeed speed{(UsbDeviceSpeed)UsbDeviceSpeed_None};
|
||||
usbDsGetSpeed(&speed);
|
||||
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbStateStr(state)).c_str(), i18n::get(GetUsbSpeedStr(speed)).c_str());
|
||||
SetSubHeading(buf);
|
||||
}
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_state == State::Connected) {
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
|
||||
log_write("inside progress box\n");
|
||||
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
|
||||
if (R_FAILED(rc)) {
|
||||
m_source->Disable();
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "MTP install failed!"_i18n);
|
||||
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("MTP install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
m_state = State::Failed;
|
||||
haze::DisableInstallMode();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder on PC"_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via MTP, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
@@ -4,22 +4,6 @@
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
// TODO: pr missing speed fields to libnx.
|
||||
enum { UsbDeviceSpeed_None = 0x0 };
|
||||
enum { UsbDeviceSpeed_Low = 0x1 };
|
||||
|
||||
constexpr u16 DEVICE_SPEED[] = {
|
||||
[UsbDeviceSpeed_None] = 0x0,
|
||||
[UsbDeviceSpeed_Low] = 0x0,
|
||||
[UsbDeviceSpeed_Full] = 0x40,
|
||||
[UsbDeviceSpeed_High] = 0x200,
|
||||
[UsbDeviceSpeed_Super] = 0x400,
|
||||
};
|
||||
|
||||
// TODO: pr this to libnx.
|
||||
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
|
||||
if (hosversionBefore(8,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
@@ -29,6 +13,17 @@ Result usbDsGetSpeed(UsbDeviceSpeed *out) {
|
||||
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
|
||||
}
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u16 DEVICE_SPEED[] = {
|
||||
[UsbDeviceSpeed_None] = 0x0,
|
||||
[UsbDeviceSpeed_Low] = 0x0,
|
||||
[UsbDeviceSpeed_Full] = 0x40,
|
||||
[UsbDeviceSpeed_High] = 0x200,
|
||||
[UsbDeviceSpeed_Super] = 0x400,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbDs::~UsbDs() {
|
||||
|
||||
Reference in New Issue
Block a user