import SwiftUI import AppKit import OnyxDesign import OnyxWidgetKit import OSLog private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "AirDrop") /// Der Weg zur Empfängerauswahl. public enum AirDrop { /// Öffnet AirDrop mit den übergebenen Dateien. /// /// - Returns: ob AirDrop überhaupt zur Verfügung stand. @MainActor @discardableResult public static func send(_ urls: [URL]) -> Bool { guard !urls.isEmpty else { return false } guard let service = NSSharingService(named: .sendViaAirDrop) else { log.error("AirDrop steht nicht zur Verfügung") return false } guard service.canPerform(withItems: urls) else { log.error("AirDrop kann diese Auswahl nicht senden") return false } service.perform(withItems: urls) return true } } /// Eine Fläche, auf die man Dateien fallen lässt, um sie zu verschicken. public struct AirDropView: View { /// Schmal, wenn sie sich die Kachel mit der Ablage teilt. private let compact: Bool @State private var isTargeted = false @State private var failed = false public init(compact: Bool = false) { self.compact = compact } public var body: some View { Group { if compact { // Nebeneinander statt übereinander: in 52 Punkten Höhe ist für // Symbol über Text kein Platz. HStack(spacing: Onyx.Space.md) { Image(systemName: "shareplay") .font(.system(size: Onyx.Symbol.large, weight: .light)) .foregroundStyle(isTargeted ? Onyx.Color.accent : Onyx.Color.textTertiary) Text(failed ? "airdrop.unavailable" : "airdrop.hint", bundle: .module) .font(Onyx.Font.caption) .foregroundStyle(failed ? Onyx.Color.warning : Onyx.Color.textSecondary) .fixedSize(horizontal: false, vertical: true) Spacer(minLength: 0) } .padding(.horizontal, Onyx.Space.md) } else { VStack(spacing: Onyx.Space.sm) { Image(systemName: "shareplay") .font(.system(size: Onyx.Symbol.xlarge, weight: .light)) .foregroundStyle(isTargeted ? Onyx.Color.accent : Onyx.Color.textTertiary) Text(failed ? "airdrop.unavailable" : "airdrop.hint", bundle: .module) .font(Onyx.Font.caption) .foregroundStyle(failed ? Onyx.Color.warning : Onyx.Color.textSecondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) // Diese Fläche ist **nur** zum Ablegen da. Ihre Kante steht deshalb // immer — ohne sie wüsste niemand, dass sie eine ist. .onyxDropZone(isTargeted: isTargeted, style: .always) // Derselbe Weg wie in der Ablage: auch hier kommen Anhänge als // Versprechen an, nicht als fertige Datei. .contentShape(.rect) .onFileDrop(isTargeted: $isTargeted) { urls in failed = !AirDrop.send(urls) } } }