krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.10.0: HutchWidgetExtension/SystemStatusWidget.swift · raw

  1import SwiftUI
  2import WidgetKit
  3
  4struct SystemStatusEntry: TimelineEntry {
  5    let date: Date
  6    let snapshot: SystemStatusWidgetSnapshot?
  7}
  8
  9struct SystemStatusTimelineProvider: TimelineProvider {
 10    func placeholder(in _: Context) -> SystemStatusEntry {
 11        SystemStatusEntry(
 12            date: .now,
 13            snapshot: SystemStatusWidgetSnapshot(
 14                services: [
 15                    .init(id: "git", name: "git.sr.ht", status: "Operational", requiresAttention: false),
 16                    .init(id: "builds", name: "builds.sr.ht", status: "Operational", requiresAttention: false),
 17                    .init(id: "lists", name: "lists.sr.ht", status: "Operational", requiresAttention: false),
 18                    .init(id: "todo", name: "todo.sr.ht", status: "Operational", requiresAttention: false),
 19                    .init(id: "meta", name: "meta.sr.ht", status: "Operational", requiresAttention: false),
 20                    .init(id: "hg", name: "hg.sr.ht", status: "Operational", requiresAttention: false),
 21                ],
 22                hasDisruption: false,
 23                overallStatusText: "All monitored services operational",
 24                bannerSummary: "",
 25                updatedAt: .now
 26            )
 27        )
 28    }
 29
 30    func getSnapshot(in _: Context, completion: @escaping (SystemStatusEntry) -> Void) {
 31        completion(
 32            SystemStatusEntry(
 33                date: .now,
 34                snapshot: SystemStatusWidgetSnapshotStore.load()
 35            )
 36        )
 37    }
 38
 39    func getTimeline(in _: Context, completion: @escaping (Timeline<SystemStatusEntry>) -> Void) {
 40        let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: .now) ?? .now.addingTimeInterval(900)
 41        let entry = SystemStatusEntry(date: .now, snapshot: SystemStatusWidgetSnapshotStore.load())
 42        completion(Timeline(entries: [entry], policy: .after(refreshDate)))
 43    }
 44}
 45
 46struct SystemStatusWidget: Widget {
 47    static let kind = SystemStatusWidgetConfiguration.kind
 48
 49    var body: some WidgetConfiguration {
 50        StaticConfiguration(kind: Self.kind, provider: SystemStatusTimelineProvider()) { entry in
 51            SystemStatusWidgetView(entry: entry)
 52        }
 53        .configurationDisplayName("SourceHut Status")
 54        .description("Current status of SourceHut services at a glance.")
 55        .supportedFamilies([.systemSmall, .systemMedium])
 56    }
 57}
 58
 59private struct SystemStatusWidgetView: View {
 60    let entry: SystemStatusEntry
 61    @Environment(\.widgetFamily) private var family
 62
 63    var body: some View {
 64        Group {
 65            if let snapshot = entry.snapshot, !snapshot.services.isEmpty {
 66                if family == .systemMedium {
 67                    mediumView(snapshot: snapshot)
 68                } else {
 69                    smallView(snapshot: snapshot)
 70                }
 71            } else {
 72                fallbackView
 73            }
 74        }
 75        .widgetURL(HutchDeepLinkURL.status)
 76        .containerBackground(for: .widget) {
 77            Color(.systemBackground)
 78        }
 79    }
 80
 81    private func smallView(snapshot: SystemStatusWidgetSnapshot) -> some View {
 82        VStack(alignment: .leading, spacing: 8) {
 83            HStack(spacing: 6) {
 84                Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
 85                    .font(.title3.weight(.semibold))
 86                    .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
 87                Spacer()
 88            }
 89
 90            Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Clear")
 91                .font(.subheadline.weight(.semibold))
 92                .lineLimit(2)
 93
 94            Spacer(minLength: 0)
 95
 96            if snapshot.hasDisruption {
 97                let disrupted = snapshot.services.filter(\.requiresAttention)
 98                Text(disrupted.map(\.name).joined(separator: ", "))
 99                    .font(.caption2)
100                    .foregroundStyle(.secondary)
101                    .lineLimit(2)
102            } else {
103                Text("\(snapshot.services.count) services monitored")
104                    .font(.caption2)
105                    .foregroundStyle(.secondary)
106            }
107        }
108        .padding()
109    }
110
111    private func mediumView(snapshot: SystemStatusWidgetSnapshot) -> some View {
112        VStack(alignment: .leading, spacing: 6) {
113            HStack(spacing: 8) {
114                Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
115                    .font(.title3.weight(.semibold))
116                    .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
117
118                Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Services Operational")
119                    .font(.subheadline.weight(.semibold))
120                    .lineLimit(1)
121
122                Spacer()
123            }
124
125            Divider()
126
127            let columns = Array(snapshot.services.prefix(8))
128            let half = (columns.count + 1) / 2
129            let leftColumn = Array(columns.prefix(half))
130            let rightColumn = Array(columns.suffix(from: half))
131
132            HStack(alignment: .top, spacing: 16) {
133                VStack(alignment: .leading, spacing: 4) {
134                    ForEach(leftColumn) { service in
135                        serviceRow(service)
136                    }
137                }
138                VStack(alignment: .leading, spacing: 4) {
139                    ForEach(rightColumn) { service in
140                        serviceRow(service)
141                    }
142                }
143                Spacer(minLength: 0)
144            }
145
146            Spacer(minLength: 0)
147        }
148        .padding(.horizontal, 16)
149        .padding(.top, 14)
150        .padding(.bottom, 12)
151    }
152
153    private func serviceRow(_ service: SystemStatusWidgetSnapshot.ServiceEntry) -> some View {
154        HStack(spacing: 6) {
155            Circle()
156                .fill(service.requiresAttention ? Color.orange : Color.green)
157                .frame(width: 6, height: 6)
158            Text(service.name)
159                .font(.caption2)
160                .foregroundStyle(service.requiresAttention ? .primary : .secondary)
161                .lineLimit(1)
162        }
163    }
164
165    private var fallbackView: some View {
166        VStack(alignment: .leading, spacing: 8) {
167            Spacer(minLength: 0)
168            Text("Open Hutch")
169                .font(.title3.weight(.semibold))
170            Text("Sign in to load system status.")
171                .font(.caption)
172                .foregroundStyle(.secondary)
173        }
174        .padding()
175    }
176}