Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine eigene Anmeldung, kein Netz, kein Schreiben zurück. Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49 Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang nicht abgedeckt ist. Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung. Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie geöffnet zu haben knipst den Apple-Kalender nicht aus. Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot, und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt die Fläche UTType.item statt public.file-url an: solche Anhänge kommen als Dateiversprechen, und loadFileRepresentation löst beides ein. Die übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort sofort gesichert statt später auf dem MainActor. Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit Zeitschaltuhr. Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer im Hauptbundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
3.5 KiB
Swift
80 lines
3.5 KiB
Swift
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
import OSLog
|
|
|
|
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Shelf")
|
|
|
|
/// Nimmt gezogene Dateien an — auch die, die es noch gar nicht gibt.
|
|
///
|
|
/// Mail, WhatsApp und die meisten Chat-Programme ziehen Anhänge nicht als
|
|
/// fertige Datei heraus, sondern als **Versprechen**: die Datei entsteht erst,
|
|
/// wenn das Ziel sagt, wohin. Genau diese Anhänge sind der Hauptgrund für eine
|
|
/// Ablage, also darf die Fläche nicht nur `public.file-url` annehmen.
|
|
///
|
|
/// `loadFileRepresentation` ist der Weg, der beides abdeckt: bei einer echten
|
|
/// Datei reicht es sie durch, bei einem Versprechen löst es dieses ein. Deshalb
|
|
/// `UTType.item` als angenommener Typ — die Wurzel, der alles entspricht.
|
|
/// Was sich nicht als Datei materialisieren lässt, fällt hinten heraus.
|
|
public struct FileDropModifier: ViewModifier {
|
|
|
|
@Binding private var isTargeted: Bool
|
|
private let onFiles: ([URL]) -> Void
|
|
|
|
public init(isTargeted: Binding<Bool>, onFiles: @escaping ([URL]) -> Void) {
|
|
self._isTargeted = isTargeted
|
|
self.onFiles = onFiles
|
|
}
|
|
|
|
public func body(content: Content) -> some View {
|
|
content.onDrop(of: [.item], isTargeted: $isTargeted) { providers in
|
|
for provider in providers { receive(provider) }
|
|
return !providers.isEmpty
|
|
}
|
|
}
|
|
|
|
private func receive(_ provider: NSItemProvider) {
|
|
provider.loadFileRepresentation(forTypeIdentifier: UTType.item.identifier) { url, error in
|
|
if let error {
|
|
log.error("Ablage: Datei nicht erhalten: \(error.localizedDescription, privacy: .public)")
|
|
return
|
|
}
|
|
guard let url else { return }
|
|
|
|
// Die übergebene Datei existiert **nur innerhalb dieses Blocks**;
|
|
// danach räumt das System sie weg. Wer sie erst auf dem MainActor
|
|
// anfassen will, greift ins Leere — also hier und sofort sichern.
|
|
guard let staged = Self.stage(url) else { return }
|
|
Task { @MainActor in onFiles([staged]) }
|
|
}
|
|
}
|
|
|
|
/// Rettet die Datei aus dem flüchtigen Verzeichnis in einen eigenen
|
|
/// Zwischenordner.
|
|
private static func stage(_ url: URL) -> URL? {
|
|
let directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent("Onyx-Drop-\(UUID().uuidString)", isDirectory: true)
|
|
do {
|
|
try FileManager.default.createDirectory(at: directory,
|
|
withIntermediateDirectories: true)
|
|
let target = directory.appendingPathComponent(url.lastPathComponent)
|
|
// Verschieben, wenn möglich: bei einem Video von zwei Gigabyte ist
|
|
// der Unterschied zum Kopieren spürbar. Über Datenträgergrenzen
|
|
// hinweg scheitert es, dann wird kopiert.
|
|
do { try FileManager.default.moveItem(at: url, to: target) }
|
|
catch { try FileManager.default.copyItem(at: url, to: target) }
|
|
return target
|
|
} catch {
|
|
log.error("Ablage: Zwischenspeichern gescheitert: \(error.localizedDescription, privacy: .public)")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension View {
|
|
/// Macht die ganze Fläche zum Ziel für gezogene Dateien.
|
|
func onFileDrop(isTargeted: Binding<Bool>,
|
|
perform: @escaping ([URL]) -> Void) -> some View {
|
|
modifier(FileDropModifier(isTargeted: isTargeted, onFiles: perform))
|
|
}
|
|
}
|