Files
onyx/Packages/OnyxKit/Sources/ShelfProvider/AirDropWidget.swift
Scarriffle d7125e28f6 Ablage und AirDrop teilen sich eine Kachel
Zwei eigene Kacheln waren zu viel des Guten: beide zeigen im Ruhezustand
nichts als eine leere Fläche mit einem Satz darin, und zusammen nahmen sie
ein Drittel des Panels.

Jetzt übereinander in einer Kachel — oben die Ablegefläche fürs Senden,
darunter die Ablage mit ihrer Liste. AirDrop steht dabei nebeneinander statt
übereinander: in 52 Punkten Höhe ist für Symbol über Text kein Platz.

Das eigenständige AirDrop-Widget entfällt. Gespeicherte Layouts, die es noch
nennen, verlieren den Eintrag beim Laden — dafür ist der Filter da.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:42:39 +02:00

89 lines
3.5 KiB
Swift

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: 8) {
Image(systemName: "shareplay")
.font(.system(size: 15, 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, 8)
} else {
VStack(spacing: 6) {
Image(systemName: "shareplay")
.font(.system(size: 20, 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)
.background {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(isTargeted ? Onyx.Color.accent : Onyx.Color.hairline,
style: StrokeStyle(lineWidth: 1, dash: [4, 3]))
}
// 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) }
.animation(Onyx.Motion.value, value: isTargeted)
}
}