@applicaster/quick-brick-player 15.1.13 → 15.1.15-alpha.4236143002
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 +5 -5
- package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +17 -39
- package/src/Player/AudioLayer/Layout/Controls/__tests__/utils.test.ts +25 -4
- package/src/Player/AudioLayer/Layout/Controls/index.tsx +2 -8
- package/src/Player/AudioLayer/Layout/Controls/utils.ts +14 -0
- package/src/Player/AudioLayer/Layout/TransportInfo/__tests__/index.test.tsx +257 -0
- package/src/Player/AudioLayer/Layout/TransportInfo/index.tsx +69 -8
- package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
- package/src/Player/AudioLayer/SpeedManager.ts +4 -0
- package/src/Player/AudioLayer/__tests__/SleepTimerManager.test.ts +78 -0
- package/src/Player/AudioLayer/__tests__/SpeedManager.test.ts +57 -0
- package/src/Player/AudioLayer/__tests__/playerActions.test.ts +369 -0
- package/src/Player/AudioLayer/playerActions.ts +294 -0
- package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-player",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.15-alpha.4236143002",
|
|
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.1.
|
|
38
|
+
"@applicaster/quick-brick-mobile-transport-controls": "15.1.15-alpha.4236143002",
|
|
39
39
|
"@applicaster/quick-brick-tv-transport-controls": "15.1.9",
|
|
40
|
-
"@applicaster/zapp-react-native-tvos-app": "15.1.
|
|
41
|
-
"@applicaster/zapp-react-native-ui-components": "15.1.
|
|
42
|
-
"@applicaster/zapp-react-native-utils": "15.1.
|
|
40
|
+
"@applicaster/zapp-react-native-tvos-app": "15.1.15-alpha.4236143002",
|
|
41
|
+
"@applicaster/zapp-react-native-ui-components": "15.1.15-alpha.4236143002",
|
|
42
|
+
"@applicaster/zapp-react-native-utils": "15.1.15-alpha.4236143002",
|
|
43
43
|
"query-string": "7.1.3",
|
|
44
44
|
"shaka-player": "4.3.5",
|
|
45
45
|
"typeface-montserrat": "^0.0.54",
|
|
@@ -12,23 +12,18 @@ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
|
12
12
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
13
13
|
import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
|
|
14
14
|
import { formatSpeed } from "./utils";
|
|
15
|
-
import {
|
|
16
|
-
getBottomSheetModalDefaultItemIcon,
|
|
17
|
-
getBottomSheetModalIconProps,
|
|
18
|
-
getBottomSheetModalItemProps,
|
|
19
|
-
getBottomSheetModalLabelProps,
|
|
20
|
-
getBottomSheetModalSelectedItemIcon,
|
|
21
|
-
} from "../Utils";
|
|
22
15
|
|
|
23
16
|
import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
24
|
-
|
|
25
17
|
import { DEFAULT_SPEED, PlaybackSpeed, SpeedController } from "./SpeedManager";
|
|
26
18
|
import {
|
|
27
19
|
dismissModal as dismissBottomSheetModal,
|
|
28
|
-
openBottomSheetModal,
|
|
29
20
|
useModalStoreState,
|
|
30
21
|
} from "@applicaster/zapp-react-native-utils/modalState";
|
|
31
|
-
import {
|
|
22
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
23
|
+
import {
|
|
24
|
+
PLAYER_ACTION_IDENTIFIERS,
|
|
25
|
+
registerPlayerActions,
|
|
26
|
+
} from "./playerActions";
|
|
32
27
|
|
|
33
28
|
const { log_debug } = createLogger({
|
|
34
29
|
subsystem: "UIComponent",
|
|
@@ -118,6 +113,14 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
|
|
|
118
113
|
setSpeedManager(player ? new SpeedController(player) : null);
|
|
119
114
|
}, [player]);
|
|
120
115
|
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!speedManager || playerState.isLive) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return registerPlayerActions({ speedController: speedManager });
|
|
122
|
+
}, [speedManager, playerState.isLive]);
|
|
123
|
+
|
|
121
124
|
useEffect(() => {
|
|
122
125
|
if (!player) return;
|
|
123
126
|
|
|
@@ -216,36 +219,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
|
|
|
216
219
|
player?.forward(seekDuration);
|
|
217
220
|
}, [seekDuration, player]);
|
|
218
221
|
|
|
219
|
-
const onSpeedSelect = React.useCallback(
|
|
220
|
-
async (item) => {
|
|
221
|
-
await speedManager?.setPlaybackSpeed(item);
|
|
222
|
-
},
|
|
223
|
-
[speedManager]
|
|
224
|
-
);
|
|
225
|
-
|
|
226
222
|
const onSpeedButtonPress = React.useCallback(() => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
onPress: onSpeedSelect,
|
|
232
|
-
title: configuration.playback_speed_title,
|
|
233
|
-
itemProps: getBottomSheetModalItemProps,
|
|
234
|
-
iconProps: getBottomSheetModalIconProps,
|
|
235
|
-
labelProps: getBottomSheetModalLabelProps,
|
|
236
|
-
getSelectedItemIcon: getBottomSheetModalSelectedItemIcon,
|
|
237
|
-
getDefaultItemIcon: getBottomSheetModalDefaultItemIcon,
|
|
238
|
-
iconPlacement: "right",
|
|
239
|
-
},
|
|
240
|
-
options: {
|
|
241
|
-
supportedOrientations: [
|
|
242
|
-
"portrait",
|
|
243
|
-
"landscape",
|
|
244
|
-
"portrait-upside-down",
|
|
245
|
-
],
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
}, [playbackSpeed, onSpeedSelect, configuration.playback_speed_title]);
|
|
223
|
+
uiActionsRegistry
|
|
224
|
+
.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
|
|
225
|
+
?.invokeAction(entry);
|
|
226
|
+
}, [entry]);
|
|
249
227
|
|
|
250
228
|
const onPlayerSeek = React.useCallback(
|
|
251
229
|
(targetTime: number) => {
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
controlsRowActionIdentifiers,
|
|
2
3
|
nextTrackButton,
|
|
3
4
|
previousTrackButton,
|
|
4
5
|
usesPlaylistControls,
|
|
5
6
|
} from "../utils";
|
|
7
|
+
import { PLAYER_ACTION_IDENTIFIERS } from "../../../playerActions";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
key === "player_controls_layout" ? layout : null) as unknown as GetValue;
|
|
9
|
+
const configuredAs = (layout?: unknown): GetValue =>
|
|
10
|
+
((key: string) =>
|
|
11
|
+
key === "player_controls_layout" ? layout : null) as unknown as GetValue;
|
|
11
12
|
|
|
13
|
+
describe("usesPlaylistControls", () => {
|
|
12
14
|
it("opts in only for the playlist controls value", () => {
|
|
13
15
|
expect(usesPlaylistControls(configuredAs("playlist_controls"))).toBe(true);
|
|
14
16
|
});
|
|
@@ -29,6 +31,25 @@ describe("usesPlaylistControls", () => {
|
|
|
29
31
|
});
|
|
30
32
|
});
|
|
31
33
|
|
|
34
|
+
describe("controlsRowActionIdentifiers", () => {
|
|
35
|
+
it("claims playback speed and sleep timer on the default layout", () => {
|
|
36
|
+
expect(
|
|
37
|
+
controlsRowActionIdentifiers(
|
|
38
|
+
configuredAs("playback_speed_and_sleep_timer")
|
|
39
|
+
)
|
|
40
|
+
).toEqual([
|
|
41
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
42
|
+
PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER,
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("claims nothing when the row shows playlist controls", () => {
|
|
47
|
+
expect(
|
|
48
|
+
controlsRowActionIdentifiers(configuredAs("playlist_controls"))
|
|
49
|
+
).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
32
53
|
describe("track control buttons", () => {
|
|
33
54
|
it("builds a previous button using the previous_track icon", () => {
|
|
34
55
|
const onPress = jest.fn();
|
|
@@ -2,10 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
3
|
|
|
4
4
|
import { dayjs } from "@applicaster/zapp-react-native-utils/dateUtils";
|
|
5
|
-
import {
|
|
6
|
-
useIsTablet,
|
|
7
|
-
useZStore,
|
|
8
|
-
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
5
|
+
import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
9
6
|
import {
|
|
10
7
|
Controls as MobileTransportControls,
|
|
11
8
|
ProgressBar,
|
|
@@ -100,11 +97,8 @@ export const Controls = (props: Props) => {
|
|
|
100
97
|
maxWidth = 320,
|
|
101
98
|
} = props;
|
|
102
99
|
|
|
103
|
-
const configuration = useZStore("playerConfiguration");
|
|
104
|
-
|
|
105
100
|
const { sleepTimerState, onSleepButtonPress } = useSleepTimerModal(
|
|
106
|
-
playerManager.getActivePlayer()
|
|
107
|
-
configuration
|
|
101
|
+
playerManager.getActivePlayer()
|
|
108
102
|
);
|
|
109
103
|
|
|
110
104
|
const { hasNext, hasPrevious, goNext, goPrevious } = usePlaylistNavigation(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PLAYER_ACTION_IDENTIFIERS } from "../../playerActions";
|
|
2
|
+
|
|
1
3
|
const playbackButtonMargins = (
|
|
2
4
|
isTablet: boolean,
|
|
3
5
|
isLiveShowFullControls: boolean
|
|
@@ -109,6 +111,18 @@ export const sleepButton = (props: ButtonsProps): ControlsButton =>
|
|
|
109
111
|
export const usesPlaylistControls = (value: GetValue): boolean =>
|
|
110
112
|
value("player_controls_layout") === "playlist_controls";
|
|
111
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Player actions this row renders as its own buttons. Consumers of the shared
|
|
116
|
+
* player action list have to filter these out to avoid showing them twice.
|
|
117
|
+
*/
|
|
118
|
+
export const controlsRowActionIdentifiers = (value: GetValue): string[] =>
|
|
119
|
+
usesPlaylistControls(value)
|
|
120
|
+
? []
|
|
121
|
+
: [
|
|
122
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
123
|
+
PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER,
|
|
124
|
+
];
|
|
125
|
+
|
|
112
126
|
type TrackButtonProps = ButtonsProps & { disabled?: boolean };
|
|
113
127
|
|
|
114
128
|
const trackButtonByName = ({
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
|
|
4
|
+
import { TransportInfo } from "../index";
|
|
5
|
+
import { resolvePlayerActions } from "@applicaster/zapp-react-native-utils/playerUtils";
|
|
6
|
+
import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
|
|
7
|
+
import { TransportInfo as MobileTransportInfo } from "@applicaster/quick-brick-mobile-transport-controls";
|
|
8
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
9
|
+
import { PLAYER_ACTION_IDENTIFIERS } from "../../../playerActions";
|
|
10
|
+
|
|
11
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
12
|
+
useIsTablet: () => false,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock("react-native-safe-area-context", () => ({
|
|
16
|
+
useSafeAreaFrame: () => ({ height: 800, width: 400 }),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock(
|
|
20
|
+
"@applicaster/zapp-react-native-ui-components/Hooks/useEntryActionsExpander",
|
|
21
|
+
() => ({ useEntryActionsExpander: () => () => [] })
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
|
|
25
|
+
usePlugins: jest.fn(() => []),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => {
|
|
29
|
+
const actual = jest.requireActual(
|
|
30
|
+
"@applicaster/zapp-react-native-utils/playerUtils"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...actual,
|
|
35
|
+
resolvePlayerActions: jest.fn(),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
jest.mock("@applicaster/quick-brick-mobile-transport-controls", () => ({
|
|
40
|
+
TransportInfo: jest.fn(() => null),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const MobileTransportInfoMock = MobileTransportInfo as unknown as jest.Mock;
|
|
44
|
+
const resolvePlayerActionsMock = resolvePlayerActions as jest.Mock;
|
|
45
|
+
const usePluginsMock = usePlugins as jest.Mock;
|
|
46
|
+
|
|
47
|
+
const action = (identifier: string) =>
|
|
48
|
+
({ identifier, action: { invokeAction: jest.fn() } }) as any;
|
|
49
|
+
|
|
50
|
+
const valueForLayout = (layout?: string): GetValue =>
|
|
51
|
+
((key: string) =>
|
|
52
|
+
key === "player_controls_layout" ? layout : null) as unknown as GetValue;
|
|
53
|
+
|
|
54
|
+
const entry: ZappEntry = {
|
|
55
|
+
id: "audio-1",
|
|
56
|
+
title: "Track",
|
|
57
|
+
type: { value: "audio" },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const layoutState = { inline: false, docked: false, isModal: true };
|
|
61
|
+
|
|
62
|
+
describe("Audio TransportInfo overflow wiring", () => {
|
|
63
|
+
let getActionsSpy: jest.SpyInstance;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.clearAllMocks();
|
|
67
|
+
|
|
68
|
+
resolvePlayerActionsMock.mockReturnValue([
|
|
69
|
+
action("favorites"),
|
|
70
|
+
action("offline-content-button"),
|
|
71
|
+
action("native_share_action"),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
getActionsSpy = jest
|
|
75
|
+
.spyOn(uiActionsRegistry, "getActions")
|
|
76
|
+
.mockImplementation((identifier: string) => {
|
|
77
|
+
if (
|
|
78
|
+
identifier === PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED ||
|
|
79
|
+
identifier === PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER
|
|
80
|
+
) {
|
|
81
|
+
return [action(identifier)];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [];
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
getActionsSpy.mockRestore();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("passes resolved actions as descriptors and enables overflow when the plugin is present", () => {
|
|
93
|
+
usePluginsMock.mockReturnValue([
|
|
94
|
+
{ identifier: "open-modal-bottom-sheet-cell-action" },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
render(
|
|
98
|
+
<TransportInfo
|
|
99
|
+
entry={entry}
|
|
100
|
+
layoutState={layoutState}
|
|
101
|
+
value={valueForLayout("playback_speed_and_sleep_timer")}
|
|
102
|
+
configuration={{ player_action_buttons: "a,b,c" }}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(resolvePlayerActionsMock).toHaveBeenCalledWith({
|
|
107
|
+
configuration: { player_action_buttons: "a,b,c" },
|
|
108
|
+
entry,
|
|
109
|
+
expandEntryActions: expect.any(Function),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const props = MobileTransportInfoMock.mock.calls[0][0];
|
|
113
|
+
|
|
114
|
+
expect(props.actionButtonDescriptors).toEqual([
|
|
115
|
+
{
|
|
116
|
+
kind: "action",
|
|
117
|
+
action: expect.objectContaining({ identifier: "favorites" }),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
kind: "action",
|
|
121
|
+
action: expect.objectContaining({
|
|
122
|
+
identifier: "offline-content-button",
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
kind: "action",
|
|
127
|
+
action: expect.objectContaining({ identifier: "native_share_action" }),
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(props.actionButtons).toBeUndefined();
|
|
132
|
+
|
|
133
|
+
// Controls row renders speed and sleep timer itself in this layout.
|
|
134
|
+
expect(getActionsSpy).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("appends playback speed and sleep timer when playlist controls layout is active", async () => {
|
|
138
|
+
usePluginsMock.mockReturnValue([
|
|
139
|
+
{ identifier: "open-modal-bottom-sheet-cell-action" },
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
resolvePlayerActionsMock.mockReturnValue([
|
|
143
|
+
action("favorites"),
|
|
144
|
+
action("offline-content-button"),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// The audio player registers these in a later effect than our first paint.
|
|
148
|
+
let registered = false;
|
|
149
|
+
|
|
150
|
+
getActionsSpy.mockImplementation((identifier: string) => {
|
|
151
|
+
if (
|
|
152
|
+
!registered ||
|
|
153
|
+
(identifier !== PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED &&
|
|
154
|
+
identifier !== PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
|
|
155
|
+
) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [action(identifier)];
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
render(
|
|
163
|
+
<TransportInfo
|
|
164
|
+
entry={entry}
|
|
165
|
+
layoutState={layoutState}
|
|
166
|
+
value={valueForLayout("playlist_controls")}
|
|
167
|
+
configuration={{ player_action_buttons: "favorites,offline" }}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(
|
|
172
|
+
MobileTransportInfoMock.mock.calls[0][0].actionButtonDescriptors
|
|
173
|
+
).toEqual([
|
|
174
|
+
{
|
|
175
|
+
kind: "action",
|
|
176
|
+
action: expect.objectContaining({ identifier: "favorites" }),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
kind: "action",
|
|
180
|
+
action: expect.objectContaining({
|
|
181
|
+
identifier: "offline-content-button",
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
// Registering for real is what tells subscribers to re-read the registry.
|
|
187
|
+
registered = true;
|
|
188
|
+
|
|
189
|
+
const unregister = uiActionsRegistry.registerAction(
|
|
190
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
191
|
+
{ invokeAction: jest.fn() }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
const latest =
|
|
196
|
+
MobileTransportInfoMock.mock.calls[
|
|
197
|
+
MobileTransportInfoMock.mock.calls.length - 1
|
|
198
|
+
][0];
|
|
199
|
+
|
|
200
|
+
expect(latest.actionButtonDescriptors).toEqual([
|
|
201
|
+
{
|
|
202
|
+
kind: "action",
|
|
203
|
+
action: expect.objectContaining({ identifier: "favorites" }),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
kind: "action",
|
|
207
|
+
action: expect.objectContaining({
|
|
208
|
+
identifier: "offline-content-button",
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
kind: "action",
|
|
213
|
+
action: expect.objectContaining({
|
|
214
|
+
identifier: PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
kind: "action",
|
|
219
|
+
action: expect.objectContaining({
|
|
220
|
+
identifier: PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER,
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
unregister();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("does not duplicate playback speed or sleep timer if already configured", () => {
|
|
230
|
+
resolvePlayerActionsMock.mockReturnValue([
|
|
231
|
+
action("favorites"),
|
|
232
|
+
action(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED),
|
|
233
|
+
action(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
render(
|
|
237
|
+
<TransportInfo
|
|
238
|
+
entry={entry}
|
|
239
|
+
layoutState={layoutState}
|
|
240
|
+
value={valueForLayout("playlist_controls")}
|
|
241
|
+
configuration={{}}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const identifiers =
|
|
246
|
+
MobileTransportInfoMock.mock.calls[0][0].actionButtonDescriptors.map(
|
|
247
|
+
(descriptor: { action: { identifier: string } }) =>
|
|
248
|
+
descriptor.action.identifier
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(identifiers).toEqual([
|
|
252
|
+
"favorites",
|
|
253
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
254
|
+
PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER,
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -3,7 +3,8 @@ import { LayoutChangeEvent, StyleSheet, View } from "react-native";
|
|
|
3
3
|
|
|
4
4
|
import { TransportInfo as MobileTransportInfo } from "@applicaster/quick-brick-mobile-transport-controls";
|
|
5
5
|
import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
6
|
-
import {
|
|
6
|
+
import { useRegistryRevision } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
7
|
+
import { useEntryActionsExpander } from "@applicaster/zapp-react-native-ui-components/Hooks/useEntryActionsExpander";
|
|
7
8
|
|
|
8
9
|
import { toPositiveNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
9
10
|
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
@@ -12,7 +13,22 @@ import { NAVBAR_HEIGHT, TABLET_LANDSCAPE } from "../const";
|
|
|
12
13
|
import { withDebug } from "../debug";
|
|
13
14
|
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
|
14
15
|
import { isSmallTabletHeight } from "../utils";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
ActionButtonDescriptor,
|
|
18
|
+
resolvePlayerActions,
|
|
19
|
+
} from "@applicaster/zapp-react-native-utils/playerUtils";
|
|
20
|
+
import {
|
|
21
|
+
RegisteredAction,
|
|
22
|
+
uiActionsRegistry,
|
|
23
|
+
} from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
24
|
+
|
|
25
|
+
import { PLAYER_ACTION_IDENTIFIERS } from "../../playerActions";
|
|
26
|
+
import { controlsRowActionIdentifiers } from "../Controls/utils";
|
|
27
|
+
|
|
28
|
+
/** Player-owned actions that join the shared action list of the overlay. */
|
|
29
|
+
const AUDIO_PLAYER_ACTION_IDENTIFIERS: string[] = Object.values(
|
|
30
|
+
PLAYER_ACTION_IDENTIFIERS
|
|
31
|
+
);
|
|
16
32
|
|
|
17
33
|
const styles = StyleSheet.create({
|
|
18
34
|
container: {
|
|
@@ -52,7 +68,8 @@ export const TransportInfo = ({
|
|
|
52
68
|
}: Props) => {
|
|
53
69
|
const isTablet = useIsTablet();
|
|
54
70
|
const safeAreaFrame = useSafeAreaFrame();
|
|
55
|
-
const
|
|
71
|
+
const registryRevision = useRegistryRevision();
|
|
72
|
+
const expandEntryActions = useEntryActionsExpander();
|
|
56
73
|
|
|
57
74
|
const [marginBottom, setMarginBottom] = React.useState(0);
|
|
58
75
|
|
|
@@ -81,11 +98,56 @@ export const TransportInfo = ({
|
|
|
81
98
|
[onLayout]
|
|
82
99
|
);
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
// Joined so the memo below is not invalidated on every render.
|
|
102
|
+
const controlsRowIdentifiersKey =
|
|
103
|
+
controlsRowActionIdentifiers(value).join(",");
|
|
104
|
+
|
|
105
|
+
const overlayActionIdentifiers = React.useMemo(() => {
|
|
106
|
+
const renderedByControlsRow = controlsRowIdentifiersKey.split(",");
|
|
107
|
+
|
|
108
|
+
return AUDIO_PLAYER_ACTION_IDENTIFIERS.filter(
|
|
109
|
+
(identifier) => !renderedByControlsRow.includes(identifier)
|
|
110
|
+
);
|
|
111
|
+
}, [controlsRowIdentifiersKey]);
|
|
112
|
+
|
|
113
|
+
const readOwnPlayerActions = React.useCallback(
|
|
114
|
+
(): RegisteredAction[] =>
|
|
115
|
+
overlayActionIdentifiers.flatMap((identifier) =>
|
|
116
|
+
uiActionsRegistry.getActions(identifier, entry)
|
|
117
|
+
),
|
|
118
|
+
[entry, overlayActionIdentifiers]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Playback speed and the sleep timer register in a later effect than our
|
|
122
|
+
// first render - speed waits for SpeedController state - so re-read whenever
|
|
123
|
+
// the registry changes instead of guessing when they will be there.
|
|
124
|
+
const playerActions = React.useMemo(
|
|
125
|
+
() => readOwnPlayerActions(),
|
|
126
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
127
|
+
[readOwnPlayerActions, registryRevision]
|
|
87
128
|
);
|
|
88
129
|
|
|
130
|
+
const actionButtonDescriptors = React.useMemo<
|
|
131
|
+
ActionButtonDescriptor[]
|
|
132
|
+
>(() => {
|
|
133
|
+
const content = resolvePlayerActions({
|
|
134
|
+
configuration,
|
|
135
|
+
entry,
|
|
136
|
+
expandEntryActions,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const seen = new Set(content.map((action) => action.identifier));
|
|
140
|
+
|
|
141
|
+
const extras = playerActions.filter(
|
|
142
|
+
(action) => action?.identifier && !seen.has(action.identifier)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return [...content, ...extras].map((action) => ({
|
|
146
|
+
kind: "action" as const,
|
|
147
|
+
action,
|
|
148
|
+
}));
|
|
149
|
+
}, [configuration, entry, playerActions, expandEntryActions]);
|
|
150
|
+
|
|
89
151
|
return (
|
|
90
152
|
<View
|
|
91
153
|
style={[
|
|
@@ -106,9 +168,8 @@ export const TransportInfo = ({
|
|
|
106
168
|
content={entry}
|
|
107
169
|
value={value}
|
|
108
170
|
visible
|
|
109
|
-
languageData={languageData}
|
|
110
171
|
layoutState={layoutState}
|
|
111
|
-
|
|
172
|
+
actionButtonDescriptors={actionButtonDescriptors}
|
|
112
173
|
/>
|
|
113
174
|
</View>
|
|
114
175
|
</View>
|
|
@@ -41,6 +41,10 @@ export class SleepTimerController {
|
|
|
41
41
|
return this.sleepTimerStateSubject.asObservable();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
get currentState(): SleepTimerState {
|
|
45
|
+
return this.sleepTimerStateSubject.getValue();
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
private async scheduleSleep(sleepDate): Promise<boolean> {
|
|
45
49
|
log_debug(`Setting sleep interval to ${sleepDate}`);
|
|
46
50
|
|
|
@@ -118,7 +122,7 @@ export class SleepTimerController {
|
|
|
118
122
|
|
|
119
123
|
log_debug("Sleep state restored");
|
|
120
124
|
|
|
121
|
-
this.setSleepTimer(item as SleepTimer, futureSleepDate);
|
|
125
|
+
await this.setSleepTimer(item as SleepTimer, futureSleepDate);
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
// Will be called if user closed the player explicitly.
|
|
@@ -33,6 +33,10 @@ export class SpeedController {
|
|
|
33
33
|
return this.playbackSpeedSubject.asObservable();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
get currentSpeed(): PlaybackSpeed {
|
|
37
|
+
return this.playbackSpeedSubject.getValue();
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
async loadSpeed() {
|
|
37
41
|
const speed = await localStorage.getItem(SPEED_PREFERENCE_KEY, NAMESPACE);
|
|
38
42
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
2
|
+
import { SLEEP_TIMER_OFF, SleepTimerController } from "../SleepTimerManager";
|
|
3
|
+
|
|
4
|
+
jest.mock(
|
|
5
|
+
"@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
|
|
6
|
+
() => ({
|
|
7
|
+
localStorage: {
|
|
8
|
+
getItem: jest.fn(),
|
|
9
|
+
setItem: jest.fn(),
|
|
10
|
+
removeItem: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
describe("SleepTimerController", () => {
|
|
16
|
+
const player = {
|
|
17
|
+
startSleepTimer: jest.fn(),
|
|
18
|
+
cancelSleepTimer: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let sleepController: SleepTimerController;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
(localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
26
|
+
(localStorage.removeItem as jest.Mock).mockResolvedValue(undefined);
|
|
27
|
+
(localStorage.getItem as jest.Mock).mockResolvedValue(undefined);
|
|
28
|
+
sleepController = new SleepTimerController(player as any);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("starts from the off state", () => {
|
|
32
|
+
expect(sleepController.currentState.timer).toEqual(SLEEP_TIMER_OFF);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("starts a sleep timer on the constructed player", async () => {
|
|
36
|
+
await sleepController.setSleepTimer({
|
|
37
|
+
label: "15 minutes",
|
|
38
|
+
value: 15,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(player.cancelSleepTimer).toHaveBeenCalled();
|
|
42
|
+
expect(player.startSleepTimer).toHaveBeenCalled();
|
|
43
|
+
expect(sleepController.currentState.timer.value).toBe(15);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("loadSleep restores an unexpired timer", async () => {
|
|
47
|
+
const futureSleepDate = Date.now() + 60 * 1000;
|
|
48
|
+
|
|
49
|
+
(localStorage.getItem as jest.Mock).mockResolvedValue({
|
|
50
|
+
label: "15 minutes",
|
|
51
|
+
value: 15,
|
|
52
|
+
futureSleepDate,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
player.startSleepTimer.mockClear();
|
|
56
|
+
|
|
57
|
+
await sleepController.loadSleep();
|
|
58
|
+
|
|
59
|
+
expect(player.startSleepTimer).toHaveBeenCalledWith(futureSleepDate);
|
|
60
|
+
expect(sleepController.currentState.timer.value).toBe(15);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("loadSleep drops an expired timer without starting a native timer", async () => {
|
|
64
|
+
(localStorage.getItem as jest.Mock).mockResolvedValue({
|
|
65
|
+
label: "15 minutes",
|
|
66
|
+
value: 15,
|
|
67
|
+
futureSleepDate: Date.now() - 1000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
player.startSleepTimer.mockClear();
|
|
71
|
+
|
|
72
|
+
await sleepController.loadSleep();
|
|
73
|
+
|
|
74
|
+
expect(player.startSleepTimer).not.toHaveBeenCalled();
|
|
75
|
+
expect(localStorage.removeItem).toHaveBeenCalled();
|
|
76
|
+
expect(sleepController.currentState.timer).toEqual(SLEEP_TIMER_OFF);
|
|
77
|
+
});
|
|
78
|
+
});
|