@shortkitsdk/react-native 0.2.0 → 0.2.2
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/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +180 -2
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
- package/ios/ShortKitBridge.swift +134 -2
- package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
- package/ios/ShortKitFeedView.swift +46 -7
- package/ios/ShortKitModule.mm +42 -0
- package/ios/ShortKitOverlayBridge.swift +23 -1
- package/ios/ShortKitPlayerNativeView.swift +186 -0
- package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
- package/ios/ShortKitWidgetNativeView.swift +168 -0
- package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
- package/package.json +1 -1
- package/src/CarouselOverlayManager.tsx +71 -0
- package/src/ShortKitContext.ts +18 -0
- package/src/ShortKitFeed.tsx +13 -0
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +161 -2
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +15 -1
- package/src/serialization.ts +16 -2
- package/src/specs/NativeShortKitModule.ts +37 -0
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +82 -3
- package/src/useShortKit.ts +5 -1
- package/src/useShortKitCarousel.ts +29 -0
- package/src/useShortKitPlayer.ts +10 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.widget.FrameLayout
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.shortkit.sdk.model.ImageCarouselItem
|
|
8
|
+
import com.shortkit.sdk.overlay.CarouselOverlay
|
|
9
|
+
import kotlinx.serialization.encodeToString
|
|
10
|
+
import kotlinx.serialization.json.Json
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A transparent [FrameLayout] that implements [CarouselOverlay] and bridges
|
|
14
|
+
* carousel overlay lifecycle calls to JS events via [ShortKitModule].
|
|
15
|
+
*
|
|
16
|
+
* The actual carousel overlay UI is rendered by React Native on the JS side
|
|
17
|
+
* through the `CarouselOverlayManager` component. This view simply relays
|
|
18
|
+
* the SDK lifecycle events so the JS carousel overlay knows when to
|
|
19
|
+
* configure, activate, reset, etc.
|
|
20
|
+
*
|
|
21
|
+
* Android equivalent of iOS `ShortKitCarouselOverlayBridge.swift`.
|
|
22
|
+
*/
|
|
23
|
+
class ShortKitCarouselOverlayBridge(context: Context) : FrameLayout(context), CarouselOverlay {
|
|
24
|
+
|
|
25
|
+
init {
|
|
26
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun configure(item: ImageCarouselItem) {
|
|
30
|
+
val json = Json.encodeToString(item)
|
|
31
|
+
val params = Arguments.createMap().apply {
|
|
32
|
+
putString("item", json)
|
|
33
|
+
}
|
|
34
|
+
ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayConfigure", params)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun resetState() {
|
|
38
|
+
ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayReset", Arguments.createMap())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun fadeOutForTransition() {
|
|
42
|
+
ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayFadeOut", Arguments.createMap())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun restoreFromTransition() {
|
|
46
|
+
ShortKitModule.shared?.emitCarouselOverlayEvent("onCarouselOverlayRestore", Arguments.createMap())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -51,12 +51,18 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
51
51
|
private var viewPager: ViewPager2? = null
|
|
52
52
|
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/** Video overlay for the currently active cell (nativeID="overlay-current"). */
|
|
55
55
|
private var currentOverlayView: View? = null
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/** Video overlay for the upcoming cell (nativeID="overlay-next"). */
|
|
58
58
|
private var nextOverlayView: View? = null
|
|
59
59
|
|
|
60
|
+
/** Carousel overlay for the currently active cell (nativeID="carousel-overlay-current"). */
|
|
61
|
+
private var currentCarouselOverlayView: View? = null
|
|
62
|
+
|
|
63
|
+
/** Carousel overlay for the upcoming cell (nativeID="carousel-overlay-next"). */
|
|
64
|
+
private var nextCarouselOverlayView: View? = null
|
|
65
|
+
|
|
60
66
|
/** The page index used for overlay transform calculations. */
|
|
61
67
|
private var currentPage: Int = 0
|
|
62
68
|
|
|
@@ -158,8 +164,12 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
158
164
|
viewPager = null
|
|
159
165
|
currentOverlayView?.translationY = 0f
|
|
160
166
|
nextOverlayView?.translationY = 0f
|
|
167
|
+
currentCarouselOverlayView?.translationY = 0f
|
|
168
|
+
nextCarouselOverlayView?.translationY = 0f
|
|
161
169
|
currentOverlayView = null
|
|
162
170
|
nextOverlayView = null
|
|
171
|
+
currentCarouselOverlayView = null
|
|
172
|
+
nextCarouselOverlayView = null
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
private fun handleScrollOffset(
|
|
@@ -193,6 +203,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
193
203
|
val h = height.toFloat()
|
|
194
204
|
currentOverlayView?.translationY = 0f
|
|
195
205
|
nextOverlayView?.translationY = h
|
|
206
|
+
currentCarouselOverlayView?.translationY = 0f
|
|
207
|
+
nextCarouselOverlayView?.translationY = h
|
|
196
208
|
}
|
|
197
209
|
pageUpdateRunnable = runnable
|
|
198
210
|
handler.postDelayed(runnable, 80)
|
|
@@ -219,17 +231,25 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
219
231
|
if (currentOverlayView == null || nextOverlayView == null) {
|
|
220
232
|
findOverlayViews()
|
|
221
233
|
}
|
|
234
|
+
if (currentCarouselOverlayView == null || nextCarouselOverlayView == null) {
|
|
235
|
+
findCarouselOverlayViews()
|
|
236
|
+
}
|
|
222
237
|
|
|
223
|
-
// Current
|
|
238
|
+
// Current overlays follow the active cell
|
|
224
239
|
currentOverlayView?.translationY = -delta
|
|
240
|
+
currentCarouselOverlayView?.translationY = -delta
|
|
225
241
|
|
|
226
|
-
// Next
|
|
242
|
+
// Next overlays: positioned one page ahead in the scroll direction
|
|
227
243
|
if (delta >= 0) {
|
|
228
244
|
// Forward scroll (or idle): next cell is below
|
|
229
|
-
|
|
245
|
+
val nextY = cellHeight - delta
|
|
246
|
+
nextOverlayView?.translationY = nextY
|
|
247
|
+
nextCarouselOverlayView?.translationY = nextY
|
|
230
248
|
} else {
|
|
231
249
|
// Backward scroll: next cell is above
|
|
232
|
-
|
|
250
|
+
val nextY = -cellHeight - delta
|
|
251
|
+
nextOverlayView?.translationY = nextY
|
|
252
|
+
nextCarouselOverlayView?.translationY = nextY
|
|
233
253
|
}
|
|
234
254
|
}
|
|
235
255
|
|
|
@@ -260,6 +280,28 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
260
280
|
}
|
|
261
281
|
}
|
|
262
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Find the sibling RN carousel overlay views by nativeID.
|
|
285
|
+
*/
|
|
286
|
+
private fun findCarouselOverlayViews() {
|
|
287
|
+
var ancestor = parent as? ViewGroup ?: return
|
|
288
|
+
while (true) {
|
|
289
|
+
for (i in 0 until ancestor.childCount) {
|
|
290
|
+
val child = ancestor.getChildAt(i)
|
|
291
|
+
if (child === this || isOwnAncestor(child)) continue
|
|
292
|
+
|
|
293
|
+
if (currentCarouselOverlayView == null) {
|
|
294
|
+
currentCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-current")
|
|
295
|
+
}
|
|
296
|
+
if (nextCarouselOverlayView == null) {
|
|
297
|
+
nextCarouselOverlayView = ReactFindViewUtil.findView(child, "carousel-overlay-next")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (currentCarouselOverlayView != null && nextCarouselOverlayView != null) return
|
|
301
|
+
ancestor = ancestor.parent as? ViewGroup ?: return
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
263
305
|
/** Check if the given view is an ancestor of this view. */
|
|
264
306
|
private fun isOwnAncestor(view: View): Boolean {
|
|
265
307
|
var current: ViewParent? = parent
|
|
@@ -5,10 +5,14 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
5
5
|
import com.facebook.react.bridge.ReactMethod
|
|
6
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
7
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
8
|
+
import com.shortkit.CarouselImage
|
|
8
9
|
import com.shortkit.ContentItem
|
|
9
10
|
import com.shortkit.ContentSignal
|
|
11
|
+
import com.shortkit.CustomFeedItem
|
|
12
|
+
import com.shortkit.ImageCarouselItem
|
|
10
13
|
import com.shortkit.FeedConfig
|
|
11
14
|
import com.shortkit.FeedHeight
|
|
15
|
+
import com.shortkit.FeedSource
|
|
12
16
|
import com.shortkit.FeedTransitionPhase
|
|
13
17
|
import com.shortkit.JsonValue
|
|
14
18
|
import com.shortkit.ShortKit
|
|
@@ -46,6 +50,8 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
46
50
|
private var listenerCount = 0
|
|
47
51
|
@Volatile
|
|
48
52
|
private var hasListeners = false
|
|
53
|
+
private var pendingFeedItems: String? = null
|
|
54
|
+
private var pendingAppendItems: String? = null
|
|
49
55
|
|
|
50
56
|
/** Expose the underlying SDK for the Fabric feed view manager. */
|
|
51
57
|
val sdk: ShortKit? get() = shortKit
|
|
@@ -89,6 +95,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
89
95
|
override fun initialize(
|
|
90
96
|
apiKey: String,
|
|
91
97
|
config: String,
|
|
98
|
+
embedId: String?,
|
|
92
99
|
clientAppName: String?,
|
|
93
100
|
clientAppVersion: String?,
|
|
94
101
|
customDimensions: String?
|
|
@@ -105,6 +112,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
105
112
|
context = context,
|
|
106
113
|
apiKey = apiKey,
|
|
107
114
|
config = feedConfig,
|
|
115
|
+
embedId = embedId,
|
|
108
116
|
userId = null,
|
|
109
117
|
adProvider = null,
|
|
110
118
|
clientAppName = clientAppName,
|
|
@@ -114,7 +122,27 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
114
122
|
this.shortKit = sdk
|
|
115
123
|
shared = this
|
|
116
124
|
|
|
125
|
+
// Replay any feed items that arrived before initialization
|
|
126
|
+
pendingFeedItems?.let { json ->
|
|
127
|
+
parseCustomFeedItems(json)?.let { sdk.setFeedItems(it) }
|
|
128
|
+
pendingFeedItems = null
|
|
129
|
+
}
|
|
130
|
+
pendingAppendItems?.let { json ->
|
|
131
|
+
parseCustomFeedItems(json)?.let { sdk.appendFeedItems(it) }
|
|
132
|
+
pendingAppendItems = null
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
subscribeToFlows(sdk.player)
|
|
136
|
+
|
|
137
|
+
sdk.delegate = object : com.shortkit.ShortKitDelegate {
|
|
138
|
+
override fun onContentTapped(contentId: String, index: Int) {
|
|
139
|
+
val params = Arguments.createMap().apply {
|
|
140
|
+
putString("contentId", contentId)
|
|
141
|
+
putInt("index", index)
|
|
142
|
+
}
|
|
143
|
+
sendEvent("onContentTapped", params)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
@ReactMethod
|
|
@@ -211,6 +239,53 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
211
239
|
shortKit?.player?.setMaxBitrate(bitrate.toInt())
|
|
212
240
|
}
|
|
213
241
|
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// Custom feed
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
@ReactMethod
|
|
247
|
+
override fun setFeedItems(items: String) {
|
|
248
|
+
val sdk = shortKit
|
|
249
|
+
if (sdk != null) {
|
|
250
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
251
|
+
sdk.setFeedItems(parsed)
|
|
252
|
+
} else {
|
|
253
|
+
pendingFeedItems = items
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@ReactMethod
|
|
258
|
+
override fun appendFeedItems(items: String) {
|
|
259
|
+
val sdk = shortKit
|
|
260
|
+
if (sdk != null) {
|
|
261
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
262
|
+
sdk.appendFeedItems(parsed)
|
|
263
|
+
} else {
|
|
264
|
+
pendingAppendItems = items
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@ReactMethod
|
|
269
|
+
override fun fetchContent(limit: Double, promise: com.facebook.react.bridge.Promise) {
|
|
270
|
+
val sdk = shortKit
|
|
271
|
+
if (sdk == null) {
|
|
272
|
+
promise.resolve("[]")
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
scope?.launch {
|
|
276
|
+
try {
|
|
277
|
+
val items = sdk.fetchContent(limit.toInt())
|
|
278
|
+
val arr = JSONArray()
|
|
279
|
+
for (item in items) {
|
|
280
|
+
arr.put(JSONObject(serializeContentItemToJSON(item)))
|
|
281
|
+
}
|
|
282
|
+
promise.resolve(arr.toString())
|
|
283
|
+
} catch (e: Exception) {
|
|
284
|
+
promise.resolve("[]")
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
214
289
|
// -----------------------------------------------------------------------
|
|
215
290
|
// Overlay lifecycle events (called by Fabric view)
|
|
216
291
|
// -----------------------------------------------------------------------
|
|
@@ -226,6 +301,14 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
226
301
|
sendEvent(name, params)
|
|
227
302
|
}
|
|
228
303
|
|
|
304
|
+
// -----------------------------------------------------------------------
|
|
305
|
+
// Carousel overlay lifecycle events
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
fun emitCarouselOverlayEvent(name: String, params: WritableMap) {
|
|
309
|
+
sendEvent(name, params)
|
|
310
|
+
}
|
|
311
|
+
|
|
229
312
|
// -----------------------------------------------------------------------
|
|
230
313
|
// Flow subscriptions
|
|
231
314
|
// -----------------------------------------------------------------------
|
|
@@ -385,6 +468,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
385
468
|
sendEvent("onPrefetchedAheadCountChanged", params)
|
|
386
469
|
}
|
|
387
470
|
}
|
|
471
|
+
|
|
472
|
+
// Remaining content count
|
|
473
|
+
newScope.launch {
|
|
474
|
+
player.remainingContentCount.collect { count ->
|
|
475
|
+
val params = Arguments.createMap().apply {
|
|
476
|
+
putInt("count", count)
|
|
477
|
+
}
|
|
478
|
+
sendEvent("onRemainingContentCountChanged", params)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
388
481
|
}
|
|
389
482
|
|
|
390
483
|
// -----------------------------------------------------------------------
|
|
@@ -425,6 +518,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
425
518
|
private fun contentItemMap(item: ContentItem): WritableMap {
|
|
426
519
|
return Arguments.createMap().apply {
|
|
427
520
|
putString("id", item.id)
|
|
521
|
+
item.playbackId?.let { putString("playbackId", it) }
|
|
428
522
|
putString("title", item.title)
|
|
429
523
|
item.description?.let { putString("description", it) }
|
|
430
524
|
putDouble("duration", item.duration)
|
|
@@ -575,13 +669,19 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
575
669
|
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
576
670
|
val videoOverlay = parseVideoOverlay(obj.optString("overlay", null))
|
|
577
671
|
|
|
672
|
+
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
673
|
+
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
674
|
+
|
|
675
|
+
val carouselOverlay = parseCarouselOverlay(obj.optString("carouselOverlay", null))
|
|
676
|
+
|
|
578
677
|
FeedConfig(
|
|
579
678
|
feedHeight = feedHeight,
|
|
580
679
|
videoOverlay = videoOverlay,
|
|
581
|
-
carouselOverlay =
|
|
680
|
+
carouselOverlay = carouselOverlay,
|
|
582
681
|
surveyOverlay = SurveyOverlayMode.None,
|
|
583
682
|
adOverlay = AdOverlayMode.None,
|
|
584
|
-
muteOnStart = muteOnStart
|
|
683
|
+
muteOnStart = muteOnStart,
|
|
684
|
+
feedSource = feedSource
|
|
585
685
|
)
|
|
586
686
|
} catch (_: Exception) {
|
|
587
687
|
FeedConfig()
|
|
@@ -644,6 +744,84 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
644
744
|
}
|
|
645
745
|
}
|
|
646
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Parse a double-stringified carousel overlay JSON.
|
|
749
|
+
* - `"\"none\""` -> None
|
|
750
|
+
* - `"{\"type\":\"custom\"}"` -> Custom with bridge overlay factory
|
|
751
|
+
*/
|
|
752
|
+
private fun parseCarouselOverlay(json: String?): CarouselOverlayMode {
|
|
753
|
+
if (json.isNullOrEmpty()) return CarouselOverlayMode.None
|
|
754
|
+
return try {
|
|
755
|
+
val parsed = json.trim()
|
|
756
|
+
|
|
757
|
+
if (parsed == "\"none\"" || parsed == "none") {
|
|
758
|
+
return CarouselOverlayMode.None
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
|
|
762
|
+
JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
|
|
763
|
+
} else {
|
|
764
|
+
JSONObject(parsed)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (inner.optString("type") == "custom") {
|
|
768
|
+
CarouselOverlayMode.Custom { ShortKitCarouselOverlayBridge(reactApplicationContext) }
|
|
769
|
+
} else {
|
|
770
|
+
CarouselOverlayMode.None
|
|
771
|
+
}
|
|
772
|
+
} catch (_: Exception) {
|
|
773
|
+
CarouselOverlayMode.None
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private fun parseCustomFeedItems(json: String): List<CustomFeedItem>? {
|
|
778
|
+
return try {
|
|
779
|
+
val arr = JSONArray(json)
|
|
780
|
+
val result = mutableListOf<CustomFeedItem>()
|
|
781
|
+
for (i in 0 until arr.length()) {
|
|
782
|
+
val obj = arr.getJSONObject(i)
|
|
783
|
+
when (obj.optString("type")) {
|
|
784
|
+
"video" -> {
|
|
785
|
+
val playbackId = obj.optString("playbackId", null) ?: continue
|
|
786
|
+
result.add(CustomFeedItem.Video(playbackId))
|
|
787
|
+
}
|
|
788
|
+
"imageCarousel" -> {
|
|
789
|
+
val itemObj = obj.optJSONObject("item") ?: continue
|
|
790
|
+
val carouselItem = parseImageCarouselItem(itemObj) ?: continue
|
|
791
|
+
result.add(CustomFeedItem.ImageCarousel(carouselItem))
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
result.ifEmpty { null }
|
|
796
|
+
} catch (_: Exception) {
|
|
797
|
+
null
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
|
|
802
|
+
val id = obj.optString("id", null) ?: return null
|
|
803
|
+
val imagesArr = obj.optJSONArray("images") ?: return null
|
|
804
|
+
val images = mutableListOf<CarouselImage>()
|
|
805
|
+
for (i in 0 until imagesArr.length()) {
|
|
806
|
+
val imgObj = imagesArr.getJSONObject(i)
|
|
807
|
+
images.add(CarouselImage(
|
|
808
|
+
url = imgObj.getString("url"),
|
|
809
|
+
alt = imgObj.optString("alt", null)
|
|
810
|
+
))
|
|
811
|
+
}
|
|
812
|
+
return ImageCarouselItem(
|
|
813
|
+
id = id,
|
|
814
|
+
images = images,
|
|
815
|
+
autoScrollInterval = if (obj.has("autoScrollInterval")) obj.getDouble("autoScrollInterval") else null,
|
|
816
|
+
caption = obj.optString("caption", null),
|
|
817
|
+
title = obj.optString("title", null),
|
|
818
|
+
description = obj.optString("description", null),
|
|
819
|
+
author = obj.optString("author", null),
|
|
820
|
+
section = obj.optString("section", null),
|
|
821
|
+
articleUrl = obj.optString("articleUrl", null)
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
647
825
|
/**
|
|
648
826
|
* Parse optional custom dimensions JSON string into map.
|
|
649
827
|
*/
|
|
@@ -2,6 +2,8 @@ package com.shortkit.reactnative
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Color
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
5
7
|
import android.view.GestureDetector
|
|
6
8
|
import android.view.MotionEvent
|
|
7
9
|
import android.widget.FrameLayout
|
|
@@ -32,6 +34,15 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
|
|
|
32
34
|
*/
|
|
33
35
|
private var currentItem: ContentItem? = null
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Deferred configure emission — cancelled if [activatePlayback] fires
|
|
39
|
+
* on the same message-queue iteration (handleSwipe re-configure of the
|
|
40
|
+
* active cell, not a prefetch for the next cell).
|
|
41
|
+
*/
|
|
42
|
+
private var pendingConfigureRunnable: Runnable? = null
|
|
43
|
+
|
|
44
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
45
|
+
|
|
35
46
|
// -----------------------------------------------------------------------
|
|
36
47
|
// Gestures
|
|
37
48
|
// -----------------------------------------------------------------------
|
|
@@ -76,7 +87,18 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
|
|
|
76
87
|
|
|
77
88
|
override fun configure(item: ContentItem) {
|
|
78
89
|
currentItem = item
|
|
79
|
-
|
|
90
|
+
|
|
91
|
+
// Defer the configure event by one message-queue tick. If activatePlayback()
|
|
92
|
+
// fires before then (handleSwipe sequence: configure → reset → activate),
|
|
93
|
+
// the event is cancelled — preventing nextItem from being overwritten
|
|
94
|
+
// with the current cell's data.
|
|
95
|
+
pendingConfigureRunnable?.let { handler.removeCallbacks(it) }
|
|
96
|
+
val runnable = Runnable {
|
|
97
|
+
currentItem?.let { ShortKitModule.shared?.emitOverlayEvent("onOverlayConfigure", it) }
|
|
98
|
+
pendingConfigureRunnable = null
|
|
99
|
+
}
|
|
100
|
+
pendingConfigureRunnable = runnable
|
|
101
|
+
handler.post(runnable)
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
override fun resetPlaybackProgress() {
|
|
@@ -85,6 +107,11 @@ class ShortKitOverlayBridge(context: Context) : FrameLayout(context), FeedOverla
|
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
override fun activatePlayback() {
|
|
110
|
+
// Cancel the pending configure — it was for the current cell (part of
|
|
111
|
+
// handleSwipe), not a prefetch for the next cell.
|
|
112
|
+
pendingConfigureRunnable?.let { handler.removeCallbacks(it) }
|
|
113
|
+
pendingConfigureRunnable = null
|
|
114
|
+
|
|
88
115
|
val item = currentItem ?: return
|
|
89
116
|
ShortKitModule.shared?.emitOverlayEvent("onOverlayActivate", item)
|
|
90
117
|
}
|
|
@@ -35,6 +35,10 @@ class ShortKitPackage : TurboReactPackage() {
|
|
|
35
35
|
override fun createViewManagers(
|
|
36
36
|
reactContext: ReactApplicationContext
|
|
37
37
|
): List<ViewManager<*, *>> {
|
|
38
|
-
return listOf(
|
|
38
|
+
return listOf(
|
|
39
|
+
ShortKitFeedViewManager(),
|
|
40
|
+
ShortKitPlayerViewManager(),
|
|
41
|
+
ShortKitWidgetViewManager(),
|
|
42
|
+
)
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import com.shortkit.sdk.config.PlayerClickAction
|
|
6
|
+
import com.shortkit.sdk.config.PlayerConfig
|
|
7
|
+
import com.shortkit.sdk.config.VideoOverlayMode
|
|
8
|
+
import com.shortkit.sdk.model.ContentItem
|
|
9
|
+
import com.shortkit.sdk.player.ShortKitPlayerView
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fabric native view wrapping [ShortKitPlayerView] for use as a
|
|
14
|
+
* single-video player in React Native.
|
|
15
|
+
*/
|
|
16
|
+
class ShortKitPlayerNativeView(context: Context) : FrameLayout(context) {
|
|
17
|
+
|
|
18
|
+
private var playerView: ShortKitPlayerView? = null
|
|
19
|
+
private var configJson: String? = null
|
|
20
|
+
private var contentItemJson: String? = null
|
|
21
|
+
|
|
22
|
+
var config: String?
|
|
23
|
+
get() = configJson
|
|
24
|
+
set(value) {
|
|
25
|
+
if (value == configJson) return
|
|
26
|
+
configJson = value
|
|
27
|
+
rebuildIfNeeded()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var contentItem: String?
|
|
31
|
+
get() = contentItemJson
|
|
32
|
+
set(value) {
|
|
33
|
+
if (value == contentItemJson) return
|
|
34
|
+
contentItemJson = value
|
|
35
|
+
applyContentItem()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var active: Boolean = true
|
|
39
|
+
set(value) {
|
|
40
|
+
if (field == value) return
|
|
41
|
+
field = value
|
|
42
|
+
applyActive()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onAttachedToWindow() {
|
|
46
|
+
super.onAttachedToWindow()
|
|
47
|
+
rebuildIfNeeded()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onDetachedFromWindow() {
|
|
51
|
+
playerView?.deactivate()
|
|
52
|
+
super.onDetachedFromWindow()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun rebuildIfNeeded() {
|
|
56
|
+
if (playerView != null) return
|
|
57
|
+
|
|
58
|
+
val sdk = ShortKitModule.shared?.sdk ?: return
|
|
59
|
+
val playerConfig = parsePlayerConfig(configJson)
|
|
60
|
+
|
|
61
|
+
val view = ShortKitPlayerView(context).apply {
|
|
62
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
63
|
+
}
|
|
64
|
+
view.initialize(sdk, playerConfig)
|
|
65
|
+
addView(view)
|
|
66
|
+
playerView = view
|
|
67
|
+
|
|
68
|
+
applyContentItem()
|
|
69
|
+
if (active) view.activate()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun applyContentItem() {
|
|
73
|
+
val json = contentItemJson ?: return
|
|
74
|
+
val view = playerView ?: return
|
|
75
|
+
val item = parseContentItem(json) ?: return
|
|
76
|
+
view.configure(item)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun applyActive() {
|
|
80
|
+
val view = playerView ?: return
|
|
81
|
+
if (active) view.activate() else view.deactivate()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private fun parsePlayerConfig(json: String?): PlayerConfig {
|
|
85
|
+
if (json.isNullOrEmpty()) return PlayerConfig()
|
|
86
|
+
return try {
|
|
87
|
+
val obj = JSONObject(json)
|
|
88
|
+
PlayerConfig(
|
|
89
|
+
cornerRadius = obj.optDouble("cornerRadius", 12.0).toFloat(),
|
|
90
|
+
clickAction = when (obj.optString("clickAction", "feed")) {
|
|
91
|
+
"feed" -> PlayerClickAction.FEED
|
|
92
|
+
"mute" -> PlayerClickAction.MUTE
|
|
93
|
+
"none" -> PlayerClickAction.NONE
|
|
94
|
+
else -> PlayerClickAction.FEED
|
|
95
|
+
},
|
|
96
|
+
autoplay = obj.optBoolean("autoplay", true),
|
|
97
|
+
loop = obj.optBoolean("loop", true),
|
|
98
|
+
muteOnStart = obj.optBoolean("muteOnStart", true),
|
|
99
|
+
videoOverlay = parseOverlay(obj),
|
|
100
|
+
)
|
|
101
|
+
} catch (_: Exception) {
|
|
102
|
+
PlayerConfig()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun parseOverlay(obj: JSONObject): VideoOverlayMode {
|
|
107
|
+
val overlay = obj.opt("overlay") ?: return VideoOverlayMode.None
|
|
108
|
+
if (overlay is JSONObject && overlay.optString("type") == "custom") {
|
|
109
|
+
return VideoOverlayMode.Custom {
|
|
110
|
+
ShortKitOverlayBridge(context)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return VideoOverlayMode.None
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun parseContentItem(json: String): ContentItem? {
|
|
117
|
+
return try {
|
|
118
|
+
val obj = JSONObject(json)
|
|
119
|
+
ContentItem(
|
|
120
|
+
id = obj.getString("id"),
|
|
121
|
+
title = obj.getString("title"),
|
|
122
|
+
description = obj.optString("description", null),
|
|
123
|
+
duration = obj.getDouble("duration"),
|
|
124
|
+
streamingUrl = obj.getString("streamingUrl"),
|
|
125
|
+
thumbnailUrl = obj.getString("thumbnailUrl"),
|
|
126
|
+
captionTracks = emptyList(),
|
|
127
|
+
customMetadata = null,
|
|
128
|
+
author = obj.optString("author", null),
|
|
129
|
+
articleUrl = obj.optString("articleUrl", null),
|
|
130
|
+
commentCount = if (obj.has("commentCount")) obj.getInt("commentCount") else null,
|
|
131
|
+
)
|
|
132
|
+
} catch (_: Exception) {
|
|
133
|
+
null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = ShortKitPlayerViewManager.REACT_CLASS)
|
|
9
|
+
class ShortKitPlayerViewManager : SimpleViewManager<ShortKitPlayerNativeView>() {
|
|
10
|
+
|
|
11
|
+
override fun getName(): String = REACT_CLASS
|
|
12
|
+
|
|
13
|
+
override fun createViewInstance(context: ThemedReactContext): ShortKitPlayerNativeView {
|
|
14
|
+
return ShortKitPlayerNativeView(context)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@ReactProp(name = "config")
|
|
18
|
+
fun setConfig(view: ShortKitPlayerNativeView, config: String?) {
|
|
19
|
+
view.config = config
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@ReactProp(name = "contentItem")
|
|
23
|
+
fun setContentItem(view: ShortKitPlayerNativeView, contentItem: String?) {
|
|
24
|
+
view.contentItem = contentItem
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@ReactProp(name = "active", defaultBoolean = true)
|
|
28
|
+
fun setActive(view: ShortKitPlayerNativeView, active: Boolean) {
|
|
29
|
+
view.active = active
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
const val REACT_CLASS = "ShortKitPlayerView"
|
|
34
|
+
}
|
|
35
|
+
}
|