@applicaster/zapp-react-native-ui-components 16.0.0-rc.6 → 16.0.0-rc.61

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.
Files changed (129) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  5. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  6. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  7. package/Components/BackgroundImage/index.ts +1 -0
  8. package/Components/CellRendererResolver/index.ts +2 -2
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  11. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  12. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  13. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  16. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  17. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  18. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  19. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  26. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  27. package/Components/Layout/TV/index.tsx +3 -4
  28. package/Components/Layout/TV/index.web.tsx +2 -3
  29. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  32. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  34. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  35. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  36. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  52. package/Components/MasterCell/dataAdapter.ts +15 -5
  53. package/Components/MasterCell/hooks/index.ts +2 -0
  54. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  55. package/Components/MasterCell/index.tsx +30 -5
  56. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  57. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  58. package/Components/MasterCell/utils/index.ts +85 -46
  59. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +395 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  62. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  63. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +874 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  66. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
  67. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  68. package/Components/ModalComponent/BottomSheetModalContent.tsx +386 -42
  69. package/Components/ModalComponent/Button/index.tsx +5 -3
  70. package/Components/ModalComponent/Header/index.tsx +20 -7
  71. package/Components/ModalComponent/Header/utils.ts +1 -1
  72. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  73. package/Components/ModalComponent/SortableList.tsx +142 -0
  74. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  75. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  76. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  77. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  78. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  79. package/Components/ModalComponent/index.tsx +11 -0
  80. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  81. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  82. package/Components/ModalComponent/presets/index.ts +27 -0
  83. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  84. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  85. package/Components/ModalComponent/presets/types.ts +35 -0
  86. package/Components/ModalComponent/showTextInput.ts +43 -0
  87. package/Components/ModalComponent/utils.ts +76 -20
  88. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  89. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  90. package/Components/OfflineHandler/hooks.ts +123 -0
  91. package/Components/OfflineHandler/index.tsx +51 -73
  92. package/Components/OfflineHandler/utils/index.ts +1 -13
  93. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  94. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  95. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  96. package/Components/PreloaderWrapper/index.tsx +1 -1
  97. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  98. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  99. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  100. package/Components/Screen/TV/hooks/index.ts +2 -0
  101. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  102. package/Components/Screen/TV/index.web.tsx +16 -7
  103. package/Components/Screen/index.tsx +8 -4
  104. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  105. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  106. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  107. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  109. package/Components/TopCutoffOverlay/index.tsx +2 -2
  110. package/Components/Transitioner/Transitioner.tsx +28 -17
  111. package/Components/VideoLive/LiveImageManager.ts +41 -43
  112. package/Components/VideoModal/utils.ts +6 -1
  113. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  114. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  115. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  116. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  117. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  118. package/Helpers/index.js +7 -40
  119. package/package.json +6 -5
  120. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  121. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  122. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  123. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  124. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  125. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  126. package/Components/Screen/utils.ts +0 -16
  127. package/Helpers/Analytics/index.js +0 -95
  128. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  129. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Modal Building Block — Item (Queue / Playlist)
