@applicaster/quick-brick-player 15.1.14 → 15.1.15-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
2
|
+
import { DEFAULT_SPEED, SpeedController } from "../SpeedManager";
|
|
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("SpeedController", () => {
|
|
16
|
+
const player = { setPlaybackRate: jest.fn() };
|
|
17
|
+
let speedController: SpeedController;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
(localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
22
|
+
(localStorage.getItem as jest.Mock).mockResolvedValue(undefined);
|
|
23
|
+
speedController = new SpeedController(player as any);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("requires a player in the constructor", () => {
|
|
27
|
+
expect(speedController.currentSpeed).toEqual(DEFAULT_SPEED);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("applies playback speed on the constructed player", async () => {
|
|
31
|
+
await speedController.setPlaybackSpeed({
|
|
32
|
+
asset: "speed_1_5",
|
|
33
|
+
value: 1.5,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
|
|
37
|
+
|
|
38
|
+
expect(speedController.currentSpeed).toEqual({
|
|
39
|
+
asset: "speed_1_5",
|
|
40
|
+
value: 1.5,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("loadSpeed restores a stored rate onto the player", async () => {
|
|
45
|
+
(localStorage.getItem as jest.Mock).mockResolvedValue({
|
|
46
|
+
asset: "speed_1_5",
|
|
47
|
+
value: 1.5,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
player.setPlaybackRate.mockClear();
|
|
51
|
+
|
|
52
|
+
await speedController.loadSpeed();
|
|
53
|
+
|
|
54
|
+
expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
|
|
55
|
+
expect(speedController.currentSpeed.value).toBe(1.5);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
|
|
2
|
+
import {
|
|
3
|
+
actionExecutor,
|
|
4
|
+
ActionResult,
|
|
5
|
+
} from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
|
|
6
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
7
|
+
import { modalOrchestrator } from "@applicaster/zapp-react-native-utils/modalState/ModalOrchestrator";
|
|
8
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
9
|
+
import { firstValueFrom } from "rxjs";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
PLAYER_ACTION_IDENTIFIERS,
|
|
13
|
+
PLAYER_ACTION_TYPES,
|
|
14
|
+
registerPlayerActions,
|
|
15
|
+
} from "../playerActions";
|
|
16
|
+
import { SpeedController } from "../SpeedManager";
|
|
17
|
+
import { SleepTimerController } from "../SleepTimerManager";
|
|
18
|
+
|
|
19
|
+
jest.mock("../../Const/sleepTimerOptions", () => ({
|
|
20
|
+
getSleepTimerOptions: (offLabel: string, sleepTimerValue: number | null) => [
|
|
21
|
+
sleepTimerValue ? { label: offLabel, value: 0 } : null,
|
|
22
|
+
{ label: "5 minutes", value: 5 },
|
|
23
|
+
{ label: "15 minutes", value: 15 },
|
|
24
|
+
],
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
|
|
28
|
+
playerManager: {
|
|
29
|
+
getActivePlayer: jest.fn(),
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
jest.mock(
|
|
34
|
+
"@applicaster/zapp-react-native-utils/modalState/ModalOrchestrator",
|
|
35
|
+
() => ({
|
|
36
|
+
modalOrchestrator: {
|
|
37
|
+
close: jest.fn(),
|
|
38
|
+
stackSize: 0,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
jest.mock(
|
|
44
|
+
"@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
|
|
45
|
+
() => ({
|
|
46
|
+
localStorage: {
|
|
47
|
+
getItem: jest.fn(),
|
|
48
|
+
setItem: jest.fn(),
|
|
49
|
+
removeItem: jest.fn(),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
describe("playerActions", () => {
|
|
55
|
+
const openedSheets: ActionType[] = [];
|
|
56
|
+
|
|
57
|
+
let player: {
|
|
58
|
+
getConfig: () => Record<string, string>;
|
|
59
|
+
setPlaybackRate: jest.Mock;
|
|
60
|
+
startSleepTimer: jest.Mock;
|
|
61
|
+
cancelSleepTimer: jest.Mock;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let speedController: SpeedController;
|
|
65
|
+
let sleepController: SleepTimerController;
|
|
66
|
+
let unregisterPlayerActions: () => void;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
openedSheets.length = 0;
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
(localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
|
|
72
|
+
(localStorage.removeItem as jest.Mock).mockResolvedValue(undefined);
|
|
73
|
+
|
|
74
|
+
player = {
|
|
75
|
+
getConfig: () => ({
|
|
76
|
+
playback_speed_title: "Playback Speed",
|
|
77
|
+
playback_sleep_title: "Sleep Timer",
|
|
78
|
+
playback_sleep_off_label: "Off",
|
|
79
|
+
sleep_timer: "sleep_timer",
|
|
80
|
+
sleep_timer_active: "sleep_timer_active",
|
|
81
|
+
speed_1: "speed_1_asset_uri",
|
|
82
|
+
speed_1_5: "speed_1_5_asset_uri",
|
|
83
|
+
}),
|
|
84
|
+
setPlaybackRate: jest.fn(),
|
|
85
|
+
startSleepTimer: jest.fn(),
|
|
86
|
+
cancelSleepTimer: jest.fn(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
(playerManager.getActivePlayer as jest.Mock).mockReturnValue(player);
|
|
90
|
+
speedController = new SpeedController(player as any);
|
|
91
|
+
sleepController = new SleepTimerController(player as any);
|
|
92
|
+
|
|
93
|
+
[
|
|
94
|
+
"openBottomSheet",
|
|
95
|
+
PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
|
|
96
|
+
PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
|
|
97
|
+
PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
98
|
+
PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
99
|
+
].forEach((type) => actionExecutor.unregisterAction(type));
|
|
100
|
+
|
|
101
|
+
actionExecutor.registerAction("openBottomSheet", async (action) => {
|
|
102
|
+
openedSheets.push(action);
|
|
103
|
+
|
|
104
|
+
return ActionResult.Success;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
unregisterPlayerActions = registerPlayerActions({
|
|
108
|
+
speedController,
|
|
109
|
+
sleepController,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
unregisterPlayerActions?.();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("does not register actions until a player controller is provided", async () => {
|
|
118
|
+
unregisterPlayerActions();
|
|
119
|
+
unregisterPlayerActions = registerPlayerActions({});
|
|
120
|
+
|
|
121
|
+
const result = await actionExecutor.handleAction({
|
|
122
|
+
type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
123
|
+
options: { value: 1.5, dismissSheet: true },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result).toBeUndefined();
|
|
127
|
+
expect(player.setPlaybackRate).not.toHaveBeenCalled();
|
|
128
|
+
|
|
129
|
+
expect(
|
|
130
|
+
uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
|
|
131
|
+
).toBeUndefined();
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
|
|
135
|
+
).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("unregisters player actions when the player goes away", async () => {
|
|
139
|
+
unregisterPlayerActions();
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
|
|
143
|
+
).toBeUndefined();
|
|
144
|
+
|
|
145
|
+
const result = await actionExecutor.handleAction({
|
|
146
|
+
type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
147
|
+
options: { value: 1.5 },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(result).toBeUndefined();
|
|
151
|
+
expect(player.setPlaybackRate).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("opens playback speed through openBottomSheet with the current speed selected", async () => {
|
|
155
|
+
await speedController.setPlaybackSpeed({
|
|
156
|
+
asset: "speed_1",
|
|
157
|
+
value: 1,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = await actionExecutor.handleAction({
|
|
161
|
+
type: PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result).toBe(ActionResult.Success);
|
|
165
|
+
|
|
166
|
+
const sheet = openedSheets[0];
|
|
167
|
+
|
|
168
|
+
expect(sheet.options.replace).toBe(true);
|
|
169
|
+
expect(sheet.options.header.title).toBe("Playback Speed");
|
|
170
|
+
expect(sheet.options.content.role).toBe("collection_selector");
|
|
171
|
+
expect(sheet.options.content.behavior.currentSelection).toEqual(["1"]);
|
|
172
|
+
|
|
173
|
+
expect(sheet.options.content.items.map((item) => item.title)).toEqual([
|
|
174
|
+
"0.8x",
|
|
175
|
+
"1x",
|
|
176
|
+
"1.2x",
|
|
177
|
+
"1.5x",
|
|
178
|
+
"2x",
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
expect(
|
|
182
|
+
sheet.options.content.items[0].extensions.tap_actions.actions[0].options
|
|
183
|
+
.dismissSheet
|
|
184
|
+
).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("opens sleep timer with the ticking countdown header", async () => {
|
|
188
|
+
const timerDate = Date.now() + 15 * 60 * 1000;
|
|
189
|
+
|
|
190
|
+
await sleepController.setSleepTimer(
|
|
191
|
+
{ label: "15 minutes", value: 15 },
|
|
192
|
+
timerDate
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const result = await actionExecutor.handleAction({
|
|
196
|
+
type: PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result).toBe(ActionResult.Success);
|
|
200
|
+
|
|
201
|
+
const sheet = openedSheets[0];
|
|
202
|
+
|
|
203
|
+
expect(sheet.options.replace).toBe(true);
|
|
204
|
+
expect(sheet.options.header.title).toBe("Sleep Timer");
|
|
205
|
+
expect(sheet.options.header.subtitle).toBeUndefined();
|
|
206
|
+
expect(sheet.options.content.behavior.currentSelection).toEqual(["15"]);
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
sheet.options.content.items[0].extensions.tap_actions.actions[0].type
|
|
210
|
+
).toBe(PLAYER_ACTION_TYPES.SET_SLEEP_TIMER);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("setPlaybackSpeed applies the rate and closes the sheet when opened from the picker", async () => {
|
|
214
|
+
const result = await actionExecutor.handleAction({
|
|
215
|
+
type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
216
|
+
options: { value: 1.5, dismissSheet: true },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(ActionResult.Success);
|
|
220
|
+
expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
|
|
221
|
+
expect(localStorage.setItem).toHaveBeenCalled();
|
|
222
|
+
expect(modalOrchestrator.close).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("setSleepTimer starts the timer, writes storage, and closes the sheet", async () => {
|
|
226
|
+
const result = await actionExecutor.handleAction({
|
|
227
|
+
type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
228
|
+
options: { value: 15, label: "15 minutes", dismissSheet: true },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result).toBe(ActionResult.Success);
|
|
232
|
+
expect(player.startSleepTimer).toHaveBeenCalled();
|
|
233
|
+
expect(localStorage.setItem).toHaveBeenCalled();
|
|
234
|
+
expect(modalOrchestrator.close).toHaveBeenCalled();
|
|
235
|
+
expect(sleepController.currentState.timer.value).toBe(15);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("setSleepTimer with value 0 cancels the timer", async () => {
|
|
239
|
+
await sleepController.setSleepTimer({
|
|
240
|
+
label: "15 minutes",
|
|
241
|
+
value: 15,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
player.cancelSleepTimer.mockClear();
|
|
245
|
+
(localStorage.removeItem as jest.Mock).mockClear();
|
|
246
|
+
(modalOrchestrator.close as jest.Mock).mockClear();
|
|
247
|
+
|
|
248
|
+
const result = await actionExecutor.handleAction({
|
|
249
|
+
type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
250
|
+
options: { value: 0, label: "Off", dismissSheet: true },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result).toBe(ActionResult.Success);
|
|
254
|
+
expect(player.cancelSleepTimer).toHaveBeenCalled();
|
|
255
|
+
expect(localStorage.removeItem).toHaveBeenCalled();
|
|
256
|
+
expect(sleepController.currentState.timer.value).toBe(0);
|
|
257
|
+
expect(modalOrchestrator.close).toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it.each([undefined, "foo"])(
|
|
261
|
+
"rejects invalid setPlaybackSpeed value %p",
|
|
262
|
+
async (value) => {
|
|
263
|
+
player.setPlaybackRate.mockClear();
|
|
264
|
+
(localStorage.setItem as jest.Mock).mockClear();
|
|
265
|
+
|
|
266
|
+
const result = await actionExecutor.handleAction({
|
|
267
|
+
type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
268
|
+
options: { value, dismissSheet: true },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(result).toBe(ActionResult.Error);
|
|
272
|
+
expect(player.setPlaybackRate).not.toHaveBeenCalled();
|
|
273
|
+
expect(localStorage.setItem).not.toHaveBeenCalled();
|
|
274
|
+
expect(modalOrchestrator.close).not.toHaveBeenCalled();
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
it.each([undefined, "foo"])(
|
|
279
|
+
"rejects invalid setSleepTimer value %p",
|
|
280
|
+
async (value) => {
|
|
281
|
+
const result = await actionExecutor.handleAction({
|
|
282
|
+
type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
|
|
283
|
+
options: { value, dismissSheet: true },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result).toBe(ActionResult.Error);
|
|
287
|
+
expect(player.startSleepTimer).not.toHaveBeenCalled();
|
|
288
|
+
expect(localStorage.setItem).not.toHaveBeenCalled();
|
|
289
|
+
expect(modalOrchestrator.close).not.toHaveBeenCalled();
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
it("does not close an unrelated sheet when setPlaybackSpeed is invoked standalone", async () => {
|
|
294
|
+
const result = await actionExecutor.handleAction({
|
|
295
|
+
type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
|
|
296
|
+
options: { value: 1.5 },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(result).toBe(ActionResult.Success);
|
|
300
|
+
expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
|
|
301
|
+
expect(modalOrchestrator.close).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("initialEntryState for sleep-timer reflects an active timer", async () => {
|
|
305
|
+
await sleepController.setSleepTimer({
|
|
306
|
+
label: "15 minutes",
|
|
307
|
+
value: 15,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const action = uiActionsRegistry.getAction(
|
|
311
|
+
PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(action.initialEntryState({} as ZappEntry)).toMatchObject({
|
|
315
|
+
state: 1,
|
|
316
|
+
asset: "sleep_timer_active",
|
|
317
|
+
label: "Sleep Timer",
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const observed = await firstValueFrom(
|
|
321
|
+
action.observeEntryState({} as ZappEntry)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(observed).toMatchObject({
|
|
325
|
+
state: 1,
|
|
326
|
+
asset: "sleep_timer_active",
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("initialEntryState for playback-speed uses the current speed icon asset", async () => {
|
|
331
|
+
const action = uiActionsRegistry.getAction(
|
|
332
|
+
PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
expect(action.initialEntryState({} as ZappEntry)).toMatchObject({
|
|
336
|
+
label: "Playback Speed",
|
|
337
|
+
asset: "speed_1_asset_uri",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await speedController.setPlaybackSpeed({
|
|
341
|
+
asset: "speed_1_5",
|
|
342
|
+
value: 1.5,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const observed = await firstValueFrom(
|
|
346
|
+
action.observeEntryState({} as ZappEntry)
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(observed).toMatchObject({
|
|
350
|
+
label: "Playback Speed",
|
|
351
|
+
asset: "speed_1_5_asset_uri",
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("registers UI action identifiers that open the matching sheets", async () => {
|
|
356
|
+
await uiActionsRegistry
|
|
357
|
+
.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
|
|
358
|
+
.invokeAction({} as ZappEntry);
|
|
359
|
+
|
|
360
|
+
await uiActionsRegistry
|
|
361
|
+
.getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
|
|
362
|
+
.invokeAction({} as ZappEntry);
|
|
363
|
+
|
|
364
|
+
expect(openedSheets.map((action) => action.options.header.title)).toEqual([
|
|
365
|
+
"Playback Speed",
|
|
366
|
+
"Sleep Timer",
|
|
367
|
+
]);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -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
|
+
}
|