@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,186 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// Fabric native view wrapping `ShortKitPlayerViewController` for use as
|
|
5
|
+
/// a single-video player in React Native.
|
|
6
|
+
///
|
|
7
|
+
/// Props (set by RCTViewManager):
|
|
8
|
+
/// - `config`: JSON string with PlayerConfig values
|
|
9
|
+
/// - `contentItem`: JSON string with ContentItem data
|
|
10
|
+
@objc public class ShortKitPlayerNativeView: UIView {
|
|
11
|
+
|
|
12
|
+
// MARK: - Props
|
|
13
|
+
|
|
14
|
+
@objc public var config: String? {
|
|
15
|
+
didSet {
|
|
16
|
+
guard config != oldValue else { return }
|
|
17
|
+
applyConfig()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc public var contentItem: String? {
|
|
22
|
+
didSet {
|
|
23
|
+
guard contentItem != oldValue else { return }
|
|
24
|
+
applyContentItem()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public var active: Bool = true {
|
|
29
|
+
didSet {
|
|
30
|
+
guard active != oldValue else { return }
|
|
31
|
+
applyActive()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Child VC
|
|
36
|
+
|
|
37
|
+
private var playerVC: ShortKitPlayerViewController?
|
|
38
|
+
private var parsedConfig: PlayerConfig?
|
|
39
|
+
|
|
40
|
+
// MARK: - Lifecycle
|
|
41
|
+
|
|
42
|
+
public override func didMoveToWindow() {
|
|
43
|
+
super.didMoveToWindow()
|
|
44
|
+
if window != nil {
|
|
45
|
+
embedPlayerVCIfNeeded()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
50
|
+
super.willMove(toWindow: newWindow)
|
|
51
|
+
if newWindow == nil {
|
|
52
|
+
removePlayerVC()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override func layoutSubviews() {
|
|
57
|
+
super.layoutSubviews()
|
|
58
|
+
playerVC?.view.frame = bounds
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MARK: - VC Containment
|
|
62
|
+
|
|
63
|
+
private func embedPlayerVCIfNeeded() {
|
|
64
|
+
guard playerVC == nil else { return }
|
|
65
|
+
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
66
|
+
guard let parentVC = findParentViewController() else { return }
|
|
67
|
+
|
|
68
|
+
let playerConfig = parsedConfig ?? PlayerConfig()
|
|
69
|
+
|
|
70
|
+
let vc = ShortKitPlayerViewController(shortKit: sdk, config: playerConfig)
|
|
71
|
+
self.playerVC = vc
|
|
72
|
+
|
|
73
|
+
// Inject content BEFORE the view loads so loadData() skips its own
|
|
74
|
+
// fetch. Accessing vc.view triggers viewDidLoad, so configure first.
|
|
75
|
+
if let json = contentItem, let item = Self.parseContentItem(json) {
|
|
76
|
+
vc.configure(with: item)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parentVC.addChild(vc)
|
|
80
|
+
vc.view.frame = bounds
|
|
81
|
+
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
82
|
+
addSubview(vc.view)
|
|
83
|
+
vc.didMove(toParent: parentVC)
|
|
84
|
+
|
|
85
|
+
// Activate based on the `active` prop (defaults to true)
|
|
86
|
+
if active {
|
|
87
|
+
vc.activate()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func removePlayerVC() {
|
|
92
|
+
guard let vc = playerVC else { return }
|
|
93
|
+
vc.deactivate()
|
|
94
|
+
vc.willMove(toParent: nil)
|
|
95
|
+
vc.view.removeFromSuperview()
|
|
96
|
+
vc.removeFromParent()
|
|
97
|
+
self.playerVC = nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// MARK: - Prop Application
|
|
101
|
+
|
|
102
|
+
private func applyConfig() {
|
|
103
|
+
guard let json = config else { return }
|
|
104
|
+
parsedConfig = Self.parsePlayerConfig(json)
|
|
105
|
+
// Config changes require re-embedding
|
|
106
|
+
if playerVC != nil {
|
|
107
|
+
removePlayerVC()
|
|
108
|
+
embedPlayerVCIfNeeded()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func applyContentItem() {
|
|
113
|
+
guard let json = contentItem, let item = Self.parseContentItem(json) else { return }
|
|
114
|
+
playerVC?.configure(with: item)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func applyActive() {
|
|
118
|
+
guard let vc = playerVC else { return }
|
|
119
|
+
if active {
|
|
120
|
+
vc.activate()
|
|
121
|
+
} else {
|
|
122
|
+
vc.deactivate()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Parsing
|
|
127
|
+
|
|
128
|
+
private static func parsePlayerConfig(_ json: String) -> PlayerConfig {
|
|
129
|
+
guard let data = json.data(using: .utf8),
|
|
130
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
131
|
+
return PlayerConfig()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
|
|
135
|
+
let autoplay = obj["autoplay"] as? Bool ?? true
|
|
136
|
+
let loop = obj["loop"] as? Bool ?? true
|
|
137
|
+
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
138
|
+
|
|
139
|
+
let clickAction: PlayerClickAction
|
|
140
|
+
switch obj["clickAction"] as? String {
|
|
141
|
+
case "feed": clickAction = .feed
|
|
142
|
+
case "mute": clickAction = .mute
|
|
143
|
+
case "none": clickAction = .none
|
|
144
|
+
default: clickAction = .feed
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let overlayMode: VideoOverlayMode
|
|
148
|
+
if let overlayObj = obj["overlay"] as? [String: Any],
|
|
149
|
+
overlayObj["type"] as? String == "custom" {
|
|
150
|
+
overlayMode = .custom { @Sendable in
|
|
151
|
+
let overlay = ShortKitOverlayBridge()
|
|
152
|
+
overlay.bridge = ShortKitBridge.shared
|
|
153
|
+
return overlay
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
overlayMode = .none
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return PlayerConfig(
|
|
160
|
+
cornerRadius: cornerRadius,
|
|
161
|
+
clickAction: clickAction,
|
|
162
|
+
autoplay: autoplay,
|
|
163
|
+
loop: loop,
|
|
164
|
+
muteOnStart: muteOnStart,
|
|
165
|
+
videoOverlay: overlayMode
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func parseContentItem(_ json: String) -> ContentItem? {
|
|
170
|
+
guard let data = json.data(using: .utf8) else { return nil }
|
|
171
|
+
return try? JSONDecoder().decode(ContentItem.self, from: data)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MARK: - Helpers
|
|
175
|
+
|
|
176
|
+
private func findParentViewController() -> UIViewController? {
|
|
177
|
+
var responder: UIResponder? = self
|
|
178
|
+
while let next = responder?.next {
|
|
179
|
+
if let vc = next as? UIViewController {
|
|
180
|
+
return vc
|
|
181
|
+
}
|
|
182
|
+
responder = next
|
|
183
|
+
}
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
|
|
3
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
4
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
5
|
+
#else
|
|
6
|
+
#import "ShortKitReactNative-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@interface ShortKitPlayerViewManager : RCTViewManager
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation ShortKitPlayerViewManager
|
|
13
|
+
|
|
14
|
+
RCT_EXPORT_MODULE(ShortKitPlayerView)
|
|
15
|
+
|
|
16
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
17
|
+
return YES;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
- (UIView *)view {
|
|
21
|
+
return [[ShortKitPlayerNativeView alloc] init];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(contentItem, NSString)
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
|
|
27
|
+
|
|
28
|
+
@end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ShortKit
|
|
3
|
+
|
|
4
|
+
/// Fabric native view wrapping `ShortKitWidgetViewController` for use as
|
|
5
|
+
/// a horizontal video carousel in React Native.
|
|
6
|
+
///
|
|
7
|
+
/// Props (set by RCTViewManager):
|
|
8
|
+
/// - `config`: JSON string with WidgetConfig values
|
|
9
|
+
/// - `items`: JSON string with ContentItem array
|
|
10
|
+
@objc public class ShortKitWidgetNativeView: UIView {
|
|
11
|
+
|
|
12
|
+
// MARK: - Props
|
|
13
|
+
|
|
14
|
+
@objc public var config: String? {
|
|
15
|
+
didSet {
|
|
16
|
+
guard config != oldValue else { return }
|
|
17
|
+
applyConfig()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc public var items: String? {
|
|
22
|
+
didSet {
|
|
23
|
+
guard items != oldValue else { return }
|
|
24
|
+
applyItems()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MARK: - Child VC
|
|
29
|
+
|
|
30
|
+
private var widgetVC: ShortKitWidgetViewController?
|
|
31
|
+
private var parsedConfig: WidgetConfig?
|
|
32
|
+
|
|
33
|
+
// MARK: - Lifecycle
|
|
34
|
+
|
|
35
|
+
public override func didMoveToWindow() {
|
|
36
|
+
super.didMoveToWindow()
|
|
37
|
+
if window != nil {
|
|
38
|
+
embedWidgetVCIfNeeded()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public override func willMove(toWindow newWindow: UIWindow?) {
|
|
43
|
+
super.willMove(toWindow: newWindow)
|
|
44
|
+
if newWindow == nil {
|
|
45
|
+
removeWidgetVC()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public override func layoutSubviews() {
|
|
50
|
+
super.layoutSubviews()
|
|
51
|
+
widgetVC?.view.frame = bounds
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - VC Containment
|
|
55
|
+
|
|
56
|
+
private func embedWidgetVCIfNeeded() {
|
|
57
|
+
guard widgetVC == nil else { return }
|
|
58
|
+
guard let sdk = ShortKitBridge.shared?.sdk else { return }
|
|
59
|
+
guard let parentVC = findParentViewController() else { return }
|
|
60
|
+
|
|
61
|
+
let widgetConfig = parsedConfig ?? WidgetConfig()
|
|
62
|
+
|
|
63
|
+
let vc = ShortKitWidgetViewController(shortKit: sdk, config: widgetConfig)
|
|
64
|
+
self.widgetVC = vc
|
|
65
|
+
|
|
66
|
+
parentVC.addChild(vc)
|
|
67
|
+
vc.view.frame = bounds
|
|
68
|
+
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
69
|
+
addSubview(vc.view)
|
|
70
|
+
vc.didMove(toParent: parentVC)
|
|
71
|
+
|
|
72
|
+
// If items were already set, apply them
|
|
73
|
+
if let json = items, let contentItems = Self.parseContentItems(json) {
|
|
74
|
+
vc.configure(with: contentItems)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func removeWidgetVC() {
|
|
79
|
+
guard let vc = widgetVC else { return }
|
|
80
|
+
vc.willMove(toParent: nil)
|
|
81
|
+
vc.view.removeFromSuperview()
|
|
82
|
+
vc.removeFromParent()
|
|
83
|
+
self.widgetVC = nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Prop Application
|
|
87
|
+
|
|
88
|
+
private func applyConfig() {
|
|
89
|
+
guard let json = config else { return }
|
|
90
|
+
parsedConfig = Self.parseWidgetConfig(json)
|
|
91
|
+
if widgetVC != nil {
|
|
92
|
+
removeWidgetVC()
|
|
93
|
+
embedWidgetVCIfNeeded()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func applyItems() {
|
|
98
|
+
guard let json = items, let contentItems = Self.parseContentItems(json) else { return }
|
|
99
|
+
widgetVC?.configure(with: contentItems)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - Parsing
|
|
103
|
+
|
|
104
|
+
private static func parseWidgetConfig(_ json: String) -> WidgetConfig {
|
|
105
|
+
guard let data = json.data(using: .utf8),
|
|
106
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
107
|
+
return WidgetConfig()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cardCount = obj["cardCount"] as? Int ?? 3
|
|
111
|
+
let cardSpacing = obj["cardSpacing"] as? CGFloat ?? 8
|
|
112
|
+
let cornerRadius = obj["cornerRadius"] as? CGFloat ?? 12
|
|
113
|
+
let autoplay = obj["autoplay"] as? Bool ?? true
|
|
114
|
+
let muteOnStart = obj["muteOnStart"] as? Bool ?? true
|
|
115
|
+
let loop = obj["loop"] as? Bool ?? true
|
|
116
|
+
let rotationInterval = obj["rotationInterval"] as? TimeInterval ?? 10000
|
|
117
|
+
|
|
118
|
+
let clickAction: PlayerClickAction
|
|
119
|
+
switch obj["clickAction"] as? String {
|
|
120
|
+
case "feed": clickAction = .feed
|
|
121
|
+
case "mute": clickAction = .mute
|
|
122
|
+
case "none": clickAction = .none
|
|
123
|
+
default: clickAction = .feed
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let overlayMode: VideoOverlayMode
|
|
127
|
+
if let overlayObj = obj["overlay"] as? [String: Any],
|
|
128
|
+
overlayObj["type"] as? String == "custom" {
|
|
129
|
+
overlayMode = .custom { @Sendable in
|
|
130
|
+
let overlay = ShortKitOverlayBridge()
|
|
131
|
+
overlay.bridge = ShortKitBridge.shared
|
|
132
|
+
return overlay
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
overlayMode = .none
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return WidgetConfig(
|
|
139
|
+
cardCount: cardCount,
|
|
140
|
+
cardSpacing: cardSpacing,
|
|
141
|
+
cornerRadius: cornerRadius,
|
|
142
|
+
autoplay: autoplay,
|
|
143
|
+
muteOnStart: muteOnStart,
|
|
144
|
+
loop: loop,
|
|
145
|
+
rotationInterval: rotationInterval / 1000.0, // JS sends ms, iOS expects seconds
|
|
146
|
+
clickAction: clickAction,
|
|
147
|
+
cardOverlay: overlayMode
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static func parseContentItems(_ json: String) -> [ContentItem]? {
|
|
152
|
+
guard let data = json.data(using: .utf8) else { return nil }
|
|
153
|
+
return try? JSONDecoder().decode([ContentItem].self, from: data)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// MARK: - Helpers
|
|
157
|
+
|
|
158
|
+
private func findParentViewController() -> UIViewController? {
|
|
159
|
+
var responder: UIResponder? = self
|
|
160
|
+
while let next = responder?.next {
|
|
161
|
+
if let vc = next as? UIViewController {
|
|
162
|
+
return vc
|
|
163
|
+
}
|
|
164
|
+
responder = next
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
|
|
3
|
+
#if __has_include(<ShortKitReactNative/ShortKitReactNative-Swift.h>)
|
|
4
|
+
#import <ShortKitReactNative/ShortKitReactNative-Swift.h>
|
|
5
|
+
#else
|
|
6
|
+
#import "ShortKitReactNative-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@interface ShortKitWidgetViewManager : RCTViewManager
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation ShortKitWidgetViewManager
|
|
13
|
+
|
|
14
|
+
RCT_EXPORT_MODULE(ShortKitWidgetView)
|
|
15
|
+
|
|
16
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
17
|
+
return YES;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
- (UIView *)view {
|
|
21
|
+
return [[ShortKitWidgetNativeView alloc] init];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(config, NSString)
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(items, NSString)
|
|
26
|
+
|
|
27
|
+
@end
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { CarouselOverlayConfig } from './types';
|
|
4
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
5
|
+
import type { ShortKitContextValue } from './ShortKitContext';
|
|
6
|
+
|
|
7
|
+
interface CarouselOverlayManagerProps {
|
|
8
|
+
carouselOverlay: CarouselOverlayConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal component that renders TWO instances of the developer's custom
|
|
13
|
+
* carousel overlay component — one for the current cell and one for the next.
|
|
14
|
+
*
|
|
15
|
+
* Works identically to `OverlayManager` but for image carousel cells.
|
|
16
|
+
* The native side finds these views by their `nativeID` and applies
|
|
17
|
+
* scroll-tracking transforms so each overlay moves with its respective
|
|
18
|
+
* carousel cell during swipe transitions.
|
|
19
|
+
*/
|
|
20
|
+
export function CarouselOverlayManager({ carouselOverlay }: CarouselOverlayManagerProps) {
|
|
21
|
+
if (
|
|
22
|
+
carouselOverlay === 'none' ||
|
|
23
|
+
typeof carouselOverlay === 'string' ||
|
|
24
|
+
carouselOverlay.type !== 'custom' ||
|
|
25
|
+
!('component' in carouselOverlay)
|
|
26
|
+
) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CarouselComponent = carouselOverlay.component;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<View style={StyleSheet.absoluteFill} nativeID="carousel-overlay-current" pointerEvents="box-none">
|
|
35
|
+
<CarouselComponent />
|
|
36
|
+
</View>
|
|
37
|
+
<View style={StyleSheet.absoluteFill} nativeID="carousel-overlay-next" pointerEvents="box-none">
|
|
38
|
+
<NextCarouselOverlayProvider>
|
|
39
|
+
<CarouselComponent />
|
|
40
|
+
</NextCarouselOverlayProvider>
|
|
41
|
+
</View>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wraps children with a modified ShortKitContext where `currentCarouselItem`
|
|
48
|
+
* is set to the provider's `nextCarouselItem`.
|
|
49
|
+
*/
|
|
50
|
+
function NextCarouselOverlayProvider({ children }: { children: React.ReactNode }) {
|
|
51
|
+
const context = useContext(ShortKitContext);
|
|
52
|
+
|
|
53
|
+
const nextValue: ShortKitContextValue | null = useMemo(() => {
|
|
54
|
+
if (!context) return null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...context,
|
|
58
|
+
currentCarouselItem: context.nextCarouselItem,
|
|
59
|
+
isCarouselActive: context.nextCarouselItem != null,
|
|
60
|
+
isCarouselTransitioning: false,
|
|
61
|
+
};
|
|
62
|
+
}, [context]);
|
|
63
|
+
|
|
64
|
+
if (!nextValue) return <>{children}</>;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ShortKitContext.Provider value={nextValue}>
|
|
68
|
+
{children}
|
|
69
|
+
</ShortKitContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/ShortKitContext.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
ContentItem,
|
|
4
|
+
ImageCarouselItem,
|
|
5
|
+
CustomFeedItem,
|
|
4
6
|
PlayerTime,
|
|
5
7
|
PlayerState,
|
|
6
8
|
CaptionTrack,
|
|
7
9
|
ContentSignal,
|
|
8
10
|
OverlayConfig,
|
|
11
|
+
CarouselOverlayConfig,
|
|
9
12
|
} from './types';
|
|
10
13
|
|
|
11
14
|
export interface ShortKitContextValue {
|
|
@@ -20,6 +23,7 @@ export interface ShortKitContextValue {
|
|
|
20
23
|
activeCaptionTrack: CaptionTrack | null;
|
|
21
24
|
activeCue: { text: string; startTime: number; endTime: number } | null;
|
|
22
25
|
prefetchedAheadCount: number;
|
|
26
|
+
remainingContentCount: number;
|
|
23
27
|
isActive: boolean;
|
|
24
28
|
isTransitioning: boolean;
|
|
25
29
|
lastOverlayTap: number;
|
|
@@ -42,10 +46,24 @@ export interface ShortKitContextValue {
|
|
|
42
46
|
// SDK operations
|
|
43
47
|
setUserId: (id: string) => void;
|
|
44
48
|
clearUserId: () => void;
|
|
49
|
+
setFeedItems: (items: CustomFeedItem[]) => void;
|
|
50
|
+
appendFeedItems: (items: CustomFeedItem[]) => void;
|
|
51
|
+
fetchContent: (limit?: number) => Promise<ContentItem[]>;
|
|
52
|
+
|
|
53
|
+
// Carousel overlay state
|
|
54
|
+
currentCarouselItem: ImageCarouselItem | null;
|
|
55
|
+
nextCarouselItem: ImageCarouselItem | null;
|
|
56
|
+
isCarouselActive: boolean;
|
|
57
|
+
isCarouselTransitioning: boolean;
|
|
58
|
+
|
|
59
|
+
// Active cell type — used by overlay managers to show/hide
|
|
60
|
+
activeCellType: 'video' | 'carousel' | null;
|
|
45
61
|
|
|
46
62
|
// Internal — used by ShortKitFeed to render custom overlays
|
|
47
63
|
/** @internal */
|
|
48
64
|
_overlayConfig: OverlayConfig;
|
|
65
|
+
/** @internal */
|
|
66
|
+
_carouselOverlayConfig: CarouselOverlayConfig;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
export const ShortKitContext = createContext<ShortKitContextValue | null>(null);
|
package/src/ShortKitFeed.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { ShortKitFeedProps } from './types';
|
|
|
4
4
|
import ShortKitFeedView from './specs/ShortKitFeedViewNativeComponent';
|
|
5
5
|
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
6
6
|
import { OverlayManager } from './OverlayManager';
|
|
7
|
+
import { CarouselOverlayManager } from './CarouselOverlayManager';
|
|
7
8
|
import { ShortKitContext } from './ShortKitContext';
|
|
8
9
|
import { deserializeContentItem } from './serialization';
|
|
9
10
|
|
|
@@ -29,6 +30,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
29
30
|
onLoop,
|
|
30
31
|
onFeedTransition,
|
|
31
32
|
onFormatChange,
|
|
33
|
+
onContentTapped,
|
|
32
34
|
} = props;
|
|
33
35
|
|
|
34
36
|
const context = useContext(ShortKitContext);
|
|
@@ -37,6 +39,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const overlayConfig = context._overlayConfig;
|
|
42
|
+
const carouselOverlayConfig = context._carouselOverlayConfig;
|
|
40
43
|
|
|
41
44
|
// ---------------------------------------------------------------------------
|
|
42
45
|
// Subscribe to feed-level events and forward to callback props
|
|
@@ -109,6 +112,14 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
109
112
|
);
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
if (onContentTapped) {
|
|
116
|
+
subscriptions.push(
|
|
117
|
+
NativeShortKitModule.onContentTapped((event) => {
|
|
118
|
+
onContentTapped(event.contentId, event.index);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
return () => {
|
|
113
124
|
for (const sub of subscriptions) {
|
|
114
125
|
sub.remove();
|
|
@@ -121,6 +132,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
121
132
|
onLoop,
|
|
122
133
|
onFeedTransition,
|
|
123
134
|
onFormatChange,
|
|
135
|
+
onContentTapped,
|
|
124
136
|
]);
|
|
125
137
|
|
|
126
138
|
// ---------------------------------------------------------------------------
|
|
@@ -133,6 +145,7 @@ export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
|
133
145
|
config="{}"
|
|
134
146
|
/>
|
|
135
147
|
<OverlayManager overlay={overlayConfig} />
|
|
148
|
+
<CarouselOverlayManager carouselOverlay={carouselOverlayConfig} />
|
|
136
149
|
</View>
|
|
137
150
|
);
|
|
138
151
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { ShortKitPlayerProps } from './types';
|
|
4
|
+
import ShortKitPlayerView from './specs/ShortKitPlayerViewNativeComponent';
|
|
5
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Single-video player component. Displays one video with thumbnail fallback
|
|
9
|
+
* and optional overlay. Wraps a native Fabric view.
|
|
10
|
+
*
|
|
11
|
+
* Must be rendered inside a `<ShortKitProvider>`.
|
|
12
|
+
*/
|
|
13
|
+
export function ShortKitPlayer(props: ShortKitPlayerProps) {
|
|
14
|
+
const { config, contentItem, active, style } = props;
|
|
15
|
+
|
|
16
|
+
const context = useContext(ShortKitContext);
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('ShortKitPlayer must be used within a ShortKitProvider');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const serializedConfig = useMemo(() => {
|
|
22
|
+
const cfg = config ?? {};
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
cornerRadius: cfg.cornerRadius ?? 12,
|
|
25
|
+
clickAction: cfg.clickAction ?? 'feed',
|
|
26
|
+
autoplay: cfg.autoplay ?? true,
|
|
27
|
+
loop: cfg.loop ?? true,
|
|
28
|
+
muteOnStart: cfg.muteOnStart ?? true,
|
|
29
|
+
overlay: cfg.overlay
|
|
30
|
+
? typeof cfg.overlay === 'string'
|
|
31
|
+
? cfg.overlay
|
|
32
|
+
: { type: 'custom' }
|
|
33
|
+
: 'none',
|
|
34
|
+
});
|
|
35
|
+
}, [config]);
|
|
36
|
+
|
|
37
|
+
const serializedItem = useMemo(() => {
|
|
38
|
+
if (!contentItem) return undefined;
|
|
39
|
+
return JSON.stringify(contentItem);
|
|
40
|
+
}, [contentItem]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={[styles.container, style]}>
|
|
44
|
+
<ShortKitPlayerView
|
|
45
|
+
style={styles.player}
|
|
46
|
+
config={serializedConfig}
|
|
47
|
+
contentItem={serializedItem}
|
|
48
|
+
active={active}
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
container: {
|
|
56
|
+
overflow: 'hidden',
|
|
57
|
+
},
|
|
58
|
+
player: {
|
|
59
|
+
flex: 1,
|
|
60
|
+
},
|
|
61
|
+
});
|