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}