// // LibraryStore.swift // Prisme // // Author: Simon-Pierre Boucher // import Foundation import Observation import SwiftData /// Owns excerpts and structured favourites. Separate store file from the /// history — each SwiftData container needs its own URL or they fight /// over the default store. @MainActor @Observable final class LibraryStore { private let container: ModelContainer /// Bumped on every mutation so observing views refresh their fetches. private var revision = 0 private var context: ModelContext { container.mainContext } init() { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let schema = Schema([Excerpt.self, StructuredFavorite.self]) do { let config = ModelConfiguration(url: base.appendingPathComponent("library.store")) container = try ModelContainer(for: schema, configurations: config) } catch { let memoryOnly = ModelConfiguration(isStoredInMemoryOnly: true) container = try! ModelContainer(for: schema, configurations: memoryOnly) } } // MARK: - Excerpts var excerpts: [Excerpt] { _ = revision let descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.savedAt, order: .reverse)] ) return (try? context.fetch(descriptor)) ?? [] } func saveExcerpt( _ selection: SelectionExcerpt, url: URL?, pageTitle: String, containerID: UUID ) { context.insert(Excerpt( text: selection.text, sourceURLString: url?.absoluteString ?? "", sourceTitle: pageTitle, sourceHost: url?.host() ?? "", sectionTitle: selection.section, domPath: selection.domPath, containerID: containerID )) persist() } func delete(_ excerpt: Excerpt) { context.delete(excerpt) persist() } // MARK: - Structured favourites var favorites: [StructuredFavorite] { _ = revision let descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.savedAt, order: .reverse)] ) return (try? context.fetch(descriptor)) ?? [] } func hasFavorite(for url: URL?) -> Bool { guard let urlString = url?.absoluteString else { return false } _ = revision var descriptor = FetchDescriptor( predicate: #Predicate { $0.urlString == urlString } ) descriptor.fetchLimit = 1 return ((try? context.fetch(descriptor))?.first) != nil } func saveFavorite( url: URL, title: String, gist: String, kindRaw: String?, outlineTitles: [String], isGenerated: Bool, containerID: UUID ) { let urlString = url.absoluteString var descriptor = FetchDescriptor( predicate: #Predicate { $0.urlString == urlString } ) descriptor.fetchLimit = 1 if let existing = (try? context.fetch(descriptor))?.first { existing.title = title existing.gist = gist existing.kindRaw = kindRaw existing.outlineTitles = outlineTitles existing.isGenerated = isGenerated existing.savedAt = Date() } else { context.insert(StructuredFavorite( urlString: urlString, host: url.host() ?? "", title: title, gist: gist, kindRaw: kindRaw, outlineTitles: outlineTitles, isGenerated: isGenerated, containerID: containerID )) } persist() } func delete(_ favorite: StructuredFavorite) { context.delete(favorite) persist() } private func persist() { try? context.save() revision += 1 } }