3
+ *
4
+ * Single-file Expo Snack demo. Three item forms driven by configuration + data.
5
+ *
6
+ * Green (configurable): background, image, nowPlayingAsset, nowPlayingLabel,
7
+ * textLabel1, textLabel2
8
+ * White (hardcoded): container padding/gutters, nowPlaying container layout,
9
+ * button sizes/padding/assets
10
+ *
11
+ * Variants (VARIANT_*) are preset configuration bundles — not passed as a field.
12
+ */
13
+
14
+ import React from "react";
15
+ import {
16
+ Image,
17
+ Platform,
18
+ Pressable,
19
+ StyleSheet,
20
+ Text,
21
+ View,
22
+ } from "react-native";
23
+ import {
24
+ MenuItem,
25
+ SelectionBehavior,
26
+ } from "@applicaster/zapp-react-native-utils/modalState/types";
27
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
28
+ import { mergeItemConfiguration } from "../utils/mergeStyles";
29
+
30
+ const styles = StyleSheet.create({
31
+ row: {
32
+ flexDirection: "row",
33
+ alignItems: "center",
34
+ },
35
+ contentColumn: {
36
+ flex: 1,
37
+ justifyContent: "center",
38
+ },
39
+ buttonsRow: {
40
+ flexDirection: "row",
41
+ alignItems: "center",
42
+ },
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Configuration (green fields)
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export type TextTransform =
50
+ | "default"
51
+ | "lowercase"
52
+ | "uppercase"
53
+ | "capitalize";
54
+
55
+ export type BackgroundConfiguration = {
56
+ defaultColor: string;
57
+ focusedColor: string;
58
+ cornerRadius: number;
59
+ };
60
+
61
+ export type ImageConfiguration = {
62
+ width: number;
63
+ height: number;
64
+ borderWidth: number;
65
+ borderColor: string;
66
+ cornerRadius: number;
67
+ };
68
+
69
+ export type LabelConfiguration = {
70
+ fontColor: string;
71
+ focusedFontColor?: string;
72
+ iosFontFamily: string;
73
+ androidFontFamily: string;
74
+ fontSize: number;
75
+ lineHeight: number;
76
+ iosLetterSpacing: number;
77
+ androidLetterSpacing: number;
78
+ textTransform: TextTransform;
79
+ };
80
+
81
+ export type NowPlayingAssetConfiguration = {
82
+ asset?: string;
83
+ };
84
+
85
+ export type ItemConfiguration = {
86
+ background: BackgroundConfiguration;
87
+ image: ImageConfiguration;
88
+ nowPlayingAsset: NowPlayingAssetConfiguration;
89
+ nowPlayingLabel: LabelConfiguration;
90
+ textLabel1: LabelConfiguration;
91
+ textLabel2: LabelConfiguration;
92
+ };
93
+
94
+ export const DEFAULT_TEXT_LABEL_1_CONFIGURATION: LabelConfiguration = {
95
+ fontColor: "#FFFFFF",
96
+ focusedFontColor: "#FFFFFF",
97
+ iosFontFamily: "SFProText-Semibold",
98
+ androidFontFamily: "Roboto-Medium",
99
+ fontSize: 15,
100
+ lineHeight: 18,
101
+ iosLetterSpacing: -0.2,
102
+ androidLetterSpacing: 0,
103
+ textTransform: "default",
104
+ };
105
+
106
+ export const DEFAULT_TEXT_LABEL_2_CONFIGURATION: LabelConfiguration = {
107
+ fontColor: "#999999",
108
+ iosFontFamily: "SFProText-Medium",
109
+ androidFontFamily: "Roboto-Medium",
110
+ fontSize: 13,
111
+ lineHeight: 16,
112
+ iosLetterSpacing: -0.1,
113
+ androidLetterSpacing: 0,
114
+ textTransform: "default",
115
+ };
116
+
117
+ export const DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION: LabelConfiguration = {
118
+ fontColor: "#FFFFFF",
119
+ iosFontFamily: "SFProText-Semibold",
120
+ androidFontFamily: "Roboto-Medium",
121
+ fontSize: 13,
122
+ lineHeight: 16,
123
+ iosLetterSpacing: 0,
124
+ androidLetterSpacing: 0,
125
+ textTransform: "default",
126
+ };
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Data
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export type ItemButtonAction = "playPause" | "drag" | "multiSelect" | "remove";
133
+
134
+ export type ItemData = {
135
+ imageUri?: string;
136
+ textLabel1: string;
137
+ textLabel2: string;
138
+ /** When set, shows the Now Playing row (equalizer + label). */
139
+ nowPlayingLabel?: string;
140
+ /** Button 2 (left most) — hardcoded action decides icon. */
141
+ leadingButton?: ItemButtonAction;
142
+ /** Button 1 (right most) — hardcoded action decides icon. */
143
+ trailingButton?: ItemButtonAction;
144
+ isSelected?: boolean;
145
+ };
146
+
147
+ export type AudioPlayerTrackItemProps = {
148
+ configuration: ItemConfiguration;
149
+ data: ItemData;
150
+ focused?: boolean;
151
+ focusedLeadingButton?: boolean;
152
+ focusedTrailingButton?: boolean;
153
+ onPress?: () => void;
154
+ onLeadingButtonPress?: () => void;
155
+ onTrailingButtonPress?: () => void;
156
+ renderHandle?: (element: React.ReactElement) => React.ReactElement;
157
+ };
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Hardcoded (white fields)
161
+ // ---------------------------------------------------------------------------
162
+
163
+ type ContainerSpec = {
164
+ paddingTop: number;
165
+ paddingRight: number;
166
+ paddingBottom: number;
167
+ paddingLeft: number;
168
+ horizontalGutter: number;
169
+ verticalGutter: number;
170
+ };
171
+
172
+ type ButtonLayoutSpec = {
173
+ assetWidth: number;
174
+ assetHeight: number;
175
+ paddingTop: number;
176
+ paddingRight: number;
177
+ paddingBottom: number;
178
+ paddingLeft: number;
179
+ };
180
+
181
+ const CONTAINER_SPEC_LARGE_IMAGE: ContainerSpec = {
182
+ paddingTop: 8,
183
+ paddingRight: 20,
184
+ paddingBottom: 8,
185
+ paddingLeft: 20,
186
+ horizontalGutter: 12,
187
+ verticalGutter: 2,
188
+ };
189
+
190
+ const CONTAINER_SPEC_DEFAULT: ContainerSpec = {
191
+ paddingTop: 8,
192
+ paddingRight: 20,
193
+ paddingBottom: 8,
194
+ paddingLeft: 20,
195
+ horizontalGutter: 12,
196
+ verticalGutter: 2,
197
+ };
198
+
199
+ const BUTTONS_CONTAINER_HORIZONTAL_GUTTER = 4;
200
+
201
+ const NOW_PLAYING_CONTAINER_SPEC = {
202
+ horizontalGutter: 4,
203
+ marginTop: 0,
204
+ marginRight: 0,
205
+ marginBottom: 6,
206
+ marginLeft: 0,
207
+ };
208
+
209
+ const NOW_PLAYING_ASSET_SPEC = {
210
+ width: 16,
211
+ height: 16,
212
+ };
213
+
214
+ const BUTTON_LAYOUT_SPEC: Record<ItemButtonAction, ButtonLayoutSpec> = {
215
+ playPause: {
216
+ assetWidth: 44,
217
+ assetHeight: 44,
218
+ paddingTop: 0,
219
+ paddingRight: 0,
220
+ paddingBottom: 0,
221
+ paddingLeft: 0,
222
+ },
223
+ drag: {
224
+ assetWidth: 24,
225
+ assetHeight: 24,
226
+ paddingTop: 10,
227
+ paddingRight: 10,
228
+ paddingBottom: 10,
229
+ paddingLeft: 10,
230
+ },
231
+ multiSelect: {
232
+ assetWidth: 24,
233
+ assetHeight: 24,
234
+ paddingTop: 10,
235
+ paddingRight: 10,
236
+ paddingBottom: 10,
237
+ paddingLeft: 10,
238
+ },
239
+ remove: {
240
+ assetWidth: 24,
241
+ assetHeight: 24,
242
+ paddingTop: 10,
243
+ paddingRight: 10,
244
+ paddingBottom: 10,
245
+ paddingLeft: 10,
246
+ },
247
+ };
248
+
249
+ const DEFAULT_THUMB_ASSET =
250
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAQklEQVR42u3PMQ0AAAgDsEmcfzNggZekRwU0beezCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAhcLQVXaABdK6k6AAAAAElFTkSuQmCC";
251
+
252
+ const DEFAULT_EQUALIZER_ASSET =
253
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAI0lEQVR42mNgGNTgPxTQzwB0DVQ3gKCBowbQwYAhkBIHBAAA9TvPMeOFaLMAAAAASUVORK5CYII=";
254
+
255
+ function getTextTransformStyle(
256
+ textTransform: TextTransform
257
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
258
+ return textTransform === "default" ? "none" : textTransform;
259
+ }
260
+
261
+ function getFontFamily(ios: string, android: string) {
262
+ return Platform.select({ ios, android, default: android });
263
+ }
264
+
265
+ /** Hardcoded: large image (72) uses wider horizontal container padding. */
266
+ function resolveContainerSpec(image: ImageConfiguration): ContainerSpec {
267
+ return image.width >= 72
268
+ ? CONTAINER_SPEC_LARGE_IMAGE
269
+ : CONTAINER_SPEC_DEFAULT;
270
+ }
271
+
272
+ export function mapMenuItemToNowPlayingData(
273
+ item: MenuItem,
274
+ isPaused?: boolean
275
+ ): ItemData {
276
+ return {
277
+ imageUri: item.icon,
278
+ textLabel1: item.title,
279
+ textLabel2: item.summary ?? "",
280
+ nowPlayingLabel: "Now Playing",
281
+ trailingButton: "playPause",
282
+ isSelected: isPaused !== undefined ? !isPaused : item.isSelected,
283
+ };
284
+ }
285
+
286
+ export function mapMenuItemToQueuedData(item: MenuItem): ItemData {
287
+ return {
288
+ imageUri: item.icon,
289
+ textLabel1: item.title,
290
+ textLabel2: item.summary ?? "",
291
+ leadingButton: "remove",
292
+ trailingButton: "drag",
293
+ isSelected: item.isSelected,
294
+ };
295
+ }
296
+
297
+ export function mapMenuItemToPlaylistData(
298
+ item: MenuItem,
299
+ behavior?: SelectionBehavior
300
+ ): ItemData {
301
+ const showSelectionWidget =
302
+ behavior?.selectMode !== "none" && item.trailingButton !== "none";
303
+
304
+ return {
305
+ imageUri: item.icon,
306
+ textLabel1: item.title,
307
+ textLabel2: item.summary ?? "",
308
+ trailingButton: showSelectionWidget
309
+ ? item.trailingButton || ("multiSelect" as ItemButtonAction)
310
+ : undefined,
311
+ isSelected: item.isSelected,
312
+ };
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Inner components
317
+ // ---------------------------------------------------------------------------
318
+
319
+ type ItemContainerProps = {
320
+ image: ImageConfiguration;
321
+ background: BackgroundConfiguration;
322
+ focused: boolean;
323
+ children: React.ReactNode;
324
+ onPress?: () => void;
325
+ };
326
+
327
+ function ItemContainer({
328
+ image,
329
+ background,
330
+ focused,
331
+ children,
332
+ onPress,
333
+ }: ItemContainerProps) {
334
+ const container = resolveContainerSpec(image);
335
+ const bg = focused ? background.focusedColor : background.defaultColor;
336
+
337
+ return (
338
+ <Pressable
339
+ onPress={onPress}
340
+ style={({ pressed }) => ({
341
+ backgroundColor: pressed ? background.focusedColor : bg,
342
+ borderRadius: background.cornerRadius,
343
+ paddingTop: container.paddingTop,
344
+ paddingRight: container.paddingRight,
345
+ paddingBottom: container.paddingBottom,
346
+ paddingLeft: container.paddingLeft,
347
+ })}
348
+ >
349
+ {children}
350
+ </Pressable>
351
+ );
352
+ }
353
+
354
+ type ItemThumbnailProps = {
355
+ configuration: ImageConfiguration;
356
+ imageUri?: string;
357
+ };
358
+
359
+ function ItemThumbnail({ configuration, imageUri }: ItemThumbnailProps) {
360
+ return (
361
+ <Image
362
+ source={{ uri: imageUri ?? DEFAULT_THUMB_ASSET }}
363
+ style={{
364
+ width: configuration.width,
365
+ height: configuration.height,
366
+ borderWidth: configuration.borderWidth,
367
+ borderColor: configuration.borderColor,
368
+ borderRadius: configuration.cornerRadius,
369
+ }}
370
+ resizeMode="cover"
371
+ />
372
+ );
373
+ }
374
+
375
+ type ItemLabelProps = {
376
+ text: string;
377
+ configuration: LabelConfiguration;
378
+ focused?: boolean;
379
+ };
380
+
381
+ function ItemLabel({ text, configuration, focused = false }: ItemLabelProps) {
382
+ const color =
383
+ focused && configuration.focusedFontColor
384
+ ? configuration.focusedFontColor
385
+ : configuration.fontColor;
386
+
387
+ return (
388
+ <Text
389
+ style={{
390
+ color,
391
+ fontSize: configuration.fontSize,
392
+ lineHeight: configuration.lineHeight,
393
+ letterSpacing:
394
+ Platform.OS === "ios"
395
+ ? configuration.iosLetterSpacing
396
+ : configuration.androidLetterSpacing,
397
+ fontFamily: getFontFamily(
398
+ configuration.iosFontFamily,
399
+ configuration.androidFontFamily
400
+ ),
401
+ textTransform: getTextTransformStyle(configuration.textTransform),
402
+ }}
403
+ numberOfLines={1}
404
+ >
405
+ {text}
406
+ </Text>
407
+ );
408
+ }
409
+
410
+ type NowPlayingRowProps = {
411
+ assetConfiguration: NowPlayingAssetConfiguration;
412
+ labelConfiguration: LabelConfiguration;
413
+ label: string;
414
+ };
415
+
416
+ function NowPlayingRow({
417
+ assetConfiguration,
418
+ labelConfiguration,
419
+ label,
420
+ }: NowPlayingRowProps) {
421
+ const asset = assetConfiguration.asset ?? DEFAULT_EQUALIZER_ASSET;
422
+
423
+ return (
424
+ <View
425
+ style={{
426
+ flexDirection: "row",
427
+ alignItems: "center",
428
+ marginTop: NOW_PLAYING_CONTAINER_SPEC.marginTop,
429
+ marginRight: NOW_PLAYING_CONTAINER_SPEC.marginRight,
430
+ marginBottom: NOW_PLAYING_CONTAINER_SPEC.marginBottom,
431
+ marginLeft: NOW_PLAYING_CONTAINER_SPEC.marginLeft,
432
+ }}
433
+ >
434
+ <Image
435
+ source={{ uri: asset }}
436
+ style={{
437
+ width: NOW_PLAYING_ASSET_SPEC.width,
438
+ height: NOW_PLAYING_ASSET_SPEC.height,
439
+ }}
440
+ resizeMode="contain"
441
+ />
442
+ <View style={{ width: NOW_PLAYING_CONTAINER_SPEC.horizontalGutter }} />
443
+ <ItemLabel text={label} configuration={labelConfiguration} />
444
+ </View>
445
+ );
446
+ }
447
+
448
+ type ItemButtonProps = {
449
+ action: ItemButtonAction;
450
+ focused?: boolean;
451
+ onPress?: () => void;
452
+ isSelected?: boolean;
453
+ };
454
+
455
+ function ItemButton({
456
+ action,
457
+ focused = false,
458
+ onPress,
459
+ isSelected = false,
460
+ }: ItemButtonProps) {
461
+ const layout = BUTTON_LAYOUT_SPEC[action];
462
+
463
+ if (!layout) {
464
+ return null;
465
+ }
466
+
467
+ const renderIcon = () => {
468
+ switch (action) {
469
+ case "playPause": {
470
+ const iconSize = layout.assetWidth;
471
+ const iconRadius = iconSize / 2;
472
+
473
+ return (
474
+ <View
475
+ style={{
476
+ width: iconSize,
477
+ height: iconSize,
478
+ borderRadius: iconRadius,
479
+ backgroundColor: "#FFFFFF",
480
+ justifyContent: "center",
481
+ alignItems: "center",
482
+ }}
483
+ >
484
+ {isSelected ? (
485
+ <View
486
+ style={{
487
+ flexDirection: "row",
488
+ justifyContent: "space-between",
489
+ width: 10,
490
+ height: 16,
491
+ }}
492
+ >
493
+ <View
494
+ style={{
495
+ width: 3.5,
496
+ height: 16,
497
+ backgroundColor: "#1e1e1e",
498
+ borderRadius: 1,
499
+ }}
500
+ />
501
+ <View
502
+ style={{
503
+ width: 3.5,
504
+ height: 16,
505
+ backgroundColor: "#1e1e1e",
506
+ borderRadius: 1,
507
+ }}
508
+ />
509
+ </View>
510
+ ) : (
511
+ <View
512
+ style={{
513
+ width: 0,
514
+ height: 0,
515
+ backgroundColor: "transparent",
516
+ borderStyle: "solid",
517
+ borderLeftWidth: 14,
518
+ borderRightWidth: 0,
519
+ borderTopWidth: 8,
520
+ borderBottomWidth: 8,
521
+ borderLeftColor: "#1e1e1e",
522
+ borderRightColor: "transparent",
523
+ borderTopColor: "transparent",
524
+ borderBottomColor: "transparent",
525
+ marginLeft: 3,
526
+ }}
527
+ />
528
+ )}
529
+ </View>
530
+ );
531
+ }
532
+
533
+ case "drag": {
534
+ return (
535
+ <View
536
+ style={{
537
+ width: 20,
538
+ height: 12,
539
+ justifyContent: "space-between",
540
+ }}
541
+ >
542
+ <View
543
+ style={{
544
+ height: 2,
545
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
546
+ borderRadius: 1,
547
+ }}
548
+ />
549
+ <View
550
+ style={{
551
+ height: 2,
552
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
553
+ borderRadius: 1,
554
+ }}
555
+ />
556
+ <View
557
+ style={{
558
+ height: 2,
559
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
560
+ borderRadius: 1,
561
+ }}
562
+ />
563
+ </View>
564
+ );
565
+ }
566
+
567
+ case "remove": {
568
+ return (
569
+ <View
570
+ style={{
571
+ width: 24,
572
+ height: 24,
573
+ borderRadius: 12,
574
+ borderWidth: 1.5,
575
+ borderColor: "rgba(255, 255, 255, 0.7)",
576
+ justifyContent: "center",
577
+ alignItems: "center",
578
+ }}
579
+ >
580
+ <View
581
+ style={{
582
+ width: 10,
583
+ height: 1.5,
584
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
585
+ borderRadius: 1,
586
+ }}
587
+ />
588
+ </View>
589
+ );
590
+ }
591
+
592
+ case "multiSelect": {
593
+ if (!isSelected) {
594
+ return (
595
+ <View
596
+ style={{
597
+ width: layout.assetWidth,
598
+ height: layout.assetHeight,
599
+ borderRadius: layout.assetWidth / 2,
600
+ borderWidth: 1.5,
601
+ borderColor: "rgba(255, 255, 255, 0.7)",
602
+ backgroundColor: "transparent",
603
+ }}
604
+ />
605
+ );
606
+ }
607
+
608
+ return (
609
+ <View
610
+ style={{
611
+ width: 24,
612
+ height: 24,
613
+ borderRadius: 12,
614
+ backgroundColor: "#FFFFFF",
615
+ justifyContent: "center",
616
+ alignItems: "center",
617
+ }}
618
+ >
619
+ <View
620
+ style={{
621
+ width: 6,
622
+ height: 10,
623
+ borderBottomWidth: 2,
624
+ borderRightWidth: 2,
625
+ borderColor: "#1e1e1e",
626
+ transform: [{ rotate: "45deg" }],
627
+ marginTop: -2,
628
+ }}
629
+ />
630
+ </View>
631
+ );
632
+ }
633
+
634
+ default:
635
+ return null;
636
+ }
637
+ };
638
+
639
+ return (
640
+ <Pressable
641
+ onPress={(e: any) => {
642
+ e?.stopPropagation?.();
643
+ onPress?.();
644
+ }}
645
+ style={({ pressed }) => [
646
+ {
647
+ paddingTop: layout.paddingTop,
648
+ paddingRight: layout.paddingRight,
649
+ paddingBottom: layout.paddingBottom,
650
+ paddingLeft: layout.paddingLeft,
651
+ opacity: pressed || focused ? 0.7 : 1,
652
+ },
653
+ ]}
654
+ >
655
+ {renderIcon()}
656
+ </Pressable>
657
+ );
658
+ }
659
+
660
+ type ItemButtonsProps = {
661
+ leadingButton?: ItemButtonAction;
662
+ trailingButton?: ItemButtonAction;
663
+ focusedLeadingButton?: boolean;
664
+ focusedTrailingButton?: boolean;
665
+ onLeadingButtonPress?: () => void;
666
+ onTrailingButtonPress?: () => void;
667
+ isSelected?: boolean;
668
+ renderHandle?: (element: React.ReactElement) => React.ReactElement;
669
+ };
670
+
671
+ function ItemButtons({
672
+ leadingButton,
673
+ trailingButton,
674
+ focusedLeadingButton = false,
675
+ focusedTrailingButton = false,
676
+ onLeadingButtonPress,
677
+ onTrailingButtonPress,
678
+ isSelected = false,
679
+ renderHandle,
680
+ }: ItemButtonsProps) {
681
+ if (!leadingButton && !trailingButton) {
682
+ return null;
683
+ }
684
+
685
+ return (
686
+ <View style={styles.buttonsRow}>
687
+ {leadingButton ? (
688
+ <>
689
+ <ItemButton
690
+ action={leadingButton}
691
+ focused={focusedLeadingButton}
692
+ onPress={onLeadingButtonPress}
693
+ isSelected={isSelected}
694
+ />
695
+ <View style={{ width: BUTTONS_CONTAINER_HORIZONTAL_GUTTER }} />
696
+ </>
697
+ ) : null}
698
+ {trailingButton ? (
699
+ trailingButton === "drag" ? (
700
+ renderHandle ? (
701
+ renderHandle(
702
+ <ItemButton
703
+ action={trailingButton}
704
+ focused={focusedTrailingButton}
705
+ onPress={onTrailingButtonPress}
706
+ isSelected={isSelected}
707
+ />
708
+ )
709
+ ) : (
710
+ <ItemButton
711
+ action={trailingButton}
712
+ focused={focusedTrailingButton}
713
+ onPress={onTrailingButtonPress}
714
+ isSelected={isSelected}
715
+ />
716
+ )
717
+ ) : (
718
+ <ItemButton
719
+ action={trailingButton}
720
+ focused={focusedTrailingButton}
721
+ onPress={onTrailingButtonPress}
722
+ isSelected={isSelected}
723
+ />
724
+ )
725
+ ) : null}
726
+ </View>
727
+ );
728
+ }
729
+
730
+ // ---------------------------------------------------------------------------
731
+ // AudioPlayerTrackItem
732
+ // ---------------------------------------------------------------------------
733
+
734
+ export function AudioPlayerTrackItem({
735
+ configuration,
736
+ data,
737
+ focused = false,
738
+ focusedLeadingButton = false,
739
+ focusedTrailingButton = false,
740
+ onPress,
741
+ onLeadingButtonPress,
742
+ onTrailingButtonPress,
743
+ renderHandle,
744
+ }: AudioPlayerTrackItemProps) {
745
+ const modalBlocksStyle = useModalBlocksStyle();
746
+ const showNowPlaying = !!data.nowPlayingLabel;
747
+
748
+ const itemBlock =
749
+ showNowPlaying && modalBlocksStyle?.nowPlayingItem
750
+ ? modalBlocksStyle.nowPlayingItem
751
+ : modalBlocksStyle?.item;
752
+
753
+ const effectiveConfig = React.useMemo(() => {
754
+ return mergeItemConfiguration(configuration, itemBlock);
755
+ }, [configuration, itemBlock]);
756
+
757
+ const {
758
+ background,
759
+ image,
760
+ textLabel1,
761
+ textLabel2,
762
+ nowPlayingLabel,
763
+ nowPlayingAsset,
764
+ } = effectiveConfig;
765
+
766
+ const container = resolveContainerSpec(image);
767
+
768
+ return (
769
+ <ItemContainer
770
+ image={image}
771
+ background={background}
772
+ focused={focused}
773
+ onPress={onPress}
774
+ >
775
+ <View style={styles.row}>
776
+ <ItemThumbnail configuration={image} imageUri={data.imageUri} />
777
+ <View style={{ width: container.horizontalGutter }} />
778
+ <View style={styles.contentColumn}>
779
+ {showNowPlaying ? (
780
+ <>
781
+ <NowPlayingRow
782
+ assetConfiguration={nowPlayingAsset}
783
+ labelConfiguration={nowPlayingLabel}
784
+ label={data.nowPlayingLabel}
785
+ />
786
+ <View style={{ height: container.verticalGutter }} />
787
+ </>
788
+ ) : null}
789
+ <ItemLabel
790
+ text={data.textLabel1}
791
+ configuration={textLabel1}
792
+ focused={focused}
793
+ />
794
+ <View style={{ height: container.verticalGutter }} />
795
+ <ItemLabel text={data.textLabel2} configuration={textLabel2} />
796
+ </View>
797
+ <ItemButtons
798
+ leadingButton={data.leadingButton}
799
+ trailingButton={data.trailingButton}
800
+ focusedLeadingButton={focusedLeadingButton}
801
+ focusedTrailingButton={focusedTrailingButton}
802
+ onLeadingButtonPress={onLeadingButtonPress}
803
+ onTrailingButtonPress={onTrailingButtonPress}
804
+ isSelected={data.isSelected}
805
+ renderHandle={renderHandle}
806
+ />
807
+ </View>
808
+ </ItemContainer>
809
+ );
810
+ }
811
+
812
+ // ---------------------------------------------------------------------------
813
+ // Variant presets — collections of configuration keys (not a variant field)
814
+ // ---------------------------------------------------------------------------
815
+
816
+ /** Variant 1 — Now Playing */
817
+ export const VARIANT_NOW_PLAYING: ItemConfiguration = {
818
+ background: {
819
+ defaultColor: "transparent",
820
+ focusedColor: "transparent",
821
+ cornerRadius: 0,
822
+ },
823
+ image: {
824
+ width: 72,
825
+ height: 72,
826
+ borderWidth: 1,
827
+ borderColor: "rgba(255, 255, 255, 0.1)",
828
+ cornerRadius: 8,
829
+ },
830
+ nowPlayingAsset: {},
831
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
832
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
833
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
834
+ };
835
+
836
+ /** Default / Variant 2 — Queued Item */
837
+ export const VARIANT_QUEUED_ITEM: ItemConfiguration = {
838
+ background: {
839
+ defaultColor: "transparent",
840
+ focusedColor: "rgba(62, 62, 62, 1)",
841
+ cornerRadius: 12,
842
+ },
843
+ image: {
844
+ width: 48,
845
+ height: 48,
846
+ borderWidth: 1,
847
+ borderColor: "rgba(255, 255, 255, 0.1)",
848
+ cornerRadius: 6,
849
+ },
850
+ nowPlayingAsset: {},
851
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
852
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
853
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
854
+ };
855
+
856
+ /** Variant 3 — Playlist Item */
857
+ export const VARIANT_PLAYLIST_ITEM: ItemConfiguration = {
858
+ background: {
859
+ defaultColor: "transparent",
860
+ focusedColor: "rgba(62, 62, 62, 1)",
861
+ cornerRadius: 12,
862
+ },
863
+ image: {
864
+ width: 48,
865
+ height: 48,
866
+ borderWidth: 1,
867
+ borderColor: "rgba(255, 255, 255, 0.1)",
868
+ cornerRadius: 6,
869
+ },
870
+ nowPlayingAsset: {},
871
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
872
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
873
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
874
+ };