import Foundation /// Liest und schreibt die Widget-Anordnung. /// /// Die Datei überlebt App-Updates und kann deshalb Widgets nennen, die es nicht /// mehr gibt. Solche Einträge fallen beim Laden weg. Was aber **nicht** passieren /// darf: dass ein Nutzer nach einem Update vor einem leeren Panel sitzt, ohne dass /// es dafür einen erkennbaren Grund gibt. Deshalb der Unterschied zwischen /// „absichtlich leer" und „nichts Brauchbares übrig". public struct LayoutStore: Sendable { private struct Document: Codable { var version: Int var placements: [WidgetPlacement] } private static let currentVersion = 1 public let url: URL public init(url: URL) { self.url = url } /// Der Normalfall: `~/Library/Application Support/Onyx/layout.json` public static func standard() -> LayoutStore { let base = FileManager.default .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("Onyx", isDirectory: true) return LayoutStore(url: base.appendingPathComponent("layout.json")) } public func save(_ placements: [WidgetPlacement]) throws { try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let document = Document(version: Self.currentVersion, placements: placements) try encoder.encode(document).write(to: url, options: .atomic) } public func load(knownWidgetIDs: Set) -> [WidgetPlacement] { guard let data = try? Data(contentsOf: url), let document = try? JSONDecoder().decode(Document.self, from: data) else { // Keine Datei oder unlesbar — beides führt zum Standardlayout. return Self.defaultLayout(knownWidgetIDs: knownWidgetIDs) } let surviving = document.placements.filter { knownWidgetIDs.contains($0.widgetID) } // Eine leere Datei ist eine Aussage: der Nutzer hat alle Widgets entfernt. // Eine Datei, aus der nach dem Filtern nichts übrig bleibt, ist dagegen // ein Zeichen dafür, dass sich die Kennungen geändert haben — hier wäre // ein leeres Panel eine stille Fehlfunktion. if surviving.isEmpty && !document.placements.isEmpty { return Self.defaultLayout(knownWidgetIDs: knownWidgetIDs) } return surviving } /// Womit Onyx beim ersten Start dasteht. Bewusst zurückhaltend: lieber /// wenige Widgets, die sofort etwas zeigen, als ein volles Panel, das /// erst konfiguriert werden will. public static func defaultLayout(knownWidgetIDs: Set) -> [WidgetPlacement] { let preferred: [(String, WidgetSize)] = [ ("calendar", .large), ("weather", .medium), ("cpu", .small), ("battery", .small), ("media", .wide), ] return preferred .filter { knownWidgetIDs.contains($0.0) } .map { WidgetPlacement(widgetID: $0.0, size: $0.1) } } }