expo-live-activity 0.4.2 → 0.5.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ extension LiveActivityAttributes.ContentState {
5
+ var hasSegmentedProgress: Bool {
6
+ currentStep != nil && (totalSteps ?? 0) > 0
7
+ }
8
+
9
+ func logSegmentedProgressWarningIfNeeded() {
10
+ #if DEBUG
11
+ if hasSegmentedProgress,
12
+ elapsedTimerStartDateInMilliseconds != nil
13
+ || timerEndDateInMilliseconds != nil
14
+ || progress != nil
15
+ {
16
+ DebugLog("⚠️[ExpoLiveActivity] Both segmented and regular progress provided; showing segmented")
17
+ }
18
+ #endif
19
+ }
20
+ }
21
+
22
+ extension LiveActivityAttributes {
23
+ var segmentActiveColor: Color {
24
+ progressSegmentActiveColor.map { Color(hex: $0) } ?? Color.blue
25
+ }
26
+
27
+ var segmentInactiveColor: Color {
28
+ progressSegmentInactiveColor.map { Color(hex: $0) } ?? Color.gray.opacity(0.3)
29
+ }
30
+
31
+ func resolvedPadding(defaultPadding: Int) -> EdgeInsets {
32
+ let top = CGFloat(
33
+ paddingDetails?.top
34
+ ?? paddingDetails?.vertical
35
+ ?? padding
36
+ ?? defaultPadding
37
+ )
38
+ let bottom = CGFloat(
39
+ paddingDetails?.bottom
40
+ ?? paddingDetails?.vertical
41
+ ?? padding
42
+ ?? defaultPadding
43
+ )
44
+ let leading = CGFloat(
45
+ paddingDetails?.left
46
+ ?? paddingDetails?.horizontal
47
+ ?? padding
48
+ ?? defaultPadding
49
+ )
50
+ let trailing = CGFloat(
51
+ paddingDetails?.right
52
+ ?? paddingDetails?.horizontal
53
+ ?? padding
54
+ ?? defaultPadding
55
+ )
56
+ return EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
57
+ }
58
+ }
@@ -0,0 +1,127 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ struct LiveActivityMediumView: View {
5
+ let contentState: LiveActivityAttributes.ContentState
6
+ let attributes: LiveActivityAttributes
7
+ @Binding var imageContainerSize: CGSize?
8
+ let alignedImage: (String, HorizontalAlignment, Bool) -> AnyView
9
+
10
+ private var hasImage: Bool {
11
+ contentState.imageName != nil
12
+ }
13
+
14
+ private var isLeftImage: Bool {
15
+ (attributes.imagePosition ?? "right").hasPrefix("left")
16
+ }
17
+
18
+ private var isStretch: Bool {
19
+ (attributes.imagePosition ?? "right").contains("Stretch")
20
+ }
21
+
22
+ private var effectiveStretch: Bool {
23
+ isStretch && hasImage
24
+ }
25
+
26
+ private var progressViewTint: Color? {
27
+ attributes.progressViewTint.map { Color(hex: $0) }
28
+ }
29
+
30
+ var body: some View {
31
+ let padding = attributes.resolvedPadding(defaultPadding: 24)
32
+
33
+ let _ = contentState.logSegmentedProgressWarningIfNeeded()
34
+
35
+ VStack(alignment: .leading) {
36
+ HStack(alignment: .center) {
37
+ if hasImage, isLeftImage {
38
+ if let imageName = contentState.imageName {
39
+ alignedImage(imageName, .leading, false)
40
+ }
41
+ }
42
+
43
+ VStack(alignment: .leading, spacing: 2) {
44
+ Text(contentState.title)
45
+ .font(.title2)
46
+ .fontWeight(.semibold)
47
+ .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
48
+
49
+ if let subtitle = contentState.subtitle {
50
+ Text(subtitle)
51
+ .font(.title3)
52
+ .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
53
+ }
54
+
55
+ if effectiveStretch {
56
+ if contentState.hasSegmentedProgress,
57
+ let currentStep = contentState.currentStep,
58
+ let totalSteps = contentState.totalSteps,
59
+ totalSteps > 0
60
+ {
61
+ SegmentedProgressView(
62
+ currentStep: currentStep,
63
+ totalSteps: totalSteps,
64
+ activeColor: attributes.segmentActiveColor,
65
+ inactiveColor: attributes.segmentInactiveColor
66
+ )
67
+ } else if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
68
+ ElapsedTimerText(
69
+ startTimeMilliseconds: startDate,
70
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
71
+ )
72
+ .font(.title3)
73
+ .fontWeight(.medium)
74
+ } else if let date = contentState.timerEndDateInMilliseconds {
75
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
76
+ .tint(progressViewTint)
77
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
78
+ } else if let progress = contentState.progress {
79
+ ProgressView(value: progress)
80
+ .tint(progressViewTint)
81
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
82
+ }
83
+ }
84
+ }.layoutPriority(1)
85
+
86
+ if hasImage, !isLeftImage {
87
+ if let imageName = contentState.imageName {
88
+ alignedImage(imageName, .trailing, false)
89
+ }
90
+ }
91
+ }
92
+
93
+ if !effectiveStretch {
94
+ if contentState.hasSegmentedProgress,
95
+ let currentStep = contentState.currentStep,
96
+ let totalSteps = contentState.totalSteps,
97
+ totalSteps > 0
98
+ {
99
+ SegmentedProgressView(
100
+ currentStep: currentStep,
101
+ totalSteps: totalSteps,
102
+ activeColor: attributes.segmentActiveColor,
103
+ inactiveColor: attributes.segmentInactiveColor
104
+ )
105
+ } else if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
106
+ ElapsedTimerText(
107
+ startTimeMilliseconds: startDate,
108
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
109
+ )
110
+ .font(.title2)
111
+ .fontWeight(.semibold)
112
+ .frame(maxWidth: .infinity, alignment: .leading)
113
+ .padding(.top, 4)
114
+ } else if let date = contentState.timerEndDateInMilliseconds {
115
+ ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date))
116
+ .tint(progressViewTint)
117
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
118
+ } else if let progress = contentState.progress {
119
+ ProgressView(value: progress)
120
+ .tint(progressViewTint)
121
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
122
+ }
123
+ }
124
+ }
125
+ .padding(padding)
126
+ }
127
+ }
@@ -0,0 +1,178 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ #if canImport(ActivityKit)
5
+ import ActivityKit
6
+
7
+ struct LiveActivitySmallView: View {
8
+ let contentState: LiveActivityAttributes.ContentState
9
+ let attributes: LiveActivityAttributes
10
+ @Binding var imageContainerSize: CGSize?
11
+ let alignedImage: (String, HorizontalAlignment, Bool) -> AnyView
12
+
13
+ // CarPlay Live Activity views don't have fixed dimensions (unlike Apple Watch),
14
+ // because the system may scale them to fit the vehicle display.
15
+ // These thresholds are derived from the CarPlay live activities test sizes listed in the documentation.
16
+ private let carPlayWidthThreshold: CGFloat = 200
17
+ private let carPlayTallHeightThreshold: CGFloat = 90
18
+
19
+ private var resolvedImageName: String? {
20
+ contentState.smallImageName ?? contentState.imageName
21
+ }
22
+
23
+ private var hasImage: Bool {
24
+ resolvedImageName != nil
25
+ }
26
+
27
+ private var isLeftImage: Bool {
28
+ (attributes.imagePosition ?? "right").hasPrefix("left")
29
+ }
30
+
31
+ private var progressViewTint: Color? {
32
+ attributes.progressViewTint.map { Color(hex: $0) }
33
+ }
34
+
35
+ private var isSubtitleDisplayed: Bool {
36
+ contentState.subtitle != nil
37
+ }
38
+
39
+ private var isTimerShownAsText: Bool {
40
+ attributes.timerType == .digital && contentState.timerEndDateInMilliseconds != nil
41
+ }
42
+
43
+ private var shouldShowProgressBar: Bool {
44
+ let hasProgress = contentState.progress != nil
45
+ let hasTimer = contentState.timerEndDateInMilliseconds != nil
46
+ return hasProgress || (hasTimer && !isSubtitleDisplayed && !isTimerShownAsText) || contentState.hasSegmentedProgress
47
+ }
48
+
49
+ var body: some View {
50
+ GeometryReader { proxy in
51
+ let w = proxy.size.width
52
+ let h = proxy.size.height
53
+
54
+ let carPlayView = w > carPlayWidthThreshold
55
+ let carPlayTallView = carPlayView && h > carPlayTallHeightThreshold
56
+
57
+ let padding = attributes.resolvedPadding(defaultPadding: carPlayView ? 14 : 8)
58
+
59
+ let fixedImageSize: CGFloat = carPlayView ? 28 : 23
60
+
61
+ let _ = contentState.logSegmentedProgressWarningIfNeeded()
62
+
63
+ VStack(alignment: .leading, spacing: 4) {
64
+ VStack(alignment: .leading, spacing: shouldShowProgressBar ? 0 : nil) {
65
+ HStack(alignment: .center, spacing: 8) {
66
+ if hasImage, isLeftImage, !isTimerShownAsText, let imageName = resolvedImageName {
67
+ if carPlayView, !carPlayTallView {
68
+ fixedSizeImage(name: imageName, size: fixedImageSize)
69
+ Spacer()
70
+ } else {
71
+ alignedImage(imageName, .leading, true)
72
+ }
73
+ }
74
+
75
+ VStack(alignment: .leading, spacing: 0) {
76
+ Text(contentState.title)
77
+ .font(carPlayView
78
+ ? (isSubtitleDisplayed || contentState.hasSegmentedProgress ? .body : .footnote)
79
+ : (isSubtitleDisplayed ? .footnote : .callout))
80
+ .fontWeight(.semibold)
81
+ .lineLimit(1)
82
+ .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor))
83
+
84
+ if let subtitle = contentState.subtitle, !(carPlayView && !carPlayTallView) {
85
+ Text(subtitle)
86
+ .font(carPlayView ? .body : .footnote)
87
+ .fontWeight(.semibold)
88
+ .lineLimit(1)
89
+ .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor))
90
+ }
91
+
92
+ if let startDate = contentState.elapsedTimerStartDateInMilliseconds {
93
+ ElapsedTimerText(
94
+ startTimeMilliseconds: startDate,
95
+ color: attributes.progressViewLabelColor.map { Color(hex: $0) }
96
+ )
97
+ .font(carPlayView
98
+ ? (isSubtitleDisplayed ? .footnote : .title2)
99
+ : (isSubtitleDisplayed ? .footnote : .callout))
100
+ .fontWeight(carPlayView && !isSubtitleDisplayed ? .semibold : .medium)
101
+ .padding(.top, isSubtitleDisplayed ? 3 : 0)
102
+ } else if let date = contentState.timerEndDateInMilliseconds, !isTimerShownAsText, !(carPlayView && isSubtitleDisplayed) {
103
+ smallTimerText(endDate: date, isSubtitleDisplayed: isSubtitleDisplayed, carPlayView: carPlayView, labelColor: attributes.progressViewLabelColor)
104
+ }
105
+ }
106
+ .layoutPriority(1)
107
+
108
+ if hasImage, !isLeftImage, !isTimerShownAsText, let imageName = resolvedImageName {
109
+ if carPlayView, !carPlayTallView {
110
+ Spacer()
111
+ fixedSizeImage(name: imageName, size: fixedImageSize)
112
+ } else {
113
+ alignedImage(imageName, .trailing, true)
114
+ }
115
+ }
116
+ }
117
+ if isTimerShownAsText, let date = contentState.timerEndDateInMilliseconds {
118
+ HStack {
119
+ if let imageName = resolvedImageName, hasImage, isLeftImage {
120
+ fixedSizeImage(name: imageName, size: fixedImageSize)
121
+ Spacer()
122
+ }
123
+ smallTimerText(endDate: date, isSubtitleDisplayed: false, carPlayView: carPlayView, labelColor: attributes.progressViewLabelColor).frame(maxWidth: .infinity, alignment: isLeftImage ? .trailing : .leading)
124
+ if let imageName = resolvedImageName, hasImage, !isLeftImage {
125
+ Spacer()
126
+ fixedSizeImage(name: imageName, size: fixedImageSize)
127
+ }
128
+ }
129
+ .frame(maxWidth: .infinity)
130
+ } else {
131
+ if contentState.hasSegmentedProgress,
132
+ let currentStep = contentState.currentStep,
133
+ let totalSteps = contentState.totalSteps,
134
+ totalSteps > 0
135
+ {
136
+ SegmentedProgressView(
137
+ currentStep: currentStep,
138
+ totalSteps: totalSteps,
139
+ activeColor: attributes.segmentActiveColor,
140
+ inactiveColor: attributes.segmentInactiveColor,
141
+ height: 6
142
+ ).padding(.bottom, 6)
143
+ } else if let progress = contentState.progress {
144
+ styledLinearProgressView(tint: progressViewTint, labelColor: attributes.progressViewLabelColor) {
145
+ ProgressView(value: progress)
146
+ }
147
+ } else if let date = contentState.timerEndDateInMilliseconds, !isSubtitleDisplayed || carPlayView {
148
+ HStack(spacing: 4) {
149
+ styledLinearProgressView(tint: progressViewTint, labelColor: attributes.progressViewLabelColor) {
150
+ ProgressView(
151
+ timerInterval: Date.toTimerInterval(miliseconds: date),
152
+ countsDown: false,
153
+ label: { EmptyView() },
154
+ currentValueLabel: { EmptyView() }
155
+ )
156
+ }
157
+ .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
158
+
159
+ if carPlayView, isSubtitleDisplayed {
160
+ Text(timerInterval: Date.toTimerInterval(miliseconds: date))
161
+ .font(.footnote)
162
+ .monospacedDigit()
163
+ .lineLimit(1)
164
+ .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor))
165
+ .multilineTextAlignment(.trailing)
166
+ .frame(maxWidth: 60, alignment: .trailing)
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ .padding(padding)
174
+ }
175
+ }
176
+ }
177
+
178
+ #endif