@applicaster/zapp-react-native-utils 15.0.0-alpha.2239032089 → 15.0.0-alpha.2413435535
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/analyticsUtils/AnalyticPlayerListener.ts +5 -2
- package/appUtils/RiverFocusManager/{index.ts → index.js} +18 -25
- package/appUtils/accessibilityManager/hooks.ts +8 -6
- package/appUtils/accessibilityManager/index.ts +28 -1
- package/appUtils/accessibilityManager/utils.ts +36 -5
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -3
- package/appUtils/focusManager/index.ios.ts +15 -33
- package/appUtils/focusManagerAux/utils/index.ts +18 -0
- package/appUtils/focusManagerAux/utils/utils.ios.ts +24 -52
- package/appUtils/platform/platformUtils.ts +107 -23
- package/appUtils/playerManager/conts.ts +21 -0
- package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
- package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
- package/arrayUtils/index.ts +5 -0
- package/cellUtils/index.ts +40 -1
- package/navigationUtils/index.ts +19 -16
- package/package.json +2 -2
- package/playerUtils/usePlayerTTS.ts +5 -2
- package/reactHooks/feed/useBatchLoading.ts +7 -1
- package/reactHooks/feed/useFeedLoader.tsx +0 -9
- package/reactHooks/feed/useInflatedUrl.ts +23 -29
- package/reactHooks/feed/usePipesCacheReset.ts +3 -1
- package/reactHooks/layout/index.ts +1 -1
- package/utils/__tests__/mapAccum.test.ts +73 -0
- package/utils/index.ts +6 -0
- package/utils/mapAccum.ts +23 -0
- package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +98 -31
- package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +25 -9
- package/zappFrameworkUtils/HookCallback/useCallbackActions.ts +6 -9
- package/appUtils/focusManagerAux/utils/index.ios.ts +0 -104
|
@@ -35,8 +35,11 @@ export class AnalyticPlayerListener
|
|
|
35
35
|
this.handleAnalyticEvent(PLAYBACK_EVENT.complete);
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
onError = (err:
|
|
39
|
-
this.handleAnalyticEvent(
|
|
38
|
+
onError = (err: QuickBrickPlayer.PlayerErrorI) => {
|
|
39
|
+
this.handleAnalyticEvent(
|
|
40
|
+
PLAYBACK_EVENT.error,
|
|
41
|
+
err.toObject?.() || { message: err.message }
|
|
42
|
+
);
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
onPlayerPause = (event) => {
|
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
1
|
+
import * as R from "ramda";
|
|
2
|
+
|
|
3
|
+
import { focusManager } from "../focusManager";
|
|
5
4
|
|
|
6
5
|
let riverFocusData = {};
|
|
7
6
|
let initialyPresentedScreenFocused = false;
|
|
8
7
|
|
|
9
8
|
export const riverFocusManager = (function () {
|
|
10
|
-
/**
|
|
11
|
-
* Create unique key that will be used for save focused group data inside specific screen
|
|
12
|
-
* @param {{ screenId: string, isInsideContainer: boolean }}
|
|
13
|
-
* screenId Unique Id of the screen from layout.json
|
|
14
|
-
* isInsideContainer If this screen a screen picker child
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
function screenFocusableGroupId({
|
|
18
|
-
screenId,
|
|
19
|
-
isInsideContainer,
|
|
20
|
-
}: {
|
|
21
|
-
screenId: string;
|
|
22
|
-
isInsideContainer: Option<boolean>;
|
|
23
|
-
}) {
|
|
24
|
-
return `${QUICK_BRICK_CONTENT}-${screenId}${
|
|
25
|
-
isNil(isInsideContainer) ? "" : "-isInsideContainer"
|
|
26
|
-
}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
9
|
function setScreenFocusableData({
|
|
30
10
|
screenFocusableGroupId,
|
|
31
11
|
groupId,
|
|
@@ -98,8 +78,8 @@ export const riverFocusManager = (function () {
|
|
|
98
78
|
}) {
|
|
99
79
|
// Check if screen should be focused
|
|
100
80
|
const shouldFocus =
|
|
101
|
-
(initialyPresentedScreenFocused === false && isEmpty(riverFocusData)) ||
|
|
102
|
-
|
|
81
|
+
(initialyPresentedScreenFocused === false && R.isEmpty(riverFocusData)) ||
|
|
82
|
+
R.compose(R.not, R.isNil)(riverFocusData[screenFocusableGroupId]) ||
|
|
103
83
|
isDeepLink;
|
|
104
84
|
|
|
105
85
|
// TODO: Uncommit it to start fixing bug where selection wrong item
|
|
@@ -138,6 +118,19 @@ export const riverFocusManager = (function () {
|
|
|
138
118
|
}
|
|
139
119
|
}
|
|
140
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Create unique key that will be used for save focused group data inside specific screen
|
|
123
|
+
* @param {{ screenId: string, isInsideContainer: boolean }}
|
|
124
|
+
* screenId Unique Id of the screen from layout.json
|
|
125
|
+
* isInsideContainer If this screen a screen picker child
|
|
126
|
+
*
|
|
127
|
+
*/
|
|
128
|
+
function screenFocusableGroupId({ screenId, isInsideContainer }) {
|
|
129
|
+
return `RiverFocusableGroup-${screenId}${
|
|
130
|
+
R.isNil(isInsideContainer) ? "" : "-isInsideContainer"
|
|
131
|
+
}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
141
134
|
return {
|
|
142
135
|
setScreenFocusableData,
|
|
143
136
|
clearAllScreensData,
|
|
@@ -17,6 +17,9 @@ export const useAccessibilityManager = (
|
|
|
17
17
|
return AccessibilityManager.getInstance();
|
|
18
18
|
}, []);
|
|
19
19
|
|
|
20
|
+
const [accessibilityManagerState, setAccessibilityManagerState] =
|
|
21
|
+
useState<AccessibilityState>(accessibilityManager.getState());
|
|
22
|
+
|
|
20
23
|
useEffect(() => {
|
|
21
24
|
if (pluginConfiguration) {
|
|
22
25
|
accessibilityManager.updateLocalizations(pluginConfiguration);
|
|
@@ -25,18 +28,17 @@ export const useAccessibilityManager = (
|
|
|
25
28
|
|
|
26
29
|
useEffect(() => {
|
|
27
30
|
const subscription = accessibilityManager.getStateAsObservable().subscribe({
|
|
28
|
-
next: () => {
|
|
29
|
-
|
|
30
|
-
// screenReaderEnabled: false
|
|
31
|
-
// reduceMotionEnabled: false
|
|
32
|
-
// boldTextEnabled: false
|
|
31
|
+
next: (newState) => {
|
|
32
|
+
setAccessibilityManagerState(newState);
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
return () => subscription.unsubscribe();
|
|
37
37
|
}, [accessibilityManager]);
|
|
38
38
|
|
|
39
|
-
return accessibilityManager
|
|
39
|
+
return Object.assign(accessibilityManager, {
|
|
40
|
+
accessibilityManagerState,
|
|
41
|
+
});
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
export const useInitialAnnouncementReady = (
|
|
@@ -36,7 +36,20 @@ export class AccessibilityManager {
|
|
|
36
36
|
false
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
private constructor() {
|
|
39
|
+
private constructor() {
|
|
40
|
+
this.ttsManager
|
|
41
|
+
.getScreenReaderEnabledAsObservable()
|
|
42
|
+
.subscribe((enabled) => {
|
|
43
|
+
const state = this.state$.getValue();
|
|
44
|
+
|
|
45
|
+
if (state.screenReaderEnabled !== enabled) {
|
|
46
|
+
this.state$.next({
|
|
47
|
+
...state,
|
|
48
|
+
screenReaderEnabled: enabled,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
public static getInstance(): AccessibilityManager {
|
|
42
55
|
if (!AccessibilityManager._instance) {
|
|
@@ -92,8 +105,15 @@ export class AccessibilityManager {
|
|
|
92
105
|
/**
|
|
93
106
|
* Adds a heading to the queue, headings will be read before the next text
|
|
94
107
|
* Each heading will be read once and removed from the queue
|
|
108
|
+
* Does nothing if screen reader is not enabled
|
|
95
109
|
*/
|
|
96
110
|
public addHeading(heading: string) {
|
|
111
|
+
const state = this.state$.getValue();
|
|
112
|
+
|
|
113
|
+
if (!state.screenReaderEnabled) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
if (!this.pendingFocusId) {
|
|
98
118
|
this.pendingFocusId = Date.now().toString();
|
|
99
119
|
}
|
|
@@ -108,6 +128,7 @@ export class AccessibilityManager {
|
|
|
108
128
|
*
|
|
109
129
|
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
110
130
|
* Only the most recent announcement will be read after the delay period.
|
|
131
|
+
* Does nothing if screen reader is not enabled
|
|
111
132
|
*/
|
|
112
133
|
public readText({
|
|
113
134
|
text,
|
|
@@ -116,6 +137,12 @@ export class AccessibilityManager {
|
|
|
116
137
|
text: string;
|
|
117
138
|
keyOfLocalizedText?: string;
|
|
118
139
|
}) {
|
|
140
|
+
const state = this.state$.getValue();
|
|
141
|
+
|
|
142
|
+
if (!state.screenReaderEnabled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
let textToRead = text;
|
|
120
147
|
|
|
121
148
|
if (keyOfLocalizedText) {
|
|
@@ -12,13 +12,44 @@ export function calculateReadingTime(
|
|
|
12
12
|
minimumPause: number = 500,
|
|
13
13
|
announcementDelay: number = 700
|
|
14
14
|
): number {
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
|
|
17
|
+
// Count words (split on whitespace and punctuation, keep alnum boundaries)
|
|
18
|
+
const words = trimmed
|
|
17
19
|
.split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
|
|
18
20
|
.filter(Boolean).length;
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Count spaces - multiple consecutive spaces add extra pause time
|
|
23
|
+
const spaceMatches: string[] = trimmed.match(/\s+/g) || [];
|
|
24
|
+
|
|
25
|
+
const totalSpaces = spaceMatches.reduce(
|
|
26
|
+
(sum: number, match: string) => sum + match.length,
|
|
27
|
+
0
|
|
23
28
|
);
|
|
29
|
+
|
|
30
|
+
const extraSpaces = Math.max(0, totalSpaces - (words - 1)); // words-1 is normal spacing
|
|
31
|
+
|
|
32
|
+
// Heuristic: punctuation increases TTS duration beyond word-based WPM.
|
|
33
|
+
// Commas typically introduce short pauses, sentence terminators longer ones.
|
|
34
|
+
const commaCount = (trimmed.match(/,/g) || []).length;
|
|
35
|
+
const semicolonCount = (trimmed.match(/;/g) || []).length;
|
|
36
|
+
const colonCount = (trimmed.match(/:/g) || []).length;
|
|
37
|
+
const dashCount = (trimmed.match(/\u2013|\u2014|-/g) || []).length; // – — -
|
|
38
|
+
const sentenceEndCount = (trimmed.match(/[.!?](?!\d)/g) || []).length;
|
|
39
|
+
|
|
40
|
+
const commaPauseMs = 220; // short prosody pause for ","
|
|
41
|
+
const midPauseMs = 260; // for ";", ":", dashes
|
|
42
|
+
const sentenceEndPauseMs = 420; // for ".", "!", "?"
|
|
43
|
+
const extraSpacePauseMs = 50; // per extra space beyond normal spacing
|
|
44
|
+
|
|
45
|
+
const punctuationPause =
|
|
46
|
+
commaCount * commaPauseMs +
|
|
47
|
+
(semicolonCount + colonCount + dashCount) * midPauseMs +
|
|
48
|
+
sentenceEndCount * sentenceEndPauseMs +
|
|
49
|
+
extraSpaces * extraSpacePauseMs;
|
|
50
|
+
|
|
51
|
+
const baseByWordsMs = (words / wordsPerMinute) * 60 * 1000;
|
|
52
|
+
const estimatedMs = Math.max(minimumPause, baseByWordsMs + punctuationPause);
|
|
53
|
+
|
|
54
|
+
return estimatedMs + announcementDelay;
|
|
24
55
|
}
|
|
@@ -69,10 +69,8 @@ exports[`focusManagerIOS should be defined 1`] = `
|
|
|
69
69
|
"getGroupRootById": [Function],
|
|
70
70
|
"getPreferredFocusChild": [Function],
|
|
71
71
|
"invokeHandler": [Function],
|
|
72
|
+
"isChildOf": [Function],
|
|
72
73
|
"isFocusOn": [Function],
|
|
73
|
-
"isFocusOnContent": [Function],
|
|
74
|
-
"isFocusOnMenu": [Function],
|
|
75
|
-
"isFocusOnTabsScreenContent": [Function],
|
|
76
74
|
"isGroupItemFocused": [Function],
|
|
77
75
|
"moveFocus": [Function],
|
|
78
76
|
"on": [Function],
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { NativeModules } from "react-native";
|
|
2
2
|
import * as R from "ramda";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
isCurrentFocusOn,
|
|
6
|
+
isChildOf as isChildOfUtils,
|
|
7
|
+
} from "../focusManagerAux/utils";
|
|
4
8
|
import { Tree } from "./treeDataStructure/Tree";
|
|
5
9
|
import { findFocusableNode } from "./treeDataStructure/Utils";
|
|
6
10
|
import { subscriber } from "../../functionUtils";
|
|
7
11
|
import { findChild } from "./utils";
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
isPartOfTabsScreenContent,
|
|
14
|
-
} from "../focusManagerAux/utils/index.ios";
|
|
14
|
+
emitRegistered,
|
|
15
|
+
emitUnregistered,
|
|
16
|
+
} from "../focusManagerAux/utils/utils.ios";
|
|
15
17
|
|
|
16
18
|
const { FocusableManagerModule } = NativeModules;
|
|
17
19
|
|
|
@@ -186,10 +188,14 @@ export const focusManager = (function () {
|
|
|
186
188
|
function register({ id, component }) {
|
|
187
189
|
const { isGroup = false } = component;
|
|
188
190
|
|
|
191
|
+
emitRegistered(id);
|
|
192
|
+
|
|
189
193
|
return isGroup ? registerGroup(id, component) : registerItem(id, component);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
function unregister(id, { group = false } = {}) {
|
|
197
|
+
emitUnregistered(id);
|
|
198
|
+
|
|
193
199
|
group ? unregisterGroup(id) : unregisterItem(id);
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -267,9 +273,7 @@ export const focusManager = (function () {
|
|
|
267
273
|
function setFocus(
|
|
268
274
|
id: string,
|
|
269
275
|
direction?: FocusManager.IOS.Direction,
|
|
270
|
-
options?: Partial<{
|
|
271
|
-
groupFocusedChanged: boolean;
|
|
272
|
-
}>,
|
|
276
|
+
options?: Partial<{ groupFocusedChanged: boolean }>,
|
|
273
277
|
callback?: any
|
|
274
278
|
) {
|
|
275
279
|
blur(direction);
|
|
@@ -408,28 +412,8 @@ export const focusManager = (function () {
|
|
|
408
412
|
return id && isCurrentFocusOn(id, currentFocusNode);
|
|
409
413
|
}
|
|
410
414
|
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return isPartOfMenu(focusableTree, currentFocusable?.props?.id);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function isFocusOnContent(): boolean {
|
|
418
|
-
const currentFocusable = getCurrentFocus();
|
|
419
|
-
|
|
420
|
-
return isPartOfContent(focusableTree, currentFocusable?.props?.id);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function isFocusOnTabsScreenContent(
|
|
424
|
-
screenPickerContentContainerId: string
|
|
425
|
-
): boolean {
|
|
426
|
-
const currentFocusable = getCurrentFocus();
|
|
427
|
-
|
|
428
|
-
return isPartOfTabsScreenContent(
|
|
429
|
-
focusableTree,
|
|
430
|
-
screenPickerContentContainerId,
|
|
431
|
-
currentFocusable?.props?.id
|
|
432
|
-
);
|
|
415
|
+
function isChildOf(childId, parentId): boolean {
|
|
416
|
+
return isChildOfUtils(focusableTree, childId, parentId);
|
|
433
417
|
}
|
|
434
418
|
|
|
435
419
|
return {
|
|
@@ -454,8 +438,6 @@ export const focusManager = (function () {
|
|
|
454
438
|
isGroupItemFocused,
|
|
455
439
|
getPreferredFocusChild,
|
|
456
440
|
isFocusOn,
|
|
457
|
-
|
|
458
|
-
isFocusOnContent,
|
|
459
|
-
isFocusOnTabsScreenContent,
|
|
441
|
+
isChildOf,
|
|
460
442
|
};
|
|
461
443
|
})();
|
|
@@ -190,3 +190,21 @@ export const isCurrentFocusOn = (id, node) => {
|
|
|
190
190
|
|
|
191
191
|
return isCurrentFocusOn(id, node.parent);
|
|
192
192
|
};
|
|
193
|
+
|
|
194
|
+
export const isChildOf = (focusableTree, childId, parentId) => {
|
|
195
|
+
if (isNil(childId) || isNil(parentId)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const childNode = focusableTree.findInTree(childId);
|
|
200
|
+
|
|
201
|
+
if (isNil(childNode)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (childNode.parent?.id === parentId) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return isChildOf(focusableTree, childNode.parent?.id, parentId);
|
|
210
|
+
};
|
|
@@ -1,63 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReplaySubject } from "rxjs";
|
|
2
2
|
import { filter } from "rxjs/operators";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { focusManager } from "../../focusManager/index.ios";
|
|
3
|
+
import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
|
|
4
|
+
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
|
|
7
5
|
|
|
8
6
|
type FocusableID = string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const emitFocused = (id: FocusableID): void => {
|
|
14
|
-
focusedSubject$.next(id);
|
|
7
|
+
type RegistrationEvent = {
|
|
8
|
+
id: FocusableID;
|
|
9
|
+
registered: boolean;
|
|
15
10
|
};
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
export const contentFocused$ = focused$.pipe(
|
|
22
|
-
filter((id) => {
|
|
23
|
-
const isContent = isPartOfContent(focusManager.focusableTree, id);
|
|
24
|
-
|
|
25
|
-
return id && isContent;
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const registeredHomeTopMenuItemSubject$ = new ReplaySubject<FocusableID>(1);
|
|
30
|
-
|
|
31
|
-
export const registeredHomeTopMenuItem$ =
|
|
32
|
-
registeredHomeTopMenuItemSubject$.asObservable();
|
|
33
|
-
|
|
34
|
-
export const homeTopMenuItemFocused$ = topMenuItemFocused$.pipe(
|
|
35
|
-
withLatestFrom(registeredHomeTopMenuItem$),
|
|
36
|
-
filter(([id, homeId]) => id === homeId)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
export const emitHomeTopMenuItemRegistered = (id) => {
|
|
40
|
-
// save homeId on registration
|
|
41
|
-
registeredHomeTopMenuItemSubject$.next(id);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const emitHomeTopMenuItemUnregistered = () => {
|
|
45
|
-
// reset homeId on unregistration
|
|
46
|
-
registeredHomeTopMenuItemSubject$.next(undefined);
|
|
47
|
-
};
|
|
12
|
+
const isFocusableButton = (id: Option<FocusableID>): boolean =>
|
|
13
|
+
id && id.includes?.(BUTTON_PREFIX);
|
|
48
14
|
|
|
49
|
-
const
|
|
50
|
-
new ReplaySubject<FocusableID>(1);
|
|
15
|
+
const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
|
|
51
16
|
|
|
52
|
-
export const
|
|
53
|
-
|
|
17
|
+
export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
|
|
18
|
+
registeredSubject$.pipe(
|
|
19
|
+
filter(
|
|
20
|
+
(value) =>
|
|
21
|
+
value.registered && focusManager.isChildOf(value.id, focusableGroupId)
|
|
22
|
+
)
|
|
23
|
+
);
|
|
54
24
|
|
|
55
|
-
export const
|
|
56
|
-
|
|
57
|
-
|
|
25
|
+
export const emitRegistered = (id: Option<FocusableID>): void => {
|
|
26
|
+
if (isFocusableButton(id)) {
|
|
27
|
+
registeredSubject$.next({ id, registered: true });
|
|
28
|
+
}
|
|
58
29
|
};
|
|
59
30
|
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
31
|
+
export const emitUnregistered = (id: Option<FocusableID>): void => {
|
|
32
|
+
if (isFocusableButton(id)) {
|
|
33
|
+
registeredSubject$.next({ id, registered: false });
|
|
34
|
+
}
|
|
63
35
|
};
|
|
@@ -170,7 +170,9 @@ export const getClosedCaptionState = () => {
|
|
|
170
170
|
*/
|
|
171
171
|
export class TTSManager {
|
|
172
172
|
private ttsState$ = new BehaviorSubject<boolean>(false);
|
|
173
|
+
private screenReaderEnabled$ = new BehaviorSubject<boolean>(false);
|
|
173
174
|
private static ttsManagerInstance: TTSManager;
|
|
175
|
+
private samsungListenerId: number | null = null;
|
|
174
176
|
|
|
175
177
|
private constructor() {
|
|
176
178
|
this.initialize();
|
|
@@ -185,23 +187,84 @@ export class TTSManager {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
async initialize() {
|
|
188
|
-
if (
|
|
190
|
+
if (isVizioPlatform()) {
|
|
191
|
+
document.addEventListener(
|
|
192
|
+
"VIZIO_TTS_ENABLED",
|
|
193
|
+
() => {
|
|
194
|
+
log_debug("Vizio screen reader enabled");
|
|
195
|
+
this.screenReaderEnabled$.next(true);
|
|
196
|
+
},
|
|
197
|
+
false
|
|
198
|
+
);
|
|
189
199
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
document.addEventListener(
|
|
201
|
+
"VIZIO_TTS_DISABLED",
|
|
202
|
+
() => {
|
|
203
|
+
log_debug("Vizio screen reader disabled");
|
|
204
|
+
this.screenReaderEnabled$.next(false);
|
|
205
|
+
},
|
|
206
|
+
false
|
|
207
|
+
);
|
|
208
|
+
}
|
|
197
209
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
if (isLgPlatform() && window.webOS?.service) {
|
|
211
|
+
try {
|
|
212
|
+
// https://webostv.developer.lge.com/develop/references/settings-service
|
|
213
|
+
window.webOS.service.request("luna://com.webos.settingsservice", {
|
|
214
|
+
method: "getSystemSettings",
|
|
215
|
+
parameters: {
|
|
216
|
+
category: "option",
|
|
217
|
+
keys: ["audioGuidance"],
|
|
218
|
+
subscribe: true, // Request a subscription to changes
|
|
219
|
+
},
|
|
220
|
+
onSuccess: (response: any) => {
|
|
221
|
+
const isEnabled = response?.settings?.audioGuidance === "on";
|
|
222
|
+
|
|
223
|
+
log_debug("LG Audio Guidance status changed", {
|
|
224
|
+
isEnabled,
|
|
225
|
+
response,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.screenReaderEnabled$.next(isEnabled);
|
|
229
|
+
},
|
|
230
|
+
onFailure: (error: any) => {
|
|
231
|
+
log_debug("webOS settings subscription failed", { error });
|
|
232
|
+
this.screenReaderEnabled$.next(false);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
log_debug("webOS settings service request error", { error });
|
|
237
|
+
this.screenReaderEnabled$.next(false);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (isSamsungPlatform() && typeof window.tizen !== "undefined") {
|
|
242
|
+
try {
|
|
243
|
+
if (
|
|
244
|
+
window.tizen.accessibility &&
|
|
245
|
+
typeof window.tizen.accessibility
|
|
246
|
+
.addVoiceGuideStatusChangeListener === "function"
|
|
247
|
+
) {
|
|
248
|
+
this.samsungListenerId =
|
|
249
|
+
window.tizen.accessibility.addVoiceGuideStatusChangeListener(
|
|
250
|
+
(enabled: boolean) => {
|
|
251
|
+
log_debug("Samsung Voice Guide status changed", { enabled });
|
|
252
|
+
this.screenReaderEnabled$.next(!!enabled);
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
log_debug("Samsung Voice Guide listener registered", {
|
|
257
|
+
listenerId: this.samsungListenerId,
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
log_debug("Samsung accessibility API not available");
|
|
261
|
+
this.screenReaderEnabled$.next(false);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
log_debug("Samsung Voice Guide listener error", { error });
|
|
265
|
+
this.screenReaderEnabled$.next(false);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
205
268
|
}
|
|
206
269
|
|
|
207
270
|
getCurrentState(): boolean {
|
|
@@ -212,31 +275,52 @@ export class TTSManager {
|
|
|
212
275
|
return this.ttsState$.asObservable();
|
|
213
276
|
}
|
|
214
277
|
|
|
278
|
+
getScreenReaderEnabledAsObservable() {
|
|
279
|
+
return this.screenReaderEnabled$.asObservable();
|
|
280
|
+
}
|
|
281
|
+
|
|
215
282
|
readText(text: string) {
|
|
216
283
|
this.ttsState$.next(true);
|
|
217
284
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
285
|
+
if (
|
|
286
|
+
isSamsungPlatform() &&
|
|
287
|
+
typeof window.tizen !== "undefined" &&
|
|
288
|
+
window.tizen.speech
|
|
289
|
+
) {
|
|
290
|
+
try {
|
|
291
|
+
const successCallback = () => {
|
|
292
|
+
log_debug("Samsung TTS play started successfully");
|
|
293
|
+
// Estimate reading time and set inactive when done
|
|
294
|
+
this.scheduleTTSComplete(text);
|
|
295
|
+
};
|
|
220
296
|
|
|
221
|
-
|
|
222
|
-
|
|
297
|
+
const errorCallback = (error: any) => {
|
|
298
|
+
log_debug("Samsung TTS error", { error: error?.message || error });
|
|
299
|
+
this.ttsState$.next(false);
|
|
300
|
+
};
|
|
223
301
|
|
|
224
|
-
|
|
225
|
-
|
|
302
|
+
// Clear any previous speech before speaking new text
|
|
303
|
+
window.tizen.speech.stop();
|
|
304
|
+
|
|
305
|
+
window.tizen.speech.speak(text, successCallback, errorCallback);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
log_debug("Samsung TTS speak() error", { error });
|
|
308
|
+
this.ttsState$.next(false);
|
|
309
|
+
}
|
|
226
310
|
}
|
|
227
311
|
|
|
228
312
|
if (isLgPlatform() && window.webOS?.service) {
|
|
229
313
|
try {
|
|
230
314
|
window.webOS.service.request("luna://com.webos.service.tts", {
|
|
231
315
|
method: "speak",
|
|
232
|
-
onFailure(error: any) {
|
|
316
|
+
onFailure: (error: any) => {
|
|
233
317
|
log_debug("There was a failure setting up webOS TTS service", {
|
|
234
318
|
error,
|
|
235
319
|
});
|
|
236
320
|
|
|
237
321
|
this.ttsState$.next(false);
|
|
238
322
|
},
|
|
239
|
-
onSuccess(response: any) {
|
|
323
|
+
onSuccess: (response: any) => {
|
|
240
324
|
log_debug("webOS TTS service is configured successfully", {
|
|
241
325
|
response,
|
|
242
326
|
});
|
|
@@ -2,6 +2,27 @@ export const userPreferencesNamespace = "user_preferences";
|
|
|
2
2
|
|
|
3
3
|
export const skipActionType = "show_skip";
|
|
4
4
|
|
|
5
|
+
export class PlayerError
|
|
6
|
+
extends Error
|
|
7
|
+
implements QuickBrickPlayer.PlayerErrorI
|
|
8
|
+
{
|
|
9
|
+
description: string;
|
|
10
|
+
|
|
11
|
+
constructor(message: string, description: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.description = description;
|
|
14
|
+
|
|
15
|
+
Object.setPrototypeOf(this, PlayerError.prototype);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toObject() {
|
|
19
|
+
return {
|
|
20
|
+
error: this.message,
|
|
21
|
+
message: this.description,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export enum SharedPlayerCallBacksKeys {
|
|
6
27
|
OnPlayerResume = "onPlayerResume",
|
|
7
28
|
OnPlayerPause = "onPlayerPause",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { allTruthy } from "..";
|
|
2
|
+
|
|
3
|
+
describe("allTruthy", () => {
|
|
4
|
+
it("should return true when all values are true", () => {
|
|
5
|
+
expect(allTruthy([true, true, true])).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("should return false when at least one value is false", () => {
|
|
9
|
+
expect(allTruthy([true, false, true])).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should return false when all values are false", () => {
|
|
13
|
+
expect(allTruthy([false, false, false])).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return false for an empty array", () => {
|
|
17
|
+
expect(allTruthy([])).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should handle single-element arrays correctly", () => {
|
|
21
|
+
expect(allTruthy([true])).toBe(true);
|
|
22
|
+
expect(allTruthy([false])).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|