@shortkitsdk/react-native 0.1.0
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/ShortKitReactNative.podspec +19 -0
- package/android/build.gradle.kts +34 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
- package/app.plugin.js +1 -0
- package/ios/ShortKitBridge.swift +537 -0
- package/ios/ShortKitFeedView.swift +207 -0
- package/ios/ShortKitFeedViewManager.mm +29 -0
- package/ios/ShortKitModule.h +25 -0
- package/ios/ShortKitModule.mm +204 -0
- package/ios/ShortKitOverlayBridge.swift +91 -0
- package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
- package/ios/ShortKitReactNative.podspec +19 -0
- package/package.json +50 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +13 -0
- package/plugin/build/withShortKitAndroid.d.ts +8 -0
- package/plugin/build/withShortKitAndroid.js +32 -0
- package/plugin/build/withShortKitIOS.d.ts +8 -0
- package/plugin/build/withShortKitIOS.js +29 -0
- package/react-native.config.js +8 -0
- package/src/OverlayManager.tsx +87 -0
- package/src/ShortKitContext.ts +51 -0
- package/src/ShortKitFeed.tsx +203 -0
- package/src/ShortKitProvider.tsx +526 -0
- package/src/index.ts +26 -0
- package/src/serialization.ts +95 -0
- package/src/specs/NativeShortKitModule.ts +201 -0
- package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
- package/src/types.ts +167 -0
- package/src/useShortKit.ts +20 -0
- package/src/useShortKitPlayer.ts +29 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
import type { Double, Int32, EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
|
|
5
|
+
// --- Event payload types (codegen-compatible inline object types) ---
|
|
6
|
+
|
|
7
|
+
type PlayerStateEvent = Readonly<{
|
|
8
|
+
state: string;
|
|
9
|
+
errorMessage?: string;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
type CurrentItemEvent = Readonly<{
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
duration: Double;
|
|
17
|
+
streamingUrl: string;
|
|
18
|
+
thumbnailUrl: string;
|
|
19
|
+
captionTracks: string; // JSON-serialized CaptionTrack[]
|
|
20
|
+
customMetadata?: string; // JSON-serialized Record<string, JSONValue>
|
|
21
|
+
author?: string;
|
|
22
|
+
articleUrl?: string;
|
|
23
|
+
commentCount?: Int32;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
type TimeUpdateEvent = Readonly<{
|
|
27
|
+
current: Double;
|
|
28
|
+
duration: Double;
|
|
29
|
+
buffered: Double;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
type MutedEvent = Readonly<{
|
|
33
|
+
isMuted: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
type PlaybackRateEvent = Readonly<{
|
|
37
|
+
rate: Double;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
type CaptionsEnabledEvent = Readonly<{
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
type CaptionTrackEvent = Readonly<{
|
|
45
|
+
language: string;
|
|
46
|
+
label: string;
|
|
47
|
+
sourceUrl: string;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
type CueEvent = Readonly<{
|
|
51
|
+
text: string;
|
|
52
|
+
startTime: Double;
|
|
53
|
+
endTime: Double;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
type LoopEvent = Readonly<{
|
|
57
|
+
contentId: string;
|
|
58
|
+
loopCount: Int32;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
type FeedTransitionEvent = Readonly<{
|
|
62
|
+
phase: string;
|
|
63
|
+
fromItem?: string; // JSON-serialized ContentItem
|
|
64
|
+
toItem?: string; // JSON-serialized ContentItem
|
|
65
|
+
direction: string;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
type FormatChangeEvent = Readonly<{
|
|
69
|
+
contentId: string;
|
|
70
|
+
fromBitrate: Double;
|
|
71
|
+
toBitrate: Double;
|
|
72
|
+
fromResolution: string;
|
|
73
|
+
toResolution: string;
|
|
74
|
+
}>;
|
|
75
|
+
|
|
76
|
+
type PrefetchedAheadCountEvent = Readonly<{
|
|
77
|
+
count: Int32;
|
|
78
|
+
}>;
|
|
79
|
+
|
|
80
|
+
type ErrorEvent = Readonly<{
|
|
81
|
+
code: string;
|
|
82
|
+
message: string;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
type ShareTappedEvent = Readonly<{
|
|
86
|
+
item: string; // JSON-serialized ContentItem
|
|
87
|
+
}>;
|
|
88
|
+
|
|
89
|
+
type SurveyResponseEvent = Readonly<{
|
|
90
|
+
surveyId: string;
|
|
91
|
+
optionId: string;
|
|
92
|
+
optionText: string;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
95
|
+
type ArticleTappedEvent = Readonly<{
|
|
96
|
+
item: string; // JSON-serialized ContentItem
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
type CommentTappedEvent = Readonly<{
|
|
100
|
+
item: string; // JSON-serialized ContentItem
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
type OverlayShareTappedEvent = Readonly<{
|
|
104
|
+
item: string; // JSON-serialized ContentItem
|
|
105
|
+
}>;
|
|
106
|
+
|
|
107
|
+
type SaveTappedEvent = Readonly<{
|
|
108
|
+
item: string; // JSON-serialized ContentItem
|
|
109
|
+
}>;
|
|
110
|
+
|
|
111
|
+
type LikeTappedEvent = Readonly<{
|
|
112
|
+
item: string; // JSON-serialized ContentItem
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
type OverlayConfigureEvent = Readonly<{
|
|
116
|
+
item: string; // JSON-serialized ContentItem
|
|
117
|
+
}>;
|
|
118
|
+
|
|
119
|
+
type OverlayActivateEvent = Readonly<{
|
|
120
|
+
item: string; // JSON-serialized ContentItem
|
|
121
|
+
}>;
|
|
122
|
+
|
|
123
|
+
type OverlayResetEvent = Readonly<{
|
|
124
|
+
item: string; // JSON-serialized ContentItem
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
type OverlayFadeOutEvent = Readonly<{
|
|
128
|
+
item: string; // JSON-serialized ContentItem
|
|
129
|
+
}>;
|
|
130
|
+
|
|
131
|
+
type OverlayRestoreEvent = Readonly<{
|
|
132
|
+
item: string; // JSON-serialized ContentItem
|
|
133
|
+
}>;
|
|
134
|
+
|
|
135
|
+
type OverlayTapEvent = Readonly<{}>;
|
|
136
|
+
|
|
137
|
+
type OverlayDoubleTapEvent = Readonly<{
|
|
138
|
+
x: Double;
|
|
139
|
+
y: Double;
|
|
140
|
+
}>;
|
|
141
|
+
|
|
142
|
+
export interface Spec extends TurboModule {
|
|
143
|
+
// --- Lifecycle ---
|
|
144
|
+
initialize(
|
|
145
|
+
apiKey: string,
|
|
146
|
+
config: string, // JSON-serialized FeedConfig
|
|
147
|
+
clientAppName?: string,
|
|
148
|
+
clientAppVersion?: string,
|
|
149
|
+
customDimensions?: string, // JSON-serialized Record<string, string>
|
|
150
|
+
): void;
|
|
151
|
+
setUserId(userId: string): void;
|
|
152
|
+
clearUserId(): void;
|
|
153
|
+
onPause(): void;
|
|
154
|
+
onResume(): void;
|
|
155
|
+
destroy(): void;
|
|
156
|
+
|
|
157
|
+
// --- Player controls ---
|
|
158
|
+
play(): void;
|
|
159
|
+
pause(): void;
|
|
160
|
+
seek(seconds: Double): void;
|
|
161
|
+
seekAndPlay(seconds: Double): void;
|
|
162
|
+
skipToNext(): void;
|
|
163
|
+
skipToPrevious(): void;
|
|
164
|
+
setMuted(muted: boolean): void;
|
|
165
|
+
setPlaybackRate(rate: Double): void;
|
|
166
|
+
setCaptionsEnabled(enabled: boolean): void;
|
|
167
|
+
selectCaptionTrack(language: string): void;
|
|
168
|
+
sendContentSignal(signal: string): void;
|
|
169
|
+
setMaxBitrate(bitrate: Double): void;
|
|
170
|
+
|
|
171
|
+
// --- Event emitters ---
|
|
172
|
+
readonly onPlayerStateChanged: EventEmitter<PlayerStateEvent>;
|
|
173
|
+
readonly onCurrentItemChanged: EventEmitter<CurrentItemEvent>;
|
|
174
|
+
readonly onTimeUpdate: EventEmitter<TimeUpdateEvent>;
|
|
175
|
+
readonly onMutedChanged: EventEmitter<MutedEvent>;
|
|
176
|
+
readonly onPlaybackRateChanged: EventEmitter<PlaybackRateEvent>;
|
|
177
|
+
readonly onCaptionsEnabledChanged: EventEmitter<CaptionsEnabledEvent>;
|
|
178
|
+
readonly onActiveCaptionTrackChanged: EventEmitter<CaptionTrackEvent>;
|
|
179
|
+
readonly onActiveCueChanged: EventEmitter<CueEvent>;
|
|
180
|
+
readonly onDidLoop: EventEmitter<LoopEvent>;
|
|
181
|
+
readonly onFeedTransition: EventEmitter<FeedTransitionEvent>;
|
|
182
|
+
readonly onFormatChange: EventEmitter<FormatChangeEvent>;
|
|
183
|
+
readonly onPrefetchedAheadCountChanged: EventEmitter<PrefetchedAheadCountEvent>;
|
|
184
|
+
readonly onError: EventEmitter<ErrorEvent>;
|
|
185
|
+
readonly onShareTapped: EventEmitter<ShareTappedEvent>;
|
|
186
|
+
readonly onSurveyResponse: EventEmitter<SurveyResponseEvent>;
|
|
187
|
+
readonly onArticleTapped: EventEmitter<ArticleTappedEvent>;
|
|
188
|
+
readonly onCommentTapped: EventEmitter<CommentTappedEvent>;
|
|
189
|
+
readonly onOverlayShareTapped: EventEmitter<OverlayShareTappedEvent>;
|
|
190
|
+
readonly onSaveTapped: EventEmitter<SaveTappedEvent>;
|
|
191
|
+
readonly onLikeTapped: EventEmitter<LikeTappedEvent>;
|
|
192
|
+
readonly onOverlayConfigure: EventEmitter<OverlayConfigureEvent>;
|
|
193
|
+
readonly onOverlayActivate: EventEmitter<OverlayActivateEvent>;
|
|
194
|
+
readonly onOverlayReset: EventEmitter<OverlayResetEvent>;
|
|
195
|
+
readonly onOverlayFadeOut: EventEmitter<OverlayFadeOutEvent>;
|
|
196
|
+
readonly onOverlayRestore: EventEmitter<OverlayRestoreEvent>;
|
|
197
|
+
readonly onOverlayTap: EventEmitter<OverlayTapEvent>;
|
|
198
|
+
readonly onOverlayDoubleTap: EventEmitter<OverlayDoubleTapEvent>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('ShortKitModule');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HostComponent, ViewProps } from 'react-native';
|
|
2
|
+
import { codegenNativeComponent } from 'react-native';
|
|
3
|
+
import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
|
|
5
|
+
export interface NativeProps extends ViewProps {
|
|
6
|
+
config: string;
|
|
7
|
+
overlayType?: WithDefault<'none' | 'custom', 'none'>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default codegenNativeComponent<NativeProps>(
|
|
11
|
+
'ShortKitFeedView',
|
|
12
|
+
{},
|
|
13
|
+
) as HostComponent<NativeProps>;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// --- Configuration ---
|
|
4
|
+
|
|
5
|
+
export interface FeedConfig {
|
|
6
|
+
feedHeight?: FeedHeight;
|
|
7
|
+
overlay?: OverlayConfig;
|
|
8
|
+
carouselMode?: CarouselMode;
|
|
9
|
+
surveyMode?: SurveyMode;
|
|
10
|
+
muteOnStart?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FeedHeight =
|
|
14
|
+
| { type: 'fullscreen' }
|
|
15
|
+
| { type: 'percentage'; value: number };
|
|
16
|
+
|
|
17
|
+
export type OverlayConfig =
|
|
18
|
+
| 'none'
|
|
19
|
+
| { type: 'custom'; component: React.ComponentType };
|
|
20
|
+
|
|
21
|
+
export type CarouselMode =
|
|
22
|
+
| 'none'
|
|
23
|
+
| { type: 'template'; name: 'default' };
|
|
24
|
+
|
|
25
|
+
export type SurveyMode =
|
|
26
|
+
| 'none'
|
|
27
|
+
| { type: 'template'; name: 'default' };
|
|
28
|
+
|
|
29
|
+
// --- Data Models ---
|
|
30
|
+
|
|
31
|
+
export interface ContentItem {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
duration: number;
|
|
36
|
+
streamingUrl: string;
|
|
37
|
+
thumbnailUrl: string;
|
|
38
|
+
captionTracks: CaptionTrack[];
|
|
39
|
+
customMetadata?: Record<string, JSONValue>;
|
|
40
|
+
author?: string;
|
|
41
|
+
articleUrl?: string;
|
|
42
|
+
commentCount?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type JSONValue =
|
|
46
|
+
| string
|
|
47
|
+
| number
|
|
48
|
+
| boolean
|
|
49
|
+
| null
|
|
50
|
+
| { [key: string]: JSONValue };
|
|
51
|
+
|
|
52
|
+
export interface CaptionTrack {
|
|
53
|
+
language: string;
|
|
54
|
+
label: string;
|
|
55
|
+
sourceUrl: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PlayerTime {
|
|
59
|
+
current: number;
|
|
60
|
+
duration: number;
|
|
61
|
+
buffered: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type PlayerState =
|
|
65
|
+
| 'idle'
|
|
66
|
+
| 'loading'
|
|
67
|
+
| 'ready'
|
|
68
|
+
| 'playing'
|
|
69
|
+
| 'paused'
|
|
70
|
+
| 'seeking'
|
|
71
|
+
| 'buffering'
|
|
72
|
+
| 'ended'
|
|
73
|
+
| { error: string };
|
|
74
|
+
|
|
75
|
+
export interface LoopEvent {
|
|
76
|
+
contentId: string;
|
|
77
|
+
loopCount: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FeedTransitionEvent {
|
|
81
|
+
phase: 'began' | 'ended';
|
|
82
|
+
from: ContentItem | null;
|
|
83
|
+
to: ContentItem | null;
|
|
84
|
+
direction: 'forward' | 'backward';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface FormatChangeEvent {
|
|
88
|
+
contentId: string;
|
|
89
|
+
fromBitrate: number;
|
|
90
|
+
toBitrate: number;
|
|
91
|
+
fromResolution: string;
|
|
92
|
+
toResolution: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ContentSignal = 'positive' | 'negative';
|
|
96
|
+
|
|
97
|
+
export interface SurveyOption {
|
|
98
|
+
id: string;
|
|
99
|
+
text: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ShortKitError {
|
|
103
|
+
code: string;
|
|
104
|
+
message: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Provider Props ---
|
|
108
|
+
|
|
109
|
+
export interface ShortKitProviderProps {
|
|
110
|
+
apiKey: string;
|
|
111
|
+
config: FeedConfig;
|
|
112
|
+
userId?: string;
|
|
113
|
+
|
|
114
|
+
clientAppName?: string;
|
|
115
|
+
clientAppVersion?: string;
|
|
116
|
+
customDimensions?: Record<string, string>;
|
|
117
|
+
children: React.ReactNode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Feed Component Props ---
|
|
121
|
+
|
|
122
|
+
export interface ShortKitFeedProps {
|
|
123
|
+
style?: ViewStyle;
|
|
124
|
+
onError?: (error: ShortKitError) => void;
|
|
125
|
+
onShareTapped?: (item: ContentItem) => void;
|
|
126
|
+
onSurveyResponse?: (surveyId: string, option: SurveyOption) => void;
|
|
127
|
+
onLoop?: (event: LoopEvent) => void;
|
|
128
|
+
onFeedTransition?: (event: FeedTransitionEvent) => void;
|
|
129
|
+
onFormatChange?: (event: FormatChangeEvent) => void;
|
|
130
|
+
onArticleTapped?: (item: ContentItem) => void;
|
|
131
|
+
onCommentTapped?: (item: ContentItem) => void;
|
|
132
|
+
onOverlayShareTapped?: (item: ContentItem) => void;
|
|
133
|
+
onSaveTapped?: (item: ContentItem) => void;
|
|
134
|
+
onLikeTapped?: (item: ContentItem) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Hook Return Types ---
|
|
138
|
+
|
|
139
|
+
export interface ShortKitPlayerState {
|
|
140
|
+
playerState: PlayerState;
|
|
141
|
+
currentItem: ContentItem | null;
|
|
142
|
+
nextItem: ContentItem | null;
|
|
143
|
+
time: PlayerTime;
|
|
144
|
+
isMuted: boolean;
|
|
145
|
+
playbackRate: number;
|
|
146
|
+
captionsEnabled: boolean;
|
|
147
|
+
activeCaptionTrack: CaptionTrack | null;
|
|
148
|
+
activeCue: { text: string; startTime: number; endTime: number } | null;
|
|
149
|
+
prefetchedAheadCount: number;
|
|
150
|
+
isActive: boolean;
|
|
151
|
+
isTransitioning: boolean;
|
|
152
|
+
lastOverlayTap: number;
|
|
153
|
+
lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
|
|
154
|
+
|
|
155
|
+
play: () => void;
|
|
156
|
+
pause: () => void;
|
|
157
|
+
seek: (seconds: number) => void;
|
|
158
|
+
seekAndPlay: (seconds: number) => void;
|
|
159
|
+
skipToNext: () => void;
|
|
160
|
+
skipToPrevious: () => void;
|
|
161
|
+
setMuted: (muted: boolean) => void;
|
|
162
|
+
setPlaybackRate: (rate: number) => void;
|
|
163
|
+
setCaptionsEnabled: (enabled: boolean) => void;
|
|
164
|
+
selectCaptionTrack: (language: string) => void;
|
|
165
|
+
sendContentSignal: (signal: ContentSignal) => void;
|
|
166
|
+
setMaxBitrate: (bitrate: number) => void;
|
|
167
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to access SDK-level operations from the nearest ShortKitProvider.
|
|
6
|
+
*
|
|
7
|
+
* Must be used within a `<ShortKitProvider>`.
|
|
8
|
+
*
|
|
9
|
+
* @returns SDK operations: `setUserId` and `clearUserId`.
|
|
10
|
+
*/
|
|
11
|
+
export function useShortKit() {
|
|
12
|
+
const context = useContext(ShortKitContext);
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error('useShortKit must be used within a ShortKitProvider');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
setUserId: context.setUserId,
|
|
18
|
+
clearUserId: context.clearUserId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
3
|
+
import type { ShortKitPlayerState } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access player state and commands from the nearest ShortKitProvider.
|
|
7
|
+
*
|
|
8
|
+
* Must be used within a `<ShortKitProvider>`.
|
|
9
|
+
*
|
|
10
|
+
* @returns Player state (playerState, currentItem, time, etc.) and
|
|
11
|
+
* command functions (play, pause, seek, etc.).
|
|
12
|
+
*/
|
|
13
|
+
export function useShortKitPlayer(): ShortKitPlayerState {
|
|
14
|
+
const context = useContext(ShortKitContext);
|
|
15
|
+
if (!context) {
|
|
16
|
+
throw new Error('useShortKitPlayer must be used within a ShortKitProvider');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Return only player-related state and commands (exclude SDK operations
|
|
20
|
+
// and internal fields)
|
|
21
|
+
const {
|
|
22
|
+
setUserId: _setUserId,
|
|
23
|
+
clearUserId: _clearUserId,
|
|
24
|
+
_overlayConfig: _overlay,
|
|
25
|
+
...playerState
|
|
26
|
+
} = context;
|
|
27
|
+
|
|
28
|
+
return playerState;
|
|
29
|
+
}
|