sitepong 0.2.3 → 0.2.4
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/java/com/sitepong/deviceid/DeviceIdHelper.kt +45 -0
- package/android/src/main/java/com/sitepong/deviceid/DeviceIdModule.kt +45 -0
- package/android/src/main/java/com/sitepong/screenrecorder/ChunkUploader.kt +78 -0
- package/android/src/main/java/com/sitepong/screenrecorder/H264Encoder.kt +163 -0
- package/android/src/main/java/com/sitepong/screenrecorder/ScreenCapture.kt +103 -0
- package/android/src/main/java/com/sitepong/screenrecorder/ScreenRecorderModule.kt +80 -0
- package/android/src/main/java/com/sitepong/screenrecorder/SensitiveViewManager.kt +19 -0
- package/app.plugin.js +426 -0
- package/dist/cdn/sitepong.min.js +5 -5
- package/dist/cdn/sitepong.min.js.map +1 -1
- package/dist/entries/rn.d.ts +188 -16
- package/dist/entries/rn.js +188 -35
- package/dist/entries/rn.js.map +1 -1
- package/dist/entries/web.js +2 -1
- package/dist/entries/web.js.map +1 -1
- package/dist/entries/web.mjs +2 -1
- package/dist/entries/web.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2 -1
- package/dist/react/index.mjs.map +1 -1
- package/expo-module.config.json +16 -0
- package/ios/DeviceId/DeviceIdModule.swift +55 -0
- package/ios/DeviceId/KeychainHelper.swift +68 -0
- package/ios/LiveActivity/ConfigStore.swift +43 -0
- package/ios/LiveActivity/DSLNode.swift +188 -0
- package/ios/LiveActivity/SitePongActivityAttributes.swift +112 -0
- package/ios/LiveActivity/SitePongLiveActivityModule.swift +250 -0
- package/ios/LiveActivity/widget/ColorHex.swift +30 -0
- package/ios/LiveActivity/widget/DSLAnimations.swift +128 -0
- package/ios/LiveActivity/widget/DSLRenderer.swift +538 -0
- package/ios/LiveActivity/widget/SitePongLiveActivityWidget.swift +393 -0
- package/ios/LiveActivity/widget/SitePongWidgetBundle.swift +84 -0
- package/ios/ScreenRecorder/ChunkUploader.swift +52 -0
- package/ios/ScreenRecorder/H264Encoder.swift +229 -0
- package/ios/ScreenRecorder/ScreenCapture.swift +149 -0
- package/ios/ScreenRecorder/ScreenRecorderModule.swift +179 -0
- package/ios/ScreenRecorder/SensitiveViewManager.swift +63 -0
- package/ios/Sitepong.podspec +57 -0
- package/ios/WatchtowerCore/Screenshotter.swift +163 -0
- package/ios/WatchtowerCore/SwiftUIModifiers.swift +78 -0
- package/ios/WatchtowerCore/Swizzling.swift +59 -0
- package/ios/WatchtowerCore/TapEvent.swift +61 -0
- package/ios/WatchtowerCore/Transport.swift +136 -0
- package/ios/WatchtowerCore/UIViewExtensions.swift +31 -0
- package/ios/WatchtowerCore/Watchtower.swift +80 -0
- package/ios/WatchtowerCore/WatchtowerEngine.swift +565 -0
- package/ios/WatchtowerCore/WatchtowerHash.swift +120 -0
- package/ios/WatchtowerCore/WatchtowerRegistry.swift +87 -0
- package/package.json +17 -10
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import Foundation
|
|
4
|
+
import WidgetKit
|
|
5
|
+
|
|
6
|
+
public class SitePongLiveActivityModule: Module {
|
|
7
|
+
/// Tasks listening for push-token rotations on each running activity.
|
|
8
|
+
private var tokenObservers: [String: Task<Void, Never>] = [:]
|
|
9
|
+
/// activityId → activityType so endActivity / token rotations can resolve type.
|
|
10
|
+
private var activityTypes: [String: String] = [:]
|
|
11
|
+
|
|
12
|
+
public func definition() -> ModuleDefinition {
|
|
13
|
+
Name("SitePongLiveActivity")
|
|
14
|
+
|
|
15
|
+
Events("liveActivityPushTokenUpdate")
|
|
16
|
+
|
|
17
|
+
AsyncFunction("areActivitiesEnabled") { () -> Bool in
|
|
18
|
+
if #available(iOS 16.2, *) {
|
|
19
|
+
return ActivityAuthorizationInfo().areActivitiesEnabled
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AsyncFunction("startActivity") { (options: [String: Any], promise: Promise) in
|
|
25
|
+
guard #available(iOS 16.2, *) else {
|
|
26
|
+
promise.reject("UNSUPPORTED", "Live Activities require iOS 16.2 or later")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
|
31
|
+
promise.reject("DISABLED", "Live Activities are disabled by the user")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let activityType = options["activityType"] as? String ?? "default"
|
|
36
|
+
let attributesDict = options["attributes"] as? [String: Any] ?? [:]
|
|
37
|
+
let contentStateDict = options["contentState"] as? [String: Any] ?? [:]
|
|
38
|
+
let alertDict = options["alert"] as? [String: Any]
|
|
39
|
+
|
|
40
|
+
do {
|
|
41
|
+
let attributes = try Self.decode(SitePongActivityAttributes.self, from: attributesDict)
|
|
42
|
+
let state = try Self.decode(SitePongActivityAttributes.ContentState.self, from: contentStateDict)
|
|
43
|
+
|
|
44
|
+
let content = ActivityContent(state: state, staleDate: nil)
|
|
45
|
+
|
|
46
|
+
var alertConfig: AlertConfiguration? = nil
|
|
47
|
+
if let alertDict = alertDict {
|
|
48
|
+
let title = alertDict["title"] as? String ?? ""
|
|
49
|
+
let body = alertDict["body"] as? String ?? ""
|
|
50
|
+
alertConfig = AlertConfiguration(
|
|
51
|
+
title: LocalizedStringResource(stringLiteral: title),
|
|
52
|
+
body: LocalizedStringResource(stringLiteral: body),
|
|
53
|
+
sound: .default
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let activity: Activity<SitePongActivityAttributes>
|
|
58
|
+
if #available(iOS 16.2, *) {
|
|
59
|
+
activity = try Activity.request(
|
|
60
|
+
attributes: attributes,
|
|
61
|
+
content: content,
|
|
62
|
+
pushType: .token
|
|
63
|
+
)
|
|
64
|
+
} else {
|
|
65
|
+
promise.reject("UNSUPPORTED", "iOS 16.2 required")
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
_ = alertConfig // alertConfig only applies to update calls in iOS 16.2+
|
|
69
|
+
|
|
70
|
+
self.activityTypes[activity.id] = activityType
|
|
71
|
+
self.observePushToken(for: activity, activityType: activityType)
|
|
72
|
+
|
|
73
|
+
// Wait up to 2 seconds for the first push token before resolving.
|
|
74
|
+
// ActivityKit on the simulator never emits a token, so without this
|
|
75
|
+
// race the promise would hang the JS caller forever.
|
|
76
|
+
Task {
|
|
77
|
+
let token = await Self.firstPushToken(for: activity, timeoutSeconds: 2)
|
|
78
|
+
promise.resolve([
|
|
79
|
+
"activityId": activity.id,
|
|
80
|
+
"pushToken": token ?? "",
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
promise.reject("DECODE_FAILED", "Failed to start activity: \(error.localizedDescription)")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
AsyncFunction("updateActivity") { (options: [String: Any], promise: Promise) in
|
|
89
|
+
guard #available(iOS 16.2, *) else {
|
|
90
|
+
promise.reject("UNSUPPORTED", "Live Activities require iOS 16.2 or later")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let activityId = options["activityId"] as? String ?? ""
|
|
95
|
+
let contentStateDict = options["contentState"] as? [String: Any] ?? [:]
|
|
96
|
+
let alertDict = options["alert"] as? [String: Any]
|
|
97
|
+
|
|
98
|
+
guard let activity = Activity<SitePongActivityAttributes>.activities.first(where: { $0.id == activityId }) else {
|
|
99
|
+
promise.reject("NOT_FOUND", "No running activity with id \(activityId)")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
do {
|
|
104
|
+
let state = try Self.decode(SitePongActivityAttributes.ContentState.self, from: contentStateDict)
|
|
105
|
+
let content = ActivityContent(state: state, staleDate: nil)
|
|
106
|
+
|
|
107
|
+
Task {
|
|
108
|
+
if let alertDict = alertDict {
|
|
109
|
+
let title = alertDict["title"] as? String ?? ""
|
|
110
|
+
let body = alertDict["body"] as? String ?? ""
|
|
111
|
+
let alert = AlertConfiguration(
|
|
112
|
+
title: LocalizedStringResource(stringLiteral: title),
|
|
113
|
+
body: LocalizedStringResource(stringLiteral: body),
|
|
114
|
+
sound: .default
|
|
115
|
+
)
|
|
116
|
+
await activity.update(content, alertConfiguration: alert)
|
|
117
|
+
} else {
|
|
118
|
+
await activity.update(content)
|
|
119
|
+
}
|
|
120
|
+
promise.resolve(nil)
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
promise.reject("DECODE_FAILED", "Failed to update activity: \(error.localizedDescription)")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - Widget DSL functions
|
|
128
|
+
|
|
129
|
+
AsyncFunction("syncWidgetConfigs") { (configsJson: String, promise: Promise) in
|
|
130
|
+
guard let data = configsJson.data(using: .utf8) else {
|
|
131
|
+
promise.reject("INVALID", "Invalid JSON string")
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
WidgetConfigStore.saveConfigs(data)
|
|
135
|
+
if #available(iOS 14.0, *) {
|
|
136
|
+
WidgetCenter.shared.reloadAllTimelines()
|
|
137
|
+
}
|
|
138
|
+
promise.resolve(nil)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
AsyncFunction("updateWidgetData") { (dataJson: String, promise: Promise) in
|
|
142
|
+
guard let data = dataJson.data(using: .utf8) else {
|
|
143
|
+
promise.reject("INVALID", "Invalid JSON string")
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
WidgetConfigStore.saveData(data)
|
|
147
|
+
if #available(iOS 14.0, *) {
|
|
148
|
+
WidgetCenter.shared.reloadAllTimelines()
|
|
149
|
+
}
|
|
150
|
+
promise.resolve(nil)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
AsyncFunction("endActivity") { (options: [String: Any], promise: Promise) in
|
|
154
|
+
guard #available(iOS 16.2, *) else {
|
|
155
|
+
promise.reject("UNSUPPORTED", "Live Activities require iOS 16.2 or later")
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let activityId = options["activityId"] as? String ?? ""
|
|
160
|
+
let contentStateDict = options["contentState"] as? [String: Any]
|
|
161
|
+
let dismissalDateMs = options["dismissalDate"] as? Double
|
|
162
|
+
|
|
163
|
+
guard let activity = Activity<SitePongActivityAttributes>.activities.first(where: { $0.id == activityId }) else {
|
|
164
|
+
promise.resolve(nil)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Task {
|
|
169
|
+
var finalContent: ActivityContent<SitePongActivityAttributes.ContentState>? = nil
|
|
170
|
+
if let contentStateDict = contentStateDict {
|
|
171
|
+
if let state = try? Self.decode(SitePongActivityAttributes.ContentState.self, from: contentStateDict) {
|
|
172
|
+
finalContent = ActivityContent(state: state, staleDate: nil)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let dismissalPolicy: ActivityUIDismissalPolicy
|
|
177
|
+
if let dismissalDateMs = dismissalDateMs {
|
|
178
|
+
let date = Date(timeIntervalSince1970: dismissalDateMs / 1000)
|
|
179
|
+
dismissalPolicy = .after(date)
|
|
180
|
+
} else {
|
|
181
|
+
dismissalPolicy = .immediate
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await activity.end(finalContent, dismissalPolicy: dismissalPolicy)
|
|
185
|
+
|
|
186
|
+
self.tokenObservers[activityId]?.cancel()
|
|
187
|
+
self.tokenObservers.removeValue(forKey: activityId)
|
|
188
|
+
self.activityTypes.removeValue(forKey: activityId)
|
|
189
|
+
|
|
190
|
+
promise.resolve(nil)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Push token observation
|
|
196
|
+
|
|
197
|
+
@available(iOS 16.2, *)
|
|
198
|
+
private func observePushToken(for activity: Activity<SitePongActivityAttributes>, activityType: String) {
|
|
199
|
+
// Cancel any prior observer for this id (idempotent)
|
|
200
|
+
tokenObservers[activity.id]?.cancel()
|
|
201
|
+
|
|
202
|
+
let task = Task { [weak self] in
|
|
203
|
+
for await tokenData in activity.pushTokenUpdates {
|
|
204
|
+
let hex = tokenData.map { String(format: "%02x", $0) }.joined()
|
|
205
|
+
self?.sendEvent(
|
|
206
|
+
"liveActivityPushTokenUpdate",
|
|
207
|
+
[
|
|
208
|
+
"activityId": activity.id,
|
|
209
|
+
"activityType": activityType,
|
|
210
|
+
"pushToken": hex,
|
|
211
|
+
]
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
tokenObservers[activity.id] = task
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - JSON helpers
|
|
219
|
+
|
|
220
|
+
private static func decode<T: Decodable>(_ type: T.Type, from dict: [String: Any]) throws -> T {
|
|
221
|
+
let data = try JSONSerialization.data(withJSONObject: dict, options: [])
|
|
222
|
+
let decoder = JSONDecoder()
|
|
223
|
+
return try decoder.decode(type, from: data)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Race the first emission of `activity.pushTokenUpdates` against a sleep.
|
|
227
|
+
/// Returns the hex-encoded token, or nil if the timeout fires first
|
|
228
|
+
/// (the common case on iOS Simulator).
|
|
229
|
+
@available(iOS 16.2, *)
|
|
230
|
+
private static func firstPushToken(
|
|
231
|
+
for activity: Activity<SitePongActivityAttributes>,
|
|
232
|
+
timeoutSeconds: Double
|
|
233
|
+
) async -> String? {
|
|
234
|
+
await withTaskGroup(of: String?.self) { group in
|
|
235
|
+
group.addTask {
|
|
236
|
+
for await tokenData in activity.pushTokenUpdates {
|
|
237
|
+
return tokenData.map { String(format: "%02x", $0) }.joined()
|
|
238
|
+
}
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
group.addTask {
|
|
242
|
+
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
|
|
243
|
+
return nil
|
|
244
|
+
}
|
|
245
|
+
let result = await group.next() ?? nil
|
|
246
|
+
group.cancelAll()
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
extension Color {
|
|
4
|
+
/// Parse a hex color string like "#3b82f6" or "#3b82f6cc" (RRGGBB or RRGGBBAA).
|
|
5
|
+
/// Returns nil for malformed input so the widget can fall back to defaults.
|
|
6
|
+
init?(hex: String?) {
|
|
7
|
+
guard let raw = hex else { return nil }
|
|
8
|
+
var s = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
9
|
+
if s.hasPrefix("#") { s.removeFirst() }
|
|
10
|
+
guard s.count == 6 || s.count == 8 else { return nil }
|
|
11
|
+
|
|
12
|
+
var rgba: UInt64 = 0
|
|
13
|
+
guard Scanner(string: s).scanHexInt64(&rgba) else { return nil }
|
|
14
|
+
|
|
15
|
+
let r, g, b, a: Double
|
|
16
|
+
if s.count == 8 {
|
|
17
|
+
r = Double((rgba >> 24) & 0xff) / 255
|
|
18
|
+
g = Double((rgba >> 16) & 0xff) / 255
|
|
19
|
+
b = Double((rgba >> 8) & 0xff) / 255
|
|
20
|
+
a = Double(rgba & 0xff) / 255
|
|
21
|
+
} else {
|
|
22
|
+
r = Double((rgba >> 16) & 0xff) / 255
|
|
23
|
+
g = Double((rgba >> 8) & 0xff) / 255
|
|
24
|
+
b = Double(rgba & 0xff) / 255
|
|
25
|
+
a = 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
self.init(.sRGB, red: r, green: g, blue: b, opacity: a)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
// MARK: - Animation view modifier
|
|
5
|
+
|
|
6
|
+
@available(iOS 16.2, *)
|
|
7
|
+
struct DSLAnimationModifier: ViewModifier {
|
|
8
|
+
let animation: DSLAnimation?
|
|
9
|
+
|
|
10
|
+
func body(content: Content) -> some View {
|
|
11
|
+
var view = AnyView(content)
|
|
12
|
+
|
|
13
|
+
// Entrance transitions — in WidgetKit these only apply as content
|
|
14
|
+
// transitions between timeline entries. Full animations work in
|
|
15
|
+
// Live Activities.
|
|
16
|
+
if let entrance = animation?.entrance {
|
|
17
|
+
let transition = entranceTransition(entrance)
|
|
18
|
+
view = AnyView(view.transition(transition))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Value transition — animate numeric content changes
|
|
22
|
+
if let valueTransition = animation?.valueTransition {
|
|
23
|
+
let anim = valueAnimation(valueTransition)
|
|
24
|
+
view = AnyView(view.animation(anim, value: 0))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return view
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func entranceTransition(_ type: String) -> AnyTransition {
|
|
31
|
+
switch type {
|
|
32
|
+
case "fadeIn":
|
|
33
|
+
return .opacity
|
|
34
|
+
case "slideUp":
|
|
35
|
+
return .move(edge: .bottom).combined(with: .opacity)
|
|
36
|
+
case "slideLeft":
|
|
37
|
+
return .move(edge: .trailing).combined(with: .opacity)
|
|
38
|
+
case "scaleIn":
|
|
39
|
+
return .scale.combined(with: .opacity)
|
|
40
|
+
default:
|
|
41
|
+
return .opacity
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func valueAnimation(_ type: String) -> Animation {
|
|
46
|
+
let duration = animation?.entranceDuration ?? 0.3
|
|
47
|
+
switch type {
|
|
48
|
+
case "spring":
|
|
49
|
+
return .spring(response: 0.5, dampingFraction: 0.7)
|
|
50
|
+
case "easeIn":
|
|
51
|
+
return .easeIn(duration: duration)
|
|
52
|
+
case "easeOut":
|
|
53
|
+
return .easeOut(duration: duration)
|
|
54
|
+
case "linear":
|
|
55
|
+
return .linear(duration: duration)
|
|
56
|
+
default:
|
|
57
|
+
return .easeInOut(duration: duration)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// MARK: - Ambient animation modifier
|
|
63
|
+
// Note: Ambient animations (pulse, shimmer, glow) only work in Live Activities,
|
|
64
|
+
// not in static Home Screen widgets where WidgetKit controls the timeline.
|
|
65
|
+
|
|
66
|
+
@available(iOS 16.2, *)
|
|
67
|
+
struct DSLAmbientModifier: ViewModifier {
|
|
68
|
+
let ambient: String?
|
|
69
|
+
let ambientDuration: Double?
|
|
70
|
+
@State private var isAnimating = false
|
|
71
|
+
|
|
72
|
+
func body(content: Content) -> some View {
|
|
73
|
+
guard let ambient = ambient else {
|
|
74
|
+
return AnyView(content)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let duration = ambientDuration ?? 2.0
|
|
78
|
+
|
|
79
|
+
switch ambient {
|
|
80
|
+
case "pulse":
|
|
81
|
+
return AnyView(
|
|
82
|
+
content
|
|
83
|
+
.opacity(isAnimating ? 0.6 : 1.0)
|
|
84
|
+
.onAppear {
|
|
85
|
+
withAnimation(.easeInOut(duration: duration).repeatForever(autoreverses: true)) {
|
|
86
|
+
isAnimating = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
case "shimmer":
|
|
91
|
+
return AnyView(
|
|
92
|
+
content
|
|
93
|
+
.opacity(isAnimating ? 0.8 : 1.0)
|
|
94
|
+
.onAppear {
|
|
95
|
+
withAnimation(.easeInOut(duration: duration).repeatForever(autoreverses: true)) {
|
|
96
|
+
isAnimating = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
case "glow":
|
|
101
|
+
return AnyView(
|
|
102
|
+
content
|
|
103
|
+
.shadow(
|
|
104
|
+
color: .white.opacity(isAnimating ? 0.4 : 0.0),
|
|
105
|
+
radius: isAnimating ? 8 : 2
|
|
106
|
+
)
|
|
107
|
+
.onAppear {
|
|
108
|
+
withAnimation(.easeInOut(duration: duration).repeatForever(autoreverses: true)) {
|
|
109
|
+
isAnimating = true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
default:
|
|
114
|
+
return AnyView(content)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - View extension
|
|
120
|
+
|
|
121
|
+
@available(iOS 16.2, *)
|
|
122
|
+
extension View {
|
|
123
|
+
func applyAnimation(_ animation: DSLAnimation?) -> some View {
|
|
124
|
+
self
|
|
125
|
+
.modifier(DSLAnimationModifier(animation: animation))
|
|
126
|
+
.modifier(DSLAmbientModifier(ambient: animation?.ambient, ambientDuration: animation?.ambientDuration))
|
|
127
|
+
}
|
|
128
|
+
}
|