107 lines
4.0 KiB
Swift
107 lines
4.0 KiB
Swift
// The watch face complication and Smart Stack widget: what waits on you or what is working, from
|
|
// `GET /v1/summary`. For the rectangular family it also reads the waiting thread, so the Smart Stack
|
|
// can offer Allow and Deny. It reads the device token and the demo flag from the shared Keychain
|
|
// group, and the app reloads it whenever its own feed or a notification changes the counts.
|
|
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
@main
|
|
struct WristCodeWidgets: WidgetBundle {
|
|
var body: some Widget {
|
|
WaitingWidget()
|
|
}
|
|
}
|
|
|
|
struct WaitingWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: "WaitingCount", provider: WaitingProvider()) { entry in
|
|
WaitingView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Waiting on you")
|
|
.description("Threads waiting for your approval or answer, or what is working now.")
|
|
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner])
|
|
}
|
|
}
|
|
|
|
struct WaitingProvider: TimelineProvider {
|
|
/// How long until WidgetKit asks again. The app reloads sooner when it learns something.
|
|
static let refreshInterval: TimeInterval = 15 * 60
|
|
|
|
func placeholder(in context: Context) -> WaitingEntry {
|
|
.sample
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping @Sendable (WaitingEntry) -> Void) {
|
|
if context.isPreview {
|
|
completion(.sample)
|
|
return
|
|
}
|
|
let family = context.family
|
|
Task { completion(await Self.load(family)) }
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<WaitingEntry>) -> Void) {
|
|
let family = context.family
|
|
Task {
|
|
let entry = await Self.load(family)
|
|
completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(Self.refreshInterval))))
|
|
}
|
|
}
|
|
|
|
/// The summary, and for the rectangular family the waiting approval's Allow and Deny.
|
|
static func load(_ family: WidgetFamily) async -> WaitingEntry {
|
|
guard let api = SharedState.api else { return WaitingEntry(date: .now, content: .notPaired) }
|
|
do {
|
|
let summary = try await api.summary()
|
|
SharedState.lastSummary = summary
|
|
let approval = family == .accessoryRectangular ? await WaitingEntry.Approval.load(for: summary, api: api) : nil
|
|
return WaitingEntry(date: .now, content: .counts(summary, stale: false), approval: approval)
|
|
} catch GatewayError.unauthorized {
|
|
return WaitingEntry(date: .now, content: .notPaired)
|
|
} catch {
|
|
guard let last = SharedState.lastSummary else { return WaitingEntry(date: .now, content: .unavailable) }
|
|
return WaitingEntry(date: .now, content: .counts(last, stale: true))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func entry(_ needsYou: Int, _ working: Int, next: Bool = true) -> WaitingEntry {
|
|
let thread = Summary.Next(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini", request: "bun test --filter ocr")
|
|
let current = Summary.Current(
|
|
hostId: "mac-mini", threadId: "t-scaffold", title: "Watch app scaffold",
|
|
activity: Activity(kind: .command, text: "bun test", startedAt: .now.addingTimeInterval(-42)))
|
|
return WaitingEntry(date: .now, content: .counts(
|
|
Summary(needsYou: needsYou, working: working, next: needsYou > 0 && next ? thread : nil, current: working > 0 ? current : nil, hostsOffline: 1),
|
|
stale: false))
|
|
}
|
|
|
|
#Preview("Circular", as: .accessoryCircular) {
|
|
WaitingWidget()
|
|
} timeline: {
|
|
entry(2, 3)
|
|
entry(0, 3)
|
|
WaitingEntry(date: .now, content: .notPaired)
|
|
}
|
|
|
|
#Preview("Rectangular", as: .accessoryRectangular) {
|
|
WaitingWidget()
|
|
} timeline: {
|
|
entry(2, 3)
|
|
entry(0, 3)
|
|
WaitingEntry(date: .now, content: .notPaired)
|
|
}
|
|
|
|
#Preview("Inline", as: .accessoryInline) {
|
|
WaitingWidget()
|
|
} timeline: {
|
|
entry(2, 3)
|
|
}
|
|
|
|
#Preview("Corner", as: .accessoryCorner) {
|
|
WaitingWidget()
|
|
} timeline: {
|
|
entry(2, 3)
|
|
entry(0, 3)
|
|
}
|