2 Commits

Author SHA1 Message Date
474f4f7714 Version bump to 1.0.2 2026-08-07 00:14:53 +02:00
846f743a5b Support recursive folder uploads via drag-and-drop and the file picker.
Create matching remote directories and enqueue contained files instead of ignoring directories.
2026-08-07 00:14:53 +02:00
4 changed files with 62 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
project(OmniMTP VERSION 1.0.1 project(OmniMTP VERSION 1.0.2
DESCRIPTION "Nintendo Switch MTP Client for macOS" DESCRIPTION "Nintendo Switch MTP Client for macOS"
LANGUAGES CXX OBJCXX OBJC C LANGUAGES CXX OBJCXX OBJC C
) )

View File

@@ -83,6 +83,8 @@ private:
void start_download(uint32_t handle, uint32_t storage_id, void start_download(uint32_t handle, uint32_t storage_id,
const std::string& filename, uint64_t size); const std::string& filename, uint64_t size);
void start_upload(const fs::path& src_path, uint64_t file_size); void start_upload(const fs::path& src_path, uint64_t file_size);
// Recursively create a remote folder and enqueue files under parent_handle.
void upload_local_tree(const fs::path& dir, uint32_t parent_handle, uint32_t storage_id);
// Recursively download an MTP association (folder) into dest (must hold device_mtx_). // Recursively download an MTP association (folder) into dest (must hold device_mtx_).
void download_remote_tree(uint32_t handle, uint32_t storage_id, const fs::path& dest); void download_remote_tree(uint32_t handle, uint32_t storage_id, const fs::path& dest);

View File

@@ -275,6 +275,49 @@ void App::start_upload(const fs::path& src, uint64_t sz) {
src.filename().string(), sz); src.filename().string(), sz);
set_status("status.uploading", {src.filename().string()}); set_status("status.uploading", {src.filename().string()});
} }
void App::upload_local_tree(const fs::path& dir, uint32_t parent_handle, uint32_t storage_id) {
if (!engine_ || !ops_) return;
const std::string name = dir.filename().string();
if (name.empty() || name == "." || name == "..") return;
uint32_t dir_handle = 0;
try {
std::lock_guard lock(device_mtx_);
if (!ops_) return;
dir_handle = ops_->create_directory(storage_id, parent_handle, name);
} catch (const std::exception& e) {
set_status("status.listing_error", {e.what()});
return;
} catch (...) {
set_status("status.listing_error", {"Failed to create folder: " + name});
return;
}
if (dir_handle == 0) {
set_status("status.listing_error", {"Failed to create folder: " + name});
return;
}
std::error_code ec;
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) {
const auto& entry = *it;
const std::string child_name = entry.path().filename().string();
if (child_name.empty() || child_name == "." || child_name == "..") continue;
// Skip macOS clutter that Switch MTP installers don't need.
if (child_name == ".DS_Store" || child_name.rfind("._", 0) == 0) continue;
std::error_code st_ec;
if (entry.is_directory(st_ec)) {
upload_local_tree(entry.path(), dir_handle, storage_id);
} else if (entry.is_regular_file(st_ec)) {
auto sz = entry.file_size(st_ec);
if (st_ec) continue;
engine_->enqueue_upload(entry.path().string(), dir_handle, storage_id,
child_name, sz);
}
}
}
void App::set_status(const std::string& key, std::vector<std::string> args, float dur) { void App::set_status(const std::string& key, std::vector<std::string> args, float dur) {
status_key_ = key; status_key_ = key;
status_args_ = std::move(args); status_args_ = std::move(args);
@@ -381,13 +424,8 @@ void App::process_finder_drops() {
drops.swap(finder_drops_); drops.swap(finder_drops_);
} }
for (auto& dropped : drops) { for (auto& dropped : drops) {
fs::path p(dropped); if (connected_.load())
std::error_code ec; do_upload(dropped);
if (fs::is_directory(p, ec)) continue;
if (!ec && connected_.load()) {
auto sz = fs::file_size(p, ec);
if (!ec) start_upload(p, sz);
}
} }
} }
@@ -419,8 +457,17 @@ void App::set_active_storage(uint32_t id) {
} }
void App::request_refresh_remote() { refresh_remote(); } void App::request_refresh_remote() { refresh_remote(); }
void App::do_upload(const std::string& src) { void App::do_upload(const std::string& src) {
if (!connected_.load() || !engine_ || remote_nav_stack_.empty()) return;
fs::path p(src); fs::path p(src);
std::error_code ec; std::error_code ec;
if (fs::is_directory(p, ec)) {
const auto& top = remote_nav_stack_.back();
set_status("status.uploading", {p.filename().string()});
upload_local_tree(p, top.handle, top.storage_id);
remote_needs_refresh_ = true;
return;
}
if (ec) return;
auto sz = fs::file_size(p, ec); auto sz = fs::file_size(p, ec);
if (!ec) start_upload(p, sz); if (!ec) start_upload(p, sz);
} }

View File

@@ -410,7 +410,7 @@ static NSString* OmniPreferredDownloadURL(NSDictionary* release) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
NSOpenPanel* panel = [NSOpenPanel openPanel]; NSOpenPanel* panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES; panel.canChooseFiles = YES;
panel.canChooseDirectories = NO; panel.canChooseDirectories = YES;
panel.allowsMultipleSelection = YES; panel.allowsMultipleSelection = YES;
[panel beginWithCompletionHandler:^(NSModalResponse r) { [panel beginWithCompletionHandler:^(NSModalResponse r) {
if (r == NSModalResponseOK) { if (r == NSModalResponseOK) {
@@ -872,9 +872,10 @@ static const CGFloat kTrafficLightInset = 72.0;
NSString* path = url.path; NSString* path = url.path;
if (!path) continue; if (!path) continue;
BOOL isDir = NO; BOOL isDir = NO;
if ([fm fileExistsAtPath:path isDirectory:&isDir] && !isDir) if ([fm fileExistsAtPath:path isDirectory:&isDir]) {
_app->do_upload(path.UTF8String); self->_app->do_upload(path.UTF8String);
handled = YES; handled = YES;
}
} }
return handled; return handled;
} }