krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgstarWidgets/ClockActivityWidget.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Sources/OrgstarWidgets/ClockActivityWidget.swift history · blame · raw

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}