Sources/OrgstarWidgets/ClockActivityWidget.swift
84 lines · 3316 bytes
1import ActivityKit
2import AppIntents
3import OrgApp
4import SwiftUI
5import WidgetKit
6
7/// The running clock on the Lock Screen and in the Dynamic Island: the heading and the time
8/// so far, which ticks by itself.
9struct ClockActivityWidget: Widget {
10 var body: some WidgetConfiguration {
11 ActivityConfiguration(for: ClockActivityAttributes.self) { context in
12 HStack(spacing: 12) {
13 Image(systemName: "stopwatch").font(.title2).foregroundStyle(.orange)
14 VStack(alignment: .leading, spacing: 2) {
15 Text("Clocked in").font(.caption).foregroundStyle(.secondary)
16 Text(context.state.heading).font(.headline).lineLimit(2)
17 }
18 Spacer(minLength: 0)
19 VStack(alignment: .trailing, spacing: 6) {
20 ElapsedText(state: context.state).font(.title3.weight(.semibold))
21 ClockOutButton()
22 }
23 }
24 .padding()
25 .widgetURL(AppLink.clock.url)
26 } dynamicIsland: { context in
27 DynamicIsland {
28 DynamicIslandExpandedRegion(.leading) {
29 Image(systemName: "stopwatch").font(.title2).foregroundStyle(.orange)
30 }
31 DynamicIslandExpandedRegion(.trailing) {
32 ElapsedText(state: context.state).font(.title3.weight(.semibold)).frame(width: 84, alignment: .trailing)
33 }
34 DynamicIslandExpandedRegion(.center) {
35 Text(context.state.heading).font(.headline).lineLimit(1)
36 }
37 DynamicIslandExpandedRegion(.bottom) {
38 ClockOutButton()
39 }
40 } compactLeading: {
41 Image(systemName: "stopwatch").foregroundStyle(.orange)
42 } compactTrailing: {
43 ElapsedText(state: context.state).frame(maxWidth: 56)
44 } minimal: {
45 Image(systemName: "stopwatch").foregroundStyle(.orange)
46 }
47 .widgetURL(AppLink.clock.url)
48 }
49 }
50}
51
52/// Time since the clock started, `1:05:09`.
53struct ElapsedText: View {
54 let state: ClockActivityAttributes.ContentState
55
56 var body: some View {
57 Text(timerInterval: state.startedAt...Date.distantFuture, countsDown: false)
58 .monospacedDigit()
59 .lineLimit(1)
60 .minimumScaleFactor(0.7)
61 .multilineTextAlignment(.trailing)
62 }
63}
64
65struct ClockOutButton: View {
66 var body: some View {
67 Button(intent: ClockOutIntent()) {
68 Label("Clock Out", systemImage: "stop.fill").font(.caption.weight(.semibold))
69 }
70 .tint(.orange)
71 }
72}
73
74#Preview("Lock Screen", as: .content, using: ClockActivityAttributes(path: "/notes/work.org", start: "2026-10-09 Fri 09:00")) {
75 ClockActivityWidget()
76} contentStates: {
77 ClockActivityAttributes.ContentState(heading: "Review the draft", startedAt: .now.addingTimeInterval(-3725))
78}
79
80#Preview("Expanded", as: .dynamicIsland(.expanded), using: ClockActivityAttributes(path: "/notes/work.org", start: "2026-10-09 Fri 09:00")) {
81 ClockActivityWidget()
82} contentStates: {
83 ClockActivityAttributes.ContentState(heading: "Review the draft", startedAt: .now.addingTimeInterval(-3725))
84}