@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.
Files changed (34) hide show
  1. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +48 -0
  2. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +48 -6
  3. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +180 -2
  4. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +28 -1
  5. package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +5 -1
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +136 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerViewManager.kt +35 -0
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +133 -0
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetViewManager.kt +30 -0
  10. package/ios/ShortKitBridge.swift +134 -2
  11. package/ios/ShortKitCarouselOverlayBridge.swift +54 -0
  12. package/ios/ShortKitFeedView.swift +46 -7
  13. package/ios/ShortKitModule.mm +42 -0
  14. package/ios/ShortKitOverlayBridge.swift +23 -1
  15. package/ios/ShortKitPlayerNativeView.swift +186 -0
  16. package/ios/ShortKitPlayerNativeViewManager.mm +28 -0
  17. package/ios/ShortKitWidgetNativeView.swift +168 -0
  18. package/ios/ShortKitWidgetNativeViewManager.mm +27 -0
  19. package/package.json +1 -1
  20. package/src/CarouselOverlayManager.tsx +71 -0
  21. package/src/ShortKitContext.ts +18 -0
  22. package/src/ShortKitFeed.tsx +13 -0
  23. package/src/ShortKitPlayer.tsx +61 -0
  24. package/src/ShortKitProvider.tsx +161 -2
  25. package/src/ShortKitWidget.tsx +63 -0
  26. package/src/index.ts +15 -1
  27. package/src/serialization.ts +16 -2
  28. package/src/specs/NativeShortKitModule.ts +37 -0
  29. package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
  30. package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
  31. package/src/types.ts +82 -3
  32. package/src/useShortKit.ts +5 -1
  33. package/src/useShortKitCarousel.ts +29 -0
  34. 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
- /** Overlay for the currently active cell (nativeID="overlay-current"). */
54
+ /** Video overlay for the currently active cell (nativeID="overlay-current"). */
55
55
  private var currentOverlayView: View? = null
56
56
 
57
- /** Overlay for the upcoming cell (nativeID="overlay-next"). */
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 overlay follows the active cell
238
+ // Current overlays follow the active cell
224
239
  currentOverlayView?.translationY = -delta
240
+ currentCarouselOverlayView?.translationY = -delta
225
241
 
226
- // Next overlay: positioned one page ahead in the scroll direction
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
- nextOverlayView?.translationY = cellHeight - delta
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
- nextOverlayView?.translationY = -cellHeight - delta
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 = CarouselOverlayMode.None,
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
- ShortKitModule.shared?.emitOverlayEvent("onOverlayConfigure", item)
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(ShortKitFeedViewManager())
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
+ }