sitepong 0.2.3 → 0.2.5

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.
Files changed (57) hide show
  1. package/android/build.gradle +39 -0
  2. package/android/src/main/java/com/sitepong/deviceid/DeviceIdHelper.kt +45 -0
  3. package/android/src/main/java/com/sitepong/deviceid/DeviceIdModule.kt +45 -0
  4. package/android/src/main/java/com/sitepong/screenrecorder/ChunkUploader.kt +78 -0
  5. package/android/src/main/java/com/sitepong/screenrecorder/H264Encoder.kt +163 -0
  6. package/android/src/main/java/com/sitepong/screenrecorder/ScreenCapture.kt +103 -0
  7. package/android/src/main/java/com/sitepong/screenrecorder/ScreenRecorderModule.kt +80 -0
  8. package/android/src/main/java/com/sitepong/screenrecorder/SensitiveViewManager.kt +19 -0
  9. package/app.plugin.js +426 -0
  10. package/dist/cdn/sitepong.min.js +5 -5
  11. package/dist/cdn/sitepong.min.js.map +1 -1
  12. package/dist/entries/rn.d.ts +188 -16
  13. package/dist/entries/rn.js +187 -35
  14. package/dist/entries/rn.js.map +1 -1
  15. package/dist/entries/web.js +2 -1
  16. package/dist/entries/web.js.map +1 -1
  17. package/dist/entries/web.mjs +2 -1
  18. package/dist/entries/web.mjs.map +1 -1
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/react/index.js +2 -1
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/index.mjs +2 -1
  28. package/dist/react/index.mjs.map +1 -1
  29. package/expo-module.config.json +16 -0
  30. package/ios/DeviceId/DeviceIdModule.swift +55 -0
  31. package/ios/DeviceId/KeychainHelper.swift +68 -0
  32. package/ios/LiveActivity/ConfigStore.swift +43 -0
  33. package/ios/LiveActivity/DSLNode.swift +188 -0
  34. package/ios/LiveActivity/SitePongActivityAttributes.swift +112 -0
  35. package/ios/LiveActivity/SitePongLiveActivityModule.swift +250 -0
  36. package/ios/LiveActivity/widget/ColorHex.swift +30 -0
  37. package/ios/LiveActivity/widget/DSLAnimations.swift +128 -0
  38. package/ios/LiveActivity/widget/DSLRenderer.swift +538 -0
  39. package/ios/LiveActivity/widget/SitePongLiveActivityWidget.swift +393 -0
  40. package/ios/LiveActivity/widget/SitePongWidgetBundle.swift +84 -0
  41. package/ios/ScreenRecorder/ChunkUploader.swift +52 -0
  42. package/ios/ScreenRecorder/H264Encoder.swift +229 -0
  43. package/ios/ScreenRecorder/ScreenCapture.swift +149 -0
  44. package/ios/ScreenRecorder/ScreenRecorderModule.swift +179 -0
  45. package/ios/ScreenRecorder/SensitiveViewManager.swift +63 -0
  46. package/ios/Sitepong.podspec +57 -0
  47. package/ios/WatchtowerCore/Screenshotter.swift +163 -0
  48. package/ios/WatchtowerCore/SwiftUIModifiers.swift +78 -0
  49. package/ios/WatchtowerCore/Swizzling.swift +59 -0
  50. package/ios/WatchtowerCore/TapEvent.swift +61 -0
  51. package/ios/WatchtowerCore/Transport.swift +136 -0
  52. package/ios/WatchtowerCore/UIViewExtensions.swift +31 -0
  53. package/ios/WatchtowerCore/Watchtower.swift +80 -0
  54. package/ios/WatchtowerCore/WatchtowerEngine.swift +565 -0
  55. package/ios/WatchtowerCore/WatchtowerHash.swift +120 -0
  56. package/ios/WatchtowerCore/WatchtowerRegistry.swift +87 -0
  57. package/package.json +17 -10
