Watch: waiting-count complication and multi-select questions
A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows GET /v1/summary. The device token moves to the App Group's Keychain access group on launch so the extension can read it without re-pairing; the app reloads the timeline when its feed counts change and on notifications. Multi-select questions toggle options and send an array of values. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d4d67295c6
commit
0994fd7d5a
18 files changed
+800
-23
No files matched your search
@@ -0,0 +1,94 @@
|
||||
// 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 WaitingEntry: TimelineEntry {
|
||||
enum Content: Equatable {
|
||||
/// Counts from the gateway. `stale` when they are the last ones seen because the gateway
|
||||
/// could not be reached.
|
||||
case counts(Summary, stale: Bool)
|
||||
case notPaired
|
||||
/// The gateway could not be reached and there is nothing saved to show.
|
||||
case unavailable
|
||||
}
|
||||
|
||||
var date: Date
|
||||
var content: Content
|
||||
|
||||
/// Ranks the complication higher in the Smart Stack while something waits.
|
||||
var relevance: TimelineEntryRelevance? {
|
||||
guard case let .counts(summary, _) = content, summary.needsYou > 0 else { return nil }
|
||||
return TimelineEntryRelevance(score: Float(summary.needsYou))
|
||||
}
|
||||
|
||||
static let sample = WaitingEntry(date: .now, content: .counts(Summary(
|
||||
needsYou: 2, working: 3,
|
||||
next: .init(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini"),
|
||||
hostsOffline: 0), stale: false))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user