krz/hutch

an ios client for sourcehut

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

v3.0.4: 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}