import Foundation import OSLog #if canImport(WidgetKit) import WidgetKit #endif private let log = Logger(subsystem: "com.magicive.marks", category: "WidgetDataStore") struct WidgetBookmark: Codable, Identifiable, Sendable { var id: String { url } let url: String let title: String let domain: String let faviconUrl: String? } struct WidgetPodcast: Codable, Identifiable, Sendable { var id: String { articleUrl } let articleUrl: String let title: String let createdAt: Date } enum WidgetDataStore { static let groupID = "group.com.magicive.marks" private static var containerURL: URL? { let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID) if url == nil { log.error("App Group container not found — group ID: \(groupID)") } return url } private static func fileURL(_ name: String) -> URL? { containerURL?.appendingPathComponent(name) } private static let encoder: JSONEncoder = { let e = JSONEncoder() e.dateEncodingStrategy = .iso8601 return e }() private static let decoder: JSONDecoder = { let d = JSONDecoder() d.dateDecodingStrategy = .iso8601 return d }() // MARK: - Bookmarks static func saveBookmarks(_ bookmarks: [WidgetBookmark]) { guard let url = fileURL("widget_bookmarks.json"), let data = try? encoder.encode(bookmarks) else { return } try? data.write(to: url, options: .atomic) reloadTimelines() } static func loadBookmarks() -> [WidgetBookmark] { guard let url = fileURL("widget_bookmarks.json") else { log.error("loadBookmarks: no container URL") return [] } guard let data = try? Data(contentsOf: url) else { log.warning("loadBookmarks: file not found at \(url.path)") return [] } guard let items = try? decoder.decode([WidgetBookmark].self, from: data) else { log.error("loadBookmarks: decode failed") return [] } log.info("loadBookmarks: loaded \(items.count) bookmarks") return items } // MARK: - Podcasts static func savePodcasts(_ podcasts: [WidgetPodcast]) { guard let url = fileURL("widget_podcasts.json"), let data = try? encoder.encode(podcasts) else { return } try? data.write(to: url, options: .atomic) reloadTimelines() } static func loadPodcasts() -> [WidgetPodcast] { guard let url = fileURL("widget_podcasts.json"), let data = try? Data(contentsOf: url), let items = try? decoder.decode([WidgetPodcast].self, from: data) else { return [] } return items } private static func reloadTimelines() { #if canImport(WidgetKit) WidgetCenter.shared.reloadAllTimelines() #endif } } // MARK: - Deep link URL helpers extension URL { static func marksBookmark(_ url: String) -> URL { var c = URLComponents() c.scheme = "marks" c.host = "bookmark" c.queryItems = [URLQueryItem(name: "url", value: url)] return c.url ?? URL(string: "marks://bookmark")! } static func marksPodcast(_ url: String) -> URL { var c = URLComponents() c.scheme = "marks" c.host = "podcast" c.queryItems = [URLQueryItem(name: "url", value: url)] return c.url ?? URL(string: "marks://podcast")! } }