@shortkitsdk/react-native 0.1.0 → 0.2.1
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/ShortKitFeedView.kt +74 -19
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +107 -95
- 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 +81 -47
- package/ios/ShortKitFeedView.swift +67 -20
- package/ios/ShortKitModule.mm +22 -5
- 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/ShortKitContext.ts +5 -0
- package/src/ShortKitFeed.tsx +5 -50
- package/src/ShortKitPlayer.tsx +61 -0
- package/src/ShortKitProvider.tsx +50 -4
- package/src/ShortKitWidget.tsx +63 -0
- package/src/index.ts +12 -0
- package/src/serialization.ts +10 -0
- package/src/specs/NativeShortKitModule.ts +18 -25
- package/src/specs/ShortKitPlayerViewNativeComponent.ts +13 -0
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +12 -0
- package/src/types.ts +78 -5
- package/src/useShortKit.ts +5 -1
|
@@ -2,6 +2,8 @@ package com.shortkit.reactnative
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.content.ContextWrapper
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
5
7
|
import android.util.Log
|
|
6
8
|
import android.view.View
|
|
7
9
|
import android.view.ViewGroup
|
|
@@ -36,6 +38,7 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
36
38
|
|
|
37
39
|
private var feedFragment: ShortKitFeedFragment? = null
|
|
38
40
|
private var fragmentContainerId: Int = View.generateViewId()
|
|
41
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
39
42
|
|
|
40
43
|
init {
|
|
41
44
|
id = fragmentContainerId
|
|
@@ -54,9 +57,12 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
54
57
|
/** Overlay for the upcoming cell (nativeID="overlay-next"). */
|
|
55
58
|
private var nextOverlayView: View? = null
|
|
56
59
|
|
|
57
|
-
/** The page index
|
|
60
|
+
/** The page index used for overlay transform calculations. */
|
|
58
61
|
private var currentPage: Int = 0
|
|
59
62
|
|
|
63
|
+
/** Deferred page update to avoid flashing stale metadata. */
|
|
64
|
+
private var pageUpdateRunnable: Runnable? = null
|
|
65
|
+
|
|
60
66
|
// -----------------------------------------------------------------------
|
|
61
67
|
// Lifecycle
|
|
62
68
|
// -----------------------------------------------------------------------
|
|
@@ -145,6 +151,8 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
private fun teardownScrollTracking() {
|
|
154
|
+
pageUpdateRunnable?.let { handler.removeCallbacks(it) }
|
|
155
|
+
pageUpdateRunnable = null
|
|
148
156
|
pageChangeCallback?.let { viewPager?.unregisterOnPageChangeCallback(it) }
|
|
149
157
|
pageChangeCallback = null
|
|
150
158
|
viewPager = null
|
|
@@ -162,6 +170,41 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
162
170
|
val cellHeight = height.toFloat()
|
|
163
171
|
if (cellHeight <= 0) return
|
|
164
172
|
|
|
173
|
+
// Detect page change, but DEFER updating currentPage.
|
|
174
|
+
//
|
|
175
|
+
// Why: when the scroll settles on a new page, overlay-current still
|
|
176
|
+
// shows the OLD page's metadata (React hasn't processed OVERLAY_ACTIVATE
|
|
177
|
+
// yet). If we update currentPage immediately, delta snaps to 0 and
|
|
178
|
+
// overlay-current becomes visible with stale data.
|
|
179
|
+
//
|
|
180
|
+
// By deferring ~80ms, overlay-next (which already shows the correct
|
|
181
|
+
// data via NextOverlayProvider) stays visible at y=0 while React
|
|
182
|
+
// processes the state update.
|
|
183
|
+
if (positionOffset == 0f) {
|
|
184
|
+
if (position != currentPage && pageUpdateRunnable == null) {
|
|
185
|
+
val targetPage = position
|
|
186
|
+
val runnable = Runnable {
|
|
187
|
+
currentPage = targetPage
|
|
188
|
+
pageUpdateRunnable = null
|
|
189
|
+
// Reapply overlay transforms now that currentPage is updated.
|
|
190
|
+
// Without this, overlay-next (static NextOverlayProvider state)
|
|
191
|
+
// stays visible at y=0 while overlay-current (live state) stays
|
|
192
|
+
// hidden — no scroll event fires to trigger handleScrollOffset.
|
|
193
|
+
val h = height.toFloat()
|
|
194
|
+
currentOverlayView?.translationY = 0f
|
|
195
|
+
nextOverlayView?.translationY = h
|
|
196
|
+
}
|
|
197
|
+
pageUpdateRunnable = runnable
|
|
198
|
+
handler.postDelayed(runnable, 80)
|
|
199
|
+
}
|
|
200
|
+
} else if (pageUpdateRunnable != null) {
|
|
201
|
+
// User is scrolling again — apply pending update immediately
|
|
202
|
+
// so transforms stay aligned for the new gesture.
|
|
203
|
+
handler.removeCallbacks(pageUpdateRunnable!!)
|
|
204
|
+
pageUpdateRunnable = null
|
|
205
|
+
currentPage = position
|
|
206
|
+
}
|
|
207
|
+
|
|
165
208
|
// positionOffset is 0.0 when settled, approaches 1.0 during forward scroll.
|
|
166
209
|
// position is the index of the page currently filling most of the screen.
|
|
167
210
|
val delta: Float = if (position >= currentPage) {
|
|
@@ -172,11 +215,6 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
172
215
|
-(cellHeight - positionOffsetPixels.toFloat())
|
|
173
216
|
}
|
|
174
217
|
|
|
175
|
-
// Update currentPage when the scroll settles
|
|
176
|
-
if (positionOffset == 0f) {
|
|
177
|
-
currentPage = position
|
|
178
|
-
}
|
|
179
|
-
|
|
180
218
|
// Find the overlay views if not cached
|
|
181
219
|
if (currentOverlayView == null || nextOverlayView == null) {
|
|
182
220
|
findOverlayViews()
|
|
@@ -196,23 +234,40 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
/**
|
|
199
|
-
* Find the sibling RN overlay views by nativeID.
|
|
200
|
-
*
|
|
201
|
-
*
|
|
237
|
+
* Find the sibling RN overlay views by nativeID.
|
|
238
|
+
*
|
|
239
|
+
* In Fabric interop, this view may be wrapped in an intermediate
|
|
240
|
+
* ViewGroup, so `getParent()` may not be the React `<View>` container
|
|
241
|
+
* from ShortKitFeed.tsx. We walk up the ancestor chain until we find
|
|
242
|
+
* the overlays.
|
|
202
243
|
*/
|
|
203
244
|
private fun findOverlayViews() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
currentOverlayView
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
nextOverlayView
|
|
245
|
+
var ancestor = parent as? ViewGroup ?: return
|
|
246
|
+
while (true) {
|
|
247
|
+
for (i in 0 until ancestor.childCount) {
|
|
248
|
+
val child = ancestor.getChildAt(i)
|
|
249
|
+
if (child === this || isOwnAncestor(child)) continue
|
|
250
|
+
|
|
251
|
+
if (currentOverlayView == null) {
|
|
252
|
+
currentOverlayView = ReactFindViewUtil.findView(child, "overlay-current")
|
|
253
|
+
}
|
|
254
|
+
if (nextOverlayView == null) {
|
|
255
|
+
nextOverlayView = ReactFindViewUtil.findView(child, "overlay-next")
|
|
256
|
+
}
|
|
214
257
|
}
|
|
258
|
+
if (currentOverlayView != null && nextOverlayView != null) return
|
|
259
|
+
ancestor = ancestor.parent as? ViewGroup ?: return
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Check if the given view is an ancestor of this view. */
|
|
264
|
+
private fun isOwnAncestor(view: View): Boolean {
|
|
265
|
+
var current: ViewParent? = parent
|
|
266
|
+
while (current != null) {
|
|
267
|
+
if (current === view) return true
|
|
268
|
+
current = current.parent
|
|
215
269
|
}
|
|
270
|
+
return false
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
/** Recursively find the first [ViewPager2] in a view hierarchy. */
|
|
@@ -5,18 +5,18 @@ 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
|
-
import com.shortkit.OverlayActionDelegate
|
|
15
18
|
import com.shortkit.ShortKit
|
|
16
|
-
import com.shortkit.ShortKitDelegate
|
|
17
|
-
import com.shortkit.ShortKitError
|
|
18
19
|
import com.shortkit.ShortKitPlayer
|
|
19
|
-
import com.shortkit.SurveyOption
|
|
20
20
|
import com.shortkit.VideoOverlayMode
|
|
21
21
|
import com.shortkit.CarouselOverlayMode
|
|
22
22
|
import com.shortkit.SurveyOverlayMode
|
|
@@ -30,8 +30,7 @@ import org.json.JSONArray
|
|
|
30
30
|
import org.json.JSONObject
|
|
31
31
|
|
|
32
32
|
class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
33
|
-
NativeShortKitModuleSpec(reactContext)
|
|
34
|
-
ShortKitDelegate {
|
|
33
|
+
NativeShortKitModuleSpec(reactContext) {
|
|
35
34
|
|
|
36
35
|
companion object {
|
|
37
36
|
const val NAME = "ShortKitModule"
|
|
@@ -94,6 +93,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
94
93
|
override fun initialize(
|
|
95
94
|
apiKey: String,
|
|
96
95
|
config: String,
|
|
96
|
+
embedId: String?,
|
|
97
97
|
clientAppName: String?,
|
|
98
98
|
clientAppVersion: String?,
|
|
99
99
|
customDimensions: String?
|
|
@@ -110,19 +110,27 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
110
110
|
context = context,
|
|
111
111
|
apiKey = apiKey,
|
|
112
112
|
config = feedConfig,
|
|
113
|
+
embedId = embedId,
|
|
113
114
|
userId = null,
|
|
114
115
|
adProvider = null,
|
|
115
116
|
clientAppName = clientAppName,
|
|
116
117
|
clientAppVersion = clientAppVersion,
|
|
117
118
|
customDimensions = dims
|
|
118
119
|
)
|
|
119
|
-
sdk.delegate = this
|
|
120
|
-
sdk.overlayActionDelegate = overlayDelegate
|
|
121
|
-
|
|
122
120
|
this.shortKit = sdk
|
|
123
121
|
shared = this
|
|
124
122
|
|
|
125
123
|
subscribeToFlows(sdk.player)
|
|
124
|
+
|
|
125
|
+
sdk.delegate = object : com.shortkit.ShortKitDelegate {
|
|
126
|
+
override fun onContentTapped(contentId: String, index: Int) {
|
|
127
|
+
val params = Arguments.createMap().apply {
|
|
128
|
+
putString("contentId", contentId)
|
|
129
|
+
putInt("index", index)
|
|
130
|
+
}
|
|
131
|
+
sendEvent("onContentTapped", params)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
@ReactMethod
|
|
@@ -220,98 +228,39 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
// -----------------------------------------------------------------------
|
|
223
|
-
//
|
|
231
|
+
// Custom feed
|
|
224
232
|
// -----------------------------------------------------------------------
|
|
225
233
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
putString("code", "network_error")
|
|
231
|
-
putString("message", error.cause?.localizedMessage ?: "Network error")
|
|
232
|
-
}
|
|
233
|
-
is ShortKitError.ApiError -> {
|
|
234
|
-
putString("code", "api_error")
|
|
235
|
-
putString("message", error.message ?: "API error")
|
|
236
|
-
}
|
|
237
|
-
is ShortKitError.PlayerError -> {
|
|
238
|
-
putString("code", error.code)
|
|
239
|
-
putString("message", error.message ?: "Player error")
|
|
240
|
-
}
|
|
241
|
-
is ShortKitError.ConfigError -> {
|
|
242
|
-
putString("code", "config_error")
|
|
243
|
-
putString("message", error.message ?: "Config error")
|
|
244
|
-
}
|
|
245
|
-
is ShortKitError.AuthError -> {
|
|
246
|
-
putString("code", "auth_error")
|
|
247
|
-
putString("message", "Invalid API key")
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
sendEvent("onError", params)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
override fun onShareTapped(item: ContentItem) {
|
|
255
|
-
val params = Arguments.createMap().apply {
|
|
256
|
-
putString("item", serializeContentItemToJSON(item))
|
|
257
|
-
}
|
|
258
|
-
sendEvent("onShareTapped", params)
|
|
234
|
+
@ReactMethod
|
|
235
|
+
override fun setFeedItems(items: String) {
|
|
236
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
237
|
+
shortKit?.setFeedItems(parsed)
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
putString("optionText", option.text)
|
|
266
|
-
}
|
|
267
|
-
sendEvent("onSurveyResponse", params)
|
|
240
|
+
@ReactMethod
|
|
241
|
+
override fun appendFeedItems(items: String) {
|
|
242
|
+
val parsed = parseCustomFeedItems(items) ?: return
|
|
243
|
+
shortKit?.appendFeedItems(parsed)
|
|
268
244
|
}
|
|
269
245
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
override fun onShareTapped(item: ContentItem) {
|
|
290
|
-
val params = Arguments.createMap().apply {
|
|
291
|
-
putString("item", serializeContentItemToJSON(item))
|
|
292
|
-
}
|
|
293
|
-
sendEvent("onOverlayShareTapped", params)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
override fun onCommentTapped(item: ContentItem) {
|
|
297
|
-
val params = Arguments.createMap().apply {
|
|
298
|
-
putString("item", serializeContentItemToJSON(item))
|
|
299
|
-
}
|
|
300
|
-
sendEvent("onCommentTapped", params)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
override fun onSaveTapped(item: ContentItem) {
|
|
304
|
-
val params = Arguments.createMap().apply {
|
|
305
|
-
putString("item", serializeContentItemToJSON(item))
|
|
306
|
-
}
|
|
307
|
-
sendEvent("onSaveTapped", params)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
override fun onLikeTapped(item: ContentItem) {
|
|
311
|
-
val params = Arguments.createMap().apply {
|
|
312
|
-
putString("item", serializeContentItemToJSON(item))
|
|
246
|
+
@ReactMethod
|
|
247
|
+
override fun fetchContent(limit: Double, promise: com.facebook.react.bridge.Promise) {
|
|
248
|
+
val sdk = shortKit
|
|
249
|
+
if (sdk == null) {
|
|
250
|
+
promise.resolve("[]")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
scope?.launch {
|
|
254
|
+
try {
|
|
255
|
+
val items = sdk.fetchContent(limit.toInt())
|
|
256
|
+
val arr = JSONArray()
|
|
257
|
+
for (item in items) {
|
|
258
|
+
arr.put(JSONObject(serializeContentItemToJSON(item)))
|
|
259
|
+
}
|
|
260
|
+
promise.resolve(arr.toString())
|
|
261
|
+
} catch (e: Exception) {
|
|
262
|
+
promise.resolve("[]")
|
|
313
263
|
}
|
|
314
|
-
sendEvent("onLikeTapped", params)
|
|
315
264
|
}
|
|
316
265
|
}
|
|
317
266
|
|
|
@@ -489,6 +438,16 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
489
438
|
sendEvent("onPrefetchedAheadCountChanged", params)
|
|
490
439
|
}
|
|
491
440
|
}
|
|
441
|
+
|
|
442
|
+
// Remaining content count
|
|
443
|
+
newScope.launch {
|
|
444
|
+
player.remainingContentCount.collect { count ->
|
|
445
|
+
val params = Arguments.createMap().apply {
|
|
446
|
+
putInt("count", count)
|
|
447
|
+
}
|
|
448
|
+
sendEvent("onRemainingContentCountChanged", params)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
492
451
|
}
|
|
493
452
|
|
|
494
453
|
// -----------------------------------------------------------------------
|
|
@@ -529,6 +488,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
529
488
|
private fun contentItemMap(item: ContentItem): WritableMap {
|
|
530
489
|
return Arguments.createMap().apply {
|
|
531
490
|
putString("id", item.id)
|
|
491
|
+
item.playbackId?.let { putString("playbackId", it) }
|
|
532
492
|
putString("title", item.title)
|
|
533
493
|
item.description?.let { putString("description", it) }
|
|
534
494
|
putDouble("duration", item.duration)
|
|
@@ -679,13 +639,17 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
679
639
|
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
680
640
|
val videoOverlay = parseVideoOverlay(obj.optString("overlay", null))
|
|
681
641
|
|
|
642
|
+
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
643
|
+
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
644
|
+
|
|
682
645
|
FeedConfig(
|
|
683
646
|
feedHeight = feedHeight,
|
|
684
647
|
videoOverlay = videoOverlay,
|
|
685
648
|
carouselOverlay = CarouselOverlayMode.None,
|
|
686
649
|
surveyOverlay = SurveyOverlayMode.None,
|
|
687
650
|
adOverlay = AdOverlayMode.None,
|
|
688
|
-
muteOnStart = muteOnStart
|
|
651
|
+
muteOnStart = muteOnStart,
|
|
652
|
+
feedSource = feedSource
|
|
689
653
|
)
|
|
690
654
|
} catch (_: Exception) {
|
|
691
655
|
FeedConfig()
|
|
@@ -748,6 +712,54 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
748
712
|
}
|
|
749
713
|
}
|
|
750
714
|
|
|
715
|
+
private fun parseCustomFeedItems(json: String): List<CustomFeedItem>? {
|
|
716
|
+
return try {
|
|
717
|
+
val arr = JSONArray(json)
|
|
718
|
+
val result = mutableListOf<CustomFeedItem>()
|
|
719
|
+
for (i in 0 until arr.length()) {
|
|
720
|
+
val obj = arr.getJSONObject(i)
|
|
721
|
+
when (obj.optString("type")) {
|
|
722
|
+
"video" -> {
|
|
723
|
+
val playbackId = obj.optString("playbackId", null) ?: continue
|
|
724
|
+
result.add(CustomFeedItem.Video(playbackId))
|
|
725
|
+
}
|
|
726
|
+
"imageCarousel" -> {
|
|
727
|
+
val itemObj = obj.optJSONObject("item") ?: continue
|
|
728
|
+
val carouselItem = parseImageCarouselItem(itemObj) ?: continue
|
|
729
|
+
result.add(CustomFeedItem.ImageCarousel(carouselItem))
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
result.ifEmpty { null }
|
|
734
|
+
} catch (_: Exception) {
|
|
735
|
+
null
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
|
|
740
|
+
val id = obj.optString("id", null) ?: return null
|
|
741
|
+
val imagesArr = obj.optJSONArray("images") ?: return null
|
|
742
|
+
val images = mutableListOf<CarouselImage>()
|
|
743
|
+
for (i in 0 until imagesArr.length()) {
|
|
744
|
+
val imgObj = imagesArr.getJSONObject(i)
|
|
745
|
+
images.add(CarouselImage(
|
|
746
|
+
url = imgObj.getString("url"),
|
|
747
|
+
alt = imgObj.optString("alt", null)
|
|
748
|
+
))
|
|
749
|
+
}
|
|
750
|
+
return ImageCarouselItem(
|
|
751
|
+
id = id,
|
|
752
|
+
images = images,
|
|
753
|
+
autoScrollInterval = if (obj.has("autoScrollInterval")) obj.getDouble("autoScrollInterval") else null,
|
|
754
|
+
caption = obj.optString("caption", null),
|
|
755
|
+
title = obj.optString("title", null),
|
|
756
|
+
description = obj.optString("description", null),
|
|
757
|
+
author = obj.optString("author", null),
|
|
758
|
+
section = obj.optString("section", null),
|
|
759
|
+
articleUrl = obj.optString("articleUrl", null)
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
751
763
|
/**
|
|
752
764
|
* Parse optional custom dimensions JSON string into map.
|
|
753
765
|
*/
|
|
@@ -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
|
+
}
|