@applicaster/zapp-react-native-utils 14.0.0-alpha.1308901965 → 14.0.0-alpha.1322105203
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/appUtils/HooksManager/Hook.ts +4 -4
- package/appUtils/HooksManager/index.ts +11 -1
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +0 -15
- package/appUtils/playerManager/useChapterMarker.tsx +0 -1
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +16 -0
- package/arrayUtils/index.ts +1 -1
- package/componentsUtils/__tests__/isTabsScreen.test.ts +38 -0
- package/componentsUtils/index.ts +4 -1
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +547 -0
- package/configurationUtils/manifestKeyParser.ts +57 -32
- package/package.json +2 -2
- package/playerUtils/index.ts +51 -0
- package/reactHooks/layout/isTablet/index.ts +12 -5
- package/reactHooks/navigation/index.ts +7 -3
- package/reactHooks/navigation/useScreenStateStore.ts +1 -4
|
@@ -65,10 +65,6 @@ export class Hook implements HookInterface {
|
|
|
65
65
|
event: (typeof HOOKS_EVENTS)[keyof typeof HOOKS_EVENTS],
|
|
66
66
|
...args
|
|
67
67
|
) {
|
|
68
|
-
if (this.state === hookState(HOOKS_EVENTS.CANCEL)) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
68
|
this.state = hookState(event);
|
|
73
69
|
this.manager.subscriber.invokeHandler(event, ...args);
|
|
74
70
|
}
|
|
@@ -198,4 +194,8 @@ export class Hook implements HookInterface {
|
|
|
198
194
|
R.eqProps("weight", nextHook, this)
|
|
199
195
|
);
|
|
200
196
|
}
|
|
197
|
+
|
|
198
|
+
isCancelled(): boolean {
|
|
199
|
+
return this.state === hookState(HOOKS_EVENTS.CANCEL);
|
|
200
|
+
}
|
|
201
201
|
}
|
|
@@ -255,7 +255,7 @@ export function HooksManager({
|
|
|
255
255
|
* @param {Array<Hook>} restOfHooks to run
|
|
256
256
|
* @returns {function} callback function
|
|
257
257
|
*/
|
|
258
|
-
function hookCallback(hookPlugin, restOfHooks, initialPayload) {
|
|
258
|
+
function hookCallback(hookPlugin: Hook, restOfHooks: Hook[], initialPayload) {
|
|
259
259
|
/**
|
|
260
260
|
* callback invoked after a hook is executed
|
|
261
261
|
* @param {object} options
|
|
@@ -273,6 +273,16 @@ export function HooksManager({
|
|
|
273
273
|
}) {
|
|
274
274
|
let callback = callbackArg;
|
|
275
275
|
|
|
276
|
+
if (hookPlugin.isCancelled()) {
|
|
277
|
+
logHookEvent(
|
|
278
|
+
hooksManagerLogger.info,
|
|
279
|
+
`hookCallback: hook was cancelled: ${hookPlugin["identifier"]}`,
|
|
280
|
+
{}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
276
286
|
if (error) {
|
|
277
287
|
logHookEvent(
|
|
278
288
|
hooksManagerLogger.error,
|
|
@@ -18,13 +18,6 @@ export const { log_verbose, log_debug, log_info, log_error } = createLogger({
|
|
|
18
18
|
parent: utilsLogger,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
type ActionChapter = {
|
|
22
|
-
type: string;
|
|
23
|
-
options?: {
|
|
24
|
-
title: string;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
|
|
28
21
|
type ChapterMarkerOriginal = {
|
|
29
22
|
id: string;
|
|
30
23
|
title: string;
|
|
@@ -33,14 +26,6 @@ type ChapterMarkerOriginal = {
|
|
|
33
26
|
actions: ActionChapter[];
|
|
34
27
|
};
|
|
35
28
|
|
|
36
|
-
export type ChapterMarkerEvent = {
|
|
37
|
-
id: string;
|
|
38
|
-
title: string;
|
|
39
|
-
start_time: number;
|
|
40
|
-
end_time: number;
|
|
41
|
-
actions: ActionChapter[];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
29
|
export type TitleSummaryEvent = {
|
|
45
30
|
title: string | number;
|
|
46
31
|
summary: string | number;
|
|
@@ -5,6 +5,9 @@ import { playerManager } from "./index";
|
|
|
5
5
|
import { useValidatePlayerConfig } from "../../playerUtils/useValidatePlayerConfig";
|
|
6
6
|
import { PlayerRole } from "./conts";
|
|
7
7
|
import { isAppleTV } from "@applicaster/zapp-react-native-ui-components/Helpers/Platform";
|
|
8
|
+
import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
|
|
9
|
+
import { KeyInputHandler } from "../keyInputHandler/KeyInputHandler";
|
|
10
|
+
import { isTV } from "../../reactUtils";
|
|
8
11
|
|
|
9
12
|
// TODO: Rename to ControllerType
|
|
10
13
|
export const usePlayerControllerSetup = ({
|
|
@@ -76,5 +79,18 @@ export const usePlayerControllerSetup = ({
|
|
|
76
79
|
};
|
|
77
80
|
}, [playerId, playerController]);
|
|
78
81
|
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!isTV()) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (playerController) {
|
|
88
|
+
const seekController = new TVSeekController(playerController);
|
|
89
|
+
playerController.seekController = seekController;
|
|
90
|
+
|
|
91
|
+
return KeyInputHandler.getInstance().addListener(seekController);
|
|
92
|
+
}
|
|
93
|
+
}, [playerController]);
|
|
94
|
+
|
|
79
95
|
return playerController;
|
|
80
96
|
};
|
package/arrayUtils/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const isIndexInRange = (index: number, length: number): boolean => {
|
|
|
93
93
|
export const makeListOfIndexes = (size: number): number[] =>
|
|
94
94
|
Array.from({ length: size }, (_, index) => index);
|
|
95
95
|
|
|
96
|
-
export const makeListOf = (value:
|
|
96
|
+
export const makeListOf = <T>(value: T, size: number): T[] => {
|
|
97
97
|
return Array(size).fill(value);
|
|
98
98
|
};
|
|
99
99
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isTabsScreen } from "..";
|
|
2
|
+
|
|
3
|
+
describe("isTabsScreen", () => {
|
|
4
|
+
it("should return true if the component type is 'screen-picker-qb-tv' and tabs_screen=true", () => {
|
|
5
|
+
const item = { component_type: "screen-picker-qb-tv", tabs_screen: true };
|
|
6
|
+
expect(isTabsScreen(item)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should return true if the component type is 'screen-picker-qb-tv' and tabs_screen=false", () => {
|
|
10
|
+
const item = { component_type: "screen-picker-qb-tv", tabs_screen: false };
|
|
11
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should return false if the component type is not 'screen-picker-qb-tv'", () => {
|
|
15
|
+
const item = { component_type: "other-component" };
|
|
16
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return false if the component type is undefined", () => {
|
|
20
|
+
const item = { component_type: undefined };
|
|
21
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return false if the item is null", () => {
|
|
25
|
+
const item = null;
|
|
26
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return false if the item is undefined", () => {
|
|
30
|
+
const item = undefined;
|
|
31
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return false if the item is an empty object", () => {
|
|
35
|
+
const item = {};
|
|
36
|
+
expect(isTabsScreen(item)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
package/componentsUtils/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ const EMPTY_GROUP_COMPONENT = "empty_group_component";
|
|
|
5
5
|
|
|
6
6
|
const GALLERY = "gallery-qb";
|
|
7
7
|
|
|
8
|
-
const SCREEN_PICKER = "screen-picker-qb-tv";
|
|
8
|
+
export const SCREEN_PICKER = "screen-picker-qb-tv";
|
|
9
9
|
|
|
10
10
|
const HORIZONTAL_LIST = "horizontal_list_qb";
|
|
11
11
|
|
|
@@ -37,3 +37,6 @@ export const isEmptyGroup = (item): boolean =>
|
|
|
37
37
|
export const isGroupInfo = (item): boolean =>
|
|
38
38
|
item?.component_type === GROUP_INFO ||
|
|
39
39
|
item?.component_type === GROUP_INFO_OLD;
|
|
40
|
+
|
|
41
|
+
export const isTabsScreen = (item): boolean =>
|
|
42
|
+
isScreenPicker(item) && item?.tabs_screen;
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { getAllSpecificStyles } from "../manifestKeyParser";
|
|
2
|
+
|
|
3
|
+
// Mock the dependencies
|
|
4
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
|
|
5
|
+
platformSelect: jest.fn((platforms) => platforms.samsung_tv), // Default to samsung for tests
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/stringUtils", () => ({
|
|
9
|
+
toCamelCase: jest.fn((str: string) => {
|
|
10
|
+
// Simple camelCase implementation for testing
|
|
11
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("getAllSpecificStyles", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("Basic functionality", () => {
|
|
21
|
+
it("should throw error if outStyles is not provided", () => {
|
|
22
|
+
expect(() => {
|
|
23
|
+
getAllSpecificStyles({
|
|
24
|
+
componentName: "button",
|
|
25
|
+
subComponentName: "",
|
|
26
|
+
configuration: {},
|
|
27
|
+
outStyles: null as any,
|
|
28
|
+
});
|
|
29
|
+
}).toThrow("outStyles is required");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should throw error if componentName is not provided", () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
getAllSpecificStyles({
|
|
35
|
+
componentName: "",
|
|
36
|
+
subComponentName: "",
|
|
37
|
+
configuration: {},
|
|
38
|
+
outStyles: {},
|
|
39
|
+
});
|
|
40
|
+
}).toThrow("componentName is required");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should initialize default key if not present", () => {
|
|
44
|
+
const outStyles = {};
|
|
45
|
+
|
|
46
|
+
getAllSpecificStyles({
|
|
47
|
+
componentName: "button",
|
|
48
|
+
subComponentName: "",
|
|
49
|
+
configuration: {},
|
|
50
|
+
outStyles,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(outStyles).toHaveProperty("default");
|
|
54
|
+
expect(outStyles["default"]).toEqual({});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle empty configuration", () => {
|
|
58
|
+
const outStyles = { default: {} };
|
|
59
|
+
|
|
60
|
+
getAllSpecificStyles({
|
|
61
|
+
componentName: "button",
|
|
62
|
+
subComponentName: "",
|
|
63
|
+
configuration: {},
|
|
64
|
+
outStyles,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(outStyles).toEqual({ default: {} });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("Style name parsing", () => {
|
|
72
|
+
it("should parse basic style without state or platform", () => {
|
|
73
|
+
const outStyles = {};
|
|
74
|
+
|
|
75
|
+
const configuration = {
|
|
76
|
+
button_style_background_color: "#FF0000",
|
|
77
|
+
button_style_border_width: 2,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
getAllSpecificStyles({
|
|
81
|
+
componentName: "button",
|
|
82
|
+
subComponentName: "",
|
|
83
|
+
configuration,
|
|
84
|
+
outStyles,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(outStyles["default"]).toEqual({
|
|
88
|
+
backgroundColor: "#FF0000",
|
|
89
|
+
borderWidth: 2,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle subComponentName correctly", () => {
|
|
94
|
+
const outStyles = {};
|
|
95
|
+
|
|
96
|
+
const configuration = {
|
|
97
|
+
player_controls_style_button_background_color: "#FF0000",
|
|
98
|
+
player_controls_style_button_text_size: 16,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
getAllSpecificStyles({
|
|
102
|
+
componentName: "player_controls",
|
|
103
|
+
subComponentName: "button",
|
|
104
|
+
configuration,
|
|
105
|
+
outStyles,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(outStyles["default"]).toEqual({
|
|
109
|
+
backgroundColor: "#FF0000",
|
|
110
|
+
textSize: 16,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should ignore keys that do not match the prefix", () => {
|
|
115
|
+
const outStyles = {};
|
|
116
|
+
|
|
117
|
+
const configuration = {
|
|
118
|
+
button_style_background_color: "#FF0000",
|
|
119
|
+
other_component_style_text_color: "#00FF00", // Should be ignored
|
|
120
|
+
random_key: "value", // Should be ignored
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
getAllSpecificStyles({
|
|
124
|
+
componentName: "button",
|
|
125
|
+
subComponentName: "",
|
|
126
|
+
configuration,
|
|
127
|
+
outStyles,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(outStyles["default"]).toEqual({
|
|
131
|
+
backgroundColor: "#FF0000",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("State handling", () => {
|
|
137
|
+
it("should handle pressed state", () => {
|
|
138
|
+
const outStyles = {};
|
|
139
|
+
|
|
140
|
+
const configuration = {
|
|
141
|
+
button_style_background_color: "#FF0000",
|
|
142
|
+
button_style_pressed_background_color: "#00FF00",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
getAllSpecificStyles({
|
|
146
|
+
componentName: "button",
|
|
147
|
+
subComponentName: "",
|
|
148
|
+
configuration,
|
|
149
|
+
outStyles,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(outStyles["default"]).toEqual({
|
|
153
|
+
backgroundColor: "#FF0000",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(outStyles["pressed"]).toEqual({
|
|
157
|
+
backgroundColor: "#00FF00",
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should handle focused state", () => {
|
|
162
|
+
const outStyles = {};
|
|
163
|
+
|
|
164
|
+
const configuration = {
|
|
165
|
+
button_style_focused_border_width: 3,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
getAllSpecificStyles({
|
|
169
|
+
componentName: "button",
|
|
170
|
+
subComponentName: "",
|
|
171
|
+
configuration,
|
|
172
|
+
outStyles,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(outStyles["focused"]).toEqual({
|
|
176
|
+
borderWidth: 3,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle selected state", () => {
|
|
181
|
+
const outStyles = {};
|
|
182
|
+
|
|
183
|
+
const configuration = {
|
|
184
|
+
button_style_selected_opacity: 0.5,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
getAllSpecificStyles({
|
|
188
|
+
componentName: "button",
|
|
189
|
+
subComponentName: "",
|
|
190
|
+
configuration,
|
|
191
|
+
outStyles,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(outStyles["selected"]).toEqual({
|
|
195
|
+
opacity: 0.5,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should handle focused_selected state", () => {
|
|
200
|
+
const outStyles = {};
|
|
201
|
+
|
|
202
|
+
const configuration = {
|
|
203
|
+
button_style_focused_selected_scale: 1.2,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
getAllSpecificStyles({
|
|
207
|
+
componentName: "button",
|
|
208
|
+
subComponentName: "",
|
|
209
|
+
configuration,
|
|
210
|
+
outStyles,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(outStyles["focused_selected"]).toEqual({
|
|
214
|
+
scale: 1.2,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should merge default styles into other states", () => {
|
|
219
|
+
const outStyles = {};
|
|
220
|
+
|
|
221
|
+
const configuration = {
|
|
222
|
+
button_style_background_color: "#FF0000",
|
|
223
|
+
button_style_border_width: 1,
|
|
224
|
+
button_style_opacity: 1,
|
|
225
|
+
button_style_pressed_background_color: "#00FF00",
|
|
226
|
+
button_style_focused_opacity: 0.8,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
getAllSpecificStyles({
|
|
230
|
+
componentName: "button",
|
|
231
|
+
subComponentName: "",
|
|
232
|
+
configuration,
|
|
233
|
+
outStyles,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(outStyles["default"]).toEqual({
|
|
237
|
+
backgroundColor: "#FF0000",
|
|
238
|
+
borderWidth: 1,
|
|
239
|
+
opacity: 1,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Pressed should have default styles merged with its specific override
|
|
243
|
+
expect(outStyles["pressed"]).toEqual({
|
|
244
|
+
backgroundColor: "#00FF00", // Override
|
|
245
|
+
borderWidth: 1, // From default
|
|
246
|
+
opacity: 1, // From default
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Focused should have default styles merged with its specific override
|
|
250
|
+
expect(outStyles["focused"]).toEqual({
|
|
251
|
+
backgroundColor: "#FF0000", // From default
|
|
252
|
+
borderWidth: 1, // From default
|
|
253
|
+
opacity: 0.8, // Override
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should preserve existing state values when merging", () => {
|
|
258
|
+
const outStyles = {
|
|
259
|
+
pressed: { existingKey: "existingValue" },
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const configuration = {
|
|
263
|
+
button_style_background_color: "#FF0000",
|
|
264
|
+
button_style_pressed_text_color: "#FFFFFF",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
getAllSpecificStyles({
|
|
268
|
+
componentName: "button",
|
|
269
|
+
subComponentName: "",
|
|
270
|
+
configuration,
|
|
271
|
+
outStyles,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(outStyles["pressed"]).toEqual({
|
|
275
|
+
existingKey: "existingValue", // Should be preserved
|
|
276
|
+
backgroundColor: "#FF0000", // From default
|
|
277
|
+
textColor: "#FFFFFF", // New pressed style
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("Platform handling", () => {
|
|
283
|
+
it("should include styles for current platform (samsung)", () => {
|
|
284
|
+
const outStyles = {};
|
|
285
|
+
|
|
286
|
+
const configuration = {
|
|
287
|
+
button_style_samsung_background_color: "#FF0000",
|
|
288
|
+
button_style_background_color: "#00FF00",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
getAllSpecificStyles({
|
|
292
|
+
componentName: "button",
|
|
293
|
+
subComponentName: "",
|
|
294
|
+
configuration,
|
|
295
|
+
outStyles,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(outStyles["default"]).toEqual({
|
|
299
|
+
backgroundColor: "#FF0000", // Samsung-specific should be included
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should skip styles for other platforms", () => {
|
|
304
|
+
const outStyles = {};
|
|
305
|
+
|
|
306
|
+
const configuration = {
|
|
307
|
+
button_style_ios_background_color: "#FF0000",
|
|
308
|
+
button_style_android_text_color: "#00FF00",
|
|
309
|
+
button_style_tvos_border_width: 2,
|
|
310
|
+
button_style_lg_opacity: 0.5,
|
|
311
|
+
button_style_background_color: "#FFFFFF",
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
getAllSpecificStyles({
|
|
315
|
+
componentName: "button",
|
|
316
|
+
subComponentName: "",
|
|
317
|
+
configuration,
|
|
318
|
+
outStyles,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(outStyles["default"]).toEqual({
|
|
322
|
+
backgroundColor: "#FFFFFF", // Only non-platform specific
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should handle platform in middle of style name", () => {
|
|
327
|
+
const outStyles = {};
|
|
328
|
+
|
|
329
|
+
const configuration = {
|
|
330
|
+
button_style_text_samsung_color: "#FF0000",
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
getAllSpecificStyles({
|
|
334
|
+
componentName: "button",
|
|
335
|
+
subComponentName: "text",
|
|
336
|
+
configuration,
|
|
337
|
+
outStyles,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(outStyles["default"]).toEqual({
|
|
341
|
+
color: "#FF0000",
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should handle platform-specific styles with states", () => {
|
|
346
|
+
const outStyles = {};
|
|
347
|
+
|
|
348
|
+
const configuration = {
|
|
349
|
+
button_style_samsung_pressed_background_color: "#FF0000",
|
|
350
|
+
button_style_ios_pressed_background_color: "#00FF00", // Should be ignored
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
getAllSpecificStyles({
|
|
354
|
+
componentName: "button",
|
|
355
|
+
subComponentName: "",
|
|
356
|
+
configuration,
|
|
357
|
+
outStyles,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(outStyles["pressed"]).toEqual({
|
|
361
|
+
backgroundColor: "#FF0000", // Only samsung style
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should work correctly when platform is Samsung", () => {
|
|
366
|
+
const outStyles = {};
|
|
367
|
+
|
|
368
|
+
const configuration = {
|
|
369
|
+
button_style_ios_background_color: "#FF0000",
|
|
370
|
+
button_style_samsung_background_color: "#00FF00",
|
|
371
|
+
button_style_background_color: "#FFFFFF",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
getAllSpecificStyles({
|
|
375
|
+
componentName: "button",
|
|
376
|
+
subComponentName: "",
|
|
377
|
+
configuration,
|
|
378
|
+
outStyles,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(outStyles["default"]).toEqual({
|
|
382
|
+
backgroundColor: "#00FF00", // Samsung-specific should win
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("Complex scenarios", () => {
|
|
388
|
+
it("should handle multiple states and platforms correctly", () => {
|
|
389
|
+
const outStyles = {};
|
|
390
|
+
|
|
391
|
+
const configuration = {
|
|
392
|
+
// Default styles
|
|
393
|
+
button_style_background_color: "#FFFFFF",
|
|
394
|
+
button_style_text_color: "#000000",
|
|
395
|
+
button_style_border_width: 1,
|
|
396
|
+
|
|
397
|
+
// Platform-specific default
|
|
398
|
+
button_style_samsung_padding: 10,
|
|
399
|
+
|
|
400
|
+
// State-specific
|
|
401
|
+
button_style_pressed_background_color: "#EEEEEE",
|
|
402
|
+
button_style_focused_scale: 1.1,
|
|
403
|
+
|
|
404
|
+
// Platform + state specific
|
|
405
|
+
button_style_samsung_pressed_opacity: 0.8,
|
|
406
|
+
button_style_ios_pressed_opacity: 0.6, // Should be ignored
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
getAllSpecificStyles({
|
|
410
|
+
componentName: "button",
|
|
411
|
+
subComponentName: "",
|
|
412
|
+
configuration,
|
|
413
|
+
outStyles,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(outStyles["default"]).toEqual({
|
|
417
|
+
backgroundColor: "#FFFFFF",
|
|
418
|
+
textColor: "#000000",
|
|
419
|
+
borderWidth: 1,
|
|
420
|
+
padding: 10,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(outStyles["pressed"]).toEqual({
|
|
424
|
+
backgroundColor: "#EEEEEE",
|
|
425
|
+
textColor: "#000000",
|
|
426
|
+
borderWidth: 1,
|
|
427
|
+
padding: 10,
|
|
428
|
+
opacity: 0.8,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(outStyles["focused"]).toEqual({
|
|
432
|
+
backgroundColor: "#FFFFFF",
|
|
433
|
+
textColor: "#000000",
|
|
434
|
+
borderWidth: 1,
|
|
435
|
+
padding: 10,
|
|
436
|
+
scale: 1.1,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("should handle edge case keys correctly", () => {
|
|
441
|
+
const outStyles = {};
|
|
442
|
+
|
|
443
|
+
const configuration = {
|
|
444
|
+
button_style_: "shouldBeIgnored", // Empty style name
|
|
445
|
+
button_style_samsung_: "shouldBeIgnored", // Platform but no style
|
|
446
|
+
button_style__pressed: "shouldBeIgnored", // No style name before state
|
|
447
|
+
button_style_valid_key: "included",
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
getAllSpecificStyles({
|
|
451
|
+
componentName: "button",
|
|
452
|
+
subComponentName: "",
|
|
453
|
+
configuration,
|
|
454
|
+
outStyles,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(outStyles["default"]).toEqual({
|
|
458
|
+
validKey: "included",
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("should handle all state suffixes in priority order", () => {
|
|
463
|
+
const outStyles = {};
|
|
464
|
+
|
|
465
|
+
const configuration = {
|
|
466
|
+
// Test that focused_selected is recognized before focused
|
|
467
|
+
button_style_focused_selected_test: "focused_selected_value",
|
|
468
|
+
button_style_focused_another: "focused_value",
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
getAllSpecificStyles({
|
|
472
|
+
componentName: "button",
|
|
473
|
+
subComponentName: "",
|
|
474
|
+
configuration,
|
|
475
|
+
outStyles,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(outStyles["focused_selected"]).toEqual({
|
|
479
|
+
test: "focused_selected_value",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(outStyles["focused"]).toEqual({
|
|
483
|
+
another: "focused_value",
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe("Immutability", () => {
|
|
489
|
+
it("should not mutate configuration object", () => {
|
|
490
|
+
const configuration = Object.freeze({
|
|
491
|
+
button_style_background_color: "#FF0000",
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const outStyles = {};
|
|
495
|
+
|
|
496
|
+
expect(() => {
|
|
497
|
+
getAllSpecificStyles({
|
|
498
|
+
componentName: "button",
|
|
499
|
+
subComponentName: "",
|
|
500
|
+
configuration,
|
|
501
|
+
outStyles,
|
|
502
|
+
});
|
|
503
|
+
}).not.toThrow();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("platform pressed overrides generic pressed for same key", () => {
|
|
507
|
+
const outStyles = {};
|
|
508
|
+
|
|
509
|
+
const configuration = {
|
|
510
|
+
button_style_pressed_background_color: "#111111", // generic
|
|
511
|
+
button_style_samsung_pressed_background_color: "#222222", // platform
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
getAllSpecificStyles({
|
|
515
|
+
componentName: "button",
|
|
516
|
+
subComponentName: "",
|
|
517
|
+
configuration,
|
|
518
|
+
outStyles,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
expect(outStyles["pressed"]["backgroundColor"]).toBe("#222222");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("should handle frozen outStyles properties", () => {
|
|
525
|
+
const outStyles = {
|
|
526
|
+
default: Object.freeze({ existingProp: "value" }),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const configuration = {
|
|
530
|
+
button_style_background_color: "#FF0000",
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
getAllSpecificStyles({
|
|
534
|
+
componentName: "button",
|
|
535
|
+
subComponentName: "",
|
|
536
|
+
configuration,
|
|
537
|
+
outStyles,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Should create new object instead of mutating frozen one
|
|
541
|
+
expect(outStyles["default"]).toEqual({
|
|
542
|
+
existingProp: "value",
|
|
543
|
+
backgroundColor: "#FF0000",
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|
|
@@ -11,8 +11,11 @@ const currentPlatform = platformSelect({
|
|
|
11
11
|
android_tv: "android",
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Do not change the order, focused_selected should be first
|
|
15
|
+
const states = ["focused_selected", "pressed", "focused", "selected"];
|
|
16
|
+
|
|
15
17
|
const platformSuffixes = ["ios", "tvos", "samsung", "lg", "android"];
|
|
18
|
+
type StateKey = (typeof states)[number] | "default";
|
|
16
19
|
|
|
17
20
|
export type GetAllSpecificStylesProps = {
|
|
18
21
|
componentName: string;
|
|
@@ -33,39 +36,47 @@ export const getAllSpecificStyles = ({
|
|
|
33
36
|
subComponentName,
|
|
34
37
|
outStyles,
|
|
35
38
|
}: GetAllSpecificStylesProps) => {
|
|
36
|
-
if (!outStyles)
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
+
if (!outStyles) throw new Error("outStyles is required");
|
|
40
|
+
if (!componentName) throw new Error("componentName is required");
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const prefix =
|
|
43
|
+
subComponentName?.length > 0
|
|
44
|
+
? `${componentName}_style_${subComponentName}_`
|
|
45
|
+
: `${componentName}_style_`;
|
|
42
46
|
|
|
43
47
|
const defaultKey = "default";
|
|
44
48
|
|
|
45
|
-
if (!outStyles[defaultKey]) {
|
|
46
|
-
|
|
47
|
-
}
|
|
49
|
+
if (!outStyles[defaultKey]) outStyles[defaultKey] = {};
|
|
50
|
+
|
|
51
|
+
const tmpGenericDict: Record<StateKey, Record<string, any>> = {};
|
|
52
|
+
const tmpPlatformDict: Record<StateKey, Record<string, any>> = {};
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
const parseKey = (key: string, value: any) => {
|
|
50
55
|
if (!key.startsWith(prefix)) {
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
let styleName = key.slice(prefix.length);
|
|
60
|
+
if (!styleName || styleName.startsWith("_")) return;
|
|
55
61
|
|
|
56
|
-
const platform = platformSuffixes.find((
|
|
57
|
-
styleName.startsWith(
|
|
62
|
+
const platform = platformSuffixes.find((prefix) =>
|
|
63
|
+
styleName.startsWith(prefix)
|
|
58
64
|
);
|
|
59
65
|
|
|
66
|
+
let tmpDict = tmpGenericDict;
|
|
67
|
+
|
|
60
68
|
if (platform) {
|
|
61
69
|
if (currentPlatform !== platform) {
|
|
62
|
-
return;
|
|
70
|
+
return;
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
tmpDict = tmpPlatformDict;
|
|
65
74
|
styleName = styleName.slice(platform.length + 1);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
let state =
|
|
77
|
+
let state = states.find((prefix) => styleName.startsWith(prefix)) as
|
|
78
|
+
| StateKey
|
|
79
|
+
| undefined;
|
|
69
80
|
|
|
70
81
|
if (state) {
|
|
71
82
|
styleName = styleName.slice(state.length + 1);
|
|
@@ -73,30 +84,44 @@ export const getAllSpecificStyles = ({
|
|
|
73
84
|
state = defaultKey;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
if (!styleName) return;
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
const camelCaseKey = toCamelCase(styleName);
|
|
79
90
|
|
|
80
|
-
if (!
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
91
|
+
if (!tmpDict[state]) tmpDict[state] = {};
|
|
92
|
+
tmpDict[state][camelCaseKey] = value;
|
|
93
|
+
};
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
for (const [key, value] of Object.entries(configuration)) {
|
|
96
|
+
parseKey(key, value);
|
|
97
|
+
}
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
const allStates = Array.from(
|
|
100
|
+
new Set<StateKey>([
|
|
101
|
+
defaultKey,
|
|
102
|
+
...(Object.keys(tmpGenericDict) as StateKey[]),
|
|
103
|
+
...(Object.keys(tmpPlatformDict) as StateKey[]),
|
|
104
|
+
...(Object.keys(outStyles) as StateKey[]),
|
|
105
|
+
])
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
for (const state of allStates) {
|
|
109
|
+
outStyles[state] = Object.assign(
|
|
110
|
+
{},
|
|
111
|
+
outStyles[state] || {},
|
|
112
|
+
tmpGenericDict[state] || {},
|
|
113
|
+
tmpPlatformDict[state] || {}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
91
116
|
|
|
92
117
|
// Merge default styles with state specific styles
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (key === defaultKey) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
118
|
+
for (const state of Object.keys(outStyles)) {
|
|
119
|
+
if (state === defaultKey) continue;
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
outStyles[state] = Object.assign(
|
|
122
|
+
{},
|
|
123
|
+
outStyles[defaultKey],
|
|
124
|
+
outStyles[state]
|
|
125
|
+
);
|
|
101
126
|
}
|
|
102
127
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "14.0.0-alpha.
|
|
3
|
+
"version": "14.0.0-alpha.1322105203",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "14.0.0-alpha.
|
|
30
|
+
"@applicaster/applicaster-types": "14.0.0-alpha.1322105203",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
package/playerUtils/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isFilledArray } from "@applicaster/zapp-react-native-utils/arrayUtils";
|
|
|
5
5
|
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
6
6
|
|
|
7
7
|
import { getBoolFromConfigValue } from "../configurationUtils";
|
|
8
|
+
import { Dimensions } from "react-native";
|
|
8
9
|
|
|
9
10
|
export { getPlayerActionButtons } from "./getPlayerActionButtons";
|
|
10
11
|
|
|
@@ -97,3 +98,53 @@ export const isAudioItem = (item: Option<ZappEntry>) => {
|
|
|
97
98
|
export const isInlineTV = (screenData) => {
|
|
98
99
|
return isTV() && isFilledArray(screenData?.ui_components);
|
|
99
100
|
};
|
|
101
|
+
|
|
102
|
+
const isPercentage = (value: string | number): boolean => {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
return value.includes("%");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getPercentageOf = (percent: string, value: number) => {
|
|
111
|
+
const percentageValue = parseFloat(percent.replace("%", ""));
|
|
112
|
+
|
|
113
|
+
if (isNaN(percentageValue)) {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (value * percentageValue) / 100;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type DimensionsT = {
|
|
121
|
+
width: number | string;
|
|
122
|
+
height: number | string | undefined;
|
|
123
|
+
aspectRatio?: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const getTabletWidth = (
|
|
127
|
+
tablet_landscape_sidebar_width,
|
|
128
|
+
dimensions: DimensionsT
|
|
129
|
+
) => {
|
|
130
|
+
const { width: SCREEN_WIDTH } = Dimensions.get("screen");
|
|
131
|
+
|
|
132
|
+
const { width } = dimensions;
|
|
133
|
+
let widthValue = Number(width);
|
|
134
|
+
|
|
135
|
+
if (isPercentage(width)) {
|
|
136
|
+
widthValue = getPercentageOf(width.toString(), SCREEN_WIDTH);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sidebarWidth = Number(tablet_landscape_sidebar_width?.replace("%", ""));
|
|
140
|
+
|
|
141
|
+
if (tablet_landscape_sidebar_width?.includes("%")) {
|
|
142
|
+
return widthValue * (1 - sidebarWidth / 100);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Number.isNaN(sidebarWidth)) {
|
|
146
|
+
return widthValue * 0.65;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return widthValue - sidebarWidth;
|
|
150
|
+
};
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { NativeModules, Platform } from "react-native";
|
|
2
2
|
|
|
3
|
+
export const isAndroidTablet = () => {
|
|
4
|
+
const { initialProps } = NativeModules.QuickBrickCommunicationModule;
|
|
5
|
+
|
|
6
|
+
return initialProps?.is_tablet;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Determines wether the given device is a tablet based on dimensions, orientation, and platform
|
|
5
11
|
* @param {Object} dimensions - Dimensions object passed to the function
|
|
6
12
|
* @param {String} orientation - Orientation enum passed to the function
|
|
7
13
|
* @returns {Boolean} isTablet - returns whether the given device is a tablet
|
|
8
14
|
*/
|
|
9
|
-
export const isTablet = (
|
|
15
|
+
export const isTablet = (
|
|
16
|
+
dimensions?: { width: number; height: number },
|
|
17
|
+
orientation?: string
|
|
18
|
+
) => {
|
|
10
19
|
if (Platform?.OS === "ios") {
|
|
11
20
|
return Platform?.isPad;
|
|
12
21
|
} else if (Platform?.OS === "android") {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return initialProps?.is_tablet;
|
|
22
|
+
return isAndroidTablet();
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
const { width } = dimensions;
|
|
25
|
+
const { width } = dimensions || {};
|
|
19
26
|
|
|
20
27
|
if (width < 600) return false;
|
|
21
28
|
if (width >= 600 && width < 840) return orientation === "portrait";
|
|
@@ -14,7 +14,7 @@ import { HOOKS_EVENTS } from "../../appUtils/HooksManager/constants";
|
|
|
14
14
|
import { getRiverFromRoute, getTargetRoute } from "../../navigationUtils";
|
|
15
15
|
import { useConnectionInfo } from "../connection";
|
|
16
16
|
|
|
17
|
-
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
17
|
+
import { isTV, isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
18
18
|
import { useNavbarState } from "../screen";
|
|
19
19
|
|
|
20
20
|
export { useNavigation } from "./useNavigation";
|
|
@@ -127,10 +127,14 @@ export function isNavBarVisible(
|
|
|
127
127
|
|
|
128
128
|
export const useBackHandler = (cb: () => boolean) => {
|
|
129
129
|
useEffect(() => {
|
|
130
|
-
|
|
130
|
+
if (!isWeb()) {
|
|
131
|
+
BackHandler.addEventListener("hardwareBackPress", cb);
|
|
132
|
+
}
|
|
131
133
|
|
|
132
134
|
return () => {
|
|
133
|
-
|
|
135
|
+
if (!isWeb()) {
|
|
136
|
+
BackHandler.removeEventListener("hardwareBackPress", cb);
|
|
137
|
+
}
|
|
134
138
|
};
|
|
135
139
|
}, [cb]);
|
|
136
140
|
};
|
|
@@ -4,8 +4,5 @@ import { useMemo } from "react";
|
|
|
4
4
|
export const useScreenStateStore = () => {
|
|
5
5
|
const route = useRoute(false);
|
|
6
6
|
|
|
7
|
-
return useMemo(
|
|
8
|
-
() => route.screenData["screenStateStore"],
|
|
9
|
-
[route.screenData["screenStateStore"]]
|
|
10
|
-
);
|
|
7
|
+
return useMemo(() => route.screenData["screenStateStore"], [route.pathname]);
|
|
11
8
|
};
|