@applicaster/zapp-react-native-ui-components 16.0.0-alpha.8101951023 → 16.0.0-alpha.8114431136
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/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +2 -2
- package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +2 -1
- package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +139 -39
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +22 -1
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +144 -23
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +52 -1
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
- package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +77 -0
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +58 -16
- package/Components/ModalComponent/BottomSheetModalContent.tsx +19 -74
- package/Components/ModalComponent/SortableList.tsx +2 -3
- 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/__tests__/showTextInput.test.ts +25 -0
- package/Components/ModalComponent/showTextInput.ts +13 -2
- package/Components/OfflineHandler/__tests__/index.test.tsx +109 -0
- package/Components/OfflineHandler/index.tsx +42 -4
- package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
- package/Components/River/ComponentsMap/ComponentsMap.tsx +4 -1
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
- package/Components/Transitioner/AnimationManager.js +1 -1
- package/Components/Transitioner/Scene.tsx +4 -3
- package/Components/Transitioner/Transitioner.tsx +71 -33
- package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
- package/Components/VideoLive/LiveImageManager.ts +8 -2
- package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
- package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
- package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
- package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
- package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
- package/assets/empty-Info.png +0 -0
- package/package.json +5 -5
|
@@ -20,13 +20,13 @@ const getValue = (key: string) => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
describe("CollapsibleTextContainer view tree", () => {
|
|
23
|
-
it("
|
|
23
|
+
it("aligns the label to the text alignment instead of stretching the cell", () => {
|
|
24
24
|
const result = CollapsibleTextContainer({
|
|
25
25
|
getValue,
|
|
26
26
|
elements: [],
|
|
27
27
|
additionalStyles: { backgroundColor: "transparent" },
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
expect(result.style.alignSelf).toBe("
|
|
30
|
+
expect(result.style.alignSelf).toBe("flex-start");
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -3,6 +3,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
|
|
|
3
3
|
import {
|
|
4
4
|
getColorFromData,
|
|
5
5
|
getKeyForLabel,
|
|
6
|
+
mapSelfAlignment,
|
|
6
7
|
} from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
7
8
|
import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
8
9
|
|
|
@@ -22,7 +23,7 @@ export const CollapsibleTextContainer = ({
|
|
|
22
23
|
return {
|
|
23
24
|
type: "CollapsibleTextContainer", // Collapsible Text Container,
|
|
24
25
|
style: {
|
|
25
|
-
alignSelf: "
|
|
26
|
+
alignSelf: mapSelfAlignment(getValue("text_alignment")),
|
|
26
27
|
marginTop: toNumberWithDefaultZero(getValue("margin_top")),
|
|
27
28
|
marginRight: toNumberWithDefaultZero(getValue("margin_right")),
|
|
28
29
|
marginBottom: toNumberWithDefaultZero(getValue("margin_bottom")),
|
|
@@ -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
|
},
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Green (configurable): background, image, nowPlayingAsset, nowPlayingLabel,
|
|
7
7
|
* textLabel1, textLabel2
|
|
8
8
|
* White (hardcoded): container padding/gutters, nowPlaying container layout,
|
|
9
|
-
* button sizes/padding/assets
|
|
9
|
+
* button sizes/padding. Play/pause icons use plugin assets when set.
|
|
10
10
|
*
|
|
11
11
|
* Variants (VARIANT_*) are preset configuration bundles — not passed as a field.
|
|
12
12
|
*/
|
|
@@ -94,6 +94,10 @@ export type ActionButtonAssetsConfiguration = {
|
|
|
94
94
|
removeAsset?: string;
|
|
95
95
|
/** Trailing drag handle on Edit Order / queue rows. */
|
|
96
96
|
dragAsset?: string;
|
|
97
|
+
/** Shown on the Now Playing row while the track is paused. */
|
|
98
|
+
playAsset?: string;
|
|
99
|
+
/** Shown on the Now Playing row while the track is playing. */
|
|
100
|
+
pauseAsset?: string;
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
export type ItemConfiguration = {
|
|
@@ -504,6 +508,23 @@ function ItemButton({
|
|
|
504
508
|
const renderIcon = () => {
|
|
505
509
|
switch (action) {
|
|
506
510
|
case "playPause": {
|
|
511
|
+
const playbackAsset = isSelected
|
|
512
|
+
? actionButtonAssets?.pauseAsset
|
|
513
|
+
: actionButtonAssets?.playAsset;
|
|
514
|
+
|
|
515
|
+
if (playbackAsset) {
|
|
516
|
+
return (
|
|
517
|
+
<Image
|
|
518
|
+
source={{ uri: playbackAsset }}
|
|
519
|
+
style={{
|
|
520
|
+
width: layout.assetWidth,
|
|
521
|
+
height: layout.assetHeight,
|
|
522
|
+
}}
|
|
523
|
+
resizeMode="contain"
|
|
524
|
+
/>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
507
528
|
const iconSize = layout.assetWidth;
|
|
508
529
|
const iconRadius = iconSize / 2;
|
|
509
530
|
|
|
@@ -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
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Pressable } from "react-native";
|
|
2
|
+
import { Image, Pressable } from "react-native";
|
|
3
3
|
import { fireEvent, render, screen } from "@testing-library/react-native";
|
|
4
|
+
import { ModalBlocksStyleContext } from "../../../ModalBlocksStyleContext";
|
|
4
5
|
import {
|
|
5
6
|
AudioPlayerTrackItem,
|
|
7
|
+
VARIANT_NOW_PLAYING,
|
|
6
8
|
VARIANT_PLAYLIST_ITEM,
|
|
7
9
|
mapMenuItemToNowPlayingData,
|
|
8
10
|
} from "../Item";
|
|
@@ -101,6 +103,55 @@ describe("mapMenuItemToNowPlayingData", () => {
|
|
|
101
103
|
expect(data.textLabel2).toBe("Artist Name");
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
it("uses the plugin play asset while paused and the pause asset while playing", () => {
|
|
107
|
+
const menuItem = {
|
|
108
|
+
id: "item_1",
|
|
109
|
+
title: "Track Title",
|
|
110
|
+
summary: "Artist Name",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const style = {
|
|
114
|
+
nowPlayingItem: {
|
|
115
|
+
actionButtons: {
|
|
116
|
+
default: {
|
|
117
|
+
playAsset: "https://example.com/play.png",
|
|
118
|
+
pauseAsset: "https://example.com/pause.png",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const { UNSAFE_getAllByType, rerender } = render(
|
|
125
|
+
<ModalBlocksStyleContext.Provider value={style}>
|
|
126
|
+
<AudioPlayerTrackItem
|
|
127
|
+
configuration={VARIANT_NOW_PLAYING}
|
|
128
|
+
data={mapMenuItemToNowPlayingData(menuItem as any, true)}
|
|
129
|
+
/>
|
|
130
|
+
</ModalBlocksStyleContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
UNSAFE_getAllByType(Image).some(
|
|
135
|
+
(node) => node.props.source?.uri === "https://example.com/play.png"
|
|
136
|
+
)
|
|
137
|
+
).toBe(true);
|
|
138
|
+
|
|
139
|
+
rerender(
|
|
140
|
+
<ModalBlocksStyleContext.Provider value={style}>
|
|
141
|
+
<AudioPlayerTrackItem
|
|
142
|
+
configuration={VARIANT_NOW_PLAYING}
|
|
143
|
+
data={mapMenuItemToNowPlayingData(menuItem as any, false)}
|
|
144
|
+
/>
|
|
145
|
+
</ModalBlocksStyleContext.Provider>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
UNSAFE_getAllByType(Image).some(
|
|
150
|
+
(node) => node.props.source?.uri === "https://example.com/pause.png"
|
|
151
|
+
)
|
|
152
|
+
).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
104
155
|
it("uses custom nowPlayingLabel when provided", () => {
|
|
105
156
|
const data = mapMenuItemToNowPlayingData(
|
|
106
157
|
item as any,
|