ce55be4a-fb5f-4981-b507-0f4.../Sources/services/AirDropService.swift
徐翔宇 c4080b5cb9 v1.0.0: initial AirDrop plugin for MioIsland
Quick-access AirDrop from the notch panel. Tap the card body, pick
files via NSOpenPanel, hand off to NSSharingService(.sendViaAirDrop).
No private APIs, no entitlements, no network — just a thin wrapper
around the same AirDrop API Finder uses.

Design constraint: drag-and-drop is intentionally not implemented.
MioIsland's notch panel auto-collapses on click-outside, which
breaks the Cmd-Tab-to-Finder / grab / drag-back workflow before
the drop target reaches. Tap-to-choose is rock solid by comparison.

Requires MioIsland host v2.2.0+ (panel size clamp floor was
lowered to 120pt in that release).
2026-04-22 08:20:47 +08:00

56 lines
1.8 KiB
Swift

//
// AirDropService.swift
// MioIsland AirDrop Plugin
//
// Thin wrapper around NSSharingService(.sendViaAirDrop). This is a
// 100% public Apple API no entitlements, no permissions, no
// private frameworks. When invoked, macOS pops the native AirDrop
// chooser sheet; we don't render the device list ourselves.
//
import AppKit
import Foundation
enum AirDropError: Error, LocalizedError {
case serviceUnavailable
case cannotPerform
var errorDescription: String? {
switch self {
case .serviceUnavailable:
return L10n.errServiceUnavailable
case .cannotPerform:
return L10n.errCannotPerform
}
}
}
enum AirDropService {
/// Invoke macOS' AirDrop chooser for the given files.
///
/// NSSharingService does NOT surface per-recipient completion,
/// so we can only distinguish "could not start" vs "started and
/// returned control to us". A returned .success(()) means the
/// chooser sheet was presented; the user may have cancelled it,
/// but from our UI perspective we treat it as handled.
static func perform(
files: [URL],
completion: @escaping (Result<Void, Error>) -> Void
) {
DispatchQueue.main.async {
guard let service = NSSharingService(named: .sendViaAirDrop) else {
completion(.failure(AirDropError.serviceUnavailable))
return
}
guard service.canPerform(withItems: files) else {
completion(.failure(AirDropError.cannotPerform))
return
}
service.perform(withItems: files)
// AirDrop chooser is modal; perform() returns right after
// presenting. Treat that as success.
completion(.success(()))
}
}
}