@applicaster/quick-brick-player 16.0.0-rc.11 → 16.0.0-rc.110
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 +6 -6
- package/src/Player/AudioLayer/AudioDescription.tsx +46 -11
- package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +38 -48
- package/src/Player/AudioLayer/Layout/Controls/__tests__/utils.test.ts +87 -0
- package/src/Player/AudioLayer/Layout/Controls/index.tsx +34 -14
- package/src/Player/AudioLayer/Layout/Controls/utils.ts +40 -0
- package/src/Player/AudioLayer/Layout/DockedControls/index.tsx +1 -13
- package/src/Player/AudioLayer/Layout/MobileLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +0 -4
- 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/PlaylistNavigationController.ts +354 -0
- package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
- package/src/Player/AudioLayer/SpeedManager.ts +4 -0
- package/src/Player/AudioLayer/__tests__/AudioDescription.test.tsx +147 -0
- package/src/Player/AudioLayer/__tests__/AudioPlayerWrapper.test.tsx +130 -0
- package/src/Player/AudioLayer/__tests__/PlaylistNavigationController.test.ts +528 -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/__tests__/usePlaylistNavigation.test.ts +539 -0
- package/src/Player/AudioLayer/playerActions.ts +294 -0
- package/src/Player/AudioLayer/usePlaylistNavigation.ts +148 -0
- package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
- package/src/Player/PlayerModal/VideoPlayerModal.tsx +18 -1
- package/src/Player/PlayerModal/__tests__/VideoPlayerModal.test.tsx +160 -0
- package/src/Player/__tests__/getNativeProps.test.ts +115 -0
- package/src/Player/__tests__/onTracksChanged.test.ts +192 -0
- package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
- package/src/Player/index.tsx +168 -40
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { map } from "rxjs";
|
|
2
|
+
import {
|
|
3
|
+
actionExecutor,
|
|
4
|
+
ActionResult,
|
|
5
|
+
} from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
|
|
6
|
+
import { ActionHandler } from "@applicaster/zapp-react-native-utils/actionsExecutor/types";
|
|
7
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
8
|
+
import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
|
|
9
|
+
import { modalOrchestrator } from "@applicaster/zapp-react-native-utils/modalState/ModalOrchestrator";
|
|
10
|
+
import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
11
|
+
|
|
12
|
+
import { speedOptions } from "../Const/speedOptions";
|
|
13
|
+
import { getSleepTimerOptions } from "../Const/sleepTimerOptions";
|
|
14
|
+
import { formatSpeed } from "./utils";
|
|
15
|
+
import { SpeedController } from "./SpeedManager";
|
|
16
|
+
import { SleepTimerController, SleepTimerState } from "./SleepTimerManager";
|
|
17
|
+
|
|
18
|
+
export const PLAYER_ACTION_IDENTIFIERS = {
|
|
19
|
+
PLAYBACK_SPEED: "playback-speed",
|
|
20
|
+
SLEEP_TIMER: "sleep-timer",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export const PLAYER_ACTION_TYPES = {
|
|
24
|
+
OPEN_PLAYBACK_SPEED: "openPlaybackSpeed",
|
|
25
|
+
OPEN_SLEEP_TIMER: "openSleepTimer",
|
|
26
|
+
SET_PLAYBACK_SPEED: "setPlaybackSpeed",
|
|
27
|
+
SET_SLEEP_TIMER: "setSleepTimer",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
function getPlayerConfig(): Record<string, any> {
|
|
31
|
+
return playerManager.getActivePlayer()?.getConfig?.() || {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function remainingTimerDate(timerDate: number): number | null {
|
|
35
|
+
return timerDate !== 0 && +timerDate > Date.now() ? timerDate : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dismissOpenedSheet(action: ActionType) {
|
|
39
|
+
if (action.options?.dismissSheet) {
|
|
40
|
+
modalOrchestrator.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function optionToEntry(
|
|
45
|
+
id: string,
|
|
46
|
+
title: string,
|
|
47
|
+
actionType: string,
|
|
48
|
+
options: Record<string, any>
|
|
49
|
+
): ZappEntry {
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
title,
|
|
53
|
+
extensions: {
|
|
54
|
+
tap_actions: {
|
|
55
|
+
actions: [
|
|
56
|
+
{ type: actionType, options: { ...options, dismissSheet: true } },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
} as ZappEntry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sleepTimerEntryState(
|
|
64
|
+
sleepState: SleepTimerState
|
|
65
|
+
): CellActionEntryState {
|
|
66
|
+
const config = getPlayerConfig();
|
|
67
|
+
const isActive = Boolean(remainingTimerDate(sleepState.timerDate));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
state: isActive ? 1 : 0,
|
|
71
|
+
label: config.playback_sleep_title || "Sleep Timer",
|
|
72
|
+
asset: isActive
|
|
73
|
+
? config.sleep_timer_active || config.sleep_timer || ""
|
|
74
|
+
: config.sleep_timer || "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function playbackSpeedEntryState(speed: {
|
|
79
|
+
asset?: string;
|
|
80
|
+
value?: number;
|
|
81
|
+
}): CellActionEntryState {
|
|
82
|
+
const config = getPlayerConfig();
|
|
83
|
+
const assetKey = speed?.asset || formatSpeed(speed?.value ?? 1);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
state: 0,
|
|
87
|
+
label: config.playback_speed_title || "Playback Speed",
|
|
88
|
+
// Manifest keys are speed_0_8 / speed_1 / … (same icons as the transport row).
|
|
89
|
+
// There is no playback_speed uploader.
|
|
90
|
+
asset: config[assetKey] || config.speed_1 || "",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function openPlaybackSpeedAction(
|
|
95
|
+
speedController: SpeedController
|
|
96
|
+
): Promise<ActionResult> {
|
|
97
|
+
const config = getPlayerConfig();
|
|
98
|
+
const current = speedController.currentSpeed;
|
|
99
|
+
|
|
100
|
+
const items = speedOptions.map((option) =>
|
|
101
|
+
optionToEntry(
|
|
102
|
+
String(option.value),
|
|
103
|
+
option.label,
|
|
104
|
+
PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
105
|
+
{ value: option.value }
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return actionExecutor.handleAction({
|
|
110
|
+
type: "openBottomSheet",
|
|
111
|
+
options: {
|
|
112
|
+
replace: true,
|
|
113
|
+
header: {
|
|
114
|
+
title: config.playback_speed_title || "Playback Speed",
|
|
115
|
+
},
|
|
116
|
+
content: {
|
|
117
|
+
role: "collection_selector",
|
|
118
|
+
behavior: {
|
|
119
|
+
selectMode: "single",
|
|
120
|
+
currentSelection: [String(current.value)],
|
|
121
|
+
},
|
|
122
|
+
items,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
} as ActionType);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function openSleepTimerAction(
|
|
129
|
+
sleepController: SleepTimerController
|
|
130
|
+
): Promise<ActionResult> {
|
|
131
|
+
const config = getPlayerConfig();
|
|
132
|
+
const sleepState = sleepController.currentState;
|
|
133
|
+
const timerRemaining = remainingTimerDate(sleepState.timerDate);
|
|
134
|
+
|
|
135
|
+
const items = getSleepTimerOptions(
|
|
136
|
+
config.playback_sleep_off_label || "Off",
|
|
137
|
+
timerRemaining
|
|
138
|
+
)
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((option) =>
|
|
141
|
+
optionToEntry(
|
|
142
|
+
String(option.value),
|
|
143
|
+
option.label,
|
|
144
|
+
PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
145
|
+
{ value: option.value, label: option.label }
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return actionExecutor.handleAction({
|
|
150
|
+
type: "openBottomSheet",
|
|
151
|
+
options: {
|
|
152
|
+
replace: true,
|
|
153
|
+
header: {
|
|
154
|
+
title: config.playback_sleep_title || "Sleep Timer",
|
|
155
|
+
},
|
|
156
|
+
content: {
|
|
157
|
+
role: "collection_selector",
|
|
158
|
+
behavior: {
|
|
159
|
+
selectMode: "single",
|
|
160
|
+
currentSelection: timerRemaining
|
|
161
|
+
? [String(sleepState.timer.value)]
|
|
162
|
+
: [],
|
|
163
|
+
},
|
|
164
|
+
items,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
} as ActionType);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function setPlaybackSpeedAction(
|
|
171
|
+
speedController: SpeedController,
|
|
172
|
+
action: ActionType
|
|
173
|
+
): Promise<ActionResult> {
|
|
174
|
+
const value = toNumber(action.options?.value);
|
|
175
|
+
|
|
176
|
+
if (!Number.isFinite(value)) {
|
|
177
|
+
return ActionResult.Error;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await speedController.setPlaybackSpeed({
|
|
181
|
+
asset: formatSpeed(value),
|
|
182
|
+
value,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
dismissOpenedSheet(action);
|
|
186
|
+
|
|
187
|
+
return ActionResult.Success;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function setSleepTimerAction(
|
|
191
|
+
sleepController: SleepTimerController,
|
|
192
|
+
action: ActionType
|
|
193
|
+
): Promise<ActionResult> {
|
|
194
|
+
const value = toNumber(action.options?.value);
|
|
195
|
+
|
|
196
|
+
if (!Number.isFinite(value)) {
|
|
197
|
+
return ActionResult.Error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await sleepController.setSleepTimer({
|
|
201
|
+
label: action.options?.label || String(value),
|
|
202
|
+
value,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
dismissOpenedSheet(action);
|
|
206
|
+
|
|
207
|
+
return ActionResult.Success;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function registerPlayerActions(options: {
|
|
211
|
+
speedController?: SpeedController;
|
|
212
|
+
sleepController?: SleepTimerController;
|
|
213
|
+
}): () => void {
|
|
214
|
+
const unregisters: Array<() => void> = [];
|
|
215
|
+
|
|
216
|
+
if (options.speedController) {
|
|
217
|
+
const speedController = options.speedController;
|
|
218
|
+
|
|
219
|
+
const speedHandlers: Array<[string, ActionHandler]> = [
|
|
220
|
+
[
|
|
221
|
+
PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
|
|
222
|
+
() => openPlaybackSpeedAction(speedController),
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
226
|
+
(action) => setPlaybackSpeedAction(speedController, action),
|
|
227
|
+
],
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
speedHandlers.forEach(([type, handler]) => {
|
|
231
|
+
actionExecutor.unregisterAction(type);
|
|
232
|
+
unregisters.push(actionExecutor.registerAction(type, handler));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
unregisters.push(
|
|
236
|
+
uiActionsRegistry.registerAction(
|
|
237
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED,
|
|
238
|
+
{
|
|
239
|
+
initialEntryState: () =>
|
|
240
|
+
playbackSpeedEntryState(speedController.currentSpeed),
|
|
241
|
+
observeEntryState: () =>
|
|
242
|
+
speedController.playbackSpeed$.pipe(
|
|
243
|
+
map((speed) => playbackSpeedEntryState(speed))
|
|
244
|
+
),
|
|
245
|
+
invokeAction: async () => {
|
|
246
|
+
await actionExecutor.handleAction({
|
|
247
|
+
type: PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (options.sleepController) {
|
|
256
|
+
const sleepController = options.sleepController;
|
|
257
|
+
|
|
258
|
+
const sleepHandlers: Array<[string, ActionHandler]> = [
|
|
259
|
+
[
|
|
260
|
+
PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
|
|
261
|
+
() => openSleepTimerAction(sleepController),
|
|
262
|
+
],
|
|
263
|
+
[
|
|
264
|
+
PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
265
|
+
(action) => setSleepTimerAction(sleepController, action),
|
|
266
|
+
],
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
sleepHandlers.forEach(([type, handler]) => {
|
|
270
|
+
actionExecutor.unregisterAction(type);
|
|
271
|
+
unregisters.push(actionExecutor.registerAction(type, handler));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
unregisters.push(
|
|
275
|
+
uiActionsRegistry.registerAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER, {
|
|
276
|
+
initialEntryState: () =>
|
|
277
|
+
sleepTimerEntryState(sleepController.currentState),
|
|
278
|
+
observeEntryState: () =>
|
|
279
|
+
sleepController.sleepTimerState$.pipe(
|
|
280
|
+
map((sleepState) => sleepTimerEntryState(sleepState))
|
|
281
|
+
),
|
|
282
|
+
invokeAction: async () => {
|
|
283
|
+
await actionExecutor.handleAction({
|
|
284
|
+
type: PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return () => {
|
|
292
|
+
unregisters.forEach((unregister) => unregister());
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
4
|
+
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
5
|
+
import { PlayerNativeCommandTypes } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
6
|
+
import { RemoteCommand } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
7
|
+
import { PlaylistNavigationController } from "./PlaylistNavigationController";
|
|
8
|
+
|
|
9
|
+
const REMOTE_COMMANDS_LISTENER_ID = "playlist-navigation-remote-commands";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Buttons native should expose on the lock screen / Control Center / CarPlay.
|
|
13
|
+
* Native enables exactly these and disables the rest. A track button takes its
|
|
14
|
+
* slot over the skip one, since the system shows only one of them per side.
|
|
15
|
+
* Without a next item next is left out, so the system shows it disabled.
|
|
16
|
+
*/
|
|
17
|
+
export const getTransportControls = ({
|
|
18
|
+
hasNext,
|
|
19
|
+
hasPrevious,
|
|
20
|
+
isLive,
|
|
21
|
+
}: {
|
|
22
|
+
hasNext: boolean;
|
|
23
|
+
hasPrevious: boolean;
|
|
24
|
+
isLive: boolean;
|
|
25
|
+
}): RemoteCommand[] => {
|
|
26
|
+
const commands = [
|
|
27
|
+
RemoteCommand.Play,
|
|
28
|
+
isLive ? RemoteCommand.Stop : RemoteCommand.Pause,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (hasPrevious) {
|
|
32
|
+
commands.push(RemoteCommand.PreviousTrack);
|
|
33
|
+
} else if (!isLive) {
|
|
34
|
+
commands.push(RemoteCommand.SkipBackward);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (hasNext) {
|
|
38
|
+
commands.push(RemoteCommand.NextTrack);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return commands;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PlaylistNavigation = {
|
|
45
|
+
hasNext: boolean;
|
|
46
|
+
hasPrevious: boolean;
|
|
47
|
+
goNext: () => Promise<void>;
|
|
48
|
+
goPrevious: () => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Thin UI binding hook that subscribes to PlaylistNavigationController's
|
|
53
|
+
* RxJS streams and exposes hasNext, hasPrevious, goNext, and goPrevious.
|
|
54
|
+
*/
|
|
55
|
+
export const usePlaylistNavigation = (
|
|
56
|
+
entry: ZappEntry,
|
|
57
|
+
playerState: QuickBrickPlayer.PlayerState
|
|
58
|
+
): PlaylistNavigation => {
|
|
59
|
+
const navigator = useNavigation();
|
|
60
|
+
// Not playerManager.getActivePlayer(): that is always the local player, so
|
|
61
|
+
// seeking would miss the receiver while casting.
|
|
62
|
+
const player = usePlayer();
|
|
63
|
+
|
|
64
|
+
const [controller] = React.useState(
|
|
65
|
+
() =>
|
|
66
|
+
new PlaylistNavigationController({
|
|
67
|
+
entry,
|
|
68
|
+
player,
|
|
69
|
+
navigator,
|
|
70
|
+
playerState,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const [hasNext, setHasNext] = React.useState(() =>
|
|
75
|
+
controller.hasNext$.getValue()
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const [hasPrevious, setHasPrevious] = React.useState(() =>
|
|
79
|
+
controller.hasPrevious$.getValue()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
controller.updateContext({ player, navigator, entry, playerState });
|
|
84
|
+
}, [controller, player, navigator, entry, playerState]);
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
const subNext = controller.hasNext$.subscribe(setHasNext);
|
|
88
|
+
const subPrev = controller.hasPrevious$.subscribe(setHasPrevious);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
subNext.unsubscribe();
|
|
92
|
+
subPrev.unsubscribe();
|
|
93
|
+
controller.destroy();
|
|
94
|
+
};
|
|
95
|
+
}, [controller]);
|
|
96
|
+
|
|
97
|
+
// Lock screen / Control Center / CarPlay next & previous go through the same
|
|
98
|
+
// controller as the on-screen buttons, so JS stays the single source of truth.
|
|
99
|
+
React.useEffect(
|
|
100
|
+
() =>
|
|
101
|
+
player?.addListener?.({
|
|
102
|
+
id: REMOTE_COMMANDS_LISTENER_ID,
|
|
103
|
+
listener: {
|
|
104
|
+
onRemoteCommand: ({ id }) => {
|
|
105
|
+
if (id === RemoteCommand.NextTrack) {
|
|
106
|
+
controller.goNext();
|
|
107
|
+
} else if (id === RemoteCommand.PreviousTrack) {
|
|
108
|
+
controller.goPrevious();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
[player, controller]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const isLive = Boolean(playerState?.isLive);
|
|
117
|
+
|
|
118
|
+
// Full snapshot: native renders exactly this and drops the rest. Each key is
|
|
119
|
+
// one kind of buttons, so they stay separate lists rather than one flat list.
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
const controls = {
|
|
122
|
+
transport: getTransportControls({ hasNext, hasPrevious, isLive }),
|
|
123
|
+
// TODO: other button kinds go in as their own keys next to `transport`,
|
|
124
|
+
// e.g. custom / native-drawn buttons ("play next", CarPlay / Android Auto
|
|
125
|
+
// row buttons). Add a composer on Player once more than this hook fills it.
|
|
126
|
+
// actions: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
player?.command?.(PlayerNativeCommandTypes.setPlayerControls, controls);
|
|
130
|
+
}, [player, hasNext, hasPrevious, isLive]);
|
|
131
|
+
|
|
132
|
+
const goNext = React.useCallback(
|
|
133
|
+
async () => controller.goNext(),
|
|
134
|
+
[controller]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const goPrevious = React.useCallback(
|
|
138
|
+
async () => controller.goPrevious(),
|
|
139
|
+
[controller]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
hasNext,
|
|
144
|
+
hasPrevious,
|
|
145
|
+
goNext,
|
|
146
|
+
goPrevious,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
|
|
2
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
4
|
import {
|
|
4
5
|
SLEEP_TIMER_OFF,
|
|
5
6
|
SleepTimerController,
|
|
6
7
|
SleepTimerState,
|
|
7
8
|
} from "./SleepTimerManager";
|
|
8
|
-
|
|
9
|
-
import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
|
|
10
|
-
|
|
11
|
-
import { SleepTimerModalHeader } from "./components/SleepTimerModalHeader";
|
|
12
|
-
import { getSleepTimerOptions } from "../Const/sleepTimerOptions";
|
|
13
9
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getBottomSheetModalSelectedItemIcon,
|
|
18
|
-
selectSleepTitle,
|
|
19
|
-
selectSleepOffLabel,
|
|
20
|
-
} from "../Utils";
|
|
10
|
+
PLAYER_ACTION_IDENTIFIERS,
|
|
11
|
+
registerPlayerActions,
|
|
12
|
+
} from "./playerActions";
|
|
21
13
|
|
|
22
|
-
export const useSleepTimerModal = (player
|
|
14
|
+
export const useSleepTimerModal = (player) => {
|
|
23
15
|
const playerState = usePlayerState("audio-player-sleep-timer-modal");
|
|
24
16
|
|
|
25
|
-
const title = configuration(selectSleepTitle);
|
|
26
|
-
const offLabel = configuration(selectSleepOffLabel);
|
|
27
|
-
|
|
28
17
|
const [sleepTimerManager, setSleepTimerManager] =
|
|
29
18
|
useState<SleepTimerController | null>(null);
|
|
30
19
|
|
|
@@ -33,19 +22,22 @@ export const useSleepTimerModal = (player, configuration) => {
|
|
|
33
22
|
);
|
|
34
23
|
|
|
35
24
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
setSleepTimerManager(new SleepTimerController(player));
|
|
38
|
-
} else {
|
|
39
|
-
setSleepTimerManager(null);
|
|
40
|
-
}
|
|
25
|
+
setSleepTimerManager(player ? new SleepTimerController(player) : null);
|
|
41
26
|
}, [player]);
|
|
42
27
|
|
|
43
28
|
useEffect(() => {
|
|
44
29
|
if (playerState?.isLive) {
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
sleepTimerManager?.setSleepTimer(SLEEP_TIMER_OFF).catch(() => undefined);
|
|
31
|
+
}
|
|
32
|
+
}, [playerState?.isLive, sleepTimerManager]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!sleepTimerManager || playerState.isLive) {
|
|
36
|
+
return;
|
|
47
37
|
}
|
|
48
|
-
|
|
38
|
+
|
|
39
|
+
return registerPlayerActions({ sleepController: sleepTimerManager });
|
|
40
|
+
}, [sleepTimerManager, playerState.isLive]);
|
|
49
41
|
|
|
50
42
|
useEffect(() => {
|
|
51
43
|
if (!playerState.isReadyToPlay) {
|
|
@@ -64,40 +56,11 @@ export const useSleepTimerModal = (player, configuration) => {
|
|
|
64
56
|
};
|
|
65
57
|
}, [sleepTimerManager]);
|
|
66
58
|
|
|
67
|
-
const onSleepSelect = useCallback(
|
|
68
|
-
async (item) => {
|
|
69
|
-
await sleepTimerManager?.setSleepTimer(item);
|
|
70
|
-
},
|
|
71
|
-
[sleepTimerManager]
|
|
72
|
-
);
|
|
73
|
-
|
|
74
59
|
const onSleepButtonPress = useCallback(() => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
openBottomSheetModal({
|
|
81
|
-
modalBottomSheetContentProps: {
|
|
82
|
-
headerComponent: SleepTimerModalHeader,
|
|
83
|
-
items: getSleepTimerOptions(offLabel, timerRemaining),
|
|
84
|
-
onPress: onSleepSelect,
|
|
85
|
-
title,
|
|
86
|
-
summary: timerRemaining,
|
|
87
|
-
itemProps: getBottomSheetModalItemProps,
|
|
88
|
-
iconProps: getBottomSheetModalIconProps,
|
|
89
|
-
labelProps: getBottomSheetModalLabelProps,
|
|
90
|
-
getSelectedItemIcon: getBottomSheetModalSelectedItemIcon,
|
|
91
|
-
},
|
|
92
|
-
options: {
|
|
93
|
-
supportedOrientations: [
|
|
94
|
-
"portrait",
|
|
95
|
-
"landscape",
|
|
96
|
-
"portrait-upside-down",
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
}, [sleepTimerState.timerDate, sleepTimerState.timer, title, onSleepSelect]);
|
|
60
|
+
uiActionsRegistry
|
|
61
|
+
.getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
|
|
62
|
+
?.invokeAction(player?.entry);
|
|
63
|
+
}, [player]);
|
|
101
64
|
|
|
102
65
|
return useMemo(
|
|
103
66
|
() => ({
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
VIDEO_TRANSITION_THRESHOLD,
|
|
13
13
|
} from "./consts";
|
|
14
14
|
import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
15
|
+
import { useIsTabletLandscape } from "@applicaster/zapp-react-native-utils/reactHooks/device/useMemoizedIsTablet";
|
|
15
16
|
import { PROGRESS_BAR_HEIGHT } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/utils";
|
|
16
17
|
import {
|
|
17
18
|
SafeAreaView,
|
|
@@ -100,7 +101,23 @@ export function VideoPlayerModal({
|
|
|
100
101
|
fullscreen || pip
|
|
101
102
|
);
|
|
102
103
|
|
|
104
|
+
/* Landscape is a tablet's regular orientation, so rotating it says nothing
|
|
105
|
+
about the modal mode. useOrientationEffect skips its orientation effects
|
|
106
|
+
for the same reason, via the same predicate. */
|
|
107
|
+
const skipOrientationEffects = useIsTabletLandscape();
|
|
108
|
+
|
|
109
|
+
/* Props are the only reconciliation left once the listener below is skipped,
|
|
110
|
+
and even where it runs, the mount value goes stale: remounting during
|
|
111
|
+
native fullscreen would otherwise strand the modal in that layout. Rotating
|
|
112
|
+
a tablet flips the predicate itself, so reconcile then too — the last
|
|
113
|
+
orientation event may have left the state behind. */
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
setFullScreenMode(fullscreen || pip);
|
|
116
|
+
}, [fullscreen, pip, skipOrientationEffects]);
|
|
117
|
+
|
|
103
118
|
React.useEffect(() => {
|
|
119
|
+
if (skipOrientationEffects) return;
|
|
120
|
+
|
|
104
121
|
const listener = addOrientationChangeListener(({ toOrientation }) => {
|
|
105
122
|
/* ignored in collapsed mode non-PiP player */
|
|
106
123
|
if (collapsed && !pip) return;
|
|
@@ -110,7 +127,7 @@ export function VideoPlayerModal({
|
|
|
110
127
|
return () => {
|
|
111
128
|
listener.remove();
|
|
112
129
|
};
|
|
113
|
-
}, [collapsed, pip]);
|
|
130
|
+
}, [collapsed, pip, skipOrientationEffects]);
|
|
114
131
|
|
|
115
132
|
const isFullScreenOrPIP = isFullScreenMode;
|
|
116
133
|
|