@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921

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 (48) hide show
  1. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  5. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  6. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  7. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  8. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  9. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
  10. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  11. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
  12. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
  13. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
  14. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
  15. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
  16. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +93 -46
  17. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
  18. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  19. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  20. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  21. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +132 -6
  22. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +202 -13
  23. package/Components/ModalComponent/BottomSheetModalContent.tsx +187 -73
  24. package/Components/ModalComponent/SortableList.tsx +36 -22
  25. package/Components/ModalComponent/TextInputContent.tsx +42 -7
  26. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
  27. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
  28. package/Components/ModalComponent/index.tsx +7 -9
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
  30. package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
  31. package/Components/ModalComponent/showTextInput.ts +11 -2
  32. package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
  33. package/Components/River/ComponentsMap/ComponentsMap.tsx +10 -12
  34. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  35. package/Components/River/__tests__/componentsMap.test.js +8 -3
  36. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  37. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  38. package/Components/Screen/index.tsx +5 -0
  39. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  40. package/Components/Transitioner/Transitioner.tsx +2 -3
  41. package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
  42. package/Components/VideoLive/LiveImageManager.ts +8 -2
  43. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  44. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  45. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  46. package/assets/empty-Info.png +0 -0
  47. package/package.json +5 -5
  48. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -11,9 +11,14 @@ jest.mock(
11
11
  () => () => null
12
12
  );
13
13
 
14
- jest.mock("react-native-safe-area-context", () => ({
15
- useSafeAreaInsets: () => ({ top: 50 }),
16
- }));
14
+ jest.mock("react-native-safe-area-context", () => {
15
+ const { View } = jest.requireActual("react-native");
16
+
17
+ return {
18
+ SafeAreaView: View,
19
+ useSafeAreaInsets: () => ({ top: 50 }),
20
+ };
21
+ });
17
22
 
18
23
  jest.mock("react-native", () => {
19
24
  const RN = jest.genMockFromModule("react-native");
@@ -141,6 +141,10 @@ const {
141
141
  useGetScreenOrientation,
142
142
  } = require("@applicaster/zapp-react-native-utils/appUtils/orientationHelper");
143
143
 
144
+ const {
145
+ useIsScreenActive,
146
+ } = require("@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive");
147
+
144
148
  const navigator = {};
145
149
  const navBarProps = { nav_items: [] };
146
150
  const screenData = {};
@@ -195,4 +199,24 @@ describe("<Screen Component />", () => {
195
199
  expect(toJSON()).toMatchSnapshot();
196
200
  });
197
201
  });
202
+
203
+ describe("when the screen is inactive", () => {
204
+ afterEach(() => {
205
+ useIsScreenActive.mockReturnValue(true);
206
+ });
207
+
208
+ it("hides the screen from VoiceOver (iOS) and TalkBack (Android)", () => {
209
+ useIsScreenActive.mockReturnValue(false);
210
+
211
+ const { UNSAFE_root } = renderWithProviders(
212
+ <Screen {...screenProps} />,
213
+ store
214
+ );
215
+
216
+ expect(UNSAFE_root.findByType(View).props).toMatchObject({
217
+ accessibilityElementsHidden: true,
218
+ importantForAccessibility: "no-hide-descendants",
219
+ });
220
+ });
221
+ });
198
222
  });
