84 lines
2.6 KiB
Swift
84 lines
2.6 KiB
Swift
import AppKit
|
|
import ShotdeckCore
|
|
|
|
@MainActor
|
|
enum Sharing {
|
|
/// Presents the AirDrop picker for `fileURL`, anchored to `view`.
|
|
/// Throws `ShotdeckError.airDropUnavailable` when the service cannot be created,
|
|
/// `canPerform` is false, or `view` is not in a visible window (a detached view
|
|
/// never produces an on-screen sheet).
|
|
static func airDrop(fileURL: URL, from view: NSView) throws {
|
|
guard let service = NSSharingService(named: .sendViaAirDrop),
|
|
service.canPerform(withItems: [fileURL]) else {
|
|
throw ShotdeckError.airDropUnavailable
|
|
}
|
|
// Presenting from a detached NSView (no window) yields a sheet that never appears.
|
|
guard let window = view.window, window.isVisible else {
|
|
throw ShotdeckError.airDropUnavailable
|
|
}
|
|
|
|
NSApp.activate()
|
|
window.makeKeyAndOrderFront(nil)
|
|
|
|
service.subject = fileURL.lastPathComponent
|
|
let session = AirDropSession(service: service, window: window, view: view)
|
|
AirDropSession.keepAlive(session)
|
|
service.delegate = session
|
|
service.perform(withItems: [fileURL])
|
|
}
|
|
}
|
|
|
|
/// Retains the sharing service for the life of the picker and supplies the real
|
|
/// on-screen window as the sheet parent. `NSSharingService.delegate` is weak.
|
|
@MainActor
|
|
private final class AirDropSession: NSObject, NSSharingServiceDelegate {
|
|
static var live: [AirDropSession] = []
|
|
|
|
let service: NSSharingService
|
|
let window: NSWindow
|
|
let view: NSView
|
|
|
|
init(service: NSSharingService, window: NSWindow, view: NSView) {
|
|
self.service = service
|
|
self.window = window
|
|
self.view = view
|
|
}
|
|
|
|
static func keepAlive(_ session: AirDropSession) {
|
|
live.append(session)
|
|
}
|
|
|
|
private func drop() {
|
|
Self.live.removeAll { $0 === self }
|
|
}
|
|
|
|
func sharingService(
|
|
_ sharingService: NSSharingService,
|
|
sourceWindowForShareItems items: [Any],
|
|
sharingContentScope: UnsafeMutablePointer<NSSharingService.SharingContentScope>
|
|
) -> NSWindow? {
|
|
sharingContentScope.pointee = .item
|
|
return window
|
|
}
|
|
|
|
func sharingService(
|
|
_ sharingService: NSSharingService,
|
|
sourceFrameOnScreenForShareItem item: Any
|
|
) -> NSRect {
|
|
let inWindow = view.convert(view.bounds, to: nil)
|
|
return window.convertToScreen(inWindow)
|
|
}
|
|
|
|
func sharingService(_ sharingService: NSSharingService, didShareItems items: [Any]) {
|
|
drop()
|
|
}
|
|
|
|
func sharingService(
|
|
_ sharingService: NSSharingService,
|
|
didFailToShareItems items: [Any],
|
|
error: any Error
|
|
) {
|
|
drop()
|
|
}
|
|
}
|