ImageRenderer draws the real views with demo data, since the watch simulator is not drivable here. README covers the widget target and how to add it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
106 lines
3.5 KiB
Swift
106 lines
3.5 KiB
Swift
// The watch face complication: how many threads are waiting on you, from `GET /v1/summary`. It
|
|
// reads the device token from the shared Keychain group and the demo flag from the App 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.")
|
|
.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
|
|
}
|
|
Task { completion(await Self.load()) }
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<WaitingEntry>) -> Void) {
|
|
Task {
|
|
let entry = await Self.load()
|
|
completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(Self.refreshInterval))))
|
|
}
|
|
}
|
|
|
|
static func load() async -> WaitingEntry {
|
|
let api: any GatewayAPI
|
|
if SharedState.demo {
|
|
api = DemoGateway.shared
|
|
} else if let credentials = DeviceStore.load() {
|
|
api = GatewayClient(baseURL: credentials.gatewayURL, token: credentials.token)
|
|
} else {
|
|
return WaitingEntry(date: .now, content: .notPaired)
|
|
}
|
|
do {
|
|
let summary = try await api.summary()
|
|
SharedState.lastSummary = summary
|
|
return WaitingEntry(date: .now, content: .counts(summary, stale: false))
|
|
} 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")
|
|
return WaitingEntry(date: .now, content: .counts(
|
|
Summary(needsYou: needsYou, working: working, next: needsYou > 0 && next ? thread : 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)
|
|
}
|