@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.
Files changed (30) hide show
  1. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  2. package/appUtils/RiverFocusManager/{index.ts → index.js} +18 -25
  3. package/appUtils/accessibilityManager/hooks.ts +8 -6
  4. package/appUtils/accessibilityManager/index.ts +28 -1
  5. package/appUtils/accessibilityManager/utils.ts +36 -5
  6. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -3
  7. package/appUtils/focusManager/index.ios.ts +15 -33
  8. package/appUtils/focusManagerAux/utils/index.ts +18 -0
  9. package/appUtils/focusManagerAux/utils/utils.ios.ts +24 -52
  10. package/appUtils/platform/platformUtils.ts +107 -23
  11. package/appUtils/playerManager/conts.ts +21 -0
  12. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  13. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  14. package/arrayUtils/index.ts +5 -0
  15. package/cellUtils/index.ts +40 -1
  16. package/navigationUtils/index.ts +19 -16
  17. package/package.json +2 -2
  18. package/playerUtils/usePlayerTTS.ts +5 -2
  19. package/reactHooks/feed/useBatchLoading.ts +7 -1
  20. package/reactHooks/feed/useFeedLoader.tsx +0 -9
  21. package/reactHooks/feed/useInflatedUrl.ts +23 -29
  22. package/reactHooks/feed/usePipesCacheReset.ts +3 -1
  23. package/reactHooks/layout/index.ts +1 -1
  24. package/utils/__tests__/mapAccum.test.ts +73 -0
  25. package/utils/index.ts +6 -0
  26. package/utils/mapAccum.ts +23 -0
  27. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +98 -31
  28. package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +25 -9
  29. package/zappFrameworkUtils/HookCallback/useCallbackActions.ts +6 -9
  30. 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: Error) => {
39
- this.handleAnalyticEvent(PLAYBACK_EVENT.error, err); // TODO: Check error format
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 { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
2
- import { QUICK_BRICK_CONTENT } from "@applicaster/quick-brick-core/const";
3
- import { isNil, isEmpty } from "@applicaster/zapp-react-native-utils/utils";
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
- isNotNil(riverFocusData[screenFocusableGroupId]) ||
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
- // TODO: handle accessibility states
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 words = text
16
- .trim()
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
- return (
21
- Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
22
- announcementDelay
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
- isCurrentFocusOn,
11
- isPartOfMenu,
12
- isPartOfContent,
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 isFocusOnMenu(): boolean {
412
- const currentFocusable = getCurrentFocus();
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
- isFocusOnMenu,
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 { Subject, ReplaySubject, withLatestFrom } from "rxjs";
1
+ import { ReplaySubject } from "rxjs";
2
2
  import { filter } from "rxjs/operators";
3
-
4
- import { isPartOfMenu, isPartOfContent } from "./index.ios";
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
- const focusedSubject$ = new Subject<FocusableID>();
10
-
11
- const focused$ = focusedSubject$.asObservable();
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
- export const topMenuItemFocused$ = focused$.pipe(
18
- filter((id) => id && isPartOfMenu(focusManager.focusableTree, id))
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 registeredScreenPickerContentContainerSubject$ =
50
- new ReplaySubject<FocusableID>(1);
15
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
51
16
 
52
- export const registeredScreenPickerContentContainer$ =
53
- registeredScreenPickerContentContainerSubject$.asObservable();
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 emitScreenPickerContentContainerRegistered = (id) => {
56
- // save screenPickerContentContainerId on registration
57
- registeredScreenPickerContentContainerSubject$.next(id);
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 emitScreenPickerContentContainerUnregistered = () => {
61
- // reset screenPickerContentContainerId on unregistration
62
- registeredScreenPickerContentContainerSubject$.next(undefined);
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 (!isVizioPlatform()) return;
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
- document.addEventListener(
191
- "VIZIO_TTS_ENABLED",
192
- () => {
193
- this.ttsState$.next(true);
194
- },
195
- false
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
- document.addEventListener(
199
- "VIZIO_TTS_DISABLED",
200
- () => {
201
- this.ttsState$.next(false);
202
- },
203
- false
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 (isSamsungPlatform() && window.speechSynthesis) {
219
- const utterance = new SpeechSynthesisUtterance(text);
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
- window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
222
- window.speechSynthesis.speak(utterance);
297
+ const errorCallback = (error: any) => {
298
+ log_debug("Samsung TTS error", { error: error?.message || error });
299
+ this.ttsState$.next(false);
300
+ };
223
301
 
224
- // Estimate reading time and set inactive when done
225
- this.scheduleTTSComplete(text);
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
+ });