All checks were successful
CI / build-and-deploy (push) Successful in 41s
- Save serverConfig to App Group on every app launch so ShareExtension can always find credentials without requiring a disconnect/reconnect flow - Add MarksWidget target with RandomBookmarkWidget (random saved bookmark, reloads hourly) using file-based App Group storage to avoid CFPreferences issues - Extract BookmarkListRow as shared component with editorial typography tweaks (17pt semibold title, 13pt domain, relative date, 32×32 favicon, italic AI summary, 20pt podcast icon, 16pt horizontal insets) - Add MarksWidget and ShareExtension Xcode schemes for direct run/debug Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
3.4 KiB
Swift
118 lines
3.4 KiB
Swift
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")!
|
|
}
|
|
}
|