@applicaster/quick-brick-player 15.0.0-alpha.4069571733 → 15.0.0-alpha.4153840309

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-player",
3
- "version": "15.0.0-alpha.4069571733",
3
+ "version": "15.0.0-alpha.4153840309",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "homepage": "https://github.com/applicaster/quickbrick#readme",
37
37
  "dependencies": {
38
- "@applicaster/quick-brick-mobile-transport-controls": "15.0.0-alpha.4069571733",
38
+ "@applicaster/quick-brick-mobile-transport-controls": "15.0.0-rc.36",
39
39
  "@applicaster/quick-brick-tv-transport-controls": "15.0.0-rc.15",
40
- "@applicaster/zapp-react-native-tvos-app": "15.0.0-alpha.4069571733",
41
- "@applicaster/zapp-react-native-ui-components": "15.0.0-alpha.4069571733",
42
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4069571733",
40
+ "@applicaster/zapp-react-native-tvos-app": "15.0.0-alpha.4153840309",
41
+ "@applicaster/zapp-react-native-ui-components": "15.0.0-alpha.4153840309",
42
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4153840309",
43
43
  "query-string": "7.1.3",
44
44
  "shaka-player": "4.3.5",
45
45
  "typeface-montserrat": "^0.0.54",
@@ -90,7 +90,8 @@ const styles = StyleSheet.create({
90
90
  marginTop: { marginTop: PROGRESS_BAR_HEIGHT },
91
91
  });
92
92
 
93
- const getValue = (key: string, stylesObj: {}) => stylesObj?.[key] ?? null;
93
+ const getValue = (key: string, stylesObj: Record<string, any>) =>
94
+ stylesObj?.[key] ?? null;
94
95
 
95
96
  const controlButtons = (
96
97
  live: boolean,
@@ -208,7 +209,7 @@ export const DockedControls = (props: Props) => {
208
209
 
209
210
  const insets = useSafeAreaInsets();
210
211
 
211
- const progresBarLayoutState = React.useMemo(
212
+ const progressBarLayoutState = React.useMemo(
212
213
  () => ({
213
214
  inline: layoutState.inline,
214
215
  docked: true,
@@ -275,7 +276,7 @@ export const DockedControls = (props: Props) => {
275
276
  content={entry}
276
277
  value={value}
277
278
  visible
278
- layoutState={progresBarLayoutState}
279
+ layoutState={progressBarLayoutState}
279
280
  onPlayerSeek={onPlayerSeek}
280
281
  playerState={playerState}
281
282
  docked
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { LayoutChangeEvent, View } from "react-native";
2
+ import { Animated, LayoutChangeEvent, View } from "react-native";
3
3
 
4
4
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
5
5
  import { toBooleanWithDefaultTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
@@ -8,6 +8,7 @@ import { FlexImage } from "./FlexImage";
8
8
 
9
9
  import { styles } from "./styles";
10
10
  import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
11
+ import { useModalAnimationContext } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
11
12
 
12
13
  type Props = {
13
14
  entry: ZappEntry;
@@ -29,15 +30,34 @@ export const PlayerImage = (props: Props) => {
29
30
  configuration.audio_player_artwork_border_radius
30
31
  );
31
32
 
33
+ const { yTranslate } = useModalAnimationContext();
34
+
35
+ const isModalExpanded = yTranslate.current.interpolate({
36
+ inputRange: [0, 1], // this should be end with `1` only when it's in initial state ( 0pos), otherwise `0`
37
+ outputRange: [1, 0],
38
+ });
39
+
40
+ // do not use styles.shadow when isModalExpanded is 0
41
+
42
+ const animatedStyle = {
43
+ opacity: isModalExpanded,
44
+ };
45
+
32
46
  return (
33
47
  <View style={styles.alignItemsCenter}>
48
+ {/* Using an Animated.View to apply shadow to the image */}
49
+ <Animated.View
50
+ style={[
51
+ animatedStyle,
52
+ shouldShowShadow ? styles.shadow : undefined,
53
+ { borderRadius },
54
+ ]}
55
+ />
34
56
  <View
35
57
  onLayout={onLayoutImage}
36
- style={[shouldShowShadow ? styles.shadow : undefined, { borderRadius }]}
58
+ style={[styles.defaultImageWrapperView, { borderRadius }]}
37
59
  >
38
- <View style={[styles.defaultImageWrapperView, { borderRadius }]}>
39
- <FlexImage entry={entry} style={styles.backgroundImageContainer} />
40
- </View>
60
+ <FlexImage entry={entry} style={styles.backgroundImageContainer} />
41
61
  </View>
42
62
  </View>
43
63
  );
@@ -23,7 +23,12 @@ export const styles = StyleSheet.create({
23
23
  overflow: "hidden",
24
24
  },
25
25
  shadow: {
26
- borderColor: "transparent",
26
+ zIndex: 0,
27
+ position: "absolute",
28
+ top: 0,
29
+ left: 0,
30
+ right: 0,
31
+ bottom: 0,
27
32
  shadowColor: SHADOW_COLOR,
28
33
  shadowOffset: { width: 0, height: 24 },
29
34
  shadowOpacity: platformSelect({
@@ -5,8 +5,8 @@ import { PanGestureHandler } from "react-native-gesture-handler";
5
5
  import { useStyles } from "./styles";
6
6
  import { useVideoModalState } from "./hooks";
7
7
  import { MODAL_COLLAPSE_RATIO, MODAL_RADIUS } from "./consts";
8
- import { useModalAnimationContext } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
9
8
  import { getWindowHeight } from "./utils";
9
+ import { useSafeAreaFrame } from "react-native-safe-area-context";
10
10
 
11
11
  type AnimatedModalProps = {
12
12
  content?: ReactElement;
@@ -17,22 +17,18 @@ export function AnimatedModal({
17
17
  content,
18
18
  collapsedContent,
19
19
  }: AnimatedModalProps) {
20
- const translateYRef = useModalAnimationContext().yTranslate;
20
+ const { translateY, collapsed, gestureHandlerProps, expand, offset } =
21
+ useVideoModalState();
21
22
 
22
- const {
23
- translateY,
24
- collapsed,
25
- collapsedHeight,
26
- gestureHandlerProps,
27
- expand,
28
- } = useVideoModalState(translateYRef);
23
+ const frame = useSafeAreaFrame();
24
+ const collapsedHeight = frame.height - offset.current;
29
25
 
30
- const styles = useStyles({ height: collapsedHeight, type: "audio" });
26
+ const height = getWindowHeight();
27
+ const heightAboveMinimised = height - collapsedHeight;
31
28
 
32
- const WINDOW_HEIGHT = getWindowHeight();
33
- const heightAboveMinimised = WINDOW_HEIGHT - collapsedHeight;
29
+ const styles = useStyles({ height: collapsedHeight, type: "audio" });
34
30
 
35
- const MODAL_COLLAPSE_START = WINDOW_HEIGHT * MODAL_COLLAPSE_RATIO;
31
+ const MODAL_COLLAPSE_START = height * MODAL_COLLAPSE_RATIO;
36
32
 
37
33
  // Interpolated opacities for smooth cross-fade
38
34
  const collapsedOpacity = translateY.interpolate({
@@ -63,7 +59,7 @@ export function AnimatedModal({
63
59
  borderTopLeftRadius: borderTopRadiusAnimated,
64
60
  borderTopRightRadius: borderTopRadiusAnimated,
65
61
  transform: [{ translateY }],
66
- height: WINDOW_HEIGHT,
62
+ height: height,
67
63
  },
68
64
  ]}
69
65
  >
@@ -73,7 +69,6 @@ export function AnimatedModal({
73
69
  {
74
70
  borderTopLeftRadius: borderTopRadiusAnimated,
75
71
  borderTopRightRadius: borderTopRadiusAnimated,
76
- height: WINDOW_HEIGHT,
77
72
  },
78
73
  ]}
79
74
  >
@@ -1,6 +1,6 @@
1
1
  import React, { ReactElement, useMemo, useRef } from "react";
2
2
 
3
- import { Animated, Pressable, StyleSheet, ViewStyle } from "react-native";
3
+ import { Animated, Pressable } from "react-native";
4
4
  import { PanGestureHandler } from "react-native-gesture-handler";
5
5
  import { useStyles } from "./styles";
6
6
  import { useConfiguration } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/utils";
@@ -12,22 +12,25 @@ import {
12
12
  VIDEO_TRANSITION_THRESHOLD,
13
13
  } from "./consts";
14
14
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
15
- import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
16
- import { useModalAnimationContext } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/useModalAnimationContext";
17
15
  import { PROGRESS_BAR_HEIGHT } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/utils";
18
- import { useSafeAreaInsets } from "react-native-safe-area-context";
16
+ import {
17
+ SafeAreaView,
18
+ useSafeAreaFrame,
19
+ useSafeAreaInsets,
20
+ } from "react-native-safe-area-context";
19
21
  import { GestureAnimatedScrollView } from "./GestureAnimatedScrollView";
20
22
  import { PlayerDetailsWrapperHeightContext } from "./context";
21
- import { getWindowHeight, getWindowWidth } from "./utils";
22
-
23
- const orientationStyles = StyleSheet.create({
24
- landscape: { flexDirection: "row" },
25
- portrait: { flexDirection: "column" },
26
- });
23
+ import {
24
+ getInsetsOffset,
25
+ getWindowHeight,
26
+ getWindowWidth,
27
+ directionStyles,
28
+ getScaledPos,
29
+ getExtraContentPadding,
30
+ } from "./utils";
31
+ import { addOrientationChangeListener } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
27
32
 
28
- const directionStyles = (isLandscape: boolean): ViewStyle => {
29
- return isLandscape ? orientationStyles.landscape : orientationStyles.portrait;
30
- };
33
+ const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
31
34
 
32
35
  type AnimatedModalProps = {
33
36
  pip?: boolean;
@@ -59,44 +62,61 @@ export function VideoPlayerModal({
59
62
  const enabled = !!modal && !fullscreen;
60
63
  const isTablet = useIsTablet();
61
64
 
62
- const WINDOW_WIDTH = useMemo(() => getWindowWidth(), []); // remember initial width, ignore rotation changes
63
- const height = useMemo(() => getWindowHeight(), []); // remember initial height, ignore rotation changes
64
- const MODAL_COLLAPSE_START = height * MODAL_COLLAPSE_RATIO;
65
+ // remember initial width, ignore rotation changes
66
+ const width = useMemo(
67
+ () =>
68
+ isTablet && !isTabletPortrait
69
+ ? style.tabletLandscapeWidth
70
+ : getWindowWidth(isTablet, !isTabletPortrait),
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ []
73
+ );
65
74
 
66
- let width = WINDOW_WIDTH;
75
+ const height = useMemo(
76
+ () => getWindowHeight(isTablet, !isTabletPortrait),
77
+ []
78
+ ); // remember initial height, ignore rotation changes
67
79
 
68
- if (isTablet && !isTabletPortrait) {
69
- width = style?.tabletLandscapeWidth || width;
70
- }
80
+ const MODAL_COLLAPSE_START = height * MODAL_COLLAPSE_RATIO;
71
81
 
72
82
  const { minimised_height: minimisedHeight } = useConfiguration();
73
83
  const scrollViewRef = useRef<typeof Animated.ScrollView | null>(null);
74
84
 
75
- const translateYRef = useModalAnimationContext().yTranslate;
76
- // used when modal is false, should not be used in animation
77
- const dummyRef = useRef(new Animated.Value(0));
85
+ const { translateY, collapsed, gestureHandlerProps, expand, offset } =
86
+ useVideoModalState();
78
87
 
79
- const {
80
- translateY,
81
- collapsed,
82
- collapsedHeight,
83
- gestureHandlerProps,
84
- expand,
85
- } = useVideoModalState(modal ? translateYRef : dummyRef);
88
+ const frame = useSafeAreaFrame();
89
+ const collapsedHeight = frame.height - offset.current;
86
90
 
87
91
  const heightAboveMinimised = height - collapsedHeight;
88
92
 
89
- const isTabletLandscape = !isTV() && isTablet && !isTabletPortrait;
90
-
91
93
  const styles = useStyles({ height: collapsedHeight });
92
94
 
93
95
  const SCALE_FACTOR = 1 / (width / aspectRatio / minimisedHeight); // Scale factor for expanded content
94
96
 
95
97
  const insets = useSafeAreaInsets();
96
98
 
97
- const isFullScreenOrPIP = fullscreen || pip;
99
+ const [isFullScreenMode, setFullScreenMode] = React.useState(
100
+ fullscreen || pip
101
+ );
102
+
103
+ React.useEffect(() => {
104
+ const listener = addOrientationChangeListener(({ toOrientation }) => {
105
+ /* ignored in collapsed mode non-PiP player */
106
+ if (collapsed && !pip) return;
107
+ setFullScreenMode(!(toOrientation === 1));
108
+ });
109
+
110
+ return () => {
111
+ listener.remove();
112
+ };
113
+ }, [collapsed, pip]);
114
+
115
+ const isFullScreenOrPIP = isFullScreenMode;
116
+
117
+ const vidHeight = width / aspectRatio;
98
118
 
99
- // Interpolated opacities for smooth cross-fade
119
+ // animated opacity for smooth cross-fade of collapsed content
100
120
  const collapsedOpacity = !isFullScreenOrPIP
101
121
  ? translateY.interpolate({
102
122
  inputRange: [MODAL_COLLAPSE_START, heightAboveMinimised],
@@ -105,6 +125,7 @@ export function VideoPlayerModal({
105
125
  })
106
126
  : new Animated.Value(0);
107
127
 
128
+ // animated opacity for extra content
108
129
  const expandedOpacity = !isFullScreenOrPIP
109
130
  ? translateY.interpolate({
110
131
  inputRange: [0, heightAboveMinimised],
@@ -113,6 +134,7 @@ export function VideoPlayerModal({
113
134
  })
114
135
  : new Animated.Value(1);
115
136
 
137
+ // animated x position for video content
116
138
  const xPosition = !isFullScreenOrPIP
117
139
  ? translateY.interpolate({
118
140
  inputRange: [height * VIDEO_TRANSITION_THRESHOLD, MODAL_COLLAPSE_START],
@@ -121,27 +143,23 @@ export function VideoPlayerModal({
121
143
  })
122
144
  : new Animated.Value(0);
123
145
 
124
- const vidHeight = width / aspectRatio;
125
-
146
+ // animated y position for video content
126
147
  const yPosition = !isFullScreenOrPIP
127
148
  ? translateY.interpolate({
128
149
  inputRange: [height * VIDEO_TRANSITION_THRESHOLD, MODAL_COLLAPSE_START],
129
150
  outputRange: [
130
151
  0,
131
- isTablet && isTabletLandscape
132
- ? -((-PROGRESS_BAR_HEIGHT + height - vidHeight * SCALE_FACTOR) / 2)
133
- : -(
134
- PROGRESS_BAR_HEIGHT +
135
- insets.top / 2 +
136
- (WINDOW_WIDTH / aspectRatio -
137
- (WINDOW_WIDTH / aspectRatio) * SCALE_FACTOR) /
138
- 2
139
- ),
152
+ -(
153
+ PROGRESS_BAR_HEIGHT +
154
+ getInsetsOffset(insets, isTabletPortrait) +
155
+ getScaledPos(height, vidHeight, minimisedHeight, isTabletPortrait)
156
+ ),
140
157
  ],
141
158
  extrapolate: "clamp",
142
159
  })
143
160
  : new Animated.Value(0);
144
161
 
162
+ // animated position for video content
145
163
  const scalePosition = !isFullScreenOrPIP
146
164
  ? translateY.interpolate({
147
165
  inputRange: [height * VIDEO_TRANSITION_THRESHOLD, MODAL_COLLAPSE_START],
@@ -152,7 +170,7 @@ export function VideoPlayerModal({
152
170
 
153
171
  const borderTopRadiusAnimated = !isFullScreenOrPIP
154
172
  ? translateY.interpolate({
155
- inputRange: [0, height - collapsedHeight],
173
+ inputRange: [0, heightAboveMinimised],
156
174
  outputRange: [MODAL_RADIUS, 0],
157
175
  extrapolate: "clamp",
158
176
  })
@@ -176,93 +194,96 @@ export function VideoPlayerModal({
176
194
  <PanGestureHandler
177
195
  enabled={enabled}
178
196
  waitFor={scrollViewRef}
197
+ activeOffsetY={[-25, 25]}
198
+ activeOffsetX={[-25, 25]}
179
199
  {...gestureHandlerProps}
180
200
  >
181
- <Animated.View
182
- pointerEvents={"auto"}
183
- style={[
184
- styles.modalWrapper,
185
- {
186
- borderTopLeftRadius: borderTopRadiusAnimated,
187
- borderTopRightRadius: borderTopRadiusAnimated,
188
- transform: [{ translateY: isFullScreenOrPIP ? 0 : translateY }],
189
- height: height,
190
- },
191
- ]}
192
- >
201
+ <AnimatedSafeAreaView pointerEvents="box-none" edges={["bottom"]}>
193
202
  <Animated.View
194
203
  pointerEvents={"auto"}
195
204
  style={[
196
- styles.modal,
197
- collapsed ? styles.collapsedModalBg : null,
205
+ styles.modalWrapper,
198
206
  {
199
207
  borderTopLeftRadius: borderTopRadiusAnimated,
200
208
  borderTopRightRadius: borderTopRadiusAnimated,
201
- height: height,
209
+ transform: [{ translateY: isFullScreenOrPIP ? 0 : translateY }],
210
+ height: frame.height,
202
211
  },
203
212
  ]}
204
213
  >
205
214
  <Animated.View
206
- pointerEvents={!collapsed ? "auto" : "none"}
215
+ pointerEvents={"auto"}
207
216
  style={[
208
- pip ? styles.modalContentPiP : styles.modalContent,
209
- directionStyles(isTabletLandscape),
217
+ styles.modal,
218
+ collapsed ? styles.collapsedModalBg : null,
219
+ {
220
+ borderTopLeftRadius: borderTopRadiusAnimated,
221
+ borderTopRightRadius: borderTopRadiusAnimated,
222
+ },
210
223
  ]}
211
224
  >
212
225
  <Animated.View
226
+ pointerEvents={!collapsed ? "auto" : "none"}
213
227
  style={[
214
- styles.expandedContentContainer,
215
- {
216
- transform: [
217
- { translateX: xPosition },
218
- { translateY: yPosition },
219
- { scale: scalePosition },
220
- ],
221
- },
228
+ pip ? styles.modalContentPiP : styles.modalContent,
229
+ directionStyles(isTabletPortrait),
222
230
  ]}
223
231
  >
224
- {content}
232
+ <Animated.View
233
+ style={[
234
+ styles.expandedContentContainer,
235
+ {
236
+ transform: [
237
+ { translateX: xPosition },
238
+ { translateY: yPosition },
239
+ { scale: scalePosition },
240
+ ],
241
+ },
242
+ ]}
243
+ >
244
+ {content}
245
+ </Animated.View>
246
+ {extraContent ? (
247
+ <GestureAnimatedScrollView
248
+ ref={scrollViewRef}
249
+ contentContainerStyle={getExtraContentPadding(insets)}
250
+ bounces={false}
251
+ overScrollMode="never"
252
+ onLayout={onScrollViewLayout}
253
+ style={[
254
+ styles.extraContentScrollView,
255
+ { opacity: expandedOpacity },
256
+ ]}
257
+ >
258
+ <PlayerDetailsWrapperHeightContext.Provider
259
+ value={playerDetailsWrapperHeight}
260
+ >
261
+ {extraContent}
262
+ </PlayerDetailsWrapperHeightContext.Provider>
263
+ </GestureAnimatedScrollView>
264
+ ) : null}
225
265
  </Animated.View>
226
- {extraContent ? (
227
- <GestureAnimatedScrollView
228
- ref={scrollViewRef}
229
- contentContainerStyle={{
230
- paddingBottom: insets.bottom,
231
- }}
232
- onLayout={onScrollViewLayout}
266
+
267
+ {!isFullScreenOrPIP ? (
268
+ <Animated.View
269
+ pointerEvents={collapsed ? "box-none" : "none"}
233
270
  style={[
234
- styles.extraContentScrollView,
235
- { opacity: expandedOpacity },
271
+ styles.collapsedBarContainer,
272
+ { opacity: collapsedOpacity },
236
273
  ]}
237
274
  >
238
- <PlayerDetailsWrapperHeightContext.Provider
239
- value={playerDetailsWrapperHeight}
275
+ <Pressable
276
+ testID="collapsedBarWrapper"
277
+ onPress={expand}
278
+ style={styles.collapsedBarWrapper}
240
279
  >
241
- {extraContent}
242
- </PlayerDetailsWrapperHeightContext.Provider>
243
- </GestureAnimatedScrollView>
280
+ {collapsedContent}
281
+ </Pressable>
282
+ </Animated.View>
244
283
  ) : null}
245
284
  </Animated.View>
246
-
247
- {!isFullScreenOrPIP ? (
248
- <Animated.View
249
- pointerEvents={collapsed ? "box-none" : "none"}
250
- style={[
251
- styles.collapsedBarContainer,
252
- { opacity: collapsedOpacity },
253
- ]}
254
- >
255
- <Pressable
256
- testID="collapsedBarWrapper"
257
- onPress={expand}
258
- style={styles.collapsedBarWrapper}
259
- >
260
- {collapsedContent}
261
- </Pressable>
262
- </Animated.View>
263
- ) : null}
264
285
  </Animated.View>
265
- </Animated.View>
286
+ </AnimatedSafeAreaView>
266
287
  </PanGestureHandler>
267
288
  );
268
289
  }
@@ -1,159 +1,132 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
- import { useSafeAreaInsets } from "react-native-safe-area-context";
2
+
3
3
  import { State } from "react-native-gesture-handler";
4
- import { Animated } from "react-native";
5
4
 
6
5
  import { VideoModalMode } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
7
- import { useConfiguration } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/utils";
8
- import { getTabBarHeight } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/getTabBarHeight";
6
+ import { useAnimationStateStore } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/utils";
9
7
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
10
- import { isMenuVisible } from "@applicaster/zapp-react-native-ui-components/Components/Screen/navigationHandler";
11
8
  import {
12
9
  ANIMATION_DURATION,
13
10
  DRAG_TO_COLLAPSE,
14
11
  DRAG_TO_EXPAND,
15
12
  } from "../consts";
16
- import { PROGRESS_BAR_HEIGHT } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/utils";
17
- import { usePlugins } from "@applicaster/zapp-react-native-redux";
18
- import { getWindowHeight } from "../utils";
19
-
20
- const bottomTabBarHeight = getTabBarHeight();
21
-
22
- export const useVideoModalState = (
23
- translateYRef: React.MutableRefObject<Animated.Value>
24
- ) => {
25
- const { minimised_height: minimisedHeight } = useConfiguration();
13
+ import { Animated } from "react-native";
14
+ import { useModalAnimationContext } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
26
15
 
27
- const WINDOW_HEIGHT = getWindowHeight();
16
+ const getAnimatedValue = (animatedValue: Animated.Value): number => {
17
+ return (animatedValue as any).__getValue() as number;
18
+ };
28
19
 
20
+ export const useVideoModalState = () => {
29
21
  const {
30
- maximiseVideoModal,
31
- minimiseVideoModal,
32
- videoModalState,
33
- currentRoute,
34
- screenData,
35
- } = useNavigation();
36
-
37
- const plugins = usePlugins();
38
-
39
- const menuVisible = isMenuVisible(currentRoute, screenData, plugins);
40
-
41
- const collapsed =
42
- videoModalState.visible &&
43
- videoModalState.mode === VideoModalMode.MINIMIZED;
44
-
45
- const { bottom } = useSafeAreaInsets();
46
-
47
- const initialBottomOffset = useRef(bottom).current;
48
-
49
- const collapsedHeight =
50
- minimisedHeight +
51
- initialBottomOffset +
52
- (menuVisible ? bottomTabBarHeight : 0) +
53
- PROGRESS_BAR_HEIGHT;
54
-
55
- const heightAboveMinimised = WINDOW_HEIGHT - collapsedHeight;
56
-
57
- const translateY = translateYRef.current;
58
-
59
- const handleExpand = useCallback(
60
- (toValue: number) => {
61
- return Animated.timing(translateYRef.current, {
22
+ yTranslate,
23
+ offset,
24
+ heightAboveMinimised,
25
+ gestureTranslationRef,
26
+ offsetAnimatedValueRef,
27
+ } = useModalAnimationContext();
28
+
29
+ const modalAnimatedValue = offsetAnimatedValueRef.current;
30
+ const gestureTranslation = gestureTranslationRef.current;
31
+
32
+ const updateModalTranslation = useCallback(
33
+ (toValue: number, duration: number = ANIMATION_DURATION) =>
34
+ Animated.timing(modalAnimatedValue, {
62
35
  toValue,
63
- duration: ANIMATION_DURATION,
36
+ duration,
64
37
  useNativeDriver: true,
65
- });
66
- },
67
- [translateYRef]
38
+ }),
39
+ [modalAnimatedValue]
68
40
  );
69
41
 
70
- const handleCollapse = useCallback(
71
- (toValue: number) => {
72
- return Animated.timing(translateYRef.current, {
73
- toValue,
74
- duration: ANIMATION_DURATION,
75
- useNativeDriver: true,
76
- });
77
- },
78
- [translateYRef]
79
- );
42
+ const translateY = yTranslate.current;
80
43
 
81
- const offset = useRef(heightAboveMinimised);
44
+ const { maximiseVideoModal, minimiseVideoModal, videoModalState } =
45
+ useNavigation();
82
46
 
83
- // Gesture handler for modal
84
- const onGestureEvent = useCallback(
85
- (event: any) => {
86
- if (event.nativeEvent.state === State.ACTIVE) {
87
- const y = event.nativeEvent.translationY;
88
- let newY = offset.current + y;
89
- if (newY < 0) newY = 0;
47
+ const videoModalMode = videoModalState.mode;
90
48
 
91
- if (newY > heightAboveMinimised) {
92
- newY = heightAboveMinimised;
93
- }
49
+ const prevModeStateRef = useRef(videoModalMode); // keep track of last non-PIP mode
50
+ const gestureStateRef = useRef<any>(State.UNDETERMINED); // keep track of gesture state
94
51
 
95
- translateY.setValue(newY);
96
- }
97
- },
98
- [collapsedHeight, translateY]
52
+ // Track last non-PIP mode
53
+ if (videoModalMode !== prevModeStateRef.current) {
54
+ if (["MAXIMIZED", "MINIMIZED"].includes(videoModalMode)) {
55
+ prevModeStateRef.current = videoModalMode;
56
+ }
57
+ }
58
+
59
+ const collapsed =
60
+ videoModalState.visible &&
61
+ ((prevModeStateRef.current === "MINIMIZED" &&
62
+ videoModalState.mode === "PIP") || // If STILL in PIP mode, check if last non-PIP mode was MINIMIZED
63
+ videoModalState.mode === VideoModalMode.MINIMIZED); // Or if currently in MINIMIZED mode
64
+
65
+ // Gesture handler for modal
66
+ const onGestureEvent = useMemo(
67
+ () =>
68
+ Animated.event([{ nativeEvent: { translationY: gestureTranslation } }], {
69
+ useNativeDriver: true,
70
+ }),
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ []
99
73
  );
100
74
 
101
75
  const onHandlerStateChange = useCallback(
102
76
  (event: any) => {
103
- if (event.nativeEvent.oldState === State.ACTIVE) {
104
- const { translationY } = event.nativeEvent;
105
- let newY = offset.current + translationY;
106
- if (newY < 0) newY = 0;
77
+ const { state: newState, oldState, translationY } = event.nativeEvent;
107
78
 
108
- if (newY > heightAboveMinimised) {
109
- newY = heightAboveMinimised;
110
- }
79
+ if (newState === State.ACTIVE || oldState === State.ACTIVE) {
80
+ useAnimationStateStore.setState({
81
+ isAnimationInProgress: newState === State.ACTIVE,
82
+ });
83
+ }
111
84
 
112
- const shouldCollapse =
113
- translationY > heightAboveMinimised * DRAG_TO_COLLAPSE;
114
-
115
- const shouldExpand =
116
- translationY < -heightAboveMinimised * DRAG_TO_EXPAND;
117
-
118
- if (!collapsed && shouldCollapse) {
119
- // Collapse
120
- Animated.timing(translateY, {
121
- toValue: heightAboveMinimised,
122
- duration: ANIMATION_DURATION,
123
- useNativeDriver: true,
124
- }).start(() => {
125
- minimiseVideoModal();
126
- offset.current = heightAboveMinimised;
127
- });
128
- } else if (collapsed && shouldExpand) {
129
- // Expand
130
- Animated.timing(translateY, {
131
- toValue: 0,
132
- duration: ANIMATION_DURATION,
133
- useNativeDriver: true,
134
- }).start(() => {
135
- maximiseVideoModal();
136
- offset.current = 0;
137
- translateY.setValue(0);
138
- });
139
- } else {
140
- // Snap back to current state
141
- Animated.spring(translateY, {
142
- toValue: collapsed ? heightAboveMinimised : 0,
143
- useNativeDriver: true,
144
- }).start(() => {
145
- offset.current = collapsed ? heightAboveMinimised : 0;
146
- translateY.setValue(offset.current);
147
- });
85
+ // Update gesture state tracking
86
+ gestureStateRef.current = newState;
87
+
88
+ if (translationY !== 0) {
89
+ if (newState === State.BEGAN) {
90
+ // clean up gesture translation
91
+ gestureTranslation.setValue(0);
92
+ } else if (
93
+ newState === State.END ||
94
+ newState === State.FAILED ||
95
+ newState === State.CANCELLED
96
+ ) {
97
+ const newY = getAnimatedValue(gestureTranslation);
98
+
99
+ // clean up gesture translation
100
+ gestureTranslation.setValue(0);
101
+
102
+ const v = getAnimatedValue(modalAnimatedValue);
103
+
104
+ // store current offset
105
+ modalAnimatedValue.setValue(v + newY);
106
+
107
+ const shouldCollapse = newY > offset.current * DRAG_TO_COLLAPSE;
108
+
109
+ const shouldExpand = newY < offset.current * DRAG_TO_EXPAND;
110
+
111
+ if (!collapsed && shouldCollapse) {
112
+ // Collapse
113
+ updateModalTranslation(offset.current).start(() => {
114
+ minimiseVideoModal();
115
+ });
116
+ } else if (collapsed && shouldExpand) {
117
+ // Expand
118
+ updateModalTranslation(0).start(() => {
119
+ maximiseVideoModal();
120
+ });
121
+ } else {
122
+ // Snap back to current state
123
+ updateModalTranslation(collapsed ? offset.current : 0).start();
124
+ }
148
125
  }
149
- } else if (event.nativeEvent.state === State.BEGAN) {
150
- // Gesture started, set offset
151
- translateY.stopAnimation((value: number) => {
152
- offset.current = value;
153
- });
154
126
  }
155
127
  },
156
- [collapsedHeight, translateY, collapsed, videoModalState]
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
129
+ [collapsed]
157
130
  );
158
131
 
159
132
  const gestureHandlerProps = useMemo(() => {
@@ -163,39 +136,34 @@ export const useVideoModalState = (
163
136
  };
164
137
  }, [onGestureEvent, onHandlerStateChange]);
165
138
 
139
+ // Track video modal state changes & calling updateAnimatedPosition
166
140
  useEffect(() => {
167
141
  if (videoModalState.visible) {
168
- if (videoModalState.mode === VideoModalMode.MAXIMIZED) {
169
- handleExpand(0).start(() => {
170
- maximiseVideoModal();
171
- offset.current = 0;
172
- translateY.setValue(0);
173
- });
174
- } else if (videoModalState.mode === VideoModalMode.MINIMIZED) {
175
- handleCollapse(heightAboveMinimised).start(() => {
176
- minimiseVideoModal();
177
- offset.current = heightAboveMinimised;
178
- translateY.setValue(heightAboveMinimised);
179
- });
142
+ const mode =
143
+ videoModalMode === "PIP" ? prevModeStateRef.current : videoModalMode;
144
+
145
+ // Prevent changes during active gesture states to avoid interference
146
+ if ([State.ACTIVE].includes(gestureStateRef.current)) {
147
+ return;
148
+ }
149
+
150
+ if (mode === VideoModalMode.MAXIMIZED) {
151
+ updateModalTranslation(0).start();
152
+ } else if (mode === VideoModalMode.MINIMIZED) {
153
+ updateModalTranslation(heightAboveMinimised).start();
180
154
  }
181
155
  }
182
- }, [videoModalState.visible, videoModalState.mode, collapsedHeight]);
156
+ // eslint-disable-next-line react-hooks/exhaustive-deps
157
+ }, [videoModalState.visible, videoModalState.mode, heightAboveMinimised]);
183
158
 
184
159
  return useMemo(
185
160
  () => ({
186
161
  collapsed,
187
- collapsedHeight,
188
162
  offset,
189
163
  gestureHandlerProps,
190
164
  translateY,
191
165
  expand: maximiseVideoModal,
192
166
  }),
193
- [
194
- collapsed,
195
- collapsedHeight,
196
- gestureHandlerProps,
197
- translateY,
198
- maximiseVideoModal,
199
- ]
167
+ [collapsed, offset, gestureHandlerProps, translateY, maximiseVideoModal]
200
168
  );
201
169
  };
@@ -63,6 +63,7 @@ export const useStyles = ({ height }: { height: number }) => {
63
63
  backgroundColor,
64
64
  elevation: 10,
65
65
  zIndex: 100,
66
+ height: "100%",
66
67
  overflow: "hidden",
67
68
  },
68
69
  modalWrapper: {
@@ -1,29 +1,111 @@
1
- import { StatusBar, Dimensions } from "react-native";
1
+ import { StyleSheet, Dimensions, ViewStyle } from "react-native";
2
2
 
3
3
  import {
4
4
  isAndroidPlatform,
5
5
  isAndroidVersionAtLeast,
6
+ isTV,
6
7
  } from "@applicaster/zapp-react-native-utils/reactUtils";
7
- import { isAndroidTablet } from "@applicaster/zapp-react-native-utils/reactHooks/layout/isTablet";
8
+ import { useIsTablet as getIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
8
9
 
9
- const MIN_SUPPORTED_ANDROID_API = 34;
10
+ const SAFE_AREA_BREAKING_API_VERSION = 35;
10
11
 
11
- const isOldAndroidDevice =
12
+ export const isOldAndroidDevice =
12
13
  isAndroidPlatform() &&
13
- !isAndroidVersionAtLeast(MIN_SUPPORTED_ANDROID_API) &&
14
- !isAndroidTablet();
14
+ !isAndroidVersionAtLeast(SAFE_AREA_BREAKING_API_VERSION);
15
15
 
16
- export const getWindowHeight = (): number => {
16
+ export const getWindowHeight = (
17
+ isTablet: boolean,
18
+ landscape: boolean
19
+ ): number => {
17
20
  const windowDimensions = Dimensions.get("window");
18
21
 
19
- return (
20
- windowDimensions.height +
21
- (isOldAndroidDevice ? (StatusBar.currentHeight ?? 0) : 0)
22
- );
22
+ if (isTablet && landscape) {
23
+ return windowDimensions.height;
24
+ }
25
+
26
+ if (windowDimensions.width > windowDimensions.height) {
27
+ return windowDimensions.width;
28
+ }
29
+
30
+ return windowDimensions.height;
31
+ };
32
+
33
+ export const getScreenHeight = (): number => {
34
+ const screenDimensions = Dimensions.get("screen");
35
+
36
+ return screenDimensions.height;
23
37
  };
24
38
 
25
- export const getWindowWidth = (): number => {
39
+ export const getWindowWidth = (
40
+ isTablet: boolean,
41
+ landscape: boolean
42
+ ): number => {
26
43
  const windowDimensions = Dimensions.get("window");
27
44
 
45
+ if (isTablet && landscape) {
46
+ return windowDimensions.width;
47
+ }
48
+
49
+ if (windowDimensions.width > windowDimensions.height) {
50
+ return windowDimensions.height;
51
+ }
52
+
28
53
  return windowDimensions.width;
29
54
  };
55
+
56
+ const getIsTabletLandscape = (isTabletPortrait: boolean): boolean => {
57
+ return !isTV() && getIsTablet() && !isTabletPortrait;
58
+ };
59
+
60
+ export const getInsetsOffset = (
61
+ insets: { top: number; bottom: number },
62
+ isTabletPortrait: boolean
63
+ ): number => {
64
+ const isTabletLandscape = getIsTabletLandscape(isTabletPortrait);
65
+
66
+ return getIsTablet() && isTabletLandscape
67
+ ? isOldAndroidDevice
68
+ ? insets.top / 2
69
+ : 0
70
+ : insets.top / 2;
71
+ };
72
+
73
+ const orientationStyles = StyleSheet.create({
74
+ landscape: { flexDirection: "row" },
75
+ portrait: { flexDirection: "column" },
76
+ });
77
+
78
+ export const directionStyles = (isTabletPortrait: boolean): ViewStyle => {
79
+ const isTabletLandscape = getIsTabletLandscape(isTabletPortrait);
80
+
81
+ return isTabletLandscape
82
+ ? orientationStyles.landscape
83
+ : orientationStyles.portrait;
84
+ };
85
+
86
+ export const getScaledPos = (
87
+ height,
88
+ vidHeight,
89
+ minimisedHeight,
90
+ isTabletPortrait
91
+ ) => {
92
+ const isTabletLandscape = getIsTabletLandscape(isTabletPortrait);
93
+
94
+ const SCALE_FACTOR = minimisedHeight / vidHeight; // Scale factor for expanded content
95
+
96
+ return (
97
+ ((isTabletLandscape ? height : vidHeight) - vidHeight * SCALE_FACTOR) / 2
98
+ );
99
+ };
100
+
101
+ export const getExtraContentPadding = (insets: {
102
+ top: number;
103
+ bottom: number;
104
+ }): ViewStyle => {
105
+ const isTablet = getIsTablet(); // not a hook, just a utility function
106
+
107
+ return {
108
+ paddingTop: isTablet ? insets.top : 0,
109
+ paddingBottom: insets.bottom,
110
+ };
111
+ };
@@ -835,25 +835,25 @@ export default class VideoPlayer extends React.Component<
835
835
  : styles.playerContainerBlack;
836
836
 
837
837
  private getBackgroundImageStyle = (
838
- dimensions: any,
838
+ dimensions: ViewStyle,
839
839
  isAudioItem: boolean
840
- ): ViewStyle => ({
841
- ...(isAudioItem ? styles.audio : { position: "absolute" }),
842
- ...dimensions,
843
- });
840
+ ): ViewStyle[] => [
841
+ isAudioItem ? styles.audio : { position: "absolute" },
842
+ dimensions,
843
+ ];
844
844
 
845
845
  /* Returns player view style based on platform and content type (audio/video) */
846
846
  private getPlayerViewStyle = (
847
847
  dimensions: ViewStyle,
848
848
  isAudioItem: boolean
849
- ): ViewStyle => {
850
- const generalStyles = isAudioItem ? styles.audio : {};
849
+ ): ViewStyle | ViewStyle[] => {
850
+ const generalStyles = isAudioItem ? styles.audio : undefined;
851
851
 
852
852
  if (isAndroidPlatform() && isAudioItem) {
853
853
  return generalStyles;
854
854
  }
855
855
 
856
- return { ...generalStyles, ...dimensions };
856
+ return [generalStyles, dimensions];
857
857
  };
858
858
 
859
859
  private renderAudioPlayer = (
@@ -876,7 +876,7 @@ export default class VideoPlayer extends React.Component<
876
876
  shouldRender: boolean,
877
877
  imageKey: string,
878
878
  entry: ZappEntry,
879
- style: ViewStyle
879
+ style: ViewStyle | ViewStyle[]
880
880
  ) => {
881
881
  if (!shouldRender) return null;
882
882
 
@@ -887,7 +887,7 @@ export default class VideoPlayer extends React.Component<
887
887
 
888
888
  private renderNativePlayer = (
889
889
  shouldRender: boolean,
890
- style: ViewStyle,
890
+ style: ViewStyle | ViewStyle[],
891
891
  nativeEvents: ReturnType<typeof this.getNativeEvents>,
892
892
  nativeProps: ReturnType<typeof this.getNativeProps>,
893
893
  pluginConfiguration: Record<string, any>
@@ -1128,10 +1128,10 @@ export default class VideoPlayer extends React.Component<
1128
1128
  isTV() && this.getPlayNextData() && !docked;
1129
1129
 
1130
1130
  const needsToRender =
1131
- (!hideTransportControls &&
1132
- !needToShowPlayNextOverlay &&
1133
- this.props.mode !== "PIP") ||
1134
- docked;
1131
+ !hideTransportControls &&
1132
+ !needToShowPlayNextOverlay &&
1133
+ this.props.mode !== "PIP" &&
1134
+ !docked;
1135
1135
 
1136
1136
  const supportsNativeControls =
1137
1137
  !!this.props.controller?.supportsNativeControls?.();
@@ -1265,13 +1265,13 @@ export default class VideoPlayer extends React.Component<
1265
1265
  </View>
1266
1266
  }
1267
1267
  />
1268
- ) : (
1268
+ ) : isModal ? (
1269
1269
  <VideoPlayerModal
1270
1270
  isTabletPortrait={isTabletPortrait}
1271
1271
  aspectRatio={aspectRatio}
1272
1272
  pip={piped}
1273
1273
  fullscreen={fullscreen}
1274
- modal={isModal}
1274
+ modal
1275
1275
  style={{
1276
1276
  tabletLandscapeWidth: getTabletWidth(
1277
1277
  this.getPluginConfiguration().tablet_landscape_sidebar_width,
@@ -1309,6 +1309,13 @@ export default class VideoPlayer extends React.Component<
1309
1309
  </View>
1310
1310
  }
1311
1311
  />
1312
+ ) : (
1313
+ <View style={style} testID={"player-view"}>
1314
+ <PlayerWrapper
1315
+ {...sharedWrapperViewProps}
1316
+ playerContent={this.renderPlayerContent}
1317
+ />
1318
+ </View>
1312
1319
  );
1313
1320
  };
1314
1321