summaryrefslogtreecommitdiff
path: root/DomainDigWidget
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDigWidget')
-rw-r--r--DomainDigWidget/DomainDigPortfolioWidget.swift189
-rw-r--r--DomainDigWidget/DomainDigWidget.entitlements10
-rw-r--r--DomainDigWidget/DomainDigWidgetBundle.swift9
-rw-r--r--DomainDigWidget/Info.plist29
4 files changed, 237 insertions, 0 deletions
diff --git a/DomainDigWidget/DomainDigPortfolioWidget.swift b/DomainDigWidget/DomainDigPortfolioWidget.swift
new file mode 100644
index 0000000..f26964c
--- /dev/null
+++ b/DomainDigWidget/DomainDigPortfolioWidget.swift
@@ -0,0 +1,189 @@
+import SwiftUI
+import WidgetKit
+
+struct DomainDigEntry: TimelineEntry {
+ let date: Date
+ let data: DomainDigWidgetData
+}
+
+struct DomainDigProvider: TimelineProvider {
+ func placeholder(in context: Context) -> DomainDigEntry {
+ DomainDigEntry(date: Date(), data: .placeholder)
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (DomainDigEntry) -> Void) {
+ let data = context.isPreview ? .placeholder : (DomainDigWidgetStore.read() ?? .placeholder)
+ completion(DomainDigEntry(date: Date(), data: data))
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) {
+ let data = DomainDigWidgetStore.read() ?? .empty
+ let entry = DomainDigEntry(date: Date(), data: data)
+ // The app reloads timelines on foreground and on watchlist changes; this
+ // periodic refresh is a backstop so cert countdowns stay roughly current.
+ let next = Calendar.current.date(byAdding: .hour, value: 6, to: Date())
+ ?? Date().addingTimeInterval(6 * 3600)
+ completion(Timeline(entries: [entry], policy: .after(next)))
+ }
+}
+
+struct DomainDigPortfolioWidget: Widget {
+ let kind = "DomainDigPortfolioWidget"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: DomainDigProvider()) { entry in
+ DomainDigWidgetView(data: entry.data)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ .configurationDisplayName("Domain Portfolio")
+ .description("Health and certificate status for your tracked domains.")
+ .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
+ }
+}
+
+struct DomainDigWidgetView: View {
+ @Environment(\.widgetFamily) private var family
+ let data: DomainDigWidgetData
+
+ var body: some View {
+ if data.totalDomains == 0 {
+ emptyState
+ } else {
+ switch family {
+ case .systemSmall:
+ smallView
+ default:
+ mediumOrLargeView
+ }
+ }
+ }
+
+ private var emptyState: some View {
+ VStack(spacing: 6) {
+ Image(systemName: "magnifyingglass")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+ Text("No tracked domains")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ }
+
+ // MARK: Small
+
+ private var smallView: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack(spacing: 4) {
+ Image(systemName: "shield.lefthalf.filled")
+ Text("DomainDig")
+ .fontWeight(.semibold)
+ Spacer()
+ }
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+
+ Text("\(data.totalDomains)")
+ .font(.system(size: 34, weight: .bold, design: .rounded))
+ Text("tracked")
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+
+ Spacer(minLength: 0)
+
+ HStack(spacing: 10) {
+ countPill(data.healthyCount, .green)
+ countPill(data.warningCount, .orange)
+ countPill(data.criticalCount, .red)
+ }
+ }
+ }
+
+ private func countPill(_ value: Int, _ color: Color) -> some View {
+ HStack(spacing: 3) {
+ Circle().fill(color).frame(width: 7, height: 7)
+ Text("\(value)").font(.caption).fontWeight(.medium)
+ }
+ }
+
+ // MARK: Medium / Large
+
+ private var mediumOrLargeView: some View {
+ VStack(alignment: .leading, spacing: 10) {
+ HStack {
+ Label("Domain Portfolio", systemImage: "shield.lefthalf.filled")
+ .font(.caption)
+ .fontWeight(.semibold)
+ .foregroundStyle(.secondary)
+ Spacer()
+ Text("\(data.totalDomains) tracked")
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+
+ HStack(spacing: 12) {
+ summaryStat(data.healthyCount, "Healthy", .green)
+ summaryStat(data.warningCount, "Warning", .orange)
+ summaryStat(data.criticalCount, "Critical", .red)
+ summaryStat(data.expiringSoonCount, "Expiring", .yellow)
+ }
+
+ Divider()
+
+ VStack(spacing: 6) {
+ ForEach(data.domains.prefix(family == .systemLarge ? 6 : 3)) { domain in
+ Link(destination: DomainDigDeepLink.url(for: .detail(domain.domain))) {
+ domainRow(domain)
+ }
+ }
+ }
+ Spacer(minLength: 0)
+ }
+ }
+
+ private func summaryStat(_ value: Int, _ label: String, _ color: Color) -> some View {
+ VStack(alignment: .leading, spacing: 1) {
+ Text("\(value)")
+ .font(.headline)
+ .foregroundStyle(color)
+ Text(label)
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func domainRow(_ domain: DomainDigWidgetDomain) -> some View {
+ HStack(spacing: 6) {
+ Circle()
+ .fill(color(for: domain.status))
+ .frame(width: 8, height: 8)
+ if domain.isPinned {
+ Image(systemName: "pin.fill")
+ .font(.system(size: 8))
+ .foregroundStyle(.secondary)
+ }
+ Text(domain.domain)
+ .font(.caption)
+ .lineLimit(1)
+ Spacer(minLength: 4)
+ Text(certLabel(for: domain))
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ private func certLabel(for domain: DomainDigWidgetDomain) -> String {
+ guard let days = domain.certDaysRemaining else { return "—" }
+ if days < 0 { return "expired" }
+ return "\(days)d"
+ }
+
+ private func color(for status: DomainDigWidgetStatus) -> Color {
+ switch status {
+ case .healthy: return .green
+ case .warning: return .orange
+ case .critical: return .red
+ }
+ }
+}
diff --git a/DomainDigWidget/DomainDigWidget.entitlements b/DomainDigWidget/DomainDigWidget.entitlements
new file mode 100644
index 0000000..eaa20ba
--- /dev/null
+++ b/DomainDigWidget/DomainDigWidget.entitlements
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.application-groups</key>
+ <array>
+ <string>group.net.cleberg.DomainDig</string>
+ </array>
+</dict>
+</plist>
diff --git a/DomainDigWidget/DomainDigWidgetBundle.swift b/DomainDigWidget/DomainDigWidgetBundle.swift
new file mode 100644
index 0000000..483995c
--- /dev/null
+++ b/DomainDigWidget/DomainDigWidgetBundle.swift
@@ -0,0 +1,9 @@
+import SwiftUI
+import WidgetKit
+
+@main
+struct DomainDigWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ DomainDigPortfolioWidget()
+ }
+}
diff --git a/DomainDigWidget/Info.plist b/DomainDigWidget/Info.plist
new file mode 100644
index 0000000..d1b64db
--- /dev/null
+++ b/DomainDigWidget/Info.plist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleDisplayName</key>
+ <string>DomainDig</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(MARKETING_VERSION)</string>
+ <key>CFBundleVersion</key>
+ <string>$(CURRENT_PROJECT_VERSION)</string>
+ <key>NSExtension</key>
+ <dict>
+ <key>NSExtensionPointIdentifier</key>
+ <string>com.apple.widgetkit-extension</string>
+ </dict>
+</dict>
+</plist>