@@ -13,6 +13,7 @@ exports[`<Screen Component /> when the navbar should be hidden renders correctly
13
13
  }
14
14
  >
15
15
  <View
16
+ accessibilityElementsHidden={false}
16
17
  importantForAccessibility="yes"
17
18
  style={
18
19
  {
@@ -58,6 +59,7 @@ exports[`<Screen Component /> when the navbar should show renders correctly 1`]
58
59
  }
59
60
  >
60
61
  <View
62
+ accessibilityElementsHidden={false}
61
63
  importantForAccessibility="yes"
62
64
  style={
63
65
  {
@@ -121,6 +121,11 @@ export function Screen(_props: Props) {
121
121
  ref={ref}
122
122
  style={style}
123
123
  importantForAccessibility={!isActive ? "no-hide-descendants" : "yes"}
124
+ // `importantForAccessibility` only affects Android (see RN docs).
125
+ // Without the iOS counterpart below, VoiceOver keeps traversing and
126
+ // focusing elements on an inactive/underlying screen (e.g. Albums In
127
+ // Order) even after a new screen (e.g. an Album) is pushed on top.
128
+ accessibilityElementsHidden={!isActive}
124
129
  >
125
130
  {isReady ? (
126
131
  <>
@@ -85,7 +85,7 @@ export const withScreenRevealManager = (Component) => {
85
85
 
86
86
  if (isFirstComponentScreenPicker(componentsToRender)) {
87
87
  // for screen-picker with have additional internal ComponentsMap, no need to add this wrapper
88
- return <Component {...props} />;
88
+ return <Component {...props} initialNumberToLoad={1} />;
89
89
  }
90
90
 
91
91
  return (
@@ -129,8 +129,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
129
129
  * The last scene in the array is removed in the "backTransitionEnd" callback,
130
130
  * only after the animation is complete.
131
131
  */
132
- backTransitionStart() {
133
- const previousRoute = this.props.navigator?.currentRoute;
132
+ backTransitionStart(previousRoute: string) {
134
133
  const currentRoute = this.props.navigator?.currentRoute;
135
134
 
136
135
  const diffLengthRoutes =
@@ -242,7 +241,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
242
241
  break;
243
242
 
244
243
  case NAV_ACTION_BACK: {
245
- this.backTransitionStart();
244
+ this.backTransitionStart(previousRoute);
246
245
  break;
247
246
  }
248
247
 
@@ -1,8 +1,13 @@
1
1
  import React from "react";
2
2
  import { Text, View } from "react-native";
3
3
 
4
- import { TransitionerComponent } from "../Transitioner";
5
- import { render } from "@testing-library/react-native";
4
+ import { AnimationManager } from "../AnimationManager";
5
+ import {
6
+ NAV_ACTION_BACK,
7
+ NAV_ACTION_PUSH,
8
+ TransitionerComponent,
9
+ } from "../Transitioner";
10
+ import { act, render } from "@testing-library/react-native";
6
11
 
7
12
  jest.mock("../Scene", () => {
8
13
  const View = jest.requireActual("react-native").View;
@@ -40,6 +45,27 @@ const transitionerComponentProps = {
40
45
 
41
46
  jest.mock("uuid");
42
47
 
48
+ const sceneTransitionConfig = () => ({
49
+ push: {
50
+ duration: 0,
51
+ easing: jest.fn(),
52
+ from: { style: {} },
53
+ to: { style: {} },
54
+ },
55
+ back: {
56
+ duration: 0,
57
+ easing: jest.fn(),
58
+ from: { style: {} },
59
+ to: { style: {} },
60
+ },
61
+ });
62
+
63
+ const sceneFor = (route) => (
64
+ <View pathname={route}>
65
+ <Text>{route}</Text>
66
+ </View>
67
+ );
68
+
43
69
  describe("<TransitionerComponent />", () => {
44
70
  it("renders correctly", () => {
45
71
  const wrapper = render(
@@ -50,4 +76,59 @@ describe("<TransitionerComponent />", () => {
50
76
 
51
77
  expect(wrapper.toJSON()).toMatchSnapshot();
52
78
  });
79
+
80
+ it("removes both intermediate scenes when backing out to the tab root", () => {
81
+ jest
82
+ .spyOn(AnimationManager.prototype, "animate")
83
+ .mockImplementation((_, done) => done());
84
+
85
+ const ref = React.createRef();
86
+ const root = "/river/root";
87
+ const child = `${root}/river/child`;
88
+ const grandchild = `${child}/river/grandchild`;
89
+
90
+ const propsFor = (route, previousAction) => ({
91
+ navigator: {
92
+ previousAction,
93
+ currentRoute: route,
94
+ screenData: {},
95
+ },
96
+ transitionConfig: sceneTransitionConfig,
97
+ layoutData: {
98
+ isTablet: false,
99
+ isTabletPortrait: false,
100
+ },
101
+ children: sceneFor(route),
102
+ });
103
+
104
+ const { rerender } = render(
105
+ <TransitionerComponent ref={ref} {...propsFor(root, "REPLACE")} />
106
+ );
107
+
108
+ act(() => {
109
+ rerender(
110
+ <TransitionerComponent
111
+ ref={ref}
112
+ {...propsFor(child, NAV_ACTION_PUSH)}
113
+ />
114
+ );
115
+ });
116
+
117
+ act(() => {
118
+ rerender(
119
+ <TransitionerComponent
120
+ ref={ref}
121
+ {...propsFor(grandchild, NAV_ACTION_PUSH)}
122
+ />
123
+ );
124
+ });
125
+
126
+ act(() => {
127
+ rerender(
128
+ <TransitionerComponent ref={ref} {...propsFor(root, NAV_ACTION_BACK)} />
129
+ );
130
+ });
131
+
132
+ expect(ref.current.state.scenes.map((scene) => scene.key)).toEqual([root]);
133
+ });
53
134
  });
@@ -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
  });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { withModalNavigationContextProvider } from "../";
4
+
5
+ describe("withModalNavigationContextProvider", () => {
6
+ it("does not re-render the wrapped component when props are unchanged", () => {
7
+ const Component = jest.fn(() => null);
8
+ const WrappedComponent = withModalNavigationContextProvider(Component);
9
+ const { rerender } = render(<WrappedComponent title="Modal" />);
10
+
11
+ rerender(<WrappedComponent title="Modal" />);
12
+
13
+ expect(Component).toHaveBeenCalledTimes(1);
14
+ });
15
+ });
@@ -6,11 +6,11 @@ export function useModalNavigationContext() {
6
6
  }
7
7
 
8
8
  export function withModalNavigationContextProvider(Component) {
9
- return function WrappedWithModalNavigationContextProvider(props) {
9
+ return React.memo(function WrappedWithModalNavigationContextProvider(props) {
10
10
  return (
11
11
  <ModalNavigationContext.Provider value={true}>
12
12
  <Component {...props} />
13
13
  </ModalNavigationContext.Provider>
14
14
  );
15
- };
15
+ });
16
16
  }
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-alpha.1593888345",
3
+ "version": "16.0.0-alpha.1701179921",
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-alpha.1593888345",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1593888345",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-alpha.1593888345",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.1593888345",
31
+ "@applicaster/applicaster-types": "16.0.0-alpha.1701179921",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1701179921",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-alpha.1701179921",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-alpha.1701179921",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "react-native-sortables": "1.7.1",
@@ -1,314 +0,0 @@
1
- /**
2
- * Modal Building Block — Action
3
- *
4
- * Single-file Expo Snack demo for the Action list item building block spec.
5
- * Three forms (text only / leading icon + text / leading + text + trailing)
6
- * are driven by data — no separate variants.
7
- *
8
- * Green (configurable): background, title, trailingIcon.chevronAsset
9
- * White (hardcoded): container padding/gutter, icon size, leading icon assets,
10
- * leading/trailing visibility logic from data
11
- */
12
-
13
- import React from "react";
14
- import {
15
- Image,
16
- Platform,
17
- Pressable,
18
- StyleSheet,
19
- Text,
20
- View,
21
- } from "react-native";
22
-
23
- const styles = StyleSheet.create({
24
- row: {
25
- flexDirection: "row",
26
- alignItems: "center",
27
- },
28
- });
29
-
30
- // ---------------------------------------------------------------------------
31
- // Configuration (green / editable fields from spec)
32
- // ---------------------------------------------------------------------------
33
-
34
- export type TextTransform =
35
- | "default"
36
- | "lowercase"
37
- | "uppercase"
38
- | "capitalize";
39
-
40
- export type BackgroundConfiguration = {
41
- defaultColor: string;
42
- focusedColor: string;
43
- cornerRadius: number;
44
- };
45
-
46
- export type TitleConfiguration = {
47
- fontColor: string;
48
- focusedFontColor: string;
49
- iosFontFamily: string;
50
- androidFontFamily: string;
51
- fontSize: number;
52
- lineHeight: number;
53
- iosLetterSpacing: number;
54
- androidLetterSpacing: number;
55
- textTransform: TextTransform;
56
- };
57
-
58
- export type TrailingIconConfiguration = {
59
- chevronAsset?: string;
60
- };
61
-
62
- export type ActionConfiguration = {
63
- background: BackgroundConfiguration;
64
- title: TitleConfiguration;
65
- trailingIcon: TrailingIconConfiguration;
66
- };
67
-
68
- export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
69
- defaultColor: "transparent",
70
- focusedColor: "rgba(62, 62, 62, 1)",
71
- cornerRadius: 12,
72
- };
73
-
74
- export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
75
- fontColor: "#FFFFFF",
76
- focusedFontColor: "#FFFFFF",
77
- iosFontFamily: "SFProText-Semibold",
78
- androidFontFamily: "Roboto-Medium",
79
- fontSize: 15,
80
- lineHeight: 24,
81
- iosLetterSpacing: -0.2,
82
- androidLetterSpacing: 0,
83
- textTransform: "default",
84
- };
85
-
86
- export const DEFAULT_TRAILING_ICON_CONFIGURATION: TrailingIconConfiguration =
87
- {};
88
-
89
- export const DEFAULT_ACTION_CONFIGURATION: ActionConfiguration = {
90
- background: DEFAULT_BACKGROUND_CONFIGURATION,
91
- title: DEFAULT_TITLE_CONFIGURATION,
92
- trailingIcon: DEFAULT_TRAILING_ICON_CONFIGURATION,
93
- };
94
-
95
- // ---------------------------------------------------------------------------
96
- // Data (runtime — controls which elements are shown)
97
- // ---------------------------------------------------------------------------
98
-
99
- export type LeadingActionType =
100
- | "addToQueue"
101
- | "addToPlaylist"
102
- | "favorite"
103
- | "share"
104
- | "sleepTimer"
105
- | "playbackSpeed";
106
-
107
- export type ActionData = {
108
- title: string;
109
- /** When set, leading icon is shown (Enable: On). */
110
- leadingActionType?: LeadingActionType;
111
- /** When true, trailing chevron is shown (Enable: On). */
112
- showTrailingChevron?: boolean;
113
- };
114
-
115
- export type AudioPlayerActionProps = {
116
- configuration: ActionConfiguration;
117
- data: ActionData;
118
- focused?: boolean;
119
- onPress?: () => void;
120
- };
121
-
122
- // ---------------------------------------------------------------------------
123
- // Hardcoded spec values (white fields)
124
- // ---------------------------------------------------------------------------
125
-
126
- const CONTAINER_SPEC = {
127
- paddingTop: 13,
128
- paddingRight: 10,
129
- paddingBottom: 13,
130
- paddingLeft: 10,
131
- horizontalGutter: 12,
132
- };
133
-
134
- const ICON_SPEC = {
135
- width: 24,
136
- height: 24,
137
- };
138
-
139
- const DEFAULT_CHEVRON_ASSET =
140
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAXUBlu2bPmPjIeWBeiGDy0Lhrbrh1/qGXqZaxSMglEw0gEA4c+IpUrpYX4AAAAASUVORK5CYII=";
141
-
142
- /** Hardcoded: action type → leading icon asset (base64 PNG). */
143
- const LEADING_ICON_ASSETS: Record<LeadingActionType, string> = {
144
- addToQueue:
145
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAUjE/xHAzS3gFgweC0Y+nEwGslkBRFFwTj8LBjNyaNgmAMAJTf+EEEZcS0AAAAASUVORK5CYII=",
146
- addToPlaylist:
147
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVR42mNgGAVDBvxHA6MWjFpAvEGkglELKLdgNJmOWkA7C0YBzQAAivu+UKRR1ZIAAAAASUVORK5CYII=",
148
- favorite:
149
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKklEQVR42mNgGAWjYBTgBP+xAGLkRi0YQhbgM4wqBo9aMHgsGAWjgCAAALyAZqh4yyZZAAAAAElFTkSuQmCC",
150
- share:
151
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
152
- sleepTimer:
153
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAYUlEQVR42tWUSQ4AIAgD+f+n8eJRliIQ6MmEZTSxJZokvipdXgZpAbzOO17AglIXI7XULxmCoEMwRGuWam6AdRsN4IJYTRH4PIDkhxTA92zUPNBcFDDHaOVR0RJ2LXG9QgemFVK8tpo4PwAAAABJRU5ErkJggg==",
154
- playbackSpeed:
155
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
156
- };
157
-
158
- function getTextTransformStyle(
159
- textTransform: TextTransform
160
- ): "none" | "lowercase" | "uppercase" | "capitalize" {
161
- return textTransform === "default" ? "none" : textTransform;
162
- }
163
-
164
- function shouldShowLeadingIcon(leadingActionType?: LeadingActionType) {
165
- return !!leadingActionType;
166
- }
167
-
168
- function shouldShowTrailingIcon(showTrailingChevron?: boolean) {
169
- return !!showTrailingChevron;
170
- }
171
-
172
- // ---------------------------------------------------------------------------
173
- // Inner components
174
- // ---------------------------------------------------------------------------
175
-
176
- type ActionContainerProps = {
177
- configuration: BackgroundConfiguration;
178
- focused: boolean;
179
- children: React.ReactNode;
180
- onPress?: () => void;
181
- };
182
-
183
- function ActionContainer({
184
- configuration,
185
- focused,
186
- children,
187
- onPress,
188
- }: ActionContainerProps) {
189
- return (
190
- <Pressable
191
- onPress={onPress}
192
- style={({ pressed }) => ({
193
- backgroundColor:
194
- pressed || focused
195
- ? configuration.focusedColor
196
- : configuration.defaultColor,
197
- borderRadius: configuration.cornerRadius,
198
- paddingTop: CONTAINER_SPEC.paddingTop,
199
- paddingRight: CONTAINER_SPEC.paddingRight,
200
- paddingBottom: CONTAINER_SPEC.paddingBottom,
201
- paddingLeft: CONTAINER_SPEC.paddingLeft,
202
- })}
203
- >
204
- {children}
205
- </Pressable>
206
- );
207
- }
208
-
209
- type ActionTitleProps = {
210
- title: string;
211
- configuration: TitleConfiguration;
212
- focused: boolean;
213
- };
214
-
215
- function ActionTitle({ title, configuration, focused }: ActionTitleProps) {
216
- const letterSpacing =
217
- Platform.OS === "ios"
218
- ? configuration.iosLetterSpacing
219
- : configuration.androidLetterSpacing;
220
-
221
- return (
222
- <Text
223
- style={{
224
- flex: 1,
225
- color: focused
226
- ? configuration.focusedFontColor
227
- : configuration.fontColor,
228
- fontSize: configuration.fontSize,
229
- lineHeight: configuration.lineHeight,
230
- letterSpacing,
231
- fontFamily: Platform.select({
232
- ios: configuration.iosFontFamily,
233
- android: configuration.androidFontFamily,
234
- default: configuration.androidFontFamily,
235
- }),
236
- textTransform: getTextTransformStyle(configuration.textTransform),
237
- }}
238
- numberOfLines={1}
239
- >
240
- {title}
241
- </Text>
242
- );
243
- }
244
-
245
- type ActionLeadingIconProps = {
246
- actionType: LeadingActionType;
247
- };
248
-
249
- function ActionLeadingIcon({ actionType }: ActionLeadingIconProps) {
250
- return (
251
- <Image
252
- source={{ uri: LEADING_ICON_ASSETS[actionType] }}
253
- style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
254
- resizeMode="contain"
255
- />
256
- );
257
- }
258
-
259
- type ActionTrailingIconProps = {
260
- configuration: TrailingIconConfiguration;
261
- };
262
-
263
- function ActionTrailingIcon({ configuration }: ActionTrailingIconProps) {
264
- const asset = configuration.chevronAsset ?? DEFAULT_CHEVRON_ASSET;
265
-
266
- return (
267
- <Image
268
- source={{ uri: asset }}
269
- style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
270
- resizeMode="contain"
271
- />
272
- );
273
- }
274
-
275
- // ---------------------------------------------------------------------------
276
- // AudioPlayerAction
277
- // ---------------------------------------------------------------------------
278
-
279
- export function AudioPlayerAction({
280
- configuration,
281
- data,
282
- focused = false,
283
- onPress,
284
- }: AudioPlayerActionProps) {
285
- const { background, title, trailingIcon } = configuration;
286
- const { title: label, leadingActionType, showTrailingChevron } = data;
287
-
288
- const showLeading = shouldShowLeadingIcon(leadingActionType);
289
- const showTrailing = shouldShowTrailingIcon(showTrailingChevron);
290
-
291
- return (
292
- <ActionContainer
293
- configuration={background}
294
- focused={focused}
295
- onPress={onPress}
296
- >
297
- <View style={styles.row}>
298
- {showLeading ? (
299
- <>
300
- <ActionLeadingIcon actionType={leadingActionType} />
301
- <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
302
- </>
303
- ) : null}
304
- <ActionTitle title={label} configuration={title} focused={focused} />
305
- {showTrailing ? (
306
- <>
307
- <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
308
- <ActionTrailingIcon configuration={trailingIcon} />
309
- </>
310
- ) : null}
311
- </View>
312
- </ActionContainer>
313
- );
314
- }