@applicaster/zapp-react-native-utils 13.0.7-alpha.7163853950 → 13.0.8-rc.0
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/accessibilityManager/const.ts +0 -4
- package/appUtils/accessibilityManager/hooks.ts +1 -35
- package/appUtils/accessibilityManager/index.ts +16 -147
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +2 -0
- package/appUtils/focusManager/index.ios.ts +10 -0
- package/appUtils/focusManager/index.ts +11 -0
- package/appUtils/focusManagerAux/utils/index.ts +18 -0
- package/appUtils/platform/platformUtils.ts +1 -32
- package/index.d.ts +0 -3
- package/package.json +2 -2
- package/playerUtils/PlayerTTS/PlayerTTS.ts +0 -230
- package/playerUtils/PlayerTTS/index.ts +0 -1
- package/playerUtils/usePlayerTTS.ts +0 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { AccessibilityManager } from "./index";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -38,37 +38,3 @@ export const useAccessibilityManager = (
|
|
|
38
38
|
|
|
39
39
|
return accessibilityManager;
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
export const useInitialAnnouncementReady = (
|
|
43
|
-
accessibilityManager: AccessibilityManager
|
|
44
|
-
) => {
|
|
45
|
-
const [isReady, setIsReady] = useState(
|
|
46
|
-
accessibilityManager.isInitialPlayerAnnouncementReady
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const subscription = accessibilityManager
|
|
51
|
-
.getInitialAnnouncementReadyObservable()
|
|
52
|
-
.subscribe(setIsReady);
|
|
53
|
-
|
|
54
|
-
return () => subscription.unsubscribe();
|
|
55
|
-
}, [accessibilityManager]);
|
|
56
|
-
|
|
57
|
-
return isReady;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export const useAnnouncementActive = (
|
|
61
|
-
accessibilityManager: AccessibilityManager
|
|
62
|
-
) => {
|
|
63
|
-
const [isActive, setIsActive] = useState(false);
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
const subscription = accessibilityManager
|
|
67
|
-
.getTTSStateObservable()
|
|
68
|
-
.subscribe(setIsActive);
|
|
69
|
-
|
|
70
|
-
return () => subscription.unsubscribe();
|
|
71
|
-
}, [accessibilityManager]);
|
|
72
|
-
|
|
73
|
-
return isActive;
|
|
74
|
-
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
1
|
import { BehaviorSubject } from "rxjs";
|
|
3
2
|
import { accessibilityManagerLogger as logger } from "./logger";
|
|
4
3
|
import { TTSManager } from "../platform/platformUtils";
|
|
@@ -9,10 +8,8 @@ import _ from "lodash";
|
|
|
9
8
|
export class AccessibilityManager {
|
|
10
9
|
private static _instance: AccessibilityManager | null = null;
|
|
11
10
|
private headingTimeout: NodeJS.Timeout | null = null;
|
|
12
|
-
private announcementDelayTimeout: NodeJS.Timeout | null = null;
|
|
13
11
|
private WORDS_PER_MINUTE = 160;
|
|
14
12
|
private MINIMUM_PAUSE = 500;
|
|
15
|
-
private ANNOUNCEMENT_DELAY = 500;
|
|
16
13
|
private state$ = new BehaviorSubject<AccessibilityState>({
|
|
17
14
|
screenReaderEnabled: false,
|
|
18
15
|
reduceMotionEnabled: false,
|
|
@@ -28,12 +25,6 @@ export class AccessibilityManager {
|
|
|
28
25
|
private ttsManager = TTSManager.getInstance();
|
|
29
26
|
private localizations: { [key: string]: string } = {};
|
|
30
27
|
private headingQueue: string[] = [];
|
|
31
|
-
private currentFocusId: string | null = null;
|
|
32
|
-
private headingFocusMap: Map<string, string> = new Map();
|
|
33
|
-
private pendingFocusId: string | null = null;
|
|
34
|
-
private isInitialPlayerAnnouncementReady$ = new BehaviorSubject<boolean>(
|
|
35
|
-
false
|
|
36
|
-
);
|
|
37
28
|
|
|
38
29
|
private constructor() {}
|
|
39
30
|
|
|
@@ -45,26 +36,6 @@ export class AccessibilityManager {
|
|
|
45
36
|
return AccessibilityManager._instance;
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
public get isInitialPlayerAnnouncementReady(): boolean {
|
|
49
|
-
return this.isInitialPlayerAnnouncementReady$.getValue();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public setInitialPlayerAnnouncementReady(): void {
|
|
53
|
-
this.isInitialPlayerAnnouncementReady$.next(true);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public resetInitialPlayerAnnouncementReady(): void {
|
|
57
|
-
this.isInitialPlayerAnnouncementReady$.next(false);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
public getInitialAnnouncementReadyObservable() {
|
|
61
|
-
return this.isInitialPlayerAnnouncementReady$.asObservable();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public getTTSStateObservable() {
|
|
65
|
-
return this.ttsManager.getStateAsObservable();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
39
|
/**
|
|
69
40
|
* The method now accepts any object with localizations using a flattened structure
|
|
70
41
|
*
|
|
@@ -75,9 +46,7 @@ export class AccessibilityManager {
|
|
|
75
46
|
* i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
|
|
76
47
|
*/
|
|
77
48
|
public updateLocalizations(localizations: { [key: string]: string }) {
|
|
78
|
-
|
|
79
|
-
this.localizations = localizations;
|
|
80
|
-
}
|
|
49
|
+
this.localizations = localizations;
|
|
81
50
|
}
|
|
82
51
|
|
|
83
52
|
public getState(): AccessibilityState {
|
|
@@ -96,11 +65,9 @@ export class AccessibilityManager {
|
|
|
96
65
|
private calculateReadingTime(text: string): number {
|
|
97
66
|
const words = text.trim().split(/\s+/).length;
|
|
98
67
|
|
|
99
|
-
return (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
103
|
-
) + 300
|
|
68
|
+
return Math.max(
|
|
69
|
+
this.MINIMUM_PAUSE,
|
|
70
|
+
(words / this.WORDS_PER_MINUTE) * 60 * 1000
|
|
104
71
|
);
|
|
105
72
|
}
|
|
106
73
|
|
|
@@ -109,20 +76,12 @@ export class AccessibilityManager {
|
|
|
109
76
|
* Each heading will be read once and removed from the queue
|
|
110
77
|
*/
|
|
111
78
|
public addHeading(heading: string) {
|
|
112
|
-
if (!this.pendingFocusId) {
|
|
113
|
-
this.pendingFocusId = Date.now().toString();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this.headingFocusMap.set(heading, this.pendingFocusId);
|
|
117
79
|
this.headingQueue.push(heading);
|
|
118
80
|
}
|
|
119
81
|
|
|
120
82
|
/**
|
|
121
83
|
* text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
|
|
122
84
|
* keyOfLocalizedText is the key to the localized text
|
|
123
|
-
*
|
|
124
|
-
* Implements a delay mechanism to reduce noise during rapid navigation.
|
|
125
|
-
* Only the most recent announcement will be read after the delay period.
|
|
126
85
|
*/
|
|
127
86
|
public readText({
|
|
128
87
|
text,
|
|
@@ -153,27 +112,19 @@ export class AccessibilityManager {
|
|
|
153
112
|
textToRead = localizedMessage;
|
|
154
113
|
}
|
|
155
114
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
115
|
+
if (this.headingQueue.length > 0) {
|
|
116
|
+
const heading = this.headingQueue.shift()!;
|
|
117
|
+
this.ttsManager?.readText(heading);
|
|
159
118
|
|
|
160
|
-
|
|
119
|
+
if (this.headingTimeout) {
|
|
120
|
+
clearTimeout(this.headingTimeout);
|
|
121
|
+
}
|
|
161
122
|
|
|
162
|
-
|
|
163
|
-
this.executeAnnouncement(textToRead, keyOfLocalizedText, focusId);
|
|
164
|
-
}, this.ANNOUNCEMENT_DELAY);
|
|
165
|
-
}
|
|
123
|
+
const pauseTime = this.calculateReadingTime(heading);
|
|
166
124
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
private executeAnnouncement(
|
|
171
|
-
textToRead: string,
|
|
172
|
-
keyOfLocalizedText?: string,
|
|
173
|
-
focusId?: string
|
|
174
|
-
) {
|
|
175
|
-
if (this.headingQueue.length > 0) {
|
|
176
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
125
|
+
this.headingTimeout = setTimeout(() => {
|
|
126
|
+
this.ttsManager?.readText(textToRead);
|
|
127
|
+
}, pauseTime);
|
|
177
128
|
} else {
|
|
178
129
|
this.ttsManager?.readText(textToRead);
|
|
179
130
|
}
|
|
@@ -185,49 +136,6 @@ export class AccessibilityManager {
|
|
|
185
136
|
});
|
|
186
137
|
}
|
|
187
138
|
|
|
188
|
-
/**
|
|
189
|
-
* Recursively processes all headings in the queue, reading them one by one
|
|
190
|
-
*/
|
|
191
|
-
private processHeadingQueue(textToRead: string, focusId?: string) {
|
|
192
|
-
// If focus has changed, abort this announcement
|
|
193
|
-
if (focusId && this.currentFocusId !== focusId) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (this.headingQueue.length === 0) {
|
|
198
|
-
if (focusId && this.currentFocusId === focusId) {
|
|
199
|
-
this.ttsManager?.readText(textToRead);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const heading = this.headingQueue.shift()!;
|
|
206
|
-
|
|
207
|
-
const headingFocusId = this.headingFocusMap.get(heading);
|
|
208
|
-
|
|
209
|
-
if (headingFocusId && headingFocusId !== focusId) {
|
|
210
|
-
// This heading belongs to a previous focus, skip it
|
|
211
|
-
this.headingFocusMap.delete(heading);
|
|
212
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.ttsManager?.readText(heading);
|
|
218
|
-
this.headingFocusMap.delete(heading); // Clean up after reading
|
|
219
|
-
|
|
220
|
-
if (this.headingTimeout) {
|
|
221
|
-
clearTimeout(this.headingTimeout);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const pauseTime = this.calculateReadingTime(heading);
|
|
225
|
-
|
|
226
|
-
this.headingTimeout = setTimeout(() => {
|
|
227
|
-
this.processHeadingQueue(textToRead, focusId);
|
|
228
|
-
}, pauseTime);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
139
|
public getButtonAccessibilityProps(name: string): AccessibilityProps {
|
|
232
140
|
const buttonName = _.toString(name);
|
|
233
141
|
|
|
@@ -235,15 +143,12 @@ export class AccessibilityManager {
|
|
|
235
143
|
|
|
236
144
|
if (!buttonConfig) {
|
|
237
145
|
return {
|
|
238
|
-
accessible: true,
|
|
239
146
|
accessibilityLabel: buttonName,
|
|
240
147
|
accessibilityHint: `Press button to perform action on ${buttonName}`,
|
|
241
148
|
"aria-label": buttonName,
|
|
242
149
|
"aria-description": `Press button to perform action on ${buttonName}`,
|
|
243
150
|
accessibilityRole: "button" as AccessibilityRole,
|
|
244
151
|
"aria-role": "button",
|
|
245
|
-
role: "button",
|
|
246
|
-
tabindex: 0,
|
|
247
152
|
};
|
|
248
153
|
}
|
|
249
154
|
|
|
@@ -257,52 +162,23 @@ export class AccessibilityManager {
|
|
|
257
162
|
`Press button to perform action on ${buttonName}`;
|
|
258
163
|
|
|
259
164
|
return {
|
|
260
|
-
accessible: true,
|
|
261
165
|
accessibilityLabel: label,
|
|
262
166
|
accessibilityHint: hint,
|
|
263
167
|
"aria-label": label,
|
|
264
168
|
"aria-description": hint,
|
|
265
169
|
accessibilityRole: "button" as AccessibilityRole,
|
|
266
170
|
"aria-role": "button",
|
|
267
|
-
role: "button",
|
|
268
|
-
tabindex: 0,
|
|
269
171
|
};
|
|
270
172
|
}
|
|
271
173
|
|
|
272
174
|
public getInputAccessibilityProps(inputName: string): AccessibilityProps {
|
|
273
175
|
return {
|
|
274
|
-
accessible: true,
|
|
275
176
|
accessibilityLabel: inputName,
|
|
276
177
|
accessibilityHint: `Enter text into ${inputName}`,
|
|
277
178
|
"aria-label": inputName,
|
|
278
179
|
"aria-description": `Enter text into ${inputName}`,
|
|
279
|
-
accessibilityRole: "
|
|
280
|
-
"aria-role": "
|
|
281
|
-
role: "text",
|
|
282
|
-
tabindex: 0,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Extracts accessibility props from component props and returns them as HTML attributes
|
|
288
|
-
* @param props - Component props containing accessibility properties
|
|
289
|
-
* @returns Object with accessibility HTML attributes
|
|
290
|
-
*/
|
|
291
|
-
public getWebAccessibilityProps(props: any): AccessibilityProps {
|
|
292
|
-
const {
|
|
293
|
-
"aria-label": ariaLabel,
|
|
294
|
-
"aria-description": ariaDescription,
|
|
295
|
-
"aria-role": ariaRole,
|
|
296
|
-
role,
|
|
297
|
-
tabindex,
|
|
298
|
-
} = props;
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
"aria-label": ariaLabel,
|
|
302
|
-
"aria-description": ariaDescription,
|
|
303
|
-
"aria-role": ariaRole,
|
|
304
|
-
role: role || ariaRole,
|
|
305
|
-
tabindex,
|
|
180
|
+
accessibilityRole: "textbox" as AccessibilityRole,
|
|
181
|
+
"aria-role": "textbox",
|
|
306
182
|
};
|
|
307
183
|
}
|
|
308
184
|
|
|
@@ -320,11 +196,4 @@ export class AccessibilityManager {
|
|
|
320
196
|
|
|
321
197
|
return this.localizations[key];
|
|
322
198
|
}
|
|
323
|
-
|
|
324
|
-
private clearAnnouncement() {
|
|
325
|
-
if (this.announcementDelayTimeout) {
|
|
326
|
-
clearTimeout(this.announcementDelayTimeout);
|
|
327
|
-
this.announcementDelayTimeout = null;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
199
|
}
|
|
@@ -24,6 +24,7 @@ exports[`focusManager should be defined 1`] = `
|
|
|
24
24
|
"invokeHandler": [Function],
|
|
25
25
|
"isCurrentFocusOnTheTopScreen": [Function],
|
|
26
26
|
"isFocusDisabled": [Function],
|
|
27
|
+
"isFocusOn": [Function],
|
|
27
28
|
"isGroupItemFocused": [Function],
|
|
28
29
|
"longPress": [Function],
|
|
29
30
|
"moveFocus": [Function],
|
|
@@ -63,6 +64,7 @@ exports[`focusManagerIOS should be defined 1`] = `
|
|
|
63
64
|
"getGroupRootById": [Function],
|
|
64
65
|
"getPreferredFocusChild": [Function],
|
|
65
66
|
"invokeHandler": [Function],
|
|
67
|
+
"isFocusOn": [Function],
|
|
66
68
|
"isGroupItemFocused": [Function],
|
|
67
69
|
"moveFocus": [Function],
|
|
68
70
|
"on": [Function],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NativeModules } from "react-native";
|
|
2
2
|
import * as R from "ramda";
|
|
3
3
|
|
|
4
|
+
import { isCurrentFocusOn } from "../focusManagerAux/utils";
|
|
4
5
|
import { Tree } from "./treeDataStructure/Tree";
|
|
5
6
|
import { findFocusableNode } from "./treeDataStructure/Utils";
|
|
6
7
|
import { subscriber } from "../../functionUtils";
|
|
@@ -391,6 +392,14 @@ export const focusManager = (function () {
|
|
|
391
392
|
return node;
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
function isFocusOn(id): boolean {
|
|
396
|
+
const currentFocusNode = focusableTree.findInTree(
|
|
397
|
+
getCurrentFocus()?.props?.id
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
return id && isCurrentFocusOn(id, currentFocusNode);
|
|
401
|
+
}
|
|
402
|
+
|
|
394
403
|
return {
|
|
395
404
|
on,
|
|
396
405
|
invokeHandler,
|
|
@@ -412,5 +421,6 @@ export const focusManager = (function () {
|
|
|
412
421
|
getGroupRootById,
|
|
413
422
|
isGroupItemFocused,
|
|
414
423
|
getPreferredFocusChild,
|
|
424
|
+
isFocusOn,
|
|
415
425
|
};
|
|
416
426
|
})();
|
|
@@ -14,6 +14,8 @@ import { subscriber } from "../../functionUtils";
|
|
|
14
14
|
import { coreLogger } from "../../logger";
|
|
15
15
|
import { ACTION } from "./utils/enums";
|
|
16
16
|
|
|
17
|
+
import { isCurrentFocusOn } from "../focusManagerAux/utils";
|
|
18
|
+
|
|
17
19
|
const logger = coreLogger.addSubsystem("focusManager");
|
|
18
20
|
|
|
19
21
|
const isFocusEnabled = (focusableItem): boolean => {
|
|
@@ -546,6 +548,14 @@ export const focusManager = (function () {
|
|
|
546
548
|
return preferredFocus[0];
|
|
547
549
|
}
|
|
548
550
|
|
|
551
|
+
function isFocusOn(id): boolean {
|
|
552
|
+
return (
|
|
553
|
+
id &&
|
|
554
|
+
isCurrentFocusOnTheTopScreen() &&
|
|
555
|
+
isCurrentFocusOn(id, currentFocusNode)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
549
559
|
/**
|
|
550
560
|
* this is the list of the functions available externally
|
|
551
561
|
* when importing the focus manager
|
|
@@ -576,5 +586,6 @@ export const focusManager = (function () {
|
|
|
576
586
|
recoverFocus,
|
|
577
587
|
isCurrentFocusOnTheTopScreen,
|
|
578
588
|
findPreferredFocusChild,
|
|
589
|
+
isFocusOn,
|
|
579
590
|
};
|
|
580
591
|
})();
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
// run this check too often could lead to performance penalty on low-end devices
|
|
10
10
|
const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
|
|
11
11
|
|
|
12
|
+
const isRoot = (node) => node?.id === "root";
|
|
13
|
+
|
|
12
14
|
type Props = {
|
|
13
15
|
maxTimeout: number;
|
|
14
16
|
conditionFn: () => boolean;
|
|
@@ -99,3 +101,19 @@ export const waitForContent = (focusableTree) => {
|
|
|
99
101
|
conditionFn: contentHasAnyChildren,
|
|
100
102
|
});
|
|
101
103
|
};
|
|
104
|
+
|
|
105
|
+
export const isCurrentFocusOn = (id, node) => {
|
|
106
|
+
if (!node) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isRoot(node)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node?.id === id) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return isCurrentFocusOn(id, node.parent);
|
|
119
|
+
};
|
|
@@ -212,16 +212,9 @@ export class TTSManager {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
readText(text: string) {
|
|
215
|
-
this.ttsState$.next(true);
|
|
216
|
-
|
|
217
215
|
if (isSamsungPlatform() && window.speechSynthesis) {
|
|
218
216
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
219
|
-
|
|
220
|
-
window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
|
|
221
217
|
window.speechSynthesis.speak(utterance);
|
|
222
|
-
|
|
223
|
-
// Estimate reading time and set inactive when done
|
|
224
|
-
this.scheduleTTSComplete(text);
|
|
225
218
|
}
|
|
226
219
|
|
|
227
220
|
if (isLgPlatform() && window.webOS?.service) {
|
|
@@ -232,47 +225,23 @@ export class TTSManager {
|
|
|
232
225
|
log_debug("There was a failure setting up webOS TTS service", {
|
|
233
226
|
error,
|
|
234
227
|
});
|
|
235
|
-
|
|
236
|
-
this.ttsState$.next(false);
|
|
237
228
|
},
|
|
238
229
|
onSuccess(response: any) {
|
|
239
230
|
log_debug("webOS TTS service is configured successfully", {
|
|
240
231
|
response,
|
|
241
232
|
});
|
|
242
|
-
|
|
243
|
-
// Estimate reading time and set inactive when done
|
|
244
|
-
this.scheduleTTSComplete(text);
|
|
245
233
|
},
|
|
246
234
|
parameters: {
|
|
247
235
|
text,
|
|
248
|
-
clear: true, // Clear any previous speech before speaking new text
|
|
249
236
|
},
|
|
250
237
|
});
|
|
251
238
|
} catch (error) {
|
|
252
239
|
log_debug("webOS TTS service error", { error });
|
|
253
|
-
this.ttsState$.next(false);
|
|
254
240
|
}
|
|
255
241
|
}
|
|
256
242
|
|
|
257
|
-
if (!window.VIZIO?.Chromevox)
|
|
258
|
-
// For platforms without TTS, estimate reading time
|
|
259
|
-
this.scheduleTTSComplete(text);
|
|
260
|
-
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
243
|
+
if (!window.VIZIO?.Chromevox) return;
|
|
263
244
|
|
|
264
245
|
window.VIZIO.Chromevox.play(text);
|
|
265
|
-
// Estimate reading time and set inactive when done
|
|
266
|
-
this.scheduleTTSComplete(text);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
private scheduleTTSComplete(text: string) {
|
|
270
|
-
// Estimate reading time based on word count (160 words per minute)
|
|
271
|
-
const words = text.trim().split(/\s+/).length;
|
|
272
|
-
const readingTime = Math.max(500, (words / 160) * 60 * 1000) + 300;
|
|
273
|
-
|
|
274
|
-
setTimeout(() => {
|
|
275
|
-
this.ttsState$.next(false);
|
|
276
|
-
}, readingTime);
|
|
277
246
|
}
|
|
278
247
|
}
|
package/index.d.ts
CHANGED
|
@@ -139,13 +139,10 @@ declare type AccessibilityState = {
|
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
declare type AccessibilityProps = {
|
|
142
|
-
accessible?: boolean;
|
|
143
142
|
accessibilityLabel?: string;
|
|
144
143
|
accessibilityHint?: string;
|
|
145
144
|
"aria-label"?: string;
|
|
146
145
|
"aria-description"?: string;
|
|
147
146
|
accessibilityRole?: string;
|
|
148
147
|
"aria-role"?: string;
|
|
149
|
-
role?: string;
|
|
150
|
-
tabindex?: number;
|
|
151
148
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.8-rc.0",
|
|
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": "13.0.
|
|
30
|
+
"@applicaster/applicaster-types": "13.0.8-rc.0",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
|
|
2
|
-
import uuidv4 from "uuid/v4";
|
|
3
|
-
import { Player } from "../../appUtils/playerManager/player";
|
|
4
|
-
|
|
5
|
-
enum SEEK_DIRECTION {
|
|
6
|
-
FORWARD = "forward",
|
|
7
|
-
REWIND = "back",
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class PlayerTTS {
|
|
11
|
-
private player: Player;
|
|
12
|
-
private accessibilityManager: AccessibilityManager;
|
|
13
|
-
private seekStartPosition: number | null = null;
|
|
14
|
-
private isSeeking: boolean = false;
|
|
15
|
-
private listenerId: string;
|
|
16
|
-
private isInitialPlayerOpen: boolean = true;
|
|
17
|
-
|
|
18
|
-
constructor(player: Player, accessibilityManager: AccessibilityManager) {
|
|
19
|
-
this.player = player;
|
|
20
|
-
this.accessibilityManager = accessibilityManager;
|
|
21
|
-
this.listenerId = `player-tts-${uuidv4()}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private numberToWords(num: number): string {
|
|
25
|
-
const ones = [
|
|
26
|
-
"",
|
|
27
|
-
"one",
|
|
28
|
-
"two",
|
|
29
|
-
"three",
|
|
30
|
-
"four",
|
|
31
|
-
"five",
|
|
32
|
-
"six",
|
|
33
|
-
"seven",
|
|
34
|
-
"eight",
|
|
35
|
-
"nine",
|
|
36
|
-
"ten",
|
|
37
|
-
"eleven",
|
|
38
|
-
"twelve",
|
|
39
|
-
"thirteen",
|
|
40
|
-
"fourteen",
|
|
41
|
-
"fifteen",
|
|
42
|
-
"sixteen",
|
|
43
|
-
"seventeen",
|
|
44
|
-
"eighteen",
|
|
45
|
-
"nineteen",
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const tens = [
|
|
49
|
-
"",
|
|
50
|
-
"",
|
|
51
|
-
"twenty",
|
|
52
|
-
"thirty",
|
|
53
|
-
"forty",
|
|
54
|
-
"fifty",
|
|
55
|
-
"sixty",
|
|
56
|
-
"seventy",
|
|
57
|
-
"eighty",
|
|
58
|
-
"ninety",
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
if (num === 0) return "zero";
|
|
62
|
-
if (num < 20) return ones[num];
|
|
63
|
-
|
|
64
|
-
const ten = Math.floor(num / 10);
|
|
65
|
-
const one = num % 10;
|
|
66
|
-
|
|
67
|
-
if (one === 0) return tens[ten];
|
|
68
|
-
|
|
69
|
-
return `${tens[ten]} ${ones[one]}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private secondsToTime(
|
|
73
|
-
seconds: number,
|
|
74
|
-
format: "natural" | "standard" = "natural"
|
|
75
|
-
): string {
|
|
76
|
-
if (seconds < 0) return format === "natural" ? "zero" : "0";
|
|
77
|
-
|
|
78
|
-
const minutes = Math.floor(seconds / 60);
|
|
79
|
-
const remainingSeconds = Math.floor(seconds % 60);
|
|
80
|
-
|
|
81
|
-
if (format === "standard") {
|
|
82
|
-
if (minutes === 0) {
|
|
83
|
-
if (remainingSeconds === 0) return "0";
|
|
84
|
-
|
|
85
|
-
return `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (remainingSeconds === 0) {
|
|
89
|
-
return `${minutes} minute${minutes !== 1 ? "s" : ""}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return `${minutes} minute${minutes !== 1 ? "s" : ""} ${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
|
|
93
|
-
} else {
|
|
94
|
-
if (minutes === 0) {
|
|
95
|
-
if (remainingSeconds === 0) return "zero";
|
|
96
|
-
|
|
97
|
-
if (remainingSeconds < 10) {
|
|
98
|
-
return `zero o ${this.numberToWords(remainingSeconds)}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return `zero ${this.numberToWords(remainingSeconds)}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (remainingSeconds === 0) {
|
|
105
|
-
return `${this.numberToWords(minutes)}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (remainingSeconds < 10) {
|
|
109
|
-
return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private announceLoading = () => {
|
|
117
|
-
this.accessibilityManager.addHeading("Loading");
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
private announcePause = () => {
|
|
121
|
-
if (!this.isSeeking) {
|
|
122
|
-
this.accessibilityManager.addHeading(
|
|
123
|
-
`Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
private announceBufferComplete = (event: any) => {
|
|
129
|
-
if (this.isInitialPlayerOpen) {
|
|
130
|
-
const timeRemaining = event.duration - event.currentTime;
|
|
131
|
-
const title = (this.player.entry.title as string) || "";
|
|
132
|
-
const summary = (this.player.entry.summary as string) || "";
|
|
133
|
-
this.accessibilityManager.addHeading(`Playing - ${title}`);
|
|
134
|
-
this.accessibilityManager.addHeading(summary);
|
|
135
|
-
|
|
136
|
-
this.accessibilityManager.addHeading(
|
|
137
|
-
`Playing from ${this.secondsToTime(event.currentTime, "standard")}`
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
this.accessibilityManager.addHeading(
|
|
141
|
-
`${this.secondsToTime(timeRemaining, "standard")} remaining.`
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
this.accessibilityManager.setInitialPlayerAnnouncementReady();
|
|
145
|
-
this.isInitialPlayerOpen = false;
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
private announceResume = () => {
|
|
150
|
-
if (!this.isSeeking && !this.isInitialPlayerOpen) {
|
|
151
|
-
this.accessibilityManager.addHeading(
|
|
152
|
-
`Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
private handleVideoProgress = (event: any) => {
|
|
158
|
-
if (event.currentTime > 0) {
|
|
159
|
-
this.seekStartPosition = event.currentTime;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
private handleSeekComplete = (event: any) => {
|
|
164
|
-
if (this.seekStartPosition !== null) {
|
|
165
|
-
const seekDirection =
|
|
166
|
-
event.currentTime > this.seekStartPosition
|
|
167
|
-
? SEEK_DIRECTION.FORWARD
|
|
168
|
-
: SEEK_DIRECTION.REWIND;
|
|
169
|
-
|
|
170
|
-
const seekAmount = Math.round(
|
|
171
|
-
Math.abs(event.currentTime - this.seekStartPosition)
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
this.accessibilityManager.readText({
|
|
175
|
-
text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
this.seekStartPosition = event.currentTime;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
this.isSeeking = false;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
private handleSeekStart = () => {
|
|
185
|
-
this.isSeeking = true;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
private handlePlayerClose = () => {
|
|
189
|
-
this.isInitialPlayerOpen = true;
|
|
190
|
-
this.accessibilityManager.resetInitialPlayerAnnouncementReady();
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
private announceAdBegin = (event: any) => {
|
|
194
|
-
if (event?.ad?.data?.duration) {
|
|
195
|
-
this.accessibilityManager.readText({
|
|
196
|
-
text: `Sponsored. Ends in ${this.secondsToTime(event.ad.data.duration, "standard")}`,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
public init(): () => void {
|
|
202
|
-
if (!this.player) {
|
|
203
|
-
return () => {};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return this.player.addListener({
|
|
207
|
-
id: this.listenerId,
|
|
208
|
-
listener: {
|
|
209
|
-
onBufferStart: this.announceLoading,
|
|
210
|
-
onBufferComplete: this.announceBufferComplete,
|
|
211
|
-
onPlayerResume: this.announceResume,
|
|
212
|
-
onPlayerPause: this.announcePause,
|
|
213
|
-
onVideoProgress: this.handleVideoProgress,
|
|
214
|
-
onPlayerSeekStart: this.handleSeekStart,
|
|
215
|
-
onPlayerSeekComplete: this.handleSeekComplete,
|
|
216
|
-
onPlayerClose: this.handlePlayerClose,
|
|
217
|
-
onAdBegin: this.announceAdBegin,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
public destroy(): void {
|
|
223
|
-
if (this.player) {
|
|
224
|
-
this.player.removeListener(this.listenerId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
this.seekStartPosition = null;
|
|
228
|
-
this.handlePlayerClose();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { PlayerTTS } from "./PlayerTTS";
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
3
|
-
import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
|
|
4
|
-
import { PlayerTTS } from "@applicaster/zapp-react-native-utils/playerUtils/PlayerTTS";
|
|
5
|
-
|
|
6
|
-
export const usePlayerTTS = () => {
|
|
7
|
-
const player = usePlayer();
|
|
8
|
-
const accessibilityManager = useAccessibilityManager({});
|
|
9
|
-
|
|
10
|
-
React.useEffect(() => {
|
|
11
|
-
if (player && accessibilityManager) {
|
|
12
|
-
const playerTTS = new PlayerTTS(player, accessibilityManager);
|
|
13
|
-
const unsubscribe = playerTTS.init();
|
|
14
|
-
|
|
15
|
-
return () => {
|
|
16
|
-
unsubscribe();
|
|
17
|
-
playerTTS.destroy();
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
}, [player, accessibilityManager]);
|
|
21
|
-
};
|