diff --git a/Packages/OnyxKit/Sources/OnyxNotch/TriggerAreaSettings.swift b/Packages/OnyxKit/Sources/OnyxNotch/TriggerAreaSettings.swift index e734f96..c7a790d 100644 --- a/Packages/OnyxKit/Sources/OnyxNotch/TriggerAreaSettings.swift +++ b/Packages/OnyxKit/Sources/OnyxNotch/TriggerAreaSettings.swift @@ -131,7 +131,7 @@ private struct TriggerPreview: View { // `NSWorkspace` gehören dorthin —, das Bild daneben. let own = Self.ownWallpaperURL() let loaded = await Task.detached(priority: .userInitiated) { - Wallpaper(image: own.flatMap { Self.thumbnail(of: $0) } ?? Self.systemWallpaper()) + Wallpaper(image: WallpaperLoader.load(own: own)) }.value Self.cache = loaded wallpaper = loaded.image @@ -172,38 +172,63 @@ private struct TriggerPreview: View { return url } +} + + +/// Lädt das Hintergrundbild der Vorschau. +/// +/// **Ein eigener Typ, kein Teil der View.** Was in einer `View` steht, ist an +/// den Hauptthread gebunden — die Konformität vererbt das, auch an statische +/// Methoden, die damit gar nichts zu tun haben. Aufgerufen wurde es trotzdem +/// nebenher; der Compiler hat das nur angemahnt, und zur Laufzeit hat die +/// Prüfung die App angehalten. Hier draußen gibt es die Bindung nicht. +enum WallpaperLoader { + + static func load(own: URL?) -> NSImage? { + own.flatMap(thumbnail(of:)) ?? systemWallpaper() + } + /// Ein Standardhintergrund von macOS. /// - /// Die Vorschaubilder aus `.thumbnails` statt der Originale: dieselben - /// Motive in ein paar Dutzend Kilobyte statt in zwanzig Megabyte. Sie - /// liegen auf jedem Mac und werden nur gelesen, nicht mitgeliefert. + /// Sie liegen auf jedem Mac und werden nur gelesen, nicht mitgeliefert. /// /// Die Reihenfolge geht von neu nach alt und endet bei „irgendein Foto": /// welche Bilder installiert sind, hängt an der macOS-Version. - private static func systemWallpaper() -> NSImage? { + static func systemWallpaper() -> NSImage? { let directory = "/System/Library/Desktop Pictures" let preferred = ["Tahoe", "Sequoia", "Sonoma", "Ventura", "Monterey", "Big Sur Mountains", "Catalina Coast"] - let thumbnails = directory + "/.thumbnails" - let available = (try? FileManager.default.contentsOfDirectory(atPath: thumbnails)) ?? [] - - for name in preferred { - // „Sonoma.heic" vor „Sonoma Dark.heic": das schlichte Motiv ist das - // bekannte, die Varianten sind Abwandlungen davon. - let matches = available - .filter { $0.hasPrefix(name) && $0.hasSuffix(".heic") } - .sorted { $0.count < $1.count } - if let match = matches.first, - let image = thumbnail(of: URL(fileURLWithPath: thumbnails + "/" + match)) { - return image + // Zuerst die Originale, dann die kleinen Bilder aus `.thumbnails`. + // + // Die Originale sind zwanzig Megabyte groß, werden aber nie ganz + // gelesen — `CGImageSource` dekodiert gleich in der gebrauchten Größe. + // Die Vorschaubilder messen 214 Punkte in der Breite und wären auf + // einem Retina-Schirm sichtbar unscharf; sie sind die Reserve für die + // Motive, die macOS erst bei Bedarf nachlädt. + for folder in [directory, directory + "/.thumbnails"] { + let available = (try? FileManager.default.contentsOfDirectory(atPath: folder)) ?? [] + for name in preferred { + // „Sonoma.heic" vor „Sonoma Dark.heic": das schlichte Motiv ist + // das bekannte, die Varianten sind Abwandlungen davon. + let matches = available + .filter { $0.hasPrefix(name) && $0.hasSuffix(".heic") } + .sorted { $0.count < $1.count } + if let match = matches.first, + let image = thumbnail(of: URL(fileURLWithPath: folder + "/" + match)) { + return image + } } } // Nichts Bekanntes dabei: dann das erste Bild, das sich lesen lässt. - for name in available.sorted() where name.hasSuffix(".heic") { - if let image = thumbnail(of: URL(fileURLWithPath: thumbnails + "/" + name)) { - return image + for folder in [directory, directory + "/.thumbnails"] { + let available = ((try? FileManager.default.contentsOfDirectory(atPath: folder)) ?? []) + .filter { $0.hasSuffix(".heic") }.sorted() + for name in available { + if let image = thumbnail(of: URL(fileURLWithPath: folder + "/" + name)) { + return image + } } } return nil @@ -215,7 +240,7 @@ private struct TriggerPreview: View { /// paar hundert Bildpunkte breit, das Original hat sechstausend. Und ein /// Hintergrundbild kann mehrere Bilder enthalten — hell und dunkel im /// selben HEIC —, von denen hier nur das erste gebraucht wird. - private static func thumbnail(of url: URL) -> NSImage? { + static func thumbnail(of url: URL) -> NSImage? { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } let options: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, diff --git a/Packages/OnyxKit/Sources/ShelfProvider/FileDropView.swift b/Packages/OnyxKit/Sources/ShelfProvider/FileDropView.swift index 5b6511d..f25c8b2 100644 --- a/Packages/OnyxKit/Sources/ShelfProvider/FileDropView.swift +++ b/Packages/OnyxKit/Sources/ShelfProvider/FileDropView.swift @@ -50,7 +50,14 @@ public struct FileDropModifier: ViewModifier { /// Rettet die Datei aus dem flüchtigen Verzeichnis in einen eigenen /// Zwischenordner. - private static func stage(_ url: URL) -> URL? { + /// + /// `nonisolated`, und das ist keine Formalie: der Block oben läuft auf + /// irgendeinem Thread, diese Methode gehört aber zu einem `ViewModifier` + /// und ist damit an den Hauptthread gebunden — die Bindung kommt über die + /// Konformität, nicht über den Inhalt. Der Compiler mahnt das nur an, zur + /// Laufzeit hält die Prüfung das Programm an. Hier ist ohnehin nichts + /// dabei, was den Hauptthread bräuchte. + nonisolated private static func stage(_ url: URL) -> URL? { let directory = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("Onyx-Drop-\(UUID().uuidString)", isDirectory: true) do {