@applicaster/zapp-react-native-ui-components 16.0.0-rc.96 → 16.0.0-rc.98
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/Input.tsx +139 -39
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +144 -23
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
- package/Components/ModalComponent/BottomSheetModalContent.tsx +10 -0
- package/Components/ModalComponent/TextInputContent.tsx +27 -4
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +74 -1
- package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +14 -1
- package/Components/ModalComponent/showTextInput.ts +11 -2
- package/Components/VideoLive/LiveImageManager.ts +8 -2
- package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
- package/assets/empty-Info.png +0 -0
- package/package.json +5 -5
|
@@ -18,11 +18,42 @@ import {
|
|
|
18
18
|
View,
|
|
19
19
|
} from "react-native";
|
|
20
20
|
|
|
21
|
+
import { resolveDefaultAssetUri } from "@applicaster/zapp-react-native-utils/actionUtils";
|
|
22
|
+
|
|
21
23
|
const styles = StyleSheet.create({
|
|
22
24
|
column: {
|
|
23
25
|
alignItems: "center",
|
|
24
26
|
width: "100%",
|
|
25
27
|
},
|
|
28
|
+
validationMessage: {
|
|
29
|
+
flexDirection: "row",
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
alignSelf: "center",
|
|
32
|
+
overflow: "hidden",
|
|
33
|
+
backgroundColor: "rgba(229,30,37,1.00)",
|
|
34
|
+
borderRadius: 100,
|
|
35
|
+
marginTop: 16,
|
|
36
|
+
paddingLeft: 8,
|
|
37
|
+
paddingRight: 12,
|
|
38
|
+
paddingTop: 8,
|
|
39
|
+
paddingBottom: 8,
|
|
40
|
+
gap: 8,
|
|
41
|
+
},
|
|
42
|
+
validationMessageIcon: {
|
|
43
|
+
width: 20,
|
|
44
|
+
height: 20,
|
|
45
|
+
alignSelf: "flex-start",
|
|
46
|
+
},
|
|
47
|
+
validationMessageText: {
|
|
48
|
+
color: "rgba(255,255,255,1.00)",
|
|
49
|
+
fontFamily: Platform.select({
|
|
50
|
+
ios: "GreycliffCF-Bold",
|
|
51
|
+
android: "Greycliff-Bold",
|
|
52
|
+
default: "Greycliff-Bold",
|
|
53
|
+
}),
|
|
54
|
+
fontSize: 13,
|
|
55
|
+
lineHeight: 20,
|
|
56
|
+
},
|
|
26
57
|
textInput: {
|
|
27
58
|
padding: 0,
|
|
28
59
|
margin: 0,
|
|
@@ -51,14 +82,22 @@ export type TextTransform =
|
|
|
51
82
|
|
|
52
83
|
export type TextAlignment = "left" | "center";
|
|
53
84
|
|
|
85
|
+
export type UnderlineConfiguration = {
|
|
86
|
+
focusedBorderColor: string;
|
|
87
|
+
borderColor: string;
|
|
88
|
+
/** Bottom underline height; 0 = none (pill search uses borderWidth instead) */
|
|
89
|
+
underlineHeight: number;
|
|
90
|
+
underlineMarginTop: number;
|
|
91
|
+
/** Radius of the underline bar itself (bottom border style). */
|
|
92
|
+
underlineCornerRadius: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
54
95
|
export type BackgroundConfiguration = {
|
|
55
96
|
defaultColor: string;
|
|
56
97
|
focusedColor: string;
|
|
57
98
|
borderColor: string;
|
|
58
99
|
focusedBorderColor: string;
|
|
59
100
|
borderWidth: number;
|
|
60
|
-
/** Bottom underline height; 0 = none (pill search uses borderWidth instead) */
|
|
61
|
-
underlineHeight: number;
|
|
62
101
|
cornerRadius: number;
|
|
63
102
|
paddingTop: number;
|
|
64
103
|
paddingRight: number;
|
|
@@ -95,6 +134,9 @@ export type TextConfiguration = {
|
|
|
95
134
|
androidLetterSpacing: number;
|
|
96
135
|
textTransform: TextTransform;
|
|
97
136
|
alignment: TextAlignment;
|
|
137
|
+
/** Vertical spacing around the input text; drives the gap above the underline. */
|
|
138
|
+
marginTop: number;
|
|
139
|
+
marginBottom: number;
|
|
98
140
|
};
|
|
99
141
|
|
|
100
142
|
export type LeadingIconConfiguration = {
|
|
@@ -117,6 +159,7 @@ export type IconsConfiguration = {
|
|
|
117
159
|
};
|
|
118
160
|
|
|
119
161
|
export type InputConfiguration = {
|
|
162
|
+
underline: UnderlineConfiguration;
|
|
120
163
|
background: BackgroundConfiguration;
|
|
121
164
|
label: LabelConfiguration;
|
|
122
165
|
text: TextConfiguration;
|
|
@@ -129,7 +172,6 @@ export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
|
|
|
129
172
|
borderColor: "rgba(255, 255, 255, 0.55)",
|
|
130
173
|
focusedBorderColor: "rgba(255, 255, 255, 0.85)",
|
|
131
174
|
borderWidth: 1,
|
|
132
|
-
underlineHeight: 0,
|
|
133
175
|
cornerRadius: 24,
|
|
134
176
|
paddingTop: 10,
|
|
135
177
|
paddingRight: 10,
|
|
@@ -165,6 +207,8 @@ export const DEFAULT_TEXT_CONFIGURATION: TextConfiguration = {
|
|
|
165
207
|
androidLetterSpacing: 0,
|
|
166
208
|
textTransform: "default",
|
|
167
209
|
alignment: "left",
|
|
210
|
+
marginTop: 0,
|
|
211
|
+
marginBottom: 0,
|
|
168
212
|
};
|
|
169
213
|
|
|
170
214
|
export const DEFAULT_ICONS_CONFIGURATION: IconsConfiguration = {
|
|
@@ -180,8 +224,17 @@ export const DEFAULT_ICONS_CONFIGURATION: IconsConfiguration = {
|
|
|
180
224
|
},
|
|
181
225
|
};
|
|
182
226
|
|
|
227
|
+
export const DEFAULT_UNDERLINE_CONFIGURATION: UnderlineConfiguration = {
|
|
228
|
+
borderColor: "rgba(255, 255, 255, 0.55)",
|
|
229
|
+
focusedBorderColor: "rgba(255, 255, 255, 0.85)",
|
|
230
|
+
underlineHeight: 0,
|
|
231
|
+
underlineMarginTop: 0,
|
|
232
|
+
underlineCornerRadius: 0,
|
|
233
|
+
};
|
|
234
|
+
|
|
183
235
|
export const DEFAULT_INPUT_CONFIGURATION: InputConfiguration = {
|
|
184
236
|
background: DEFAULT_BACKGROUND_CONFIGURATION,
|
|
237
|
+
underline: DEFAULT_UNDERLINE_CONFIGURATION,
|
|
185
238
|
label: DEFAULT_LABEL_CONFIGURATION,
|
|
186
239
|
text: DEFAULT_TEXT_CONFIGURATION,
|
|
187
240
|
icons: DEFAULT_ICONS_CONFIGURATION,
|
|
@@ -203,6 +256,7 @@ export type ItemProps = {
|
|
|
203
256
|
value?: string;
|
|
204
257
|
focused?: boolean;
|
|
205
258
|
focusedClearButton?: boolean;
|
|
259
|
+
validationMessage?: string;
|
|
206
260
|
onChangeText?: (text: string) => void;
|
|
207
261
|
onClearPress?: () => void;
|
|
208
262
|
onFocus?: () => void;
|
|
@@ -243,7 +297,7 @@ function shouldShowLabel(label: LabelConfiguration, labelText?: string) {
|
|
|
243
297
|
}
|
|
244
298
|
|
|
245
299
|
function isUnderlineLayout(configuration: InputConfiguration) {
|
|
246
|
-
return configuration.
|
|
300
|
+
return configuration.underline.underlineHeight > 0;
|
|
247
301
|
}
|
|
248
302
|
|
|
249
303
|
// ---------------------------------------------------------------------------
|
|
@@ -323,7 +377,7 @@ function InputContainer({
|
|
|
323
377
|
}
|
|
324
378
|
|
|
325
379
|
type InputUnderlineProps = {
|
|
326
|
-
configuration:
|
|
380
|
+
configuration: UnderlineConfiguration;
|
|
327
381
|
focused: boolean;
|
|
328
382
|
};
|
|
329
383
|
|
|
@@ -337,7 +391,9 @@ function InputUnderline({ configuration, focused }: InputUnderlineProps) {
|
|
|
337
391
|
style={[
|
|
338
392
|
styles.underline,
|
|
339
393
|
{
|
|
394
|
+
marginTop: configuration.underlineMarginTop,
|
|
340
395
|
height: configuration.underlineHeight,
|
|
396
|
+
borderRadius: configuration.underlineCornerRadius,
|
|
341
397
|
backgroundColor: focused
|
|
342
398
|
? configuration.focusedBorderColor
|
|
343
399
|
: configuration.borderColor,
|
|
@@ -427,6 +483,8 @@ function InputField({
|
|
|
427
483
|
}),
|
|
428
484
|
textTransform: getTextTransformStyle(configuration.textTransform),
|
|
429
485
|
textAlign: configuration.alignment,
|
|
486
|
+
marginTop: configuration.marginTop,
|
|
487
|
+
marginBottom: configuration.marginBottom,
|
|
430
488
|
},
|
|
431
489
|
]}
|
|
432
490
|
/>
|
|
@@ -479,12 +537,13 @@ export function AudioPlayerInput({
|
|
|
479
537
|
value = "",
|
|
480
538
|
focused = false,
|
|
481
539
|
focusedClearButton = false,
|
|
540
|
+
validationMessage,
|
|
482
541
|
onChangeText,
|
|
483
542
|
onClearPress,
|
|
484
543
|
onFocus,
|
|
485
544
|
onBlur,
|
|
486
545
|
}: ItemProps) {
|
|
487
|
-
const { background, label, text, icons } = configuration;
|
|
546
|
+
const { background, label, text, icons, underline } = configuration;
|
|
488
547
|
const inputRef = useRef<TextInputRef>(null);
|
|
489
548
|
const underlineLayout = isUnderlineLayout(configuration);
|
|
490
549
|
const showLabel = shouldShowLabel(label, data.label);
|
|
@@ -545,21 +604,40 @@ export function AudioPlayerInput({
|
|
|
545
604
|
</>
|
|
546
605
|
) : null}
|
|
547
606
|
{underlineLayout ? (
|
|
548
|
-
<InputUnderline configuration={
|
|
607
|
+
<InputUnderline configuration={underline} focused={focused} />
|
|
549
608
|
) : null}
|
|
550
609
|
</InputContainer>
|
|
551
610
|
</Pressable>
|
|
552
611
|
);
|
|
553
612
|
|
|
554
|
-
if (!showLabel || !data.label) {
|
|
555
|
-
return inputRow;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
613
|
return (
|
|
559
614
|
<View style={styles.column}>
|
|
560
|
-
|
|
561
|
-
|
|
615
|
+
{showLabel && data.label ? (
|
|
616
|
+
<>
|
|
617
|
+
<InputLabel text={data.label} configuration={label} />
|
|
618
|
+
<View style={{ height: background.verticalGutter }} />
|
|
619
|
+
</>
|
|
620
|
+
) : null}
|
|
562
621
|
{inputRow}
|
|
622
|
+
{validationMessage ? (
|
|
623
|
+
<View style={styles.validationMessage} accessibilityRole="alert">
|
|
624
|
+
<Image
|
|
625
|
+
source={{
|
|
626
|
+
uri: resolveDefaultAssetUri(
|
|
627
|
+
require("../../../../assets/empty-Info.png")
|
|
628
|
+
),
|
|
629
|
+
}}
|
|
630
|
+
style={styles.validationMessageIcon}
|
|
631
|
+
/>
|
|
632
|
+
<Text
|
|
633
|
+
style={styles.validationMessageText}
|
|
634
|
+
numberOfLines={1}
|
|
635
|
+
ellipsizeMode="tail"
|
|
636
|
+
>
|
|
637
|
+
{validationMessage}
|
|
638
|
+
</Text>
|
|
639
|
+
</View>
|
|
640
|
+
) : null}
|
|
563
641
|
</View>
|
|
564
642
|
);
|
|
565
643
|
}
|
|
@@ -572,55 +650,77 @@ export function AudioPlayerInput({
|
|
|
572
650
|
export const VARIANT_SEARCH_INPUT: InputConfiguration =
|
|
573
651
|
DEFAULT_INPUT_CONFIGURATION;
|
|
574
652
|
|
|
575
|
-
/**
|
|
653
|
+
/**
|
|
654
|
+
* Name playlist — centered title + 32pt text over a rounded bottom underline.
|
|
655
|
+
*
|
|
656
|
+
* Values mirror the manifest configuration the
|
|
657
|
+
* `quick-brick-component-text-field` plugin renders from; the comment on each
|
|
658
|
+
* line names the reference key it comes from.
|
|
659
|
+
*/
|
|
576
660
|
export const VARIANT_NAME_PLAYLIST_INPUT: InputConfiguration = {
|
|
577
661
|
background: {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
662
|
+
// text_input_background_*_background_color
|
|
663
|
+
defaultColor: "rgba(30,30,30,0.00)",
|
|
664
|
+
focusedColor: "rgba(46,46,46,0.00)",
|
|
665
|
+
// text_input_style_border_*_border_color
|
|
666
|
+
borderColor: "rgba(255,255,255,0.25)",
|
|
667
|
+
focusedBorderColor: "rgba(255,255,255,1.00)",
|
|
668
|
+
// border_border_style is "bottom", so the box border is off and the
|
|
669
|
+
// underline below draws the border instead.
|
|
582
670
|
borderWidth: 0,
|
|
583
|
-
underlineHeight: 1,
|
|
584
671
|
cornerRadius: 0,
|
|
672
|
+
// text_input_style_input_padding_*
|
|
585
673
|
paddingTop: 0,
|
|
586
674
|
paddingRight: 0,
|
|
587
|
-
paddingBottom:
|
|
675
|
+
paddingBottom: 0,
|
|
588
676
|
paddingLeft: 0,
|
|
589
|
-
horizontalGutter: 0,
|
|
590
|
-
verticalGutter:
|
|
591
|
-
height: 0,
|
|
677
|
+
horizontalGutter: 0, // text_input_style_input_horizontal_gutter
|
|
678
|
+
verticalGutter: 0, // component_style_vertical_gutter
|
|
679
|
+
height: 0, // hug content
|
|
680
|
+
},
|
|
681
|
+
underline: {
|
|
682
|
+
borderColor: "rgba(255,255,255,0.25)",
|
|
683
|
+
focusedBorderColor: "rgba(255,255,255,1.00)",
|
|
684
|
+
underlineHeight: 2, // text_input_style_border_border_size
|
|
685
|
+
// The gap above the bar comes from the text's marginBottom, matching how
|
|
686
|
+
// the plugin applies `text_label_margin_bottom` to the input itself.
|
|
687
|
+
underlineMarginTop: 0,
|
|
688
|
+
underlineCornerRadius: 4, // ..._border_bottom_border_corner_radius
|
|
592
689
|
},
|
|
593
690
|
label: {
|
|
594
|
-
enabled: true,
|
|
595
|
-
fontColor: "rgba(
|
|
596
|
-
iosFontFamily: "
|
|
597
|
-
androidFontFamily: "
|
|
598
|
-
fontSize:
|
|
599
|
-
lineHeight:
|
|
600
|
-
iosLetterSpacing:
|
|
691
|
+
enabled: true, // title_enable
|
|
692
|
+
fontColor: "rgba(255,255,255,0.50)", // title_style_color
|
|
693
|
+
iosFontFamily: "GreycliffCF-Bold", // title_style_ios_font_family
|
|
694
|
+
androidFontFamily: "Greycliff-Bold", // title_style_android_font_family
|
|
695
|
+
fontSize: 14, // title_style_font_size
|
|
696
|
+
lineHeight: 32, // title_style_line_height
|
|
697
|
+
iosLetterSpacing: 0,
|
|
601
698
|
androidLetterSpacing: 0,
|
|
602
|
-
textTransform: "default",
|
|
699
|
+
textTransform: "default", // title_style_text_transform: "none"
|
|
603
700
|
},
|
|
604
701
|
text: {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
702
|
+
// text_input_style_text_label_*
|
|
703
|
+
fontColor: "rgba(255,255,255,1.00)", // active_label_color
|
|
704
|
+
focusedFontColor: "rgba(255,255,255,1.00)",
|
|
705
|
+
placeholderColor: "rgba(255,255,255,0.50)", // placeholder_label_color
|
|
706
|
+
cursorColor: "rgba(249,183,72,1.00)", // text_input_style_cursor_cursor_color
|
|
707
|
+
iosFontFamily: "GreycliffCF-Bold",
|
|
708
|
+
androidFontFamily: "Greycliff-Bold",
|
|
611
709
|
fontSize: 32,
|
|
612
710
|
lineHeight: 40,
|
|
613
|
-
iosLetterSpacing:
|
|
711
|
+
iosLetterSpacing: 0,
|
|
614
712
|
androidLetterSpacing: 0,
|
|
615
713
|
textTransform: "default",
|
|
616
714
|
alignment: "center",
|
|
715
|
+
marginTop: 20, // text_label_margin_top
|
|
716
|
+
marginBottom: 20, // text_label_margin_bottom
|
|
617
717
|
},
|
|
618
718
|
icons: {
|
|
619
719
|
width: 20,
|
|
620
720
|
height: 20,
|
|
621
721
|
leadingIcon: { enabled: false },
|
|
622
722
|
trailingIcon: {
|
|
623
|
-
enabled: false,
|
|
723
|
+
enabled: false, // clear_button_enable
|
|
624
724
|
backgroundColor: "rgba(58, 58, 60, 1)",
|
|
625
725
|
focusedBackgroundColor: "rgba(72, 72, 74, 1)",
|
|
626
726
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { View } from "react-native";
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
AudioPlayerTrackItem,
|
|
5
5
|
VARIANT_NOW_PLAYING,
|
|
@@ -7,13 +7,57 @@ 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";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_EMPTY_MESSAGE = "Queued items will be added here";
|
|
15
|
+
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
divider: {
|
|
18
|
+
height: StyleSheet.hairlineWidth,
|
|
19
|
+
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
20
|
+
marginHorizontal: 20,
|
|
21
|
+
marginVertical: 8,
|
|
22
|
+
},
|
|
23
|
+
emptyMessage: {
|
|
24
|
+
paddingTop: 32,
|
|
25
|
+
paddingBottom: 32,
|
|
26
|
+
paddingHorizontal: 40,
|
|
27
|
+
textAlign: "center",
|
|
28
|
+
color: "#999999",
|
|
29
|
+
fontSize: 13,
|
|
30
|
+
lineHeight: 16,
|
|
31
|
+
},
|
|
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
|
+
};
|
|
12
54
|
|
|
13
55
|
type NowPlayingHeaderSectionProps = {
|
|
14
56
|
onClear?: () => void;
|
|
15
57
|
nextUpTitle?: string;
|
|
16
58
|
nowPlayingTitle?: string;
|
|
59
|
+
isEmpty?: boolean;
|
|
60
|
+
emptyMessage?: string;
|
|
17
61
|
clearLabel?: string;
|
|
18
62
|
};
|
|
19
63
|
|
|
@@ -21,20 +65,104 @@ export function NowPlayingHeaderSection({
|
|
|
21
65
|
onClear,
|
|
22
66
|
nextUpTitle = "Next Up",
|
|
23
67
|
nowPlayingTitle = "Now Playing",
|
|
68
|
+
isEmpty = false,
|
|
69
|
+
emptyMessage = DEFAULT_EMPTY_MESSAGE,
|
|
24
70
|
clearLabel,
|
|
25
71
|
}: NowPlayingHeaderSectionProps) {
|
|
26
|
-
const
|
|
27
|
-
|
|
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
|
+
};
|
|
28
158
|
|
|
29
159
|
const renderNowPlayingTrack = () => {
|
|
30
|
-
if (!
|
|
160
|
+
if (!currentEntry) {
|
|
31
161
|
return null;
|
|
32
162
|
}
|
|
33
163
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const imageGroup = Array.isArray(mediaGroup)
|
|
37
|
-
? 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")
|
|
38
166
|
: null;
|
|
39
167
|
|
|
40
168
|
const firstImage = Array.isArray(imageGroup?.media_item)
|
|
@@ -42,26 +170,18 @@ export function NowPlayingHeaderSection({
|
|
|
42
170
|
: imageGroup?.media_item?.src;
|
|
43
171
|
|
|
44
172
|
const nowPlayingItem: MenuItem = {
|
|
45
|
-
id: String(
|
|
46
|
-
title: (
|
|
47
|
-
summary: (
|
|
173
|
+
id: String(currentEntry.id ?? "now_playing"),
|
|
174
|
+
title: (currentEntry.title as string) || String(currentEntry.id),
|
|
175
|
+
summary: (currentEntry.summary as string) ?? "",
|
|
48
176
|
icon: firstImage,
|
|
49
177
|
};
|
|
50
178
|
|
|
51
179
|
const data = mapMenuItemToNowPlayingData(
|
|
52
180
|
nowPlayingItem,
|
|
53
|
-
|
|
181
|
+
isPaused,
|
|
54
182
|
nowPlayingTitle
|
|
55
183
|
);
|
|
56
184
|
|
|
57
|
-
const handlePlayPause = () => {
|
|
58
|
-
if (playerState?.isPaused) {
|
|
59
|
-
player?.play();
|
|
60
|
-
} else {
|
|
61
|
-
player?.pause();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
185
|
return (
|
|
66
186
|
<AudioPlayerTrackItem
|
|
67
187
|
configuration={VARIANT_NOW_PLAYING}
|
|
@@ -79,6 +199,7 @@ export function NowPlayingHeaderSection({
|
|
|
79
199
|
data={{ title: nextUpTitle, clearLabel }}
|
|
80
200
|
onClear={onClear}
|
|
81
201
|
/>
|
|
202
|
+
{isEmpty ? <Text style={styles.emptyMessage}>{emptyMessage}</Text> : null}
|
|
82
203
|
</View>
|
|
83
204
|
);
|
|
84
205
|
}
|
package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { NowPlayingHeaderSection } from "../NowPlayingHeaderSection";
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
|
|
7
|
+
() => ({
|
|
8
|
+
usePlayer: () => null,
|
|
9
|
+
})
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState",
|
|
14
|
+
() => ({
|
|
15
|
+
usePlayerState: () => ({ isPaused: true }),
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const EMPTY_COPY = "Queued items will be added here";
|
|
20
|
+
|
|
21
|
+
describe("NowPlayingHeaderSection empty queue", () => {
|
|
22
|
+
it("renders the empty message under Next Up when the list is empty", () => {
|
|
23
|
+
render(<NowPlayingHeaderSection isEmpty emptyMessage={EMPTY_COPY} />);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByText("Next Up")).toBeTruthy();
|
|
26
|
+
expect(screen.getByText(EMPTY_COPY)).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not render the empty message when the list has items", () => {
|
|
30
|
+
render(
|
|
31
|
+
<NowPlayingHeaderSection isEmpty={false} emptyMessage={EMPTY_COPY} />
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.queryByText(EMPTY_COPY)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -92,6 +92,8 @@ type ModalComponentProps = {
|
|
|
92
92
|
contentComponentProps?: Record<string, any>;
|
|
93
93
|
styleOverrides?: ModalBlocksConfig | Record<string, any>;
|
|
94
94
|
measurementPhase?: boolean;
|
|
95
|
+
/** Copy under Next Up when the queue has no upcoming items. */
|
|
96
|
+
emptyMessage?: string;
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
@@ -115,6 +117,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
115
117
|
contentComponentProps = {},
|
|
116
118
|
styleOverrides: propStyleOverrides,
|
|
117
119
|
measurementPhase = false,
|
|
120
|
+
emptyMessage,
|
|
118
121
|
} = props;
|
|
119
122
|
|
|
120
123
|
const route = useRef(currentRoute);
|
|
@@ -385,6 +388,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
385
388
|
const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
|
|
386
389
|
const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
|
|
387
390
|
const hasAdd = hasOperation(operations, OPERATIONS.ADD);
|
|
391
|
+
const isQueueEmpty = hasNowPlaying && (itemsToRender?.length ?? 0) === 0;
|
|
388
392
|
|
|
389
393
|
const handleDeleteCollection = useCallback(() => {
|
|
390
394
|
editableCollection?.deleteCollection?.();
|
|
@@ -409,6 +413,8 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
409
413
|
onClear={hasClearAll ? handleDeleteCollection : undefined}
|
|
410
414
|
nextUpTitle={nextUpTitle}
|
|
411
415
|
nowPlayingTitle={nowPlayingTitle}
|
|
416
|
+
isEmpty={isQueueEmpty}
|
|
417
|
+
emptyMessage={emptyMessage}
|
|
412
418
|
clearLabel={clearLabel}
|
|
413
419
|
/>
|
|
414
420
|
),
|
|
@@ -435,6 +441,8 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
435
441
|
handleDeleteCollection,
|
|
436
442
|
nextUpTitle,
|
|
437
443
|
nowPlayingTitle,
|
|
444
|
+
isQueueEmpty,
|
|
445
|
+
emptyMessage,
|
|
438
446
|
clearLabel,
|
|
439
447
|
hasAdd,
|
|
440
448
|
handleCreatePlaylist,
|
|
@@ -545,6 +553,8 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
545
553
|
onClear={hasClearAll ? handleDeleteCollection : undefined}
|
|
546
554
|
nextUpTitle={nextUpTitle}
|
|
547
555
|
nowPlayingTitle={nowPlayingTitle}
|
|
556
|
+
isEmpty={isQueueEmpty}
|
|
557
|
+
emptyMessage={emptyMessage}
|
|
548
558
|
clearLabel={clearLabel}
|
|
549
559
|
/>
|
|
550
560
|
) : null}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AudioPlayerButton,
|
|
10
10
|
VARIANT_TEXT_ONLY,
|
|
11
11
|
} from "./AudioPlayer/Components/Button";
|
|
12
|
+
import { logger } from "@applicaster/zapp-react-native-utils/logger";
|
|
12
13
|
|
|
13
14
|
export type TextInputContentProps = {
|
|
14
15
|
/** Injected by the sheet host; unused for layout (header is owned by ModalComponent). */
|
|
@@ -19,17 +20,25 @@ export type TextInputContentProps = {
|
|
|
19
20
|
inputLabel?: string;
|
|
20
21
|
defaultValue?: string;
|
|
21
22
|
buttonLabel: string;
|
|
23
|
+
validationLabel?: string;
|
|
24
|
+
maxLength?: number;
|
|
22
25
|
actions: ActionType[];
|
|
23
26
|
actionContext?: Record<string, any>;
|
|
24
27
|
};
|
|
25
28
|
|
|
29
|
+
// Spacing mirrors the `component_style_*` keys in text input component.
|
|
26
30
|
const styles = StyleSheet.create({
|
|
27
31
|
container: {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
backgroundColor: "transparent", // component_style_background_color
|
|
33
|
+
paddingLeft: 40, // component_style_padding_left
|
|
34
|
+
paddingRight: 40, // component_style_padding_right
|
|
35
|
+
paddingTop: 0, // component_style_padding_top
|
|
36
|
+
paddingBottom: 0, // component_style_padding_bottom
|
|
37
|
+
marginTop: 20, // component_style_margin_top
|
|
38
|
+
marginBottom: 20, // component_style_margin_bottom
|
|
30
39
|
},
|
|
31
40
|
inputWrap: {
|
|
32
|
-
|
|
41
|
+
// Gap down to the submit button; owned by the modal, not the reference.
|
|
33
42
|
marginBottom: 24,
|
|
34
43
|
},
|
|
35
44
|
});
|
|
@@ -45,11 +54,24 @@ export function TextInputContent(props: TextInputContentProps) {
|
|
|
45
54
|
buttonLabel,
|
|
46
55
|
actions,
|
|
47
56
|
actionContext,
|
|
57
|
+
maxLength = 255,
|
|
58
|
+
validationLabel,
|
|
48
59
|
} = props;
|
|
49
60
|
|
|
50
61
|
const [textValue, setTextValue] = useState(defaultValue);
|
|
51
62
|
const [submitting, setSubmitting] = useState(false);
|
|
52
63
|
|
|
64
|
+
const validationMessage =
|
|
65
|
+
textValue.length > maxLength
|
|
66
|
+
? validationLabel || "Too many characters"
|
|
67
|
+
: undefined;
|
|
68
|
+
|
|
69
|
+
if (!validationLabel) {
|
|
70
|
+
logger.warn(
|
|
71
|
+
"Modal/TextInputContent: Validation label (validationLabel) is not provided, using default message 'Too many characters'."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
const onSubmit = useCallback(async () => {
|
|
54
76
|
if (submitting) {
|
|
55
77
|
return;
|
|
@@ -97,6 +119,7 @@ export function TextInputContent(props: TextInputContentProps) {
|
|
|
97
119
|
label: inputLabel,
|
|
98
120
|
}}
|
|
99
121
|
value={textValue}
|
|
122
|
+
validationMessage={validationMessage}
|
|
100
123
|
onChangeText={setTextValue}
|
|
101
124
|
onClearPress={() => setTextValue("")}
|
|
102
125
|
/>
|
|
@@ -105,7 +128,7 @@ export function TextInputContent(props: TextInputContentProps) {
|
|
|
105
128
|
configuration={VARIANT_TEXT_ONLY}
|
|
106
129
|
data={{ title: buttonLabel }}
|
|
107
130
|
onPress={onSubmit}
|
|
108
|
-
disabled={submitting}
|
|
131
|
+
disabled={!!validationMessage || submitting}
|
|
109
132
|
/>
|
|
110
133
|
</View>
|
|
111
134
|
);
|
|
@@ -83,9 +83,14 @@ jest.mock("../AudioPlayer/Components/Item", () => ({
|
|
|
83
83
|
}));
|
|
84
84
|
|
|
85
85
|
const mockAudioPlayerButtonProps: Array<any> = [];
|
|
86
|
+
const mockNowPlayingHeaderProps: Array<any> = [];
|
|
86
87
|
|
|
87
88
|
jest.mock("../AudioPlayer/Components", () => ({
|
|
88
|
-
NowPlayingHeaderSection: () =>
|
|
89
|
+
NowPlayingHeaderSection: (props: any) => {
|
|
90
|
+
mockNowPlayingHeaderProps.push(props);
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
89
94
|
AudioPlayerButton: (props: any) => {
|
|
90
95
|
const { Text } = require("react-native");
|
|
91
96
|
|
|
@@ -216,6 +221,74 @@ describe("BottomSheetModalContent role/behavior cells", () => {
|
|
|
216
221
|
});
|
|
217
222
|
});
|
|
218
223
|
|
|
224
|
+
it("shows the empty Next Up message only when now playing is on and the list is empty", () => {
|
|
225
|
+
mockNowPlayingHeaderProps.length = 0;
|
|
226
|
+
const { BehaviorSubject } = require("rxjs");
|
|
227
|
+
|
|
228
|
+
const emptyQueue = new ContentViewModel({
|
|
229
|
+
title: "Queue",
|
|
230
|
+
items: [],
|
|
231
|
+
role: "dynamic_collection",
|
|
232
|
+
dynamicCollection: {
|
|
233
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
emptyQueue.editableCollection = {
|
|
238
|
+
items$: new BehaviorSubject([]),
|
|
239
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
240
|
+
move: jest.fn(),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const { rerender } = render(
|
|
244
|
+
<BottomSheetModalContent
|
|
245
|
+
{...baseProps}
|
|
246
|
+
items={[]}
|
|
247
|
+
emptyMessage="Queued items will be added here"
|
|
248
|
+
viewModel={emptyQueue}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
|
|
253
|
+
isEmpty: true,
|
|
254
|
+
emptyMessage: "Queued items will be added here",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const withItem = new ContentViewModel({
|
|
258
|
+
title: "Queue",
|
|
259
|
+
items: [{ id: "t1", title: "Track 1" }],
|
|
260
|
+
role: "dynamic_collection",
|
|
261
|
+
dynamicCollection: {
|
|
262
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
withItem.editableCollection = {
|
|
267
|
+
items$: new BehaviorSubject([{ id: "t1", title: "Track 1" }]),
|
|
268
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
269
|
+
move: jest.fn(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
rerender(
|
|
273
|
+
<BottomSheetModalContent
|
|
274
|
+
{...baseProps}
|
|
275
|
+
items={[{ id: "t1", title: "Track 1", label: "Track 1" }]}
|
|
276
|
+
emptyMessage="Queued items will be added here"
|
|
277
|
+
viewModel={withItem}
|
|
278
|
+
/>
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
|
|
282
|
+
isEmpty: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
mockNowPlayingHeaderProps.length = 0;
|
|
286
|
+
|
|
287
|
+
render(<BottomSheetModalContent {...baseProps} />);
|
|
288
|
+
|
|
289
|
+
expect(mockNowPlayingHeaderProps).toHaveLength(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
219
292
|
it("honors disableScrollViewWrap in contentComponentProps to bypass ScrollView wrapping", () => {
|
|
220
293
|
const CustomComponent = () => (
|
|
221
294
|
<React.Fragment>Custom Content</React.Fragment>
|
|
@@ -19,7 +19,7 @@ jest.mock(
|
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
jest.mock("../AudioPlayer/Components/Input", () => ({
|
|
22
|
-
AudioPlayerInput: ({ value, onChangeText, data }: any) => {
|
|
22
|
+
AudioPlayerInput: ({ value, onChangeText, data, validationMessage }: any) => {
|
|
23
23
|
const { TextInput, Text } = require("react-native");
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -31,6 +31,9 @@ jest.mock("../AudioPlayer/Components/Input", () => ({
|
|
|
31
31
|
onChangeText={onChangeText}
|
|
32
32
|
placeholder={data?.placeholder}
|
|
33
33
|
/>
|
|
34
|
+
{validationMessage ? (
|
|
35
|
+
<Text testID="input-validation-message">{validationMessage}</Text>
|
|
36
|
+
) : null}
|
|
34
37
|
</>
|
|
35
38
|
);
|
|
36
39
|
},
|
|
@@ -87,6 +90,16 @@ describe("TextInputContent", () => {
|
|
|
87
90
|
expect(screen.getByText("Update")).toBeTruthy();
|
|
88
91
|
});
|
|
89
92
|
|
|
93
|
+
it("shows validation below the input when its value is too long", () => {
|
|
94
|
+
render(<TextInputContent maxLength={100} {...baseProps} />);
|
|
95
|
+
|
|
96
|
+
fireEvent.changeText(screen.getByTestId("playlist-input"), "x".repeat(101));
|
|
97
|
+
|
|
98
|
+
expect(screen.getByTestId("input-validation-message")).toHaveTextContent(
|
|
99
|
+
"Too many characters"
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
90
103
|
it("on submit merges name into sendCloudEvent data then dismisses", async () => {
|
|
91
104
|
render(<TextInputContent {...baseProps} />);
|
|
92
105
|
|
|
@@ -13,8 +13,15 @@ export async function showTextInputAction(
|
|
|
13
13
|
action: ActionType,
|
|
14
14
|
context?: Record<string, any>
|
|
15
15
|
): Promise<ActionResult> {
|
|
16
|
-
const {
|
|
17
|
-
|
|
16
|
+
const {
|
|
17
|
+
headerTitle,
|
|
18
|
+
inputLabel,
|
|
19
|
+
defaultValue,
|
|
20
|
+
buttonLabel,
|
|
21
|
+
validationLabel,
|
|
22
|
+
actions,
|
|
23
|
+
maxLength,
|
|
24
|
+
} = action.options || {};
|
|
18
25
|
|
|
19
26
|
if (!headerTitle || !buttonLabel || !actions || !actions.length) {
|
|
20
27
|
return ActionResult.Error;
|
|
@@ -30,10 +37,12 @@ export async function showTextInputAction(
|
|
|
30
37
|
content: {
|
|
31
38
|
component: TextInputContent,
|
|
32
39
|
props: {
|
|
40
|
+
maxLength,
|
|
33
41
|
inputLabel,
|
|
34
42
|
defaultValue: defaultValue ?? "",
|
|
35
43
|
buttonLabel,
|
|
36
44
|
actions,
|
|
45
|
+
validationLabel,
|
|
37
46
|
},
|
|
38
47
|
},
|
|
39
48
|
},
|
|
@@ -213,10 +213,16 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
213
213
|
log_debug(
|
|
214
214
|
`pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
|
|
215
215
|
);
|
|
216
|
-
} else {
|
|
217
|
-
item.player.pause();
|
|
218
216
|
}
|
|
219
217
|
|
|
218
|
+
// Pause even when the player is not ready yet: the underlying player may
|
|
219
|
+
// still be loading and would otherwise start playing on its own once it
|
|
220
|
+
// has buffered, leaving the live-image audible underneath the primary
|
|
221
|
+
// player. Note this is best effort - the web controller's pause() only
|
|
222
|
+
// logs while the player component is unmounted - so the player itself also
|
|
223
|
+
// refuses buffer-driven starts when it was never asked to play.
|
|
224
|
+
item.player.pause();
|
|
225
|
+
|
|
220
226
|
// Fake close event, because we unmount native view
|
|
221
227
|
item.player.onPlayerClose();
|
|
222
228
|
item.setMode?.(LiveImageType.Image);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { LiveImageManager } = require("../LiveImageManager");
|
|
2
2
|
|
|
3
3
|
describe("LiveImageManager", () => {
|
|
4
|
+
const flushPromises = () => new Promise(setImmediate);
|
|
5
|
+
|
|
4
6
|
it("should subscribe to LiveImageManager events", () => {
|
|
5
7
|
const sampleListener = {
|
|
6
8
|
id: "1",
|
|
@@ -78,4 +80,61 @@ describe("LiveImageManager", () => {
|
|
|
78
80
|
expect(mockOnEnd1).toHaveBeenCalledWith(eventBody);
|
|
79
81
|
expect(mockOnEnd2).toHaveBeenCalledWith(eventBody);
|
|
80
82
|
});
|
|
83
|
+
|
|
84
|
+
it("should start a prepared live image when the mounted player loads", async () => {
|
|
85
|
+
const manager = new LiveImageManager();
|
|
86
|
+
|
|
87
|
+
const item = {
|
|
88
|
+
playerId: "live-image",
|
|
89
|
+
tag: "Live image",
|
|
90
|
+
player: {
|
|
91
|
+
playerId: "live-image",
|
|
92
|
+
playerState: { isReadyToPlay: false },
|
|
93
|
+
getEntry: jest.fn(),
|
|
94
|
+
play: jest.fn(),
|
|
95
|
+
},
|
|
96
|
+
getPlayer: jest.fn(),
|
|
97
|
+
prepareForPlayback: jest.fn().mockResolvedValue(true),
|
|
98
|
+
setMode: jest.fn(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
manager.playLiveImage(item);
|
|
102
|
+
await flushPromises();
|
|
103
|
+
|
|
104
|
+
expect(item.setMode).toHaveBeenCalledWith("video");
|
|
105
|
+
expect(item.player.play).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
manager.onLiveImageVideoLoaded(item);
|
|
108
|
+
|
|
109
|
+
expect(item.player.play).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should not start a live image that was paused before the player loaded", async () => {
|
|
113
|
+
const manager = new LiveImageManager();
|
|
114
|
+
|
|
115
|
+
const item = {
|
|
116
|
+
playerId: "live-image",
|
|
117
|
+
tag: "Live image",
|
|
118
|
+
player: {
|
|
119
|
+
playerId: "live-image",
|
|
120
|
+
playerState: { isReadyToPlay: false },
|
|
121
|
+
getEntry: jest.fn(),
|
|
122
|
+
play: jest.fn(),
|
|
123
|
+
pause: jest.fn(),
|
|
124
|
+
onPlayerClose: jest.fn(),
|
|
125
|
+
},
|
|
126
|
+
getPlayer: jest.fn(),
|
|
127
|
+
prepareForPlayback: jest.fn().mockResolvedValue(true),
|
|
128
|
+
setMode: jest.fn(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
manager.playLiveImage(item);
|
|
132
|
+
await flushPromises();
|
|
133
|
+
manager.pauseLiveImage(item);
|
|
134
|
+
manager.onLiveImageVideoLoaded(item);
|
|
135
|
+
|
|
136
|
+
expect(item.player.pause).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(item.player.play).not.toHaveBeenCalled();
|
|
138
|
+
expect(item.setMode).toHaveBeenLastCalledWith("image");
|
|
139
|
+
});
|
|
81
140
|
});
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.98",
|
|
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": "16.0.0-rc.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-rc.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-rc.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-rc.
|
|
31
|
+
"@applicaster/applicaster-types": "16.0.0-rc.98",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-rc.98",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-rc.98",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-rc.98",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"react-native-sortables": "1.7.1",
|