@applicaster/zapp-react-native-ui-components 15.1.16-rc.0 → 15.1.16
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/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +118 -22
- package/Components/ModalComponent/SortableList.tsx +2 -3
- package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
- package/Components/River/TV/withTVEventHandler.tsx +6 -0
- package/Components/Transitioner/Transitioner.tsx +2 -3
- package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
- package/package.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
AudioPlayerTrackItem,
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
} from "./Item";
|
|
8
8
|
import { AudioPlayerHeader, VARIANT_SECTION_HEADER } from "./Header";
|
|
9
9
|
import type { MenuItem } from "@applicaster/zapp-react-native-utils/modalState/types";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
|
|
11
|
+
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
12
|
+
import type { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_EMPTY_MESSAGE = "Queued items will be added here";
|
|
14
15
|
|
|
@@ -30,6 +31,27 @@ const styles = StyleSheet.create({
|
|
|
30
31
|
},
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
const isPrimaryOrCastingPlayer = (player?: Player | null): boolean => {
|
|
35
|
+
if (!player) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
player.playerRole === PlayerRole.Primary ||
|
|
41
|
+
player.playerRole === PlayerRole.Chromecast
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getTargetPlayer = (): Player | null => {
|
|
46
|
+
if (playerManager.isCasting()) {
|
|
47
|
+
return playerManager.getCastingReceiver() || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const active = playerManager.getActivePlayer();
|
|
51
|
+
|
|
52
|
+
return isPrimaryOrCastingPlayer(active) ? active : null;
|
|
53
|
+
};
|
|
54
|
+
|
|
33
55
|
type NowPlayingHeaderSectionProps = {
|
|
34
56
|
onClear?: () => void;
|
|
35
57
|
nextUpTitle?: string;
|
|
@@ -47,18 +69,100 @@ export function NowPlayingHeaderSection({
|
|
|
47
69
|
emptyMessage = DEFAULT_EMPTY_MESSAGE,
|
|
48
70
|
clearLabel,
|
|
49
71
|
}: NowPlayingHeaderSectionProps) {
|
|
50
|
-
const
|
|
51
|
-
|
|
72
|
+
const [activePlayer, setActivePlayer] = useState<Player | null>(
|
|
73
|
+
getTargetPlayer
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const [currentEntry, setCurrentEntry] = useState<ZappEntry | null>(
|
|
77
|
+
() => getTargetPlayer()?.entry || null
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const [isPaused, setIsPaused] = useState<boolean>(() =>
|
|
81
|
+
Boolean(getTargetPlayer()?.isPaused())
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const handleActivePlayerUpdate = () => {
|
|
86
|
+
const player = getTargetPlayer();
|
|
87
|
+
|
|
88
|
+
if (player) {
|
|
89
|
+
setActivePlayer(player);
|
|
90
|
+
|
|
91
|
+
if (player.entry) {
|
|
92
|
+
setCurrentEntry(player.entry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setIsPaused(Boolean(player.isPaused()));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
handleActivePlayerUpdate();
|
|
100
|
+
|
|
101
|
+
const removeLifecycle = playerManager.addLifecycleListener({
|
|
102
|
+
onRegistered: (registeredPlayer) => {
|
|
103
|
+
if (isPrimaryOrCastingPlayer(registeredPlayer)) {
|
|
104
|
+
handleActivePlayerUpdate();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onCastingChanged: handleActivePlayerUpdate,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
removeLifecycle?.();
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!activePlayer) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const syncPlayerState = () => {
|
|
121
|
+
if (activePlayer.entry) {
|
|
122
|
+
setCurrentEntry(activePlayer.entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setIsPaused(Boolean(activePlayer.isPaused()));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
syncPlayerState();
|
|
129
|
+
|
|
130
|
+
const removeListener = activePlayer.addListener({
|
|
131
|
+
id: "queue-now-playing-header",
|
|
132
|
+
listener: {
|
|
133
|
+
onPlayerPause: () => setIsPaused(true),
|
|
134
|
+
onPlayerResume: () => setIsPaused(false),
|
|
135
|
+
onVideoLoad: syncPlayerState,
|
|
136
|
+
onLoad: syncPlayerState,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
removeListener?.();
|
|
142
|
+
};
|
|
143
|
+
}, [activePlayer]);
|
|
144
|
+
|
|
145
|
+
const handlePlayPause = () => {
|
|
146
|
+
if (!activePlayer) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (isPaused) {
|
|
151
|
+
activePlayer.play();
|
|
152
|
+
setIsPaused(false);
|
|
153
|
+
} else {
|
|
154
|
+
activePlayer.pause();
|
|
155
|
+
setIsPaused(true);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
52
158
|
|
|
53
159
|
const renderNowPlayingTrack = () => {
|
|
54
|
-
if (!
|
|
160
|
+
if (!currentEntry) {
|
|
55
161
|
return null;
|
|
56
162
|
}
|
|
57
163
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const imageGroup = Array.isArray(mediaGroup)
|
|
61
|
-
? mediaGroup.find((g: any) => g.type === "image")
|
|
164
|
+
const imageGroup = Array.isArray(currentEntry.media_group)
|
|
165
|
+
? currentEntry.media_group.find((g: any) => g.type === "image")
|
|
62
166
|
: null;
|
|
63
167
|
|
|
64
168
|
const firstImage = Array.isArray(imageGroup?.media_item)
|
|
@@ -66,26 +170,18 @@ export function NowPlayingHeaderSection({
|
|
|
66
170
|
: imageGroup?.media_item?.src;
|
|
67
171
|
|
|
68
172
|
const nowPlayingItem: MenuItem = {
|
|
69
|
-
id: String(
|
|
70
|
-
title: (
|
|
71
|
-
summary: (
|
|
173
|
+
id: String(currentEntry.id ?? "now_playing"),
|
|
174
|
+
title: (currentEntry.title as string) || String(currentEntry.id),
|
|
175
|
+
summary: (currentEntry.summary as string) ?? "",
|
|
72
176
|
icon: firstImage,
|
|
73
177
|
};
|
|
74
178
|
|
|
75
179
|
const data = mapMenuItemToNowPlayingData(
|
|
76
180
|
nowPlayingItem,
|
|
77
|
-
|
|
181
|
+
isPaused,
|
|
78
182
|
nowPlayingTitle
|
|
79
183
|
);
|
|
80
184
|
|
|
81
|
-
const handlePlayPause = () => {
|
|
82
|
-
if (playerState?.isPaused) {
|
|
83
|
-
player?.play();
|
|
84
|
-
} else {
|
|
85
|
-
player?.pause();
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
185
|
return (
|
|
90
186
|
<AudioPlayerTrackItem
|
|
91
187
|
configuration={VARIANT_NOW_PLAYING}
|
|
@@ -87,9 +87,7 @@ export const SortableList = ({
|
|
|
87
87
|
const [localData, setLocalData] = useState(sortableData);
|
|
88
88
|
|
|
89
89
|
useEffect(() => {
|
|
90
|
-
setLocalData(
|
|
91
|
-
localData !== sortableData ? sortableData : localData
|
|
92
|
-
);
|
|
90
|
+
setLocalData(sortableData);
|
|
93
91
|
}, [sortableData]);
|
|
94
92
|
|
|
95
93
|
const handleDragEnd = useCallback(
|
|
@@ -142,6 +140,7 @@ export const SortableList = ({
|
|
|
142
140
|
>
|
|
143
141
|
{renderHeader}
|
|
144
142
|
<Sortable.Grid
|
|
143
|
+
key={`sortable-grid-${localData.length}`}
|
|
145
144
|
scrollableRef={scrollableRef}
|
|
146
145
|
customHandle
|
|
147
146
|
columns={columns}
|
|
@@ -403,10 +403,17 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
403
403
|
const { eventType } = event;
|
|
404
404
|
|
|
405
405
|
if (!isLanguageOverlayVisible && eventType === "menu") {
|
|
406
|
+
// First Back dismisses Play Next, the next one closes the player
|
|
407
|
+
if (playNextOverlayState) {
|
|
408
|
+
playNextOverlayState.handleUserCancelPlayNext();
|
|
409
|
+
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
406
413
|
close();
|
|
407
414
|
}
|
|
408
415
|
},
|
|
409
|
-
[close]
|
|
416
|
+
[close, playNextOverlayState]
|
|
410
417
|
);
|
|
411
418
|
|
|
412
419
|
// Effects
|
|
@@ -12,6 +12,12 @@ export const withTvEventHandler = (Component) => {
|
|
|
12
12
|
const isStandaloneFullscreen = useIsStandaloneFullscreen();
|
|
13
13
|
|
|
14
14
|
const remoteHandler = (event) => {
|
|
15
|
+
// A nested River (e.g. a ScreenPicker tab) shares the screen with its parent
|
|
16
|
+
// River, which already handles Back. Handling it here too would go back twice.
|
|
17
|
+
if (props.isInsideContainer) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
const { eventType } = event;
|
|
16
22
|
|
|
17
23
|
const canGoBack = navigator.canGoBack();
|
|
@@ -129,8 +129,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
|
|
|
129
129
|
* The last scene in the array is removed in the "backTransitionEnd" callback,
|
|
130
130
|
* only after the animation is complete.
|
|
131
131
|
*/
|
|
132
|
-
backTransitionStart() {
|
|
133
|
-
const previousRoute = this.props.navigator?.currentRoute;
|
|
132
|
+
backTransitionStart(previousRoute: string) {
|
|
134
133
|
const currentRoute = this.props.navigator?.currentRoute;
|
|
135
134
|
|
|
136
135
|
const diffLengthRoutes =
|
|
@@ -242,7 +241,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
|
|
|
242
241
|
break;
|
|
243
242
|
|
|
244
243
|
case NAV_ACTION_BACK: {
|
|
245
|
-
this.backTransitionStart();
|
|
244
|
+
this.backTransitionStart(previousRoute);
|
|
246
245
|
break;
|
|
247
246
|
}
|
|
248
247
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Text, View } from "react-native";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AnimationManager } from "../AnimationManager";
|
|
5
|
+
import {
|
|
6
|
+
NAV_ACTION_BACK,
|
|
7
|
+
NAV_ACTION_PUSH,
|
|
8
|
+
TransitionerComponent,
|
|
9
|
+
} from "../Transitioner";
|
|
10
|
+
import { act, render } from "@testing-library/react-native";
|
|
6
11
|
|
|
7
12
|
jest.mock("../Scene", () => {
|
|
8
13
|
const View = jest.requireActual("react-native").View;
|
|
@@ -40,6 +45,27 @@ const transitionerComponentProps = {
|
|
|
40
45
|
|
|
41
46
|
jest.mock("uuid");
|
|
42
47
|
|
|
48
|
+
const sceneTransitionConfig = () => ({
|
|
49
|
+
push: {
|
|
50
|
+
duration: 0,
|
|
51
|
+
easing: jest.fn(),
|
|
52
|
+
from: { style: {} },
|
|
53
|
+
to: { style: {} },
|
|
54
|
+
},
|
|
55
|
+
back: {
|
|
56
|
+
duration: 0,
|
|
57
|
+
easing: jest.fn(),
|
|
58
|
+
from: { style: {} },
|
|
59
|
+
to: { style: {} },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const sceneFor = (route) => (
|
|
64
|
+
<View pathname={route}>
|
|
65
|
+
<Text>{route}</Text>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
|
|
43
69
|
describe("<TransitionerComponent />", () => {
|
|
44
70
|
it("renders correctly", () => {
|
|
45
71
|
const wrapper = render(
|
|
@@ -50,4 +76,59 @@ describe("<TransitionerComponent />", () => {
|
|
|
50
76
|
|
|
51
77
|
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
52
78
|
});
|
|
79
|
+
|
|
80
|
+
it("removes both intermediate scenes when backing out to the tab root", () => {
|
|
81
|
+
jest
|
|
82
|
+
.spyOn(AnimationManager.prototype, "animate")
|
|
83
|
+
.mockImplementation((_, done) => done());
|
|
84
|
+
|
|
85
|
+
const ref = React.createRef();
|
|
86
|
+
const root = "/river/root";
|
|
87
|
+
const child = `${root}/river/child`;
|
|
88
|
+
const grandchild = `${child}/river/grandchild`;
|
|
89
|
+
|
|
90
|
+
const propsFor = (route, previousAction) => ({
|
|
91
|
+
navigator: {
|
|
92
|
+
previousAction,
|
|
93
|
+
currentRoute: route,
|
|
94
|
+
screenData: {},
|
|
95
|
+
},
|
|
96
|
+
transitionConfig: sceneTransitionConfig,
|
|
97
|
+
layoutData: {
|
|
98
|
+
isTablet: false,
|
|
99
|
+
isTabletPortrait: false,
|
|
100
|
+
},
|
|
101
|
+
children: sceneFor(route),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const { rerender } = render(
|
|
105
|
+
<TransitionerComponent ref={ref} {...propsFor(root, "REPLACE")} />
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
act(() => {
|
|
109
|
+
rerender(
|
|
110
|
+
<TransitionerComponent
|
|
111
|
+
ref={ref}
|
|
112
|
+
{...propsFor(child, NAV_ACTION_PUSH)}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
rerender(
|
|
119
|
+
<TransitionerComponent
|
|
120
|
+
ref={ref}
|
|
121
|
+
{...propsFor(grandchild, NAV_ACTION_PUSH)}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
act(() => {
|
|
127
|
+
rerender(
|
|
128
|
+
<TransitionerComponent ref={ref} {...propsFor(root, NAV_ACTION_BACK)} />
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(ref.current.state.scenes.map((scene) => scene.key)).toEqual([root]);
|
|
133
|
+
});
|
|
53
134
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.1.16
|
|
3
|
+
"version": "15.1.16",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "15.1.16
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.1.16
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.1.16
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.1.16
|
|
31
|
+
"@applicaster/applicaster-types": "15.1.16",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.1.16",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.1.16",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.1.16",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"react-native-sortables": "1.7.1",
|