@applicaster/zapp-react-native-utils 15.0.0-rc.9 → 15.0.0-rc.90

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 (107) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +3 -6
  2. package/actionsExecutor/feedDecorator.ts +6 -6
  3. package/adsUtils/index.ts +2 -2
  4. package/analyticsUtils/README.md +1 -1
  5. package/analyticsUtils/analyticsMapper.ts +10 -2
  6. package/appUtils/HooksManager/index.ts +10 -10
  7. package/appUtils/RiverFocusManager/{index.js → index.ts} +25 -18
  8. package/appUtils/accessibilityManager/__tests__/utils.test.ts +360 -0
  9. package/appUtils/accessibilityManager/const.ts +4 -0
  10. package/appUtils/accessibilityManager/hooks.ts +20 -13
  11. package/appUtils/accessibilityManager/index.ts +28 -1
  12. package/appUtils/accessibilityManager/utils.ts +59 -8
  13. package/appUtils/contextKeysManager/__tests__/getKeys/failure.test.ts +7 -2
  14. package/appUtils/contextKeysManager/__tests__/getKeys/success.test.ts +48 -0
  15. package/appUtils/contextKeysManager/contextResolver.ts +51 -22
  16. package/appUtils/contextKeysManager/index.ts +65 -10
  17. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +4 -0
  18. package/appUtils/focusManager/index.ios.ts +59 -3
  19. package/appUtils/focusManagerAux/utils/index.ios.ts +122 -0
  20. package/appUtils/focusManagerAux/utils/index.ts +19 -1
  21. package/appUtils/focusManagerAux/utils/utils.ios.ts +231 -0
  22. package/appUtils/keyCodes/keys/keys.web.ts +1 -4
  23. package/appUtils/orientationHelper.ts +2 -4
  24. package/appUtils/platform/platformUtils.ts +117 -18
  25. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  26. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  27. package/appUtils/playerManager/player.ts +4 -0
  28. package/appUtils/playerManager/playerNative.ts +29 -16
  29. package/appUtils/playerManager/usePlayerState.tsx +14 -2
  30. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  31. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  32. package/arrayUtils/index.ts +5 -0
  33. package/cellUtils/index.ts +32 -0
  34. package/configurationUtils/__tests__/imageSrcFromMediaItem.test.ts +38 -0
  35. package/configurationUtils/__tests__/manifestKeyParser.test.ts +26 -26
  36. package/configurationUtils/index.ts +17 -11
  37. package/focusManager/aux/index.ts +1 -1
  38. package/manifestUtils/defaultManifestConfigurations/player.js +96 -11
  39. package/manifestUtils/keys.js +21 -0
  40. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  41. package/manifestUtils/tvAction/container/index.js +1 -1
  42. package/navigationUtils/index.ts +15 -5
  43. package/package.json +4 -4
  44. package/playerUtils/usePlayerTTS.ts +8 -3
  45. package/pluginUtils/index.ts +4 -0
  46. package/reactHooks/advertising/index.ts +2 -2
  47. package/reactHooks/app/__tests__/useAppState.test.ts +1 -1
  48. package/reactHooks/autoscrolling/__tests__/useTrackCurrentAutoScrollingElement.test.ts +1 -1
  49. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +1 -2
  50. package/reactHooks/cell-click/__tests__/index.test.js +1 -3
  51. package/reactHooks/configuration/__tests__/index.test.tsx +1 -1
  52. package/reactHooks/connection/__tests__/index.test.js +1 -1
  53. package/reactHooks/debugging/__tests__/index.test.js +4 -4
  54. package/reactHooks/device/useMemoizedIsTablet.ts +3 -3
  55. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +32 -23
  56. package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +19 -19
  57. package/reactHooks/feed/__tests__/useEntryScreenId.test.tsx +4 -1
  58. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +42 -30
  59. package/reactHooks/feed/__tests__/useFeedRefresh.test.tsx +1 -1
  60. package/reactHooks/feed/__tests__/{useInflatedUrl.test.ts → useInflatedUrl.test.tsx} +62 -7
  61. package/reactHooks/feed/useBatchLoading.ts +7 -1
  62. package/reactHooks/feed/useEntryScreenId.ts +2 -2
  63. package/reactHooks/feed/useInflatedUrl.ts +43 -17
  64. package/reactHooks/feed/usePipesCacheReset.ts +3 -1
  65. package/reactHooks/flatList/useLoadNextPageIfNeeded.ts +13 -16
  66. package/reactHooks/hookModal/hooks/useHookModalScreenData.ts +12 -8
  67. package/reactHooks/layout/__tests__/index.test.tsx +1 -1
  68. package/reactHooks/layout/__tests__/useLayoutVersion.test.tsx +1 -1
  69. package/reactHooks/layout/index.ts +1 -1
  70. package/reactHooks/layout/useDimensions/__tests__/{useDimensions.test.ts → useDimensions.test.tsx} +105 -25
  71. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -2
  72. package/reactHooks/navigation/__tests__/index.test.tsx +40 -9
  73. package/reactHooks/navigation/index.ts +27 -11
  74. package/reactHooks/navigation/useRoute.ts +11 -7
  75. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
  76. package/reactHooks/player/__tests__/useAutoSeek._test.tsx +1 -1
  77. package/reactHooks/player/__tests__/useTapSeek._test.ts +1 -1
  78. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +1 -1
  79. package/reactHooks/resolvers/__tests__/useComponentResolver.test.tsx +1 -1
  80. package/reactHooks/resolvers/useCellResolver.ts +6 -2
  81. package/reactHooks/resolvers/useComponentResolver.ts +8 -2
  82. package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +2 -2
  83. package/reactHooks/screen/__tests__/useScreenBackgroundColor.test.tsx +1 -1
  84. package/reactHooks/screen/__tests__/useScreenData.test.tsx +1 -1
  85. package/reactHooks/screen/__tests__/useTargetScreenData.test.tsx +12 -4
  86. package/reactHooks/screen/useTargetScreenData.ts +4 -2
  87. package/reactHooks/state/useRefWithInitialValue.ts +10 -0
  88. package/reactHooks/state/useRivers.ts +1 -1
  89. package/reactHooks/usePluginConfiguration.ts +2 -2
  90. package/reactHooks/utils/__tests__/index.test.js +1 -1
  91. package/screenState/__tests__/index.test.ts +1 -1
  92. package/searchUtils/const.ts +7 -0
  93. package/searchUtils/index.ts +3 -0
  94. package/services/storageServiceSync.web.ts +1 -1
  95. package/stringUtils/index.ts +1 -1
  96. package/testUtils/index.tsx +30 -21
  97. package/utils/__tests__/mapAccum.test.ts +73 -0
  98. package/utils/__tests__/mergeRight.test.ts +48 -0
  99. package/utils/__tests__/selectors.test.ts +124 -0
  100. package/utils/index.ts +20 -0
  101. package/utils/mapAccum.ts +23 -0
  102. package/utils/mergeRight.ts +5 -0
  103. package/utils/path.ts +6 -3
  104. package/utils/pathOr.ts +5 -1
  105. package/utils/selectors.ts +46 -0
  106. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +49 -12
  107. package/zappFrameworkUtils/HookCallback/hookCallbackManifestExtensions.config.js +1 -1
