@idealyst/live-activity 1.2.114
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.
- package/android/build.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityModule.kt +299 -0
- package/android/src/main/java/io/idealyst/liveactivity/IdealystLiveActivityPackage.kt +16 -0
- package/android/src/main/java/io/idealyst/liveactivity/LiveUpdateNotification.kt +265 -0
- package/idealyst-live-activity.podspec +22 -0
- package/ios/IdealystLiveActivity-Bridging-Header.h +2 -0
- package/ios/IdealystLiveActivity.mm +55 -0
- package/ios/IdealystLiveActivity.swift +571 -0
- package/ios/Templates/ActivityAttributes.swift +66 -0
- package/ios/Templates/DeliveryActivityView.swift +143 -0
- package/ios/Templates/IdealystActivityBundle.swift +18 -0
- package/ios/Templates/MediaActivityView.swift +124 -0
- package/ios/Templates/ProgressActivityView.swift +164 -0
- package/ios/Templates/TimerActivityView.swift +110 -0
- package/package.json +80 -0
- package/src/NativeLiveActivitySpec.ts +49 -0
- package/src/activity/activity.native.ts +198 -0
- package/src/activity/activity.web.ts +78 -0
- package/src/activity/useLiveActivity.ts +267 -0
- package/src/constants.ts +39 -0
- package/src/errors.ts +57 -0
- package/src/index.native.ts +59 -0
- package/src/index.ts +61 -0
- package/src/index.web.ts +2 -0
- package/src/templates/presets.ts +91 -0
- package/src/templates/types.ts +14 -0
- package/src/types.ts +343 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import ActivityKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 16.2, *)
|
|
6
|
+
struct DeliveryActivityView: Widget {
|
|
7
|
+
var body: some WidgetConfiguration {
|
|
8
|
+
ActivityConfiguration(for: DeliveryAttributes.self) { context in
|
|
9
|
+
// Lock screen presentation
|
|
10
|
+
DeliveryLockScreenView(context: context)
|
|
11
|
+
.padding()
|
|
12
|
+
} dynamicIsland: { context in
|
|
13
|
+
DynamicIsland {
|
|
14
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
15
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
16
|
+
Text(context.attributes.startLabel)
|
|
17
|
+
.font(.caption2)
|
|
18
|
+
.foregroundStyle(.secondary)
|
|
19
|
+
if let driver = context.state.driverName {
|
|
20
|
+
Text(driver)
|
|
21
|
+
.font(.caption)
|
|
22
|
+
.bold()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
28
|
+
VStack(alignment: .trailing, spacing: 2) {
|
|
29
|
+
Text(context.attributes.endLabel)
|
|
30
|
+
.font(.caption2)
|
|
31
|
+
.foregroundStyle(.secondary)
|
|
32
|
+
if let etaMs = context.state.eta {
|
|
33
|
+
Text(Date(timeIntervalSince1970: etaMs / 1000), style: .relative)
|
|
34
|
+
.font(.caption)
|
|
35
|
+
.bold()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
DynamicIslandExpandedRegion(.center) {
|
|
41
|
+
Text(context.state.status)
|
|
42
|
+
.font(.headline)
|
|
43
|
+
.lineLimit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
47
|
+
ProgressView(value: context.state.progress)
|
|
48
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
49
|
+
.padding(.top, 4)
|
|
50
|
+
}
|
|
51
|
+
} compactLeading: {
|
|
52
|
+
Image(systemName: context.attributes.icon ?? "car.fill")
|
|
53
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
54
|
+
} compactTrailing: {
|
|
55
|
+
if let etaMs = context.state.eta {
|
|
56
|
+
Text(Date(timeIntervalSince1970: etaMs / 1000), style: .relative)
|
|
57
|
+
.font(.caption)
|
|
58
|
+
.monospacedDigit()
|
|
59
|
+
} else {
|
|
60
|
+
Text("\(Int(context.state.progress * 100))%")
|
|
61
|
+
.font(.caption)
|
|
62
|
+
.monospacedDigit()
|
|
63
|
+
}
|
|
64
|
+
} minimal: {
|
|
65
|
+
Image(systemName: context.attributes.icon ?? "car.fill")
|
|
66
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@available(iOS 16.2, *)
|
|
73
|
+
private struct DeliveryLockScreenView: View {
|
|
74
|
+
let context: ActivityViewContext<DeliveryAttributes>
|
|
75
|
+
|
|
76
|
+
var body: some View {
|
|
77
|
+
VStack(spacing: 12) {
|
|
78
|
+
HStack {
|
|
79
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
80
|
+
Text(context.state.status)
|
|
81
|
+
.font(.headline)
|
|
82
|
+
if let subtitle = context.state.subtitle {
|
|
83
|
+
Text(subtitle)
|
|
84
|
+
.font(.subheadline)
|
|
85
|
+
.foregroundStyle(.secondary)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
Spacer()
|
|
89
|
+
if let etaMs = context.state.eta {
|
|
90
|
+
VStack(alignment: .trailing, spacing: 2) {
|
|
91
|
+
Text("ETA")
|
|
92
|
+
.font(.caption2)
|
|
93
|
+
.foregroundStyle(.secondary)
|
|
94
|
+
Text(Date(timeIntervalSince1970: etaMs / 1000), style: .relative)
|
|
95
|
+
.font(.subheadline)
|
|
96
|
+
.bold()
|
|
97
|
+
.monospacedDigit()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
VStack(spacing: 4) {
|
|
103
|
+
ProgressView(value: context.state.progress)
|
|
104
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
105
|
+
|
|
106
|
+
HStack {
|
|
107
|
+
Text(context.attributes.startLabel)
|
|
108
|
+
.font(.caption2)
|
|
109
|
+
.foregroundStyle(.secondary)
|
|
110
|
+
Spacer()
|
|
111
|
+
Text(context.attributes.endLabel)
|
|
112
|
+
.font(.caption2)
|
|
113
|
+
.foregroundStyle(.secondary)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
.padding()
|
|
118
|
+
.background(.ultraThinMaterial)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Helpers
|
|
123
|
+
|
|
124
|
+
private func accentColorFrom(_ hex: String?) -> Color {
|
|
125
|
+
guard let hex = hex else { return .blue }
|
|
126
|
+
return Color(hex: hex) ?? .blue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
extension Color {
|
|
130
|
+
init?(hex: String) {
|
|
131
|
+
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
132
|
+
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
133
|
+
|
|
134
|
+
var rgb: UInt64 = 0
|
|
135
|
+
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
|
|
136
|
+
|
|
137
|
+
let r = Double((rgb & 0xFF0000) >> 16) / 255.0
|
|
138
|
+
let g = Double((rgb & 0x00FF00) >> 8) / 255.0
|
|
139
|
+
let b = Double(rgb & 0x0000FF) / 255.0
|
|
140
|
+
|
|
141
|
+
self.init(red: r, green: g, blue: b)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
/// Widget bundle that registers all Idealyst pre-built Live Activity templates.
|
|
5
|
+
/// This is used as the @main entry point for the Widget Extension target.
|
|
6
|
+
///
|
|
7
|
+
/// When using the CLI generator to scaffold a custom Live Activity,
|
|
8
|
+
/// add your custom widget to this bundle.
|
|
9
|
+
@available(iOS 16.2, *)
|
|
10
|
+
@main
|
|
11
|
+
struct IdealystActivityBundle: WidgetBundle {
|
|
12
|
+
var body: some Widget {
|
|
13
|
+
DeliveryActivityView()
|
|
14
|
+
TimerActivityView()
|
|
15
|
+
MediaActivityView()
|
|
16
|
+
ProgressActivityView()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import ActivityKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 16.2, *)
|
|
6
|
+
struct MediaActivityView: Widget {
|
|
7
|
+
var body: some WidgetConfiguration {
|
|
8
|
+
ActivityConfiguration(for: MediaAttributes.self) { context in
|
|
9
|
+
// Lock screen presentation
|
|
10
|
+
MediaLockScreenView(context: context)
|
|
11
|
+
.padding()
|
|
12
|
+
} dynamicIsland: { context in
|
|
13
|
+
DynamicIsland {
|
|
14
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
15
|
+
if let artworkUri = context.attributes.artworkUri {
|
|
16
|
+
AsyncImage(url: URL(string: artworkUri)) { image in
|
|
17
|
+
image.resizable().aspectRatio(contentMode: .fill)
|
|
18
|
+
} placeholder: {
|
|
19
|
+
Image(systemName: "music.note")
|
|
20
|
+
}
|
|
21
|
+
.frame(width: 48, height: 48)
|
|
22
|
+
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
23
|
+
} else {
|
|
24
|
+
Image(systemName: "music.note")
|
|
25
|
+
.font(.title2)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
30
|
+
Image(systemName: context.state.isPlaying ? "pause.fill" : "play.fill")
|
|
31
|
+
.font(.title2)
|
|
32
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DynamicIslandExpandedRegion(.center) {
|
|
36
|
+
VStack(spacing: 2) {
|
|
37
|
+
Text(context.state.trackTitle)
|
|
38
|
+
.font(.headline)
|
|
39
|
+
.lineLimit(1)
|
|
40
|
+
if let artist = context.state.artist {
|
|
41
|
+
Text(artist)
|
|
42
|
+
.font(.caption)
|
|
43
|
+
.foregroundStyle(.secondary)
|
|
44
|
+
.lineLimit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
50
|
+
if let progress = context.state.progress {
|
|
51
|
+
ProgressView(value: progress)
|
|
52
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
53
|
+
.padding(.top, 4)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} compactLeading: {
|
|
57
|
+
Image(systemName: context.state.isPlaying ? "music.note" : "pause.fill")
|
|
58
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
59
|
+
} compactTrailing: {
|
|
60
|
+
Text(context.state.trackTitle)
|
|
61
|
+
.font(.caption)
|
|
62
|
+
.lineLimit(1)
|
|
63
|
+
} minimal: {
|
|
64
|
+
Image(systemName: context.state.isPlaying ? "music.note" : "pause.fill")
|
|
65
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@available(iOS 16.2, *)
|
|
72
|
+
private struct MediaLockScreenView: View {
|
|
73
|
+
let context: ActivityViewContext<MediaAttributes>
|
|
74
|
+
|
|
75
|
+
var body: some View {
|
|
76
|
+
HStack(spacing: 12) {
|
|
77
|
+
if let artworkUri = context.attributes.artworkUri {
|
|
78
|
+
AsyncImage(url: URL(string: artworkUri)) { image in
|
|
79
|
+
image.resizable().aspectRatio(contentMode: .fill)
|
|
80
|
+
} placeholder: {
|
|
81
|
+
RoundedRectangle(cornerRadius: 8)
|
|
82
|
+
.fill(.quaternary)
|
|
83
|
+
.overlay(
|
|
84
|
+
Image(systemName: "music.note")
|
|
85
|
+
.foregroundStyle(.secondary)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
.frame(width: 56, height: 56)
|
|
89
|
+
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
93
|
+
Text(context.state.trackTitle)
|
|
94
|
+
.font(.headline)
|
|
95
|
+
.lineLimit(1)
|
|
96
|
+
|
|
97
|
+
if let artist = context.state.artist {
|
|
98
|
+
Text(artist)
|
|
99
|
+
.font(.subheadline)
|
|
100
|
+
.foregroundStyle(.secondary)
|
|
101
|
+
.lineLimit(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if let progress = context.state.progress {
|
|
105
|
+
ProgressView(value: progress)
|
|
106
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Spacer()
|
|
111
|
+
|
|
112
|
+
Image(systemName: context.state.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
|
113
|
+
.font(.largeTitle)
|
|
114
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
115
|
+
}
|
|
116
|
+
.padding()
|
|
117
|
+
.background(.ultraThinMaterial)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func accentColorFrom(_ hex: String?) -> Color {
|
|
122
|
+
guard let hex = hex else { return .blue }
|
|
123
|
+
return Color(hex: hex) ?? .blue
|
|
124
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import ActivityKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 16.2, *)
|
|
6
|
+
struct ProgressActivityView: Widget {
|
|
7
|
+
var body: some WidgetConfiguration {
|
|
8
|
+
ActivityConfiguration(for: ProgressAttributes.self) { context in
|
|
9
|
+
// Lock screen presentation
|
|
10
|
+
ProgressLockScreenView(context: context)
|
|
11
|
+
.padding()
|
|
12
|
+
} dynamicIsland: { context in
|
|
13
|
+
DynamicIsland {
|
|
14
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
15
|
+
Image(systemName: context.attributes.icon ?? "arrow.down.circle")
|
|
16
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
17
|
+
.font(.title2)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
21
|
+
if context.state.indeterminate == true {
|
|
22
|
+
ProgressView()
|
|
23
|
+
.progressViewStyle(.circular)
|
|
24
|
+
} else {
|
|
25
|
+
Text("\(Int(context.state.progress * 100))%")
|
|
26
|
+
.font(.title3)
|
|
27
|
+
.bold()
|
|
28
|
+
.monospacedDigit()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DynamicIslandExpandedRegion(.center) {
|
|
33
|
+
Text(context.state.status)
|
|
34
|
+
.font(.headline)
|
|
35
|
+
.lineLimit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
39
|
+
VStack(spacing: 4) {
|
|
40
|
+
if context.state.indeterminate == true {
|
|
41
|
+
ProgressView()
|
|
42
|
+
.progressViewStyle(.linear)
|
|
43
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
44
|
+
} else {
|
|
45
|
+
ProgressView(value: context.state.progress)
|
|
46
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if let subtitle = context.state.subtitle {
|
|
50
|
+
Text(subtitle)
|
|
51
|
+
.font(.caption2)
|
|
52
|
+
.foregroundStyle(.secondary)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
.padding(.top, 4)
|
|
56
|
+
}
|
|
57
|
+
} compactLeading: {
|
|
58
|
+
ProgressRing(
|
|
59
|
+
progress: context.state.progress,
|
|
60
|
+
color: accentColorFrom(context.attributes.accentColor),
|
|
61
|
+
isIndeterminate: context.state.indeterminate == true
|
|
62
|
+
)
|
|
63
|
+
.frame(width: 20, height: 20)
|
|
64
|
+
} compactTrailing: {
|
|
65
|
+
if context.state.indeterminate == true {
|
|
66
|
+
ProgressView()
|
|
67
|
+
.progressViewStyle(.circular)
|
|
68
|
+
.scaleEffect(0.5)
|
|
69
|
+
} else {
|
|
70
|
+
Text("\(Int(context.state.progress * 100))%")
|
|
71
|
+
.font(.caption)
|
|
72
|
+
.monospacedDigit()
|
|
73
|
+
}
|
|
74
|
+
} minimal: {
|
|
75
|
+
ProgressRing(
|
|
76
|
+
progress: context.state.progress,
|
|
77
|
+
color: accentColorFrom(context.attributes.accentColor),
|
|
78
|
+
isIndeterminate: context.state.indeterminate == true
|
|
79
|
+
)
|
|
80
|
+
.frame(width: 20, height: 20)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@available(iOS 16.2, *)
|
|
87
|
+
private struct ProgressLockScreenView: View {
|
|
88
|
+
let context: ActivityViewContext<ProgressAttributes>
|
|
89
|
+
|
|
90
|
+
var body: some View {
|
|
91
|
+
VStack(spacing: 12) {
|
|
92
|
+
HStack {
|
|
93
|
+
Image(systemName: context.attributes.icon ?? "arrow.down.circle")
|
|
94
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
95
|
+
.font(.title3)
|
|
96
|
+
|
|
97
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
98
|
+
Text(context.attributes.title)
|
|
99
|
+
.font(.headline)
|
|
100
|
+
Text(context.state.status)
|
|
101
|
+
.font(.subheadline)
|
|
102
|
+
.foregroundStyle(.secondary)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Spacer()
|
|
106
|
+
|
|
107
|
+
if context.state.indeterminate != true {
|
|
108
|
+
Text("\(Int(context.state.progress * 100))%")
|
|
109
|
+
.font(.title2)
|
|
110
|
+
.bold()
|
|
111
|
+
.monospacedDigit()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if context.state.indeterminate == true {
|
|
116
|
+
ProgressView()
|
|
117
|
+
.progressViewStyle(.linear)
|
|
118
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
119
|
+
} else {
|
|
120
|
+
ProgressView(value: context.state.progress)
|
|
121
|
+
.tint(accentColorFrom(context.attributes.accentColor))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if let subtitle = context.state.subtitle {
|
|
125
|
+
Text(subtitle)
|
|
126
|
+
.font(.caption)
|
|
127
|
+
.foregroundStyle(.secondary)
|
|
128
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
.padding()
|
|
132
|
+
.background(.ultraThinMaterial)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Progress Ring (for compact/minimal Dynamic Island)
|
|
137
|
+
|
|
138
|
+
private struct ProgressRing: View {
|
|
139
|
+
let progress: Double
|
|
140
|
+
let color: Color
|
|
141
|
+
let isIndeterminate: Bool
|
|
142
|
+
|
|
143
|
+
var body: some View {
|
|
144
|
+
ZStack {
|
|
145
|
+
Circle()
|
|
146
|
+
.stroke(color.opacity(0.2), lineWidth: 2)
|
|
147
|
+
if isIndeterminate {
|
|
148
|
+
Circle()
|
|
149
|
+
.trim(from: 0, to: 0.3)
|
|
150
|
+
.stroke(color, style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
|
151
|
+
} else {
|
|
152
|
+
Circle()
|
|
153
|
+
.trim(from: 0, to: progress)
|
|
154
|
+
.stroke(color, style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
|
155
|
+
.rotationEffect(.degrees(-90))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func accentColorFrom(_ hex: String?) -> Color {
|
|
162
|
+
guard let hex = hex else { return .blue }
|
|
163
|
+
return Color(hex: hex) ?? .blue
|
|
164
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import ActivityKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 16.2, *)
|
|
6
|
+
struct TimerActivityView: Widget {
|
|
7
|
+
var body: some WidgetConfiguration {
|
|
8
|
+
ActivityConfiguration(for: TimerAttributes.self) { context in
|
|
9
|
+
// Lock screen presentation
|
|
10
|
+
TimerLockScreenView(context: context)
|
|
11
|
+
.padding()
|
|
12
|
+
} dynamicIsland: { context in
|
|
13
|
+
DynamicIsland {
|
|
14
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
15
|
+
Image(systemName: context.attributes.icon ?? "timer")
|
|
16
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
17
|
+
.font(.title2)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
21
|
+
timerText(for: context)
|
|
22
|
+
.font(.title2)
|
|
23
|
+
.bold()
|
|
24
|
+
.monospacedDigit()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DynamicIslandExpandedRegion(.center) {
|
|
28
|
+
Text(context.attributes.title)
|
|
29
|
+
.font(.headline)
|
|
30
|
+
.lineLimit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
34
|
+
if let subtitle = context.state.subtitle {
|
|
35
|
+
Text(subtitle)
|
|
36
|
+
.font(.caption)
|
|
37
|
+
.foregroundStyle(.secondary)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} compactLeading: {
|
|
41
|
+
Image(systemName: context.attributes.icon ?? "timer")
|
|
42
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
43
|
+
} compactTrailing: {
|
|
44
|
+
timerText(for: context)
|
|
45
|
+
.font(.caption)
|
|
46
|
+
.monospacedDigit()
|
|
47
|
+
} minimal: {
|
|
48
|
+
Image(systemName: context.attributes.icon ?? "timer")
|
|
49
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ViewBuilder
|
|
55
|
+
private func timerText(for context: ActivityViewContext<TimerAttributes>) -> some View {
|
|
56
|
+
let endDate = Date(timeIntervalSince1970: context.state.endTime / 1000)
|
|
57
|
+
if context.state.isPaused == true {
|
|
58
|
+
Text(endDate, style: .relative)
|
|
59
|
+
} else {
|
|
60
|
+
Text(timerInterval: Date.now...endDate, countsDown: !(context.attributes.showElapsed ?? false))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@available(iOS 16.2, *)
|
|
66
|
+
private struct TimerLockScreenView: View {
|
|
67
|
+
let context: ActivityViewContext<TimerAttributes>
|
|
68
|
+
|
|
69
|
+
var body: some View {
|
|
70
|
+
VStack(spacing: 12) {
|
|
71
|
+
HStack {
|
|
72
|
+
Image(systemName: context.attributes.icon ?? "timer")
|
|
73
|
+
.foregroundColor(accentColorFrom(context.attributes.accentColor))
|
|
74
|
+
.font(.title2)
|
|
75
|
+
|
|
76
|
+
Text(context.attributes.title)
|
|
77
|
+
.font(.headline)
|
|
78
|
+
|
|
79
|
+
Spacer()
|
|
80
|
+
|
|
81
|
+
let endDate = Date(timeIntervalSince1970: context.state.endTime / 1000)
|
|
82
|
+
if context.state.isPaused == true {
|
|
83
|
+
Text(endDate, style: .relative)
|
|
84
|
+
.font(.title)
|
|
85
|
+
.bold()
|
|
86
|
+
.monospacedDigit()
|
|
87
|
+
} else {
|
|
88
|
+
Text(timerInterval: Date.now...endDate, countsDown: !(context.attributes.showElapsed ?? false))
|
|
89
|
+
.font(.title)
|
|
90
|
+
.bold()
|
|
91
|
+
.monospacedDigit()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if let subtitle = context.state.subtitle {
|
|
96
|
+
Text(subtitle)
|
|
97
|
+
.font(.subheadline)
|
|
98
|
+
.foregroundStyle(.secondary)
|
|
99
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
.padding()
|
|
103
|
+
.background(.ultraThinMaterial)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func accentColorFrom(_ hex: String?) -> Color {
|
|
108
|
+
guard let hex = hex else { return .blue }
|
|
109
|
+
return Color(hex: hex) ?? .blue
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/live-activity",
|
|
3
|
+
"version": "1.2.114",
|
|
4
|
+
"description": "Cross-platform Live Activities for React Native (iOS ActivityKit + Android Live Updates)",
|
|
5
|
+
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/live-activity#readme",
|
|
6
|
+
"readme": "README.md",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"react-native": "src/index.native.ts",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
14
|
+
"directory": "packages/live-activity"
|
|
15
|
+
},
|
|
16
|
+
"author": "Idealyst <contact@idealyst.io>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"react-native": "./src/index.native.ts",
|
|
24
|
+
"browser": {
|
|
25
|
+
"types": "./src/index.web.ts",
|
|
26
|
+
"import": "./src/index.web.ts",
|
|
27
|
+
"require": "./src/index.web.ts"
|
|
28
|
+
},
|
|
29
|
+
"default": {
|
|
30
|
+
"types": "./src/index.ts",
|
|
31
|
+
"import": "./src/index.ts",
|
|
32
|
+
"require": "./src/index.ts"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
38
|
+
"publish:npm": "npm publish"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0",
|
|
42
|
+
"react-native": ">=0.76.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"react-native": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^19.1.0",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"codegenConfig": {
|
|
54
|
+
"name": "IdealystLiveActivitySpec",
|
|
55
|
+
"type": "modules",
|
|
56
|
+
"jsSrcsDir": "src",
|
|
57
|
+
"android": {
|
|
58
|
+
"javaPackageName": "io.idealyst.liveactivity"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"src",
|
|
63
|
+
"ios",
|
|
64
|
+
"android",
|
|
65
|
+
"*.podspec",
|
|
66
|
+
"README.md"
|
|
67
|
+
],
|
|
68
|
+
"keywords": [
|
|
69
|
+
"react",
|
|
70
|
+
"react-native",
|
|
71
|
+
"live-activity",
|
|
72
|
+
"activitykit",
|
|
73
|
+
"dynamic-island",
|
|
74
|
+
"live-updates",
|
|
75
|
+
"notifications",
|
|
76
|
+
"ios",
|
|
77
|
+
"android",
|
|
78
|
+
"cross-platform"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
// Availability
|
|
6
|
+
isSupported(): boolean;
|
|
7
|
+
isEnabled(): Promise<boolean>;
|
|
8
|
+
|
|
9
|
+
// Lifecycle
|
|
10
|
+
startActivity(
|
|
11
|
+
templateType: string,
|
|
12
|
+
attributesJson: string,
|
|
13
|
+
contentStateJson: string,
|
|
14
|
+
optionsJson: string,
|
|
15
|
+
): Promise<string>;
|
|
16
|
+
|
|
17
|
+
updateActivity(
|
|
18
|
+
activityId: string,
|
|
19
|
+
contentStateJson: string,
|
|
20
|
+
alertConfigJson: string | null,
|
|
21
|
+
): Promise<void>;
|
|
22
|
+
|
|
23
|
+
endActivity(
|
|
24
|
+
activityId: string,
|
|
25
|
+
finalContentStateJson: string | null,
|
|
26
|
+
dismissalPolicy: string,
|
|
27
|
+
dismissAfter: number,
|
|
28
|
+
): Promise<void>;
|
|
29
|
+
|
|
30
|
+
endAllActivities(
|
|
31
|
+
dismissalPolicy: string,
|
|
32
|
+
dismissAfter: number,
|
|
33
|
+
): Promise<void>;
|
|
34
|
+
|
|
35
|
+
// Queries
|
|
36
|
+
getActivity(activityId: string): Promise<string | null>;
|
|
37
|
+
listActivities(): Promise<string>;
|
|
38
|
+
|
|
39
|
+
// Push tokens
|
|
40
|
+
getPushToken(activityId: string): Promise<string | null>;
|
|
41
|
+
|
|
42
|
+
// Events
|
|
43
|
+
addListener(eventName: string): void;
|
|
44
|
+
removeListeners(count: number): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default TurboModuleRegistry.getEnforcing<Spec>(
|
|
48
|
+
'IdealystLiveActivity',
|
|
49
|
+
);
|