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.
- 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 +187 -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,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
|
+
}
|