@@ -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)
@@ -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
+ };
@@ -0,0 +1,231 @@
1
+ import { ReplaySubject, Subject } from "rxjs";
2
+ import { filter, switchMap, take, withLatestFrom, map } from "rxjs/operators";
3
+
4
+ import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
5
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
6
+ import { isPartOfMenu, isPartOfContent } from "./index.ios";
7
+
8
+ type FocusableID = string;
9
+ type RegistrationEvent = {
10
+ id: FocusableID;
11
+ registered: boolean;
12
+ };
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
+
33
+ const isFocusableButton = (id: Option<FocusableID>): boolean =>
34
+ id && id.includes?.(BUTTON_PREFIX);
35
+
36
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
37
+
38
+ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
39
+ registeredSubject$.pipe(
40
+ filter(
41
+ (value) =>
42
+ value.registered && focusManager.isChildOf(value.id, focusableGroupId)
43
+ )
44
+ );
45
+
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 => {
142
+ if (isFocusableButton(id)) {
143
+ registeredSubject$.next({ id, registered: true });
144
+ }
145
+
146
+ if (isGroup && id) {
147
+ focusableGroupRegistrationSubject$.next({ id });
148
+ }
149
+
150
+ if (!isGroup && id && groupId) {
151
+ focusableViewRegistrationSubject$.next({ id, groupId });
152
+ }
153
+ };
154
+
155
+ // unregistration on RN level(into RN focusManager)
156
+ export const emitUnregistered = (id: Option<FocusableID>): void => {
157
+ if (isFocusableButton(id)) {
158
+ registeredSubject$.next({ id, registered: false });
159
+ }
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 { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
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["vizioDebug"] = {
82
+ window.vizioDebug = {
83
83
  setCCEnabled: (isCCEnabled: boolean) => {
84
84
  this.ccState$.next(isCCEnabled);
85
85
  },
@@ -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,116 @@ 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
+ // Fallback to false if the service is not available
238
+ this.screenReaderEnabled$.next(false);
239
+ }
240
+ }
241
+
242
+ if (isSamsungPlatform() && typeof window.webapis !== "undefined") {
243
+ try {
244
+ if (
245
+ window.webapis?.tvinfo &&
246
+ typeof window.webapis.tvinfo.getMenuValue === "function" &&
247
+ typeof window.webapis.tvinfo.addCaptionChangeListener === "function"
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
+
281
+ this.samsungListenerId =
282
+ window.webapis.tvinfo.addCaptionChangeListener(
283
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY,
284
+ onChange
285
+ );
286
+
287
+ log_debug("Samsung Voice Guide listener registered", {
288
+ listenerId: this.samsungListenerId,
289
+ });
290
+ } else {
291
+ log_debug("Samsung TvInfo API not available");
292
+ this.screenReaderEnabled$.next(false);
293
+ }
294
+ } catch (error) {
295
+ log_debug("Samsung Voice Guide listener error", { error });
296
+ // Fallback to false if the service is not available
297
+ this.screenReaderEnabled$.next(false);
298
+ }
299
+ }
205
300
  }
206
301
 
207
302
  getCurrentState(): boolean {
@@ -212,6 +307,10 @@ export class TTSManager {
212
307
  return this.ttsState$.asObservable();
213
308
  }
214
309
 
310
+ getScreenReaderEnabledAsObservable() {
311
+ return this.screenReaderEnabled$.asObservable();
312
+ }
313
+
215
314
  readText(text: string) {
216
315
  this.ttsState$.next(true);
217
316
 
@@ -229,14 +328,14 @@ export class TTSManager {
229
328
  try {
230
329
  window.webOS.service.request("luna://com.webos.service.tts", {
231
330
  method: "speak",
232
- onFailure(error: any) {
331
+ onFailure: (error: any) => {
233
332
  log_debug("There was a failure setting up webOS TTS service", {
234
333
  error,
235
334
  });
236
335
 
237
336
  this.ttsState$.next(false);
238
337
  },
239
- onSuccess(response: any) {
338
+ onSuccess: (response: any) => {
240
339
  log_debug("webOS TTS service is configured successfully", {
241
340
  response,
242
341
  });