krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.15.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 switch family {
67 case .systemMedium:
68 mediumView(snapshot: snapshot)
69 default:
70 smallView(snapshot: snapshot)
71 }
72 } else {
73 fallbackView
74 }
75 }
76 .widgetURL(URL(string: "hutch://status"))
77 .containerBackground(for: .widget) {
78 Color(.systemBackground)
79 }
80 }
81
82 private func smallView(snapshot: SystemStatusWidgetSnapshot) -> some View {
83 VStack(alignment: .leading, spacing: 8) {
84 HStack(spacing: 6) {
85 Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
86 .font(.title3.weight(.semibold))
87 .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
88 Spacer()
89 }
90
91 Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Clear")
92 .font(.subheadline.weight(.semibold))
93 .lineLimit(2)
94
95 Spacer(minLength: 0)
96
97 if snapshot.hasDisruption {
98 let disrupted = snapshot.services.filter(\.requiresAttention)
99 Text(disrupted.map(\.name).joined(separator: ", "))
100 .font(.caption2)
101 .foregroundStyle(.secondary)
102 .lineLimit(2)
103 } else {
104 Text("\(snapshot.services.count) services monitored")
105 .font(.caption2)
106 .foregroundStyle(.secondary)
107 }
108 }
109 .padding()
110 }
111
112 private func mediumView(snapshot: SystemStatusWidgetSnapshot) -> some View {
113 VStack(alignment: .leading, spacing: 6) {
114 HStack(spacing: 8) {
115 Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
116 .font(.title3.weight(.semibold))
117 .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
118
119 Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Services Operational")
120 .font(.subheadline.weight(.semibold))
121 .lineLimit(1)
122
123 Spacer()
124 }
125
126 Divider()
127
128 let columns = Array(snapshot.services.prefix(8))
129 let half = (columns.count + 1) / 2
130 let leftColumn = Array(columns.prefix(half))
131 let rightColumn = Array(columns.suffix(from: half))
132
133 HStack(alignment: .top, spacing: 16) {
134 VStack(alignment: .leading, spacing: 4) {
135 ForEach(leftColumn) { service in
136 serviceRow(service)
137 }
138 }
139 VStack(alignment: .leading, spacing: 4) {
140 ForEach(rightColumn) { service in
141 serviceRow(service)
142 }
143 }
144 Spacer(minLength: 0)
145 }
146
147 Spacer(minLength: 0)
148 }
149 .padding(.horizontal, 16)
150 .padding(.top, 14)
151 .padding(.bottom, 12)
152 }
153
154 private func serviceRow(_ service: SystemStatusWidgetSnapshot.ServiceEntry) -> some View {
155 HStack(spacing: 6) {
156 Circle()
157 .fill(service.requiresAttention ? Color.orange : Color.green)
158 .frame(width: 6, height: 6)
159 Text(service.name)
160 .font(.caption2)
161 .foregroundStyle(service.requiresAttention ? .primary : .secondary)
162 .lineLimit(1)
163 }
164 }
165
166 private var fallbackView: some View {
167 VStack(alignment: .leading, spacing: 8) {
168 Spacer(minLength: 0)
169 Text("Open Hutch")
170 .font(.title3.weight(.semibold))
171 Text("Sign in to load system status.")
172 .font(.caption)
173 .foregroundStyle(.secondary)
174 }
175 .padding()
176 }
177}