import Foundation import SwiftData /// SwiftData-Model für ein Quote aus dem `index-min.json`-Snapshot. /// Lebt in einem App-Group-`ModelContainer`, damit Widget + /// ShareExtension lesend zugreifen können. @Model final class CachedQuote { /// Stabiler Slug, dient als Primary-Key (eindeutig via Unique-Index). @Attribute(.unique) var slug: String var text: String var authorSlug: String? var authorName: String? var language: String? /// Komma-getrennte Slug-Liste (SwiftData mag arrays of String mäßig). var themesCSV: String var regionsCSV: String /// Wann zuletzt aus dem Snapshot importiert. var importedAt: Date init( slug: String, text: String, authorSlug: String?, authorName: String?, language: String?, themes: [String], regions: [String], importedAt: Date = Date() ) { self.slug = slug self.text = text self.authorSlug = authorSlug self.authorName = authorName self.language = language themesCSV = themes.joined(separator: ",") regionsCSV = regions.joined(separator: ",") self.importedAt = importedAt } var themes: [String] { themesCSV.isEmpty ? [] : themesCSV.split(separator: ",").map(String.init) } var regions: [String] { regionsCSV.isEmpty ? [] : regionsCSV.split(separator: ",").map(String.init) } } /// SwiftData-Marker für „wann zuletzt erfolgreich gesynct" + Total- /// Count. Einzeiliger Singleton — ein einziges Objekt im Container. @Model final class SnapshotMeta { @Attribute(.unique) var id: String var generatedAt: Date? var lastSyncedAt: Date? var totalCount: Int init( id: String = "default", generatedAt: Date? = nil, lastSyncedAt: Date? = nil, totalCount: Int = 0 ) { self.id = id self.generatedAt = generatedAt self.lastSyncedAt = lastSyncedAt self.totalCount = totalCount } } /// Schema-Helper für ModelContainer-Setup. App + Widget + ShareExt /// rufen `SnapshotContainer.make()` auf und teilen so denselben /// SwiftData-Store unter der App-Group. /// /// Der App-Group-Identifier ist hier hartkodiert, damit das File ohne /// AppConfig-Dependency auch von der Widget-Extension konsumierbar /// ist (Widget-Target kompiliert nur Source-File-Whitelist aus /// `project.yml`). enum SnapshotContainer { static let appGroup = "group.ev.mana.zitare" /// Default-URL für den Store: in der App-Group, damit alle drei /// Extensions ihn sehen. Fällt zurück auf den App-Container, wenn /// die App-Group (noch) nicht aktiviert ist — siehe Apple-Dev- /// Portal-Blocker in `PLAN.md`. static func defaultStoreURL(appGroup: String = appGroup) -> URL { let fm = FileManager.default if let groupURL = fm.containerURL(forSecurityApplicationGroupIdentifier: appGroup) { return groupURL.appendingPathComponent("snapshot.store") } // Fallback: App-eigener Documents-Container (Widget sieht das // dann nicht — wird in Release mit funktionierender App-Group // automatisch übersprungen). let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSTemporaryDirectory()) return docs.appendingPathComponent("snapshot.store") } /// Baut einen `ModelContainer` für die `CachedQuote` + `SnapshotMeta`- /// Models. `inMemory: true` für Unit-Tests. static func make(inMemory: Bool = false) throws -> ModelContainer { let schema = Schema([CachedQuote.self, SnapshotMeta.self]) let storeURL = defaultStoreURL() let config = if inMemory { ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) } else { ModelConfiguration("snapshot", schema: schema, url: storeURL) } return try ModelContainer(for: schema, configurations: [config]) } }