diff options
Diffstat (limited to 'Shared')
| -rw-r--r-- | Shared/DomainDigDeepLink.swift | 71 | ||||
| -rw-r--r-- | Shared/DomainDigWidgetData.swift | 82 |
2 files changed, 153 insertions, 0 deletions
diff --git a/Shared/DomainDigDeepLink.swift b/Shared/DomainDigDeepLink.swift new file mode 100644 index 0000000..ba1d64b --- /dev/null +++ b/Shared/DomainDigDeepLink.swift @@ -0,0 +1,71 @@ +import Foundation + +/// Shared builder/parser for the `domaindig://` URL scheme, used by the intents +/// and app (to open/route) and by the widget (to deep-link into a domain). +/// +/// Lives in `Shared/` so it compiles into both the app and the widget target. +/// It relies only on Foundation and no actor isolation, so it is safe in the +/// widget extension (`APPLICATION_EXTENSION_API_ONLY`). +enum DomainDigDeepLink { + static let scheme = "domaindig" + + enum Action: Equatable { + case inspect(String) + case watch(String) + case detail(String) + case sweep + + var host: String { + switch self { + case .inspect: return "inspect" + case .watch: return "watch" + case .detail: return "domain" + case .sweep: return "sweep" + } + } + + /// The domain the action targets, if any. `.sweep` has no domain. + var domain: String? { + switch self { + case let .inspect(domain), let .watch(domain), let .detail(domain): + return domain + case .sweep: + return nil + } + } + } + + static func url(for action: Action) -> URL { + var components = URLComponents() + components.scheme = scheme + components.host = action.host + if let domain = action.domain { + components.queryItems = [URLQueryItem(name: "domain", value: domain)] + } + // The scheme and host are fixed and any domain is percent-encoded by + // URLComponents, so this is always a valid URL. + return components.url! + } + + static func action(from url: URL) -> Action? { + guard url.scheme == scheme else { return nil } + + if url.host() == "sweep" { + return .sweep + } + + let domain = URLComponents(url: url, resolvingAgainstBaseURL: false)? + .queryItems? + .first { $0.name == "domain" }? + .value? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !domain.isEmpty else { return nil } + + switch url.host() { + case "inspect": return .inspect(domain) + case "watch": return .watch(domain) + case "domain": return .detail(domain) + default: return nil + } + } +} diff --git a/Shared/DomainDigWidgetData.swift b/Shared/DomainDigWidgetData.swift new file mode 100644 index 0000000..5ceae48 --- /dev/null +++ b/Shared/DomainDigWidgetData.swift @@ -0,0 +1,82 @@ +import Foundation + +/// App Group + snapshot shared between the app (writer) and the widget (reader). +/// +/// Deliberately self-contained: it references no app-target types (TrackedDomain, +/// PortfolioSnapshot, DomainHealth, …) so it compiles unchanged into the widget +/// extension. The app maps its richer types into these DTOs. +enum DomainDigWidgetStore { + static let appGroupID = "group.net.cleberg.DomainDig" + private static let key = "widgetData" + + private static var defaults: UserDefaults? { + UserDefaults(suiteName: appGroupID) + } + + static func write(_ data: DomainDigWidgetData) { + guard let defaults, let encoded = try? JSONEncoder().encode(data) else { return } + defaults.set(encoded, forKey: key) + } + + static func read() -> DomainDigWidgetData? { + guard let defaults, + let encoded = defaults.data(forKey: key), + let data = try? JSONDecoder().decode(DomainDigWidgetData.self, from: encoded) + else { return nil } + return data + } +} + +struct DomainDigWidgetData: Codable, Sendable { + var generatedAt: Date + var totalDomains: Int + var healthyCount: Int + var warningCount: Int + var criticalCount: Int + var expiringSoonCount: Int + var unreachableCount: Int + var domains: [DomainDigWidgetDomain] + + static let placeholder = DomainDigWidgetData( + generatedAt: .distantPast, + totalDomains: 3, + healthyCount: 2, + warningCount: 1, + criticalCount: 0, + expiringSoonCount: 1, + unreachableCount: 0, + domains: [ + DomainDigWidgetDomain(domain: "example.com", isPinned: true, status: .healthy, certDaysRemaining: 240, lastChange: nil), + DomainDigWidgetDomain(domain: "cleberg.net", isPinned: false, status: .warning, certDaysRemaining: 21, lastChange: nil), + DomainDigWidgetDomain(domain: "example.org", isPinned: false, status: .healthy, certDaysRemaining: 88, lastChange: nil) + ] + ) + + /// Empty state used before the app has written any data. + static let empty = DomainDigWidgetData( + generatedAt: .distantPast, + totalDomains: 0, + healthyCount: 0, + warningCount: 0, + criticalCount: 0, + expiringSoonCount: 0, + unreachableCount: 0, + domains: [] + ) +} + +struct DomainDigWidgetDomain: Codable, Sendable, Identifiable { + var domain: String + var isPinned: Bool + var status: DomainDigWidgetStatus + var certDaysRemaining: Int? + var lastChange: Date? + + var id: String { domain } +} + +enum DomainDigWidgetStatus: String, Codable, Sendable { + case healthy + case warning + case critical +} |