@@ -0,0 +1,393 @@
1
+ import ActivityKit
2
+ import SwiftUI
3
+ import WidgetKit
4
+
5
+ // MARK: - Widget bundle entry point
6
+
7
+ @available(iOS 17.0, *)
8
+ @main
9
+ struct SitePongLiveActivityBundle: WidgetBundle {
10
+ var body: some Widget {
11
+ SitePongLiveActivityWidget()
12
+ SitePongHomeWidget()
13
+ }
14
+ }
15
+
16
+ // MARK: - Widget definition
17
+
18
+ @available(iOS 16.2, *)
19
+ struct SitePongLiveActivityWidget: Widget {
20
+ var body: some WidgetConfiguration {
21
+ ActivityConfiguration(for: SitePongActivityAttributes.self) { context in
22
+ // Lock screen / banner presentation
23
+ LockScreenView(state: context.state, attributes: context.attributes)
24
+ .activityBackgroundTint(
25
+ context.state.backgroundColor.flatMap(Color.init(hex:))
26
+ )
27
+ .activitySystemActionForegroundColor(.primary)
28
+ } dynamicIsland: { context in
29
+ DynamicIsland {
30
+ DynamicIslandExpandedRegion(.leading) {
31
+ if let icon = context.state.leadingIcon {
32
+ IconView(spec: icon, defaultSize: 22)
33
+ }
34
+ }
35
+ DynamicIslandExpandedRegion(.trailing) {
36
+ if let icon = context.state.trailingIcon {
37
+ IconView(spec: icon, defaultSize: 22)
38
+ }
39
+ }
40
+ DynamicIslandExpandedRegion(.center) {
41
+ VStack(alignment: .leading, spacing: 2) {
42
+ if let title = context.state.title {
43
+ StyledText(text: title, style: context.state.titleStyle, defaultSize: 15, defaultWeight: .semibold)
44
+ }
45
+ if let subtitle = context.state.subtitle {
46
+ StyledText(text: subtitle, style: context.state.subtitleStyle, defaultSize: 13, defaultWeight: .regular, defaultColor: .secondary)
47
+ }
48
+ }
49
+ }
50
+ DynamicIslandExpandedRegion(.bottom) {
51
+ if let progress = context.state.progress {
52
+ ProgressSection(spec: progress)
53
+ }
54
+ }
55
+ } compactLeading: {
56
+ CompactSlot(slot: context.state.dynamicIsland?.compactLeading, fallbackIcon: context.state.leadingIcon)
57
+ } compactTrailing: {
58
+ CompactSlot(slot: context.state.dynamicIsland?.compactTrailing, fallbackIcon: context.state.trailingIcon)
59
+ } minimal: {
60
+ if let icon = context.state.dynamicIsland?.minimal ?? context.state.leadingIcon {
61
+ IconView(spec: icon, defaultSize: 16)
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // MARK: - Lock screen layout
69
+
70
+ @available(iOS 16.2, *)
71
+ struct LockScreenView: View {
72
+ let state: SitePongActivityAttributes.ContentState
73
+ let attributes: SitePongActivityAttributes
74
+
75
+ var body: some View {
76
+ if let dslLayout = state.dslLayout {
77
+ // DSL-driven layout
78
+ DSLView(node: dslLayout, data: [:])
79
+ .padding(16)
80
+ } else {
81
+ // Fixed slot-based layout (backward compatible)
82
+ fixedLayout
83
+ }
84
+ }
85
+
86
+ @ViewBuilder
87
+ private var fixedLayout: some View {
88
+ VStack(alignment: .leading, spacing: 12) {
89
+ HStack(alignment: .center, spacing: 12) {
90
+ if let icon = state.leadingIcon {
91
+ IconView(spec: icon, defaultSize: 28)
92
+ }
93
+
94
+ VStack(alignment: .leading, spacing: 2) {
95
+ if let title = state.title {
96
+ StyledText(
97
+ text: title,
98
+ style: state.titleStyle,
99
+ defaultSize: 16,
100
+ defaultWeight: .semibold
101
+ )
102
+ }
103
+ if let subtitle = state.subtitle {
104
+ StyledText(
105
+ text: subtitle,
106
+ style: state.subtitleStyle,
107
+ defaultSize: 14,
108
+ defaultWeight: .regular,
109
+ defaultColor: .secondary
110
+ )
111
+ }
112
+ }
113
+
114
+ Spacer(minLength: 0)
115
+
116
+ if let icon = state.trailingIcon {
117
+ IconView(spec: icon, defaultSize: 28)
118
+ }
119
+ }
120
+
121
+ if let progress = state.progress {
122
+ ProgressSection(spec: progress)
123
+ }
124
+ }
125
+ .padding(16)
126
+ }
127
+ }
128
+
129
+ // MARK: - Compact Dynamic Island slots
130
+
131
+ @available(iOS 16.2, *)
132
+ struct CompactSlot: View {
133
+ let slot: DynamicIslandSlot?
134
+ let fallbackIcon: IconSpec?
135
+
136
+ var body: some View {
137
+ if let slot = slot {
138
+ if let icon = slot.icon {
139
+ IconView(spec: icon, defaultSize: 16)
140
+ } else if let text = slot.text {
141
+ StyledText(text: text, style: slot.style, defaultSize: 13, defaultWeight: .medium)
142
+ } else {
143
+ EmptyView()
144
+ }
145
+ } else if let icon = fallbackIcon {
146
+ IconView(spec: icon, defaultSize: 16)
147
+ } else {
148
+ EmptyView()
149
+ }
150
+ }
151
+ }
152
+
153
+ // MARK: - Progress section
154
+
155
+ @available(iOS 16.2, *)
156
+ struct ProgressSection: View {
157
+ let spec: ProgressSpec
158
+
159
+ var body: some View {
160
+ switch spec.style {
161
+ case "bar":
162
+ FlatBar(spec: spec)
163
+ case "bar-with-icon":
164
+ BarWithIcon(spec: spec)
165
+ case "circular":
166
+ CircularRing(spec: spec)
167
+ case "timer":
168
+ TimerSection(spec: spec)
169
+ default:
170
+ FlatBar(spec: spec)
171
+ }
172
+ }
173
+ }
174
+
175
+ @available(iOS 16.2, *)
176
+ struct FlatBar: View {
177
+ let spec: ProgressSpec
178
+
179
+ var body: some View {
180
+ VStack(alignment: .leading, spacing: 4) {
181
+ if let label = spec.label {
182
+ StyledText(
183
+ text: label,
184
+ style: spec.labelStyle,
185
+ defaultSize: 12,
186
+ defaultWeight: .regular,
187
+ defaultColor: .secondary
188
+ )
189
+ }
190
+
191
+ GeometryReader { geo in
192
+ ZStack(alignment: .leading) {
193
+ Capsule()
194
+ .fill(Color(hex: spec.trackColor) ?? Color.secondary.opacity(0.2))
195
+ Capsule()
196
+ .fill(Color(hex: spec.fillColor) ?? Color.accentColor)
197
+ .frame(width: geo.size.width * clampedValue)
198
+ }
199
+ }
200
+ .frame(height: 6)
201
+ }
202
+ }
203
+
204
+ private var clampedValue: Double {
205
+ max(0, min(1, spec.value ?? 0))
206
+ }
207
+ }
208
+
209
+ @available(iOS 16.2, *)
210
+ struct BarWithIcon: View {
211
+ let spec: ProgressSpec
212
+
213
+ var body: some View {
214
+ VStack(alignment: .leading, spacing: 6) {
215
+ if let label = spec.label {
216
+ StyledText(
217
+ text: label,
218
+ style: spec.labelStyle,
219
+ defaultSize: 12,
220
+ defaultWeight: .regular,
221
+ defaultColor: .secondary
222
+ )
223
+ }
224
+
225
+ GeometryReader { geo in
226
+ let trackHeight: CGFloat = 6
227
+ let iconSize: CGFloat = 22
228
+ let usable = max(0, geo.size.width - iconSize)
229
+ let xOffset = usable * clampedValue
230
+
231
+ ZStack(alignment: .leading) {
232
+ Capsule()
233
+ .fill(Color(hex: spec.trackColor) ?? Color.secondary.opacity(0.2))
234
+ .frame(height: trackHeight)
235
+ .padding(.horizontal, iconSize / 2)
236
+
237
+ Capsule()
238
+ .fill(Color(hex: spec.fillColor) ?? Color.accentColor)
239
+ .frame(width: max(trackHeight, xOffset + trackHeight), height: trackHeight)
240
+ .padding(.leading, iconSize / 2)
241
+
242
+ if let symbol = spec.icon {
243
+ Image(systemName: symbol)
244
+ .font(.system(size: iconSize - 4, weight: symbolWeight(spec.iconWeight)))
245
+ .foregroundStyle(Color(hex: spec.iconColor) ?? Color.accentColor)
246
+ .frame(width: iconSize, height: iconSize)
247
+ .offset(x: xOffset)
248
+ }
249
+ }
250
+ .frame(height: max(iconSize, trackHeight))
251
+ }
252
+ .frame(height: 24)
253
+ }
254
+ }
255
+
256
+ private var clampedValue: Double {
257
+ max(0, min(1, spec.value ?? 0))
258
+ }
259
+ }
260
+
261
+ @available(iOS 16.2, *)
262
+ struct CircularRing: View {
263
+ let spec: ProgressSpec
264
+
265
+ var body: some View {
266
+ ZStack {
267
+ Circle()
268
+ .stroke(
269
+ Color(hex: spec.trackColor) ?? Color.secondary.opacity(0.2),
270
+ lineWidth: 5
271
+ )
272
+ Circle()
273
+ .trim(from: 0, to: clampedValue)
274
+ .stroke(
275
+ Color(hex: spec.fillColor) ?? Color.accentColor,
276
+ style: StrokeStyle(lineWidth: 5, lineCap: .round)
277
+ )
278
+ .rotationEffect(.degrees(-90))
279
+
280
+ if let symbol = spec.icon {
281
+ Image(systemName: symbol)
282
+ .font(.system(size: 14, weight: .semibold))
283
+ .foregroundStyle(Color(hex: spec.iconColor) ?? Color.accentColor)
284
+ }
285
+ }
286
+ .frame(width: 38, height: 38)
287
+ }
288
+
289
+ private var clampedValue: Double {
290
+ max(0, min(1, spec.value ?? 0))
291
+ }
292
+ }
293
+
294
+ @available(iOS 16.2, *)
295
+ struct TimerSection: View {
296
+ let spec: ProgressSpec
297
+
298
+ var body: some View {
299
+ if let endDateMs = spec.endDate {
300
+ let end = Date(timeIntervalSince1970: endDateMs / 1000)
301
+ ProgressView(
302
+ timerInterval: Date()...end,
303
+ countsDown: true
304
+ ) {
305
+ EmptyView()
306
+ }
307
+ .progressViewStyle(.linear)
308
+ .tint(Color(hex: spec.fillColor) ?? Color.accentColor)
309
+ } else if let startDateMs = spec.startDate {
310
+ let start = Date(timeIntervalSince1970: startDateMs / 1000)
311
+ // Open-ended elapsed timer; use one hour as a default ceiling.
312
+ let endCap = start.addingTimeInterval(60 * 60)
313
+ ProgressView(
314
+ timerInterval: start...endCap,
315
+ countsDown: false
316
+ ) {
317
+ EmptyView()
318
+ }
319
+ .progressViewStyle(.linear)
320
+ .tint(Color(hex: spec.fillColor) ?? Color.accentColor)
321
+ } else {
322
+ EmptyView()
323
+ }
324
+ }
325
+ }
326
+
327
+ // MARK: - Reusable styled views
328
+
329
+ @available(iOS 16.2, *)
330
+ struct StyledText: View {
331
+ let text: String
332
+ let style: TextStyleSpec?
333
+ let defaultSize: Double
334
+ let defaultWeight: Font.Weight
335
+ var defaultColor: Color = .primary
336
+
337
+ var body: some View {
338
+ let size = style?.size ?? defaultSize
339
+ let weight = textWeight(style?.weight) ?? defaultWeight
340
+ let color = style?.color.flatMap(Color.init(hex:)) ?? defaultColor
341
+ let italic = style?.italic ?? false
342
+ let monoDigit = style?.monospacedDigit ?? false
343
+
344
+ var font = Font.system(size: size, weight: weight)
345
+ if monoDigit {
346
+ font = font.monospacedDigit()
347
+ }
348
+
349
+ return Text(text)
350
+ .font(font)
351
+ .italic(italic)
352
+ .foregroundStyle(color)
353
+ .lineLimit(1)
354
+ }
355
+ }
356
+
357
+ @available(iOS 16.2, *)
358
+ struct IconView: View {
359
+ let spec: IconSpec
360
+ let defaultSize: Double
361
+
362
+ var body: some View {
363
+ let size = spec.size ?? defaultSize
364
+ let weight = symbolWeight(spec.weight)
365
+ let color = spec.color.flatMap(Color.init(hex:)) ?? .primary
366
+
367
+ Image(systemName: spec.symbol)
368
+ .font(.system(size: size, weight: weight))
369
+ .foregroundStyle(color)
370
+ }
371
+ }
372
+
373
+ // MARK: - Style helpers
374
+
375
+ func symbolWeight(_ name: String?) -> Font.Weight {
376
+ switch name {
377
+ case "ultraLight": return .ultraLight
378
+ case "thin": return .thin
379
+ case "light": return .light
380
+ case "regular": return .regular
381
+ case "medium": return .medium
382
+ case "semibold": return .semibold
383
+ case "bold": return .bold
384
+ case "heavy": return .heavy
385
+ case "black": return .black
386
+ default: return .regular
387
+ }
388
+ }
389
+
390
+ func textWeight(_ name: String?) -> Font.Weight? {
391
+ guard let name = name else { return nil }
392
+ return symbolWeight(name)
393
+ }
@@ -0,0 +1,84 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ // MARK: - Timeline Entry
5
+
6
+ @available(iOS 16.2, *)
7
+ struct SitePongWidgetEntry: TimelineEntry {
8
+ let date: Date
9
+ let rootNode: DSLNode?
10
+ let data: [String: AnyCodable]
11
+ }
12
+
13
+ // MARK: - Timeline Provider
14
+
15
+ @available(iOS 16.2, *)
16
+ struct SitePongTimelineProvider: TimelineProvider {
17
+ func placeholder(in context: Context) -> SitePongWidgetEntry {
18
+ SitePongWidgetEntry(date: Date(), rootNode: nil, data: [:])
19
+ }
20
+
21
+ func getSnapshot(in context: Context, completion: @escaping (SitePongWidgetEntry) -> Void) {
22
+ let configs = WidgetConfigStore.loadConfigs()
23
+ let data = WidgetConfigStore.loadData()
24
+ let entry = SitePongWidgetEntry(
25
+ date: Date(),
26
+ rootNode: configs?.first,
27
+ data: data
28
+ )
29
+ completion(entry)
30
+ }
31
+
32
+ func getTimeline(in context: Context, completion: @escaping (Timeline<SitePongWidgetEntry>) -> Void) {
33
+ let configs = WidgetConfigStore.loadConfigs()
34
+ let data = WidgetConfigStore.loadData()
35
+ let entry = SitePongWidgetEntry(
36
+ date: Date(),
37
+ rootNode: configs?.first,
38
+ data: data
39
+ )
40
+ // Refresh every 30 minutes; the app can also trigger reloads via WidgetCenter
41
+ let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
42
+ let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
43
+ completion(timeline)
44
+ }
45
+ }
46
+
47
+ // MARK: - Widget View
48
+
49
+ @available(iOS 16.2, *)
50
+ struct SitePongHomeWidgetView: View {
51
+ let entry: SitePongWidgetEntry
52
+
53
+ var body: some View {
54
+ if let rootNode = entry.rootNode {
55
+ DSLView(node: rootNode, data: entry.data)
56
+ } else {
57
+ VStack(spacing: 4) {
58
+ Image(systemName: "square.grid.2x2")
59
+ .font(.system(size: 24, weight: .light))
60
+ .foregroundStyle(.secondary)
61
+ Text("Configure in app")
62
+ .font(.system(size: 12))
63
+ .foregroundStyle(.secondary)
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ // MARK: - Widget Definition
70
+
71
+ @available(iOS 17.0, *)
72
+ struct SitePongHomeWidget: Widget {
73
+ let kind: String = "SitePongHomeWidget"
74
+
75
+ var body: some WidgetConfiguration {
76
+ StaticConfiguration(kind: kind, provider: SitePongTimelineProvider()) { entry in
77
+ SitePongHomeWidgetView(entry: entry)
78
+ .containerBackground(.fill.tertiary, for: .widget)
79
+ }
80
+ .configurationDisplayName("SitePong")
81
+ .description("Display custom widgets powered by SitePong.")
82
+ .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
83
+ }
84
+ }
@@ -0,0 +1,52 @@
1
+ import Foundation
2
+
3
+ class ChunkUploader {
4
+ private let endpoint: String
5
+ private let apiKey: String
6
+ private let sessionId: String
7
+ private var chunkSequence: Int = 0
8
+
9
+ init(endpoint: String, apiKey: String, sessionId: String) {
10
+ self.endpoint = endpoint
11
+ self.apiKey = apiKey
12
+ self.sessionId = sessionId
13
+ }
14
+
15
+ /// Upload the given H.264 data to the ingest server.
16
+ /// Called with data drained from the encoder's rolling buffer on flush.
17
+ func uploadData(_ data: Data) async throws {
18
+ guard !data.isEmpty else { return }
19
+
20
+ let seq = chunkSequence
21
+ chunkSequence += 1
22
+
23
+ let url = URL(string: "\(endpoint)/api/replays/video")!
24
+ var request = URLRequest(url: url)
25
+ request.httpMethod = "POST"
26
+ request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
27
+ request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
28
+ request.setValue(sessionId, forHTTPHeaderField: "X-Session-ID")
29
+ request.setValue(String(seq), forHTTPHeaderField: "X-Chunk-Sequence")
30
+ request.setValue("error", forHTTPHeaderField: "X-Trigger")
31
+ request.httpBody = data
32
+ request.timeoutInterval = 10
33
+
34
+ do {
35
+ let (_, response) = try await URLSession.shared.data(for: request)
36
+ if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
37
+ print("[SitePong ScreenRecorder] Upload failed with status \(httpResponse.statusCode)")
38
+ }
39
+ } catch {
40
+ // Retry once
41
+ do {
42
+ let (_, response) = try await URLSession.shared.data(for: request)
43
+ if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
44
+ print("[SitePong ScreenRecorder] Upload retry failed with status \(httpResponse.statusCode)")
45
+ }
46
+ } catch {
47
+ print("[SitePong ScreenRecorder] Upload retry failed: \(error.localizedDescription)")
48
+ // Drop the chunk — don't block recording
49
+ }
50
+ }
51
+ }
52
+ }