@applicaster/zapp-react-native-utils 15.0.0-alpha.8621453569 → 15.0.0-alpha.9300744523
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/actionsExecutor/ActionExecutorContext.tsx +3 -6
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/adsUtils/index.ts +2 -2
- package/analyticsUtils/README.md +1 -1
- package/appUtils/HooksManager/index.ts +10 -10
- package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
- package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
- package/appUtils/accessibilityManager/const.ts +4 -0
- package/appUtils/accessibilityManager/hooks.ts +8 -24
- package/appUtils/accessibilityManager/utils.ts +25 -5
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -0
- package/appUtils/focusManager/index.ios.ts +43 -4
- package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
- package/appUtils/focusManagerAux/utils/index.ts +1 -1
- package/appUtils/focusManagerAux/utils/utils.ios.ts +199 -3
- package/appUtils/keyCodes/keys/keys.web.ts +1 -4
- package/appUtils/orientationHelper.ts +2 -4
- package/appUtils/platform/platformUtils.ts +51 -35
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
- package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
- package/appUtils/playerManager/player.ts +4 -0
- package/appUtils/playerManager/playerNative.ts +29 -16
- package/appUtils/playerManager/usePlayerState.tsx +14 -2
- package/cellUtils/index.ts +1 -8
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
- package/focusManager/aux/index.ts +1 -1
- package/manifestUtils/defaultManifestConfigurations/player.js +96 -11
- package/manifestUtils/keys.js +21 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
- package/manifestUtils/tvAction/container/index.js +1 -1
- package/package.json +4 -4
- package/playerUtils/usePlayerTTS.ts +8 -3
- package/pluginUtils/index.ts +4 -0
- package/reactHooks/advertising/index.ts +2 -2
- package/reactHooks/debugging/__tests__/index.test.js +4 -4
- package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
- package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +3 -0
- package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
- package/reactHooks/feed/useEntryScreenId.ts +2 -2
- package/reactHooks/feed/useInflatedUrl.ts +43 -17
- package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
- package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
- package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
- package/reactHooks/navigation/index.ts +11 -6
- package/reactHooks/navigation/useRoute.ts +8 -6
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
- package/reactHooks/resolvers/useCellResolver.ts +6 -2
- package/reactHooks/resolvers/useComponentResolver.ts +8 -2
- package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +1 -1
- package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +11 -3
- package/reactHooks/screen/useTargetScreenData.ts +4 -2
- package/reactHooks/state/useRivers.ts +1 -1
- package/reactHooks/usePluginConfiguration.ts +2 -2
- package/searchUtils/const.ts +7 -0
- package/searchUtils/index.ts +3 -0
- package/stringUtils/index.ts +1 -1
- package/testUtils/index.tsx +30 -21
- package/utils/__tests__/mergeRight.test.ts +48 -0
- package/utils/__tests__/selectors.test.ts +124 -0
- package/utils/index.ts +13 -0
- package/utils/mergeRight.ts +5 -0
- package/utils/path.ts +6 -3
- package/utils/pathOr.ts +5 -1
- package/utils/selectors.ts +46 -0
- package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +34 -11
- package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
|
@@ -1,20 +1,40 @@
|
|
|
1
|
+
import { createLogger } from "../../logger";
|
|
2
|
+
|
|
3
|
+
const { log_error } = createLogger({
|
|
4
|
+
category: "AccessibilityManager",
|
|
5
|
+
subsystem: "AppUtils",
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Calculates the reading time for a given text based on word count
|
|
3
|
-
* @param text - The text to calculate the reading time for
|
|
4
|
-
* @param wordsPerMinute - Words per minute reading speed (default:
|
|
10
|
+
* @param text - The text to calculate the reading time for (string or number)
|
|
11
|
+
* @param wordsPerMinute - Words per minute reading speed (default: 140)
|
|
5
12
|
* @param minimumPause - Minimum pause time in milliseconds (default: 500)
|
|
6
13
|
* @param announcementDelay - Additional delay for announcement in milliseconds (default: 700)
|
|
7
14
|
* @returns The reading time in milliseconds
|
|
8
15
|
*/
|
|
9
16
|
export function calculateReadingTime(
|
|
10
|
-
text: string,
|
|
17
|
+
text: string | number,
|
|
11
18
|
wordsPerMinute: number = 140,
|
|
12
19
|
minimumPause: number = 500,
|
|
13
20
|
announcementDelay: number = 700
|
|
14
21
|
): number {
|
|
15
|
-
|
|
22
|
+
if (typeof text !== "string" && typeof text !== "number") {
|
|
23
|
+
log_error(
|
|
24
|
+
`Invalid text input for reading time calculation got: ${
|
|
25
|
+
typeof text === "symbol" ? String(text) : text
|
|
26
|
+
}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = typeof text === "number" ? String(text) : text.trim();
|
|
33
|
+
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
16
37
|
|
|
17
|
-
// Count words (split on whitespace and punctuation, keep alnum boundaries)
|
|
18
38
|
const words = trimmed
|
|
19
39
|
.split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
|
|
20
40
|
.filter(Boolean).length;
|
|
@@ -71,6 +71,9 @@ exports[`focusManagerIOS should be defined 1`] = `
|
|
|
71
71
|
"invokeHandler": [Function],
|
|
72
72
|
"isChildOf": [Function],
|
|
73
73
|
"isFocusOn": [Function],
|
|
74
|
+
"isFocusOnContent": [Function],
|
|
75
|
+
"isFocusOnMenu": [Function],
|
|
76
|
+
"isFocusOnTabsScreenContent": [Function],
|
|
74
77
|
"isGroupItemFocused": [Function],
|
|
75
78
|
"moveFocus": [Function],
|
|
76
79
|
"on": [Function],
|
|
@@ -4,7 +4,11 @@ import * as R from "ramda";
|
|
|
4
4
|
import {
|
|
5
5
|
isCurrentFocusOn,
|
|
6
6
|
isChildOf as isChildOfUtils,
|
|
7
|
-
|
|
7
|
+
isPartOfMenu,
|
|
8
|
+
isPartOfContent,
|
|
9
|
+
isPartOfTabsScreenContent,
|
|
10
|
+
} from "../focusManagerAux/utils/index.ios";
|
|
11
|
+
|
|
8
12
|
import { Tree } from "./treeDataStructure/Tree";
|
|
9
13
|
import { findFocusableNode } from "./treeDataStructure/Utils";
|
|
10
14
|
import { subscriber } from "../../functionUtils";
|
|
@@ -188,9 +192,15 @@ export const focusManager = (function () {
|
|
|
188
192
|
function register({ id, component }) {
|
|
189
193
|
const { isGroup = false } = component;
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
if (isGroup) {
|
|
196
|
+
registerGroup(id, component);
|
|
197
|
+
} else {
|
|
198
|
+
registerItem(id, component);
|
|
199
|
+
}
|
|
192
200
|
|
|
193
|
-
|
|
201
|
+
const groupId = component?.props?.groupId;
|
|
202
|
+
|
|
203
|
+
emitRegistered({ id, groupId, isGroup });
|
|
194
204
|
}
|
|
195
205
|
|
|
196
206
|
function unregister(id, { group = false } = {}) {
|
|
@@ -273,7 +283,9 @@ export const focusManager = (function () {
|
|
|
273
283
|
function setFocus(
|
|
274
284
|
id: string,
|
|
275
285
|
direction?: FocusManager.IOS.Direction,
|
|
276
|
-
options?: Partial<{
|
|
286
|
+
options?: Partial<{
|
|
287
|
+
groupFocusedChanged: boolean;
|
|
288
|
+
}>,
|
|
277
289
|
callback?: any
|
|
278
290
|
) {
|
|
279
291
|
blur(direction);
|
|
@@ -412,6 +424,30 @@ export const focusManager = (function () {
|
|
|
412
424
|
return id && isCurrentFocusOn(id, currentFocusNode);
|
|
413
425
|
}
|
|
414
426
|
|
|
427
|
+
function isFocusOnMenu(): boolean {
|
|
428
|
+
const currentFocusable = getCurrentFocus();
|
|
429
|
+
|
|
430
|
+
return isPartOfMenu(focusableTree, currentFocusable?.props?.id);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isFocusOnContent(): boolean {
|
|
434
|
+
const currentFocusable = getCurrentFocus();
|
|
435
|
+
|
|
436
|
+
return isPartOfContent(focusableTree, currentFocusable?.props?.id);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isFocusOnTabsScreenContent(
|
|
440
|
+
screenPickerContentContainerId: string
|
|
441
|
+
): boolean {
|
|
442
|
+
const currentFocusable = getCurrentFocus();
|
|
443
|
+
|
|
444
|
+
return isPartOfTabsScreenContent(
|
|
445
|
+
focusableTree,
|
|
446
|
+
screenPickerContentContainerId,
|
|
447
|
+
currentFocusable?.props?.id
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
415
451
|
function isChildOf(childId, parentId): boolean {
|
|
416
452
|
return isChildOfUtils(focusableTree, childId, parentId);
|
|
417
453
|
}
|
|
@@ -438,6 +474,9 @@ export const focusManager = (function () {
|
|
|
438
474
|
isGroupItemFocused,
|
|
439
475
|
getPreferredFocusChild,
|
|
440
476
|
isFocusOn,
|
|
477
|
+
isFocusOnMenu,
|
|
478
|
+
isFocusOnContent,
|
|
479
|
+
isFocusOnTabsScreenContent,
|
|
441
480
|
isChildOf,
|
|
442
481
|
};
|
|
443
482
|
})();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isNil, startsWith } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
QUICK_BRICK_CONTENT,
|
|
5
|
+
QUICK_BRICK_NAVBAR,
|
|
6
|
+
} from "@applicaster/quick-brick-core/const";
|
|
7
|
+
|
|
8
|
+
const isNavBar = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
|
|
9
|
+
const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
|
|
10
|
+
const isRoot = (node) => node?.id === "root";
|
|
11
|
+
|
|
12
|
+
export const isPartOfTabsScreenContent = (
|
|
13
|
+
focusableTree,
|
|
14
|
+
screenPickerContentContainerId,
|
|
15
|
+
id
|
|
16
|
+
) => {
|
|
17
|
+
const node = focusableTree.findInTree(id);
|
|
18
|
+
|
|
19
|
+
if (isNil(node)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isRoot(node)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isNavBar(node)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isContent(node)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (node?.id === screenPickerContentContainerId) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return isPartOfTabsScreenContent(
|
|
40
|
+
focusableTree,
|
|
41
|
+
screenPickerContentContainerId,
|
|
42
|
+
node.parent?.id
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isPartOfMenu = (focusableTree, id): boolean => {
|
|
47
|
+
const node = focusableTree.findInTree(id);
|
|
48
|
+
|
|
49
|
+
if (isNil(node)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isRoot(node)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isNavBar(node)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isContent(node)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return isPartOfMenu(focusableTree, node.parent?.id);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const isPartOfContent = (focusableTree, id) => {
|
|
69
|
+
const node = focusableTree.findInTree(id);
|
|
70
|
+
|
|
71
|
+
if (isNil(node)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isRoot(node)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isNavBar(node)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isContent(node)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return isPartOfContent(focusableTree, node.parent?.id);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const isCurrentFocusOn = (id, node) => {
|
|
91
|
+
if (!node) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isRoot(node)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (node?.id === id) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return isCurrentFocusOn(id, node.parent);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const isChildOf = (focusableTree, childId, parentId) => {
|
|
107
|
+
if (isNil(childId) || isNil(parentId)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const childNode = focusableTree.findInTree(childId);
|
|
112
|
+
|
|
113
|
+
if (isNil(childNode)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (childNode.parent?.id === parentId) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return isChildOf(focusableTree, childNode.parent?.id, parentId);
|
|
122
|
+
};
|
|
@@ -102,7 +102,7 @@ export const getNavbarNode = (focusableTree) => {
|
|
|
102
102
|
|
|
103
103
|
export const waitForContent = (focusableTree) => {
|
|
104
104
|
const contentHasAnyChildren = (): boolean => {
|
|
105
|
-
const countOfChildren = pathOr(
|
|
105
|
+
const countOfChildren = pathOr<number>(
|
|
106
106
|
0,
|
|
107
107
|
["children", "length"],
|
|
108
108
|
getContentNode(focusableTree)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ReplaySubject } from "rxjs";
|
|
2
|
-
import { filter } from "rxjs/operators";
|
|
1
|
+
import { ReplaySubject, Subject } from "rxjs";
|
|
2
|
+
import { filter, switchMap, take, withLatestFrom, map } from "rxjs/operators";
|
|
3
|
+
|
|
3
4
|
import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
|
|
4
5
|
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
|
|
6
|
+
import { isPartOfMenu, isPartOfContent } from "./index.ios";
|
|
5
7
|
|
|
6
8
|
type FocusableID = string;
|
|
7
9
|
type RegistrationEvent = {
|
|
@@ -9,6 +11,25 @@ type RegistrationEvent = {
|
|
|
9
11
|
registered: boolean;
|
|
10
12
|
};
|
|
11
13
|
|
|
14
|
+
let focusableViewRegistrationSubject$ = new Subject<{
|
|
15
|
+
id: FocusableID;
|
|
16
|
+
groupId: FocusableID;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
let focusableGroupRegistrationSubject$ = new ReplaySubject<{
|
|
20
|
+
id: FocusableID;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
let focusableNativeViewRegistrationSubject$ = new Subject<{
|
|
24
|
+
id: FocusableID;
|
|
25
|
+
groupId: FocusableID;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
let focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
|
|
29
|
+
id: FocusableID;
|
|
30
|
+
groupId: FocusableID;
|
|
31
|
+
}>();
|
|
32
|
+
|
|
12
33
|
const isFocusableButton = (id: Option<FocusableID>): boolean =>
|
|
13
34
|
id && id.includes?.(BUTTON_PREFIX);
|
|
14
35
|
|
|
@@ -22,14 +43,189 @@ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
|
|
|
22
43
|
)
|
|
23
44
|
);
|
|
24
45
|
|
|
25
|
-
export const
|
|
46
|
+
export const resetFocusableRegistration = () => {
|
|
47
|
+
// complete the old subject so subscribers are notified and resources are freed
|
|
48
|
+
if (!focusableViewRegistrationSubject$.closed) {
|
|
49
|
+
focusableViewRegistrationSubject$.complete();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!focusableGroupRegistrationSubject$.closed) {
|
|
53
|
+
focusableGroupRegistrationSubject$.complete();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!focusableNativeViewRegistrationSubject$.closed) {
|
|
57
|
+
focusableNativeViewRegistrationSubject$.complete();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!focusableNativeGroupRegistrationSubject$.closed) {
|
|
61
|
+
focusableNativeGroupRegistrationSubject$.complete();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
focusableViewRegistrationSubject$ = new Subject<{
|
|
65
|
+
id: FocusableID;
|
|
66
|
+
groupId: FocusableID;
|
|
67
|
+
}>();
|
|
68
|
+
|
|
69
|
+
focusableGroupRegistrationSubject$ = new ReplaySubject<{
|
|
70
|
+
id: FocusableID;
|
|
71
|
+
}>();
|
|
72
|
+
|
|
73
|
+
focusableNativeViewRegistrationSubject$ = new Subject<{
|
|
74
|
+
id: FocusableID;
|
|
75
|
+
groupId: FocusableID;
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
focusableNativeGroupRegistrationSubject$ = new ReplaySubject<{
|
|
79
|
+
id: FocusableID;
|
|
80
|
+
groupId: FocusableID;
|
|
81
|
+
}>();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const focusableNativeViewRegistration = ({ focusableView, focusableGroup }) => {
|
|
85
|
+
return focusableNativeViewRegistrationSubject$.pipe(
|
|
86
|
+
filter(
|
|
87
|
+
(focusableNativeView) => focusableNativeView.id === focusableView.id
|
|
88
|
+
),
|
|
89
|
+
take(1),
|
|
90
|
+
switchMap((focusableNativeView) =>
|
|
91
|
+
// start waiting registration of its parent FocusableNativeGroup
|
|
92
|
+
focusableNativeGroupRegistrationSubject$.pipe(
|
|
93
|
+
filter(
|
|
94
|
+
(focusableNativeGroup) =>
|
|
95
|
+
focusableNativeGroup.id === focusableNativeView.groupId
|
|
96
|
+
),
|
|
97
|
+
take(1),
|
|
98
|
+
map((focusableNativeGroup) => ({
|
|
99
|
+
focusableNativeGroup,
|
|
100
|
+
focusableNativeView,
|
|
101
|
+
focusableView,
|
|
102
|
+
focusableGroup,
|
|
103
|
+
}))
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const firstFocusableViewRegistrationFactory = () =>
|
|
110
|
+
focusableViewRegistrationSubject$.pipe(
|
|
111
|
+
take(1), // we care about only first FocusableView registration
|
|
112
|
+
switchMap((focusableView) =>
|
|
113
|
+
// start waiting registration of its parent FocusableGroup
|
|
114
|
+
focusableGroupRegistrationSubject$.pipe(
|
|
115
|
+
filter((focusableGroup) => focusableGroup.id === focusableView.groupId),
|
|
116
|
+
take(1),
|
|
117
|
+
map((focusableGroup) => ({
|
|
118
|
+
focusableView,
|
|
119
|
+
focusableGroup,
|
|
120
|
+
}))
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
// start waiting registration for FocusableNativeView and its parent FocusableNativeGroup
|
|
124
|
+
switchMap(({ focusableView, focusableGroup }) =>
|
|
125
|
+
focusableNativeViewRegistration({
|
|
126
|
+
focusableView,
|
|
127
|
+
focusableGroup,
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// registration on RN level(into RN focusManager)
|
|
133
|
+
export const emitRegistered = ({
|
|
134
|
+
id,
|
|
135
|
+
groupId,
|
|
136
|
+
isGroup,
|
|
137
|
+
}: {
|
|
138
|
+
id: Option<FocusableID>;
|
|
139
|
+
groupId: Option<FocusableID>;
|
|
140
|
+
isGroup: boolean;
|
|
141
|
+
}): void => {
|
|
26
142
|
if (isFocusableButton(id)) {
|
|
27
143
|
registeredSubject$.next({ id, registered: true });
|
|
28
144
|
}
|
|
145
|
+
|
|
146
|
+
if (isGroup && id) {
|
|
147
|
+
focusableGroupRegistrationSubject$.next({ id });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isGroup && id && groupId) {
|
|
151
|
+
focusableViewRegistrationSubject$.next({ id, groupId });
|
|
152
|
+
}
|
|
29
153
|
};
|
|
30
154
|
|
|
155
|
+
// unregistration on RN level(into RN focusManager)
|
|
31
156
|
export const emitUnregistered = (id: Option<FocusableID>): void => {
|
|
32
157
|
if (isFocusableButton(id)) {
|
|
33
158
|
registeredSubject$.next({ id, registered: false });
|
|
34
159
|
}
|
|
35
160
|
};
|
|
161
|
+
|
|
162
|
+
// registration focusableNativeView and focusableNativeGroup
|
|
163
|
+
export const emitNativeRegistered = ({
|
|
164
|
+
id,
|
|
165
|
+
groupId,
|
|
166
|
+
isGroup,
|
|
167
|
+
}: {
|
|
168
|
+
id: Option<FocusableID>;
|
|
169
|
+
groupId: Option<FocusableID>;
|
|
170
|
+
isGroup: boolean;
|
|
171
|
+
}): void => {
|
|
172
|
+
if (!isGroup && id && groupId) {
|
|
173
|
+
focusableNativeViewRegistrationSubject$.next({ id, groupId });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isGroup && id && groupId) {
|
|
177
|
+
focusableNativeGroupRegistrationSubject$.next({ id, groupId });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// /////
|
|
182
|
+
|
|
183
|
+
const focusedSubject$ = new Subject<FocusableID>();
|
|
184
|
+
|
|
185
|
+
const focused$ = focusedSubject$.asObservable();
|
|
186
|
+
|
|
187
|
+
export const emitFocused = (id: FocusableID): void => {
|
|
188
|
+
focusedSubject$.next(id);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const topMenuItemFocused$ = focused$.pipe(
|
|
192
|
+
filter((id) => id && isPartOfMenu(focusManager.focusableTree, id))
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
export const contentFocused$ = focused$.pipe(
|
|
196
|
+
filter((id) => {
|
|
197
|
+
const isContent = isPartOfContent(focusManager.focusableTree, id);
|
|
198
|
+
|
|
199
|
+
return id && isContent;
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const createFocusableRegistry = () => {
|
|
204
|
+
const subject$ = new ReplaySubject<FocusableID | undefined>(1);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
observable$: subject$.asObservable(),
|
|
208
|
+
register: (id: FocusableID) => {
|
|
209
|
+
// save focusable_id on registration
|
|
210
|
+
subject$.next(id);
|
|
211
|
+
},
|
|
212
|
+
unregister: () => {
|
|
213
|
+
// reset focusable_id on unregistration
|
|
214
|
+
subject$.next(undefined);
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/// HOME_TOP_MENU_ITEM
|
|
220
|
+
export const HomeTopMenuItemRegistry = createFocusableRegistry();
|
|
221
|
+
|
|
222
|
+
export const homeTopMenuItemFocused$ = topMenuItemFocused$.pipe(
|
|
223
|
+
withLatestFrom(HomeTopMenuItemRegistry.observable$),
|
|
224
|
+
filter(([id, homeId]) => id === homeId)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/// SCREEN_PICKER
|
|
228
|
+
export const ScreenPickerContentContainerRegistry = createFocusableRegistry();
|
|
229
|
+
|
|
230
|
+
/// SEARCH_INPUT
|
|
231
|
+
export const SearchInputRegistry = createFocusableRegistry();
|
|
@@ -10,10 +10,7 @@ import { Platform } from "react-native";
|
|
|
10
10
|
* platformKeys[Platform.OS] should only include keys
|
|
11
11
|
* that are unique to that platform, i.e. Exit: { keyCode: 10182 }
|
|
12
12
|
*/
|
|
13
|
-
export const KEYS = Object.assign(
|
|
14
|
-
platformKeys["web"],
|
|
15
|
-
platformKeys[Platform.OS]
|
|
16
|
-
);
|
|
13
|
+
export const KEYS = Object.assign(platformKeys.web, platformKeys[Platform.OS]);
|
|
17
14
|
|
|
18
15
|
export const ARROW_KEYS = [
|
|
19
16
|
KEYS.ArrowUp,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ReactNative from "react-native";
|
|
2
|
-
import {
|
|
2
|
+
import { useAppData } from "@applicaster/zapp-react-native-redux/hooks";
|
|
3
3
|
|
|
4
4
|
import { isTV, platformSelect } from "../reactUtils";
|
|
5
5
|
import { useIsTablet } from "../reactHooks";
|
|
@@ -184,9 +184,7 @@ export const getScreenOrientation = ({
|
|
|
184
184
|
|
|
185
185
|
export const useGetScreenOrientation = (screenData) => {
|
|
186
186
|
const isTablet = useIsTablet();
|
|
187
|
-
|
|
188
|
-
const { appData } = usePickFromState(["appData"]);
|
|
189
|
-
const isTabletPortrait = appData?.isTabletPortrait;
|
|
187
|
+
const { isTabletPortrait } = useAppData();
|
|
190
188
|
|
|
191
189
|
return getScreenOrientation({
|
|
192
190
|
screenData,
|
|
@@ -79,7 +79,7 @@ export class ClosedCaptioningManager {
|
|
|
79
79
|
private constructor() {
|
|
80
80
|
this.initialize();
|
|
81
81
|
|
|
82
|
-
window
|
|
82
|
+
window.vizioDebug = {
|
|
83
83
|
setCCEnabled: (isCCEnabled: boolean) => {
|
|
84
84
|
this.ccState$.next(isCCEnabled);
|
|
85
85
|
},
|
|
@@ -209,10 +209,11 @@ export class TTSManager {
|
|
|
209
209
|
|
|
210
210
|
if (isLgPlatform() && window.webOS?.service) {
|
|
211
211
|
try {
|
|
212
|
+
// https://webostv.developer.lge.com/develop/references/settings-service
|
|
212
213
|
window.webOS.service.request("luna://com.webos.settingsservice", {
|
|
213
214
|
method: "getSystemSettings",
|
|
214
215
|
parameters: {
|
|
215
|
-
category: "
|
|
216
|
+
category: "option",
|
|
216
217
|
keys: ["audioGuidance"],
|
|
217
218
|
subscribe: true, // Request a subscription to changes
|
|
218
219
|
},
|
|
@@ -233,34 +234,66 @@ export class TTSManager {
|
|
|
233
234
|
});
|
|
234
235
|
} catch (error) {
|
|
235
236
|
log_debug("webOS settings service request error", { error });
|
|
237
|
+
// Fallback to false if the service is not available
|
|
236
238
|
this.screenReaderEnabled$.next(false);
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
if (isSamsungPlatform() && typeof window.
|
|
242
|
+
if (isSamsungPlatform() && typeof window.webapis !== "undefined") {
|
|
241
243
|
try {
|
|
242
244
|
if (
|
|
243
|
-
window.
|
|
244
|
-
typeof window.
|
|
245
|
-
|
|
245
|
+
window.webapis?.tvinfo &&
|
|
246
|
+
typeof window.webapis.tvinfo.getMenuValue === "function" &&
|
|
247
|
+
typeof window.webapis.tvinfo.addCaptionChangeListener === "function"
|
|
246
248
|
) {
|
|
249
|
+
// Get initial Voice Guide status
|
|
250
|
+
const initialStatus = window.webapis.tvinfo.getMenuValue(
|
|
251
|
+
window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const isEnabled =
|
|
255
|
+
initialStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
|
|
256
|
+
|
|
257
|
+
log_debug("Samsung Voice Guide initial status", {
|
|
258
|
+
isEnabled,
|
|
259
|
+
initialStatus,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
this.screenReaderEnabled$.next(isEnabled);
|
|
263
|
+
|
|
264
|
+
// Listen for Voice Guide status changes
|
|
265
|
+
const onChange = () => {
|
|
266
|
+
const currentStatus = window.webapis.tvinfo.getMenuValue(
|
|
267
|
+
window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const enabled =
|
|
271
|
+
currentStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
|
|
272
|
+
|
|
273
|
+
log_debug("Samsung Voice Guide status changed", {
|
|
274
|
+
enabled,
|
|
275
|
+
currentStatus,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this.screenReaderEnabled$.next(enabled);
|
|
279
|
+
};
|
|
280
|
+
|
|
247
281
|
this.samsungListenerId =
|
|
248
|
-
window.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.screenReaderEnabled$.next(!!enabled);
|
|
252
|
-
}
|
|
282
|
+
window.webapis.tvinfo.addCaptionChangeListener(
|
|
283
|
+
window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY,
|
|
284
|
+
onChange
|
|
253
285
|
);
|
|
254
286
|
|
|
255
287
|
log_debug("Samsung Voice Guide listener registered", {
|
|
256
288
|
listenerId: this.samsungListenerId,
|
|
257
289
|
});
|
|
258
290
|
} else {
|
|
259
|
-
log_debug("Samsung
|
|
291
|
+
log_debug("Samsung TvInfo API not available");
|
|
260
292
|
this.screenReaderEnabled$.next(false);
|
|
261
293
|
}
|
|
262
294
|
} catch (error) {
|
|
263
295
|
log_debug("Samsung Voice Guide listener error", { error });
|
|
296
|
+
// Fallback to false if the service is not available
|
|
264
297
|
this.screenReaderEnabled$.next(false);
|
|
265
298
|
}
|
|
266
299
|
}
|
|
@@ -281,31 +314,14 @@ export class TTSManager {
|
|
|
281
314
|
readText(text: string) {
|
|
282
315
|
this.ttsState$.next(true);
|
|
283
316
|
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
typeof window.tizen !== "undefined" &&
|
|
287
|
-
window.tizen.speech
|
|
288
|
-
) {
|
|
289
|
-
try {
|
|
290
|
-
const successCallback = () => {
|
|
291
|
-
log_debug("Samsung TTS play started successfully");
|
|
292
|
-
// Estimate reading time and set inactive when done
|
|
293
|
-
this.scheduleTTSComplete(text);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const errorCallback = (error: any) => {
|
|
297
|
-
log_debug("Samsung TTS error", { error: error?.message || error });
|
|
298
|
-
this.ttsState$.next(false);
|
|
299
|
-
};
|
|
317
|
+
if (isSamsungPlatform() && window.speechSynthesis) {
|
|
318
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
300
319
|
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
|
|
321
|
+
window.speechSynthesis.speak(utterance);
|
|
303
322
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
log_debug("Samsung TTS speak() error", { error });
|
|
307
|
-
this.ttsState$.next(false);
|
|
308
|
-
}
|
|
323
|
+
// Estimate reading time and set inactive when done
|
|
324
|
+
this.scheduleTTSComplete(text);
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
if (isLgPlatform() && window.webOS?.service) {
|