@accessibility-rn-js/react-native-accessibility-toolkit 1.0.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/CHECKLIST.md +228 -0
- package/INSTALLATION_GUIDE.md +270 -0
- package/PLUGIN_STATUS.md +206 -0
- package/README.md +251 -0
- package/package.json +49 -0
- package/setup.sh +40 -0
- package/src/components/AccessibilityButton.js +56 -0
- package/src/components/AccessibleText.js +50 -0
- package/src/context/AccessibilityContext.js +125 -0
- package/src/hooks/useDynamicColors.js +32 -0
- package/src/hooks/usePageRead.js +75 -0
- package/src/hooks/useThemeColors.js +169 -0
- package/src/index.js +49 -0
- package/src/services/AccessibilityStorage.js +40 -0
- package/src/services/NativeAccessibilityBridge.js +171 -0
- package/src/services/TTSService.js +117 -0
- package/src/utils/AccessibilityUtils.js +223 -0
- package/src/utils/Colors.js +203 -0
- package/src/utils/Fonts.js +24 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useThemeColors.js
|
|
3
|
+
* Hook to get dynamic colors based on accessibility settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { useAccessibility } from '../context/AccessibilityContext';
|
|
8
|
+
|
|
9
|
+
// Helper to convert hex to RGB
|
|
10
|
+
const hexToRgb = (hex) => {
|
|
11
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
12
|
+
return result
|
|
13
|
+
? {
|
|
14
|
+
r: parseInt(result[1], 16),
|
|
15
|
+
g: parseInt(result[2], 16),
|
|
16
|
+
b: parseInt(result[3], 16),
|
|
17
|
+
}
|
|
18
|
+
: null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Helper to convert RGB to hex
|
|
22
|
+
const rgbToHex = (r, g, b) => {
|
|
23
|
+
return '#' + [r, g, b].map((x) => {
|
|
24
|
+
const hex = Math.round(x).toString(16);
|
|
25
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
26
|
+
}).join('');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Apply grayscale to a color
|
|
30
|
+
const applyGrayscale = (hex) => {
|
|
31
|
+
const rgb = hexToRgb(hex);
|
|
32
|
+
if (!rgb) return hex;
|
|
33
|
+
const gray = Math.round(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
|
|
34
|
+
return rgbToHex(gray, gray, gray);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Apply saturation adjustment
|
|
38
|
+
const applySaturation = (hex, factor) => {
|
|
39
|
+
const rgb = hexToRgb(hex);
|
|
40
|
+
if (!rgb) return hex;
|
|
41
|
+
|
|
42
|
+
const gray = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
|
|
43
|
+
const r = Math.max(0, Math.min(255, gray + factor * (rgb.r - gray)));
|
|
44
|
+
const g = Math.max(0, Math.min(255, gray + factor * (rgb.g - gray)));
|
|
45
|
+
const b = Math.max(0, Math.min(255, gray + factor * (rgb.b - gray)));
|
|
46
|
+
|
|
47
|
+
return rgbToHex(r, g, b);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Invert a color
|
|
51
|
+
const invertColor = (hex) => {
|
|
52
|
+
const rgb = hexToRgb(hex);
|
|
53
|
+
if (!rgb) return hex;
|
|
54
|
+
return rgbToHex(255 - rgb.r, 255 - rgb.g, 255 - rgb.b);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const useThemeColors = () => {
|
|
58
|
+
const {
|
|
59
|
+
colorTheme,
|
|
60
|
+
highContrast,
|
|
61
|
+
colorInversion,
|
|
62
|
+
greyscale,
|
|
63
|
+
lowSaturation,
|
|
64
|
+
highSaturation,
|
|
65
|
+
} = useAccessibility();
|
|
66
|
+
|
|
67
|
+
const colors = useMemo(() => {
|
|
68
|
+
const isDark = colorTheme === 'dark';
|
|
69
|
+
|
|
70
|
+
// Base colors
|
|
71
|
+
let baseColors;
|
|
72
|
+
|
|
73
|
+
if (highContrast) {
|
|
74
|
+
// High contrast colors
|
|
75
|
+
if (isDark) {
|
|
76
|
+
baseColors = {
|
|
77
|
+
background: '#000000',
|
|
78
|
+
text: '#FFFFFF',
|
|
79
|
+
primary: '#FFFFFF',
|
|
80
|
+
secondary: '#FFFF00',
|
|
81
|
+
border: '#FFFFFF',
|
|
82
|
+
card: '#1A1A1A',
|
|
83
|
+
placeholder: '#CCCCCC',
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
baseColors = {
|
|
87
|
+
background: '#FFFFFF',
|
|
88
|
+
text: '#000000',
|
|
89
|
+
primary: '#0000FF',
|
|
90
|
+
secondary: '#000080',
|
|
91
|
+
border: '#000000',
|
|
92
|
+
card: '#F5F5F5',
|
|
93
|
+
placeholder: '#666666',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Normal colors
|
|
98
|
+
if (isDark) {
|
|
99
|
+
baseColors = {
|
|
100
|
+
background: '#1E1E1E',
|
|
101
|
+
text: '#FFFFFF',
|
|
102
|
+
primary: '#007AFF',
|
|
103
|
+
secondary: '#5856D6',
|
|
104
|
+
border: '#3A3A3C',
|
|
105
|
+
card: '#2C2C2E',
|
|
106
|
+
placeholder: '#8E8E93',
|
|
107
|
+
};
|
|
108
|
+
} else {
|
|
109
|
+
baseColors = {
|
|
110
|
+
background: '#FFFFFF',
|
|
111
|
+
text: '#000000',
|
|
112
|
+
primary: '#007AFF',
|
|
113
|
+
secondary: '#5856D6',
|
|
114
|
+
border: '#C7C7CC',
|
|
115
|
+
card: '#F2F2F7',
|
|
116
|
+
placeholder: '#8E8E93',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Apply color transformations
|
|
122
|
+
let processedColors = { ...baseColors };
|
|
123
|
+
|
|
124
|
+
// Apply grayscale
|
|
125
|
+
if (greyscale) {
|
|
126
|
+
processedColors = Object.keys(processedColors).reduce((acc, key) => {
|
|
127
|
+
acc[key] = applyGrayscale(processedColors[key]);
|
|
128
|
+
return acc;
|
|
129
|
+
}, {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Apply saturation adjustments
|
|
133
|
+
if (lowSaturation && !greyscale) {
|
|
134
|
+
processedColors = Object.keys(processedColors).reduce((acc, key) => {
|
|
135
|
+
if (key !== 'background' && key !== 'text') {
|
|
136
|
+
acc[key] = applySaturation(processedColors[key], 0.3);
|
|
137
|
+
} else {
|
|
138
|
+
acc[key] = processedColors[key];
|
|
139
|
+
}
|
|
140
|
+
return acc;
|
|
141
|
+
}, {});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (highSaturation && !greyscale && !lowSaturation) {
|
|
145
|
+
processedColors = Object.keys(processedColors).reduce((acc, key) => {
|
|
146
|
+
if (key !== 'background' && key !== 'text') {
|
|
147
|
+
acc[key] = applySaturation(processedColors[key], 2.0);
|
|
148
|
+
} else {
|
|
149
|
+
acc[key] = processedColors[key];
|
|
150
|
+
}
|
|
151
|
+
return acc;
|
|
152
|
+
}, {});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Apply color inversion
|
|
156
|
+
if (colorInversion) {
|
|
157
|
+
processedColors = Object.keys(processedColors).reduce((acc, key) => {
|
|
158
|
+
acc[key] = invertColor(processedColors[key]);
|
|
159
|
+
return acc;
|
|
160
|
+
}, {});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return processedColors;
|
|
164
|
+
}, [colorTheme, highContrast, colorInversion, greyscale, lowSaturation, highSaturation]);
|
|
165
|
+
|
|
166
|
+
return colors;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default useThemeColors;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Main exports for the accessibility toolkit
|
|
2
|
+
export { AccessibilityProvider, useAccessibility } from './context/AccessibilityContext';
|
|
3
|
+
|
|
4
|
+
// Components
|
|
5
|
+
export { default as AccessibilityButton } from './components/AccessibilityButton';
|
|
6
|
+
export { default as AccessibleText } from './components/AccessibleText';
|
|
7
|
+
|
|
8
|
+
// Hooks
|
|
9
|
+
export { default as useDynamicColors } from './hooks/useDynamicColors';
|
|
10
|
+
export { default as usePageRead } from './hooks/usePageRead';
|
|
11
|
+
export { default as useThemeColors } from './hooks/useThemeColors';
|
|
12
|
+
|
|
13
|
+
// Services
|
|
14
|
+
export { default as TTSService } from './services/TTSService';
|
|
15
|
+
export { default as NativeAccessibilityBridge } from './services/NativeAccessibilityBridge';
|
|
16
|
+
export {
|
|
17
|
+
loadAccessibilityPreferences,
|
|
18
|
+
saveAccessibilityPreferences,
|
|
19
|
+
clearAccessibilityPreferences
|
|
20
|
+
} from './services/AccessibilityStorage';
|
|
21
|
+
|
|
22
|
+
// Utils
|
|
23
|
+
export {
|
|
24
|
+
ACCESSIBILITY_PROFILES,
|
|
25
|
+
TEXT_ALIGNMENT,
|
|
26
|
+
COLOR_THEMES,
|
|
27
|
+
DEFAULT_ACCESSIBILITY_STATE,
|
|
28
|
+
PROFILE_CONFIGS,
|
|
29
|
+
applyProfile,
|
|
30
|
+
getAdjustedFontSize,
|
|
31
|
+
getAdjustedLineHeight,
|
|
32
|
+
getButtonPadding,
|
|
33
|
+
MIN_TOUCH_TARGET,
|
|
34
|
+
} from './utils/AccessibilityUtils';
|
|
35
|
+
|
|
36
|
+
export { getColors } from './utils/Colors';
|
|
37
|
+
|
|
38
|
+
// Default export for convenience
|
|
39
|
+
export default {
|
|
40
|
+
AccessibilityProvider,
|
|
41
|
+
useAccessibility,
|
|
42
|
+
AccessibilityButton,
|
|
43
|
+
AccessibleText,
|
|
44
|
+
useDynamicColors,
|
|
45
|
+
usePageRead,
|
|
46
|
+
useThemeColors,
|
|
47
|
+
TTSService,
|
|
48
|
+
NativeAccessibilityBridge,
|
|
49
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { DEFAULT_ACCESSIBILITY_STATE } from '../utils/AccessibilityUtils';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = '@accessibility_preferences';
|
|
5
|
+
|
|
6
|
+
// Save accessibility preferences
|
|
7
|
+
export const saveAccessibilityPreferences = async (preferences) => {
|
|
8
|
+
try {
|
|
9
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
|
10
|
+
return true;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Error saving accessibility preferences:', error);
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Load accessibility preferences
|
|
18
|
+
export const loadAccessibilityPreferences = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
21
|
+
if (stored) {
|
|
22
|
+
return JSON.parse(stored);
|
|
23
|
+
}
|
|
24
|
+
return DEFAULT_ACCESSIBILITY_STATE;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error loading accessibility preferences:', error);
|
|
27
|
+
return DEFAULT_ACCESSIBILITY_STATE;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Clear accessibility preferences
|
|
32
|
+
export const clearAccessibilityPreferences = async () => {
|
|
33
|
+
try {
|
|
34
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error clearing accessibility preferences:', error);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeAccessibilityBridge.js
|
|
3
|
+
* Bridge between React Native native accessibility features and custom implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AccessibilityInfo, Platform } from 'react-native';
|
|
7
|
+
|
|
8
|
+
class NativeAccessibilityBridge {
|
|
9
|
+
listeners = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize native accessibility detection
|
|
13
|
+
*/
|
|
14
|
+
async initialize(updateCallback) {
|
|
15
|
+
try {
|
|
16
|
+
// Detect Screen Reader
|
|
17
|
+
const screenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled();
|
|
18
|
+
|
|
19
|
+
// Detect Reduced Motion (iOS only)
|
|
20
|
+
let reducedMotionEnabled = false;
|
|
21
|
+
if (Platform.OS === 'ios') {
|
|
22
|
+
reducedMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Detect Bold Text (iOS only)
|
|
26
|
+
let boldTextEnabled = false;
|
|
27
|
+
if (Platform.OS === 'ios') {
|
|
28
|
+
boldTextEnabled = await AccessibilityInfo.isBoldTextEnabled();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Detect Grayscale (iOS only)
|
|
32
|
+
let grayscaleEnabled = false;
|
|
33
|
+
if (Platform.OS === 'ios') {
|
|
34
|
+
grayscaleEnabled = await AccessibilityInfo.isGrayscaleEnabled();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Detect Invert Colors (iOS only)
|
|
38
|
+
let invertColorsEnabled = false;
|
|
39
|
+
if (Platform.OS === 'ios') {
|
|
40
|
+
invertColorsEnabled = await AccessibilityInfo.isInvertColorsEnabled();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Detect Reduce Transparency (iOS only)
|
|
44
|
+
let reduceTransparencyEnabled = false;
|
|
45
|
+
if (Platform.OS === 'ios') {
|
|
46
|
+
reduceTransparencyEnabled = await AccessibilityInfo.isReduceTransparencyEnabled();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update with native settings
|
|
50
|
+
updateCallback({
|
|
51
|
+
nativeScreenReader: screenReaderEnabled,
|
|
52
|
+
nativeReducedMotion: reducedMotionEnabled,
|
|
53
|
+
nativeBoldText: boldTextEnabled,
|
|
54
|
+
nativeGrayscale: grayscaleEnabled,
|
|
55
|
+
nativeInvertColors: invertColorsEnabled,
|
|
56
|
+
nativeReduceTransparency: reduceTransparencyEnabled,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Setup listeners
|
|
60
|
+
this.setupListeners(updateCallback);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
screenReaderEnabled,
|
|
64
|
+
reducedMotionEnabled,
|
|
65
|
+
boldTextEnabled,
|
|
66
|
+
grayscaleEnabled,
|
|
67
|
+
invertColorsEnabled,
|
|
68
|
+
reduceTransparencyEnabled,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error initializing native accessibility:', error);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Setup event listeners for native accessibility changes
|
|
78
|
+
*/
|
|
79
|
+
setupListeners(updateCallback) {
|
|
80
|
+
// Screen Reader changed
|
|
81
|
+
const screenReaderListener = AccessibilityInfo.addEventListener(
|
|
82
|
+
'screenReaderChanged',
|
|
83
|
+
(enabled) => {
|
|
84
|
+
updateCallback({ nativeScreenReader: enabled });
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
this.listeners.push(screenReaderListener);
|
|
88
|
+
|
|
89
|
+
// Reduced Motion changed (iOS)
|
|
90
|
+
if (Platform.OS === 'ios') {
|
|
91
|
+
const reducedMotionListener = AccessibilityInfo.addEventListener(
|
|
92
|
+
'reduceMotionChanged',
|
|
93
|
+
(enabled) => {
|
|
94
|
+
updateCallback({ nativeReducedMotion: enabled });
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
this.listeners.push(reducedMotionListener);
|
|
98
|
+
|
|
99
|
+
// Bold Text changed
|
|
100
|
+
const boldTextListener = AccessibilityInfo.addEventListener(
|
|
101
|
+
'boldTextChanged',
|
|
102
|
+
(enabled) => {
|
|
103
|
+
updateCallback({ nativeBoldText: enabled });
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
this.listeners.push(boldTextListener);
|
|
107
|
+
|
|
108
|
+
// Grayscale changed
|
|
109
|
+
const grayscaleListener = AccessibilityInfo.addEventListener(
|
|
110
|
+
'grayscaleChanged',
|
|
111
|
+
(enabled) => {
|
|
112
|
+
updateCallback({ nativeGrayscale: enabled });
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
this.listeners.push(grayscaleListener);
|
|
116
|
+
|
|
117
|
+
// Invert Colors changed
|
|
118
|
+
const invertColorsListener = AccessibilityInfo.addEventListener(
|
|
119
|
+
'invertColorsChanged',
|
|
120
|
+
(enabled) => {
|
|
121
|
+
updateCallback({ nativeInvertColors: enabled });
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
this.listeners.push(invertColorsListener);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Announce message for screen readers
|
|
130
|
+
*/
|
|
131
|
+
announce(message, options = {}) {
|
|
132
|
+
AccessibilityInfo.announceForAccessibility(message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Announce message for screen readers (with queue option for iOS)
|
|
137
|
+
*/
|
|
138
|
+
announceForAccessibilityWithOptions(message, options = {}) {
|
|
139
|
+
if (Platform.OS === 'ios') {
|
|
140
|
+
AccessibilityInfo.announceForAccessibilityWithOptions(
|
|
141
|
+
message,
|
|
142
|
+
options // { queue: true/false }
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
AccessibilityInfo.announceForAccessibility(message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set accessibility focus to a specific element
|
|
151
|
+
*/
|
|
152
|
+
setAccessibilityFocus(reactTag) {
|
|
153
|
+
if (Platform.OS === 'ios') {
|
|
154
|
+
AccessibilityInfo.setAccessibilityFocus(reactTag);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clean up listeners
|
|
160
|
+
*/
|
|
161
|
+
cleanup() {
|
|
162
|
+
this.listeners.forEach(listener => {
|
|
163
|
+
if (listener && listener.remove) {
|
|
164
|
+
listener.remove();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
this.listeners = [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default new NativeAccessibilityBridge();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTSService.js
|
|
3
|
+
* Text-to-Speech service for reading page content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Platform } from 'react-native';
|
|
7
|
+
import Tts from 'react-native-tts';
|
|
8
|
+
|
|
9
|
+
class TTSService {
|
|
10
|
+
isSpeaking = false;
|
|
11
|
+
isPaused = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize TTS
|
|
15
|
+
*/
|
|
16
|
+
async init() {
|
|
17
|
+
try {
|
|
18
|
+
// Set up event listeners
|
|
19
|
+
Tts.addEventListener('tts-start', () => {
|
|
20
|
+
this.isSpeaking = true;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
Tts.addEventListener('tts-finish', () => {
|
|
24
|
+
this.isSpeaking = false;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Tts.addEventListener('tts-cancel', () => {
|
|
28
|
+
this.isSpeaking = false;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Set default settings
|
|
32
|
+
await Tts.setDefaultLanguage('en-US');
|
|
33
|
+
await Tts.setDefaultRate(0.5);
|
|
34
|
+
await Tts.setDefaultPitch(1.0);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('TTS Init Error:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Speak text aloud
|
|
42
|
+
*/
|
|
43
|
+
async speak(text, options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
language = 'en-US',
|
|
46
|
+
pitch = 1.0,
|
|
47
|
+
rate = 0.5,
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
this.isSpeaking = true;
|
|
52
|
+
this.isPaused = false;
|
|
53
|
+
|
|
54
|
+
await Tts.setDefaultLanguage(language);
|
|
55
|
+
await Tts.setDefaultRate(rate);
|
|
56
|
+
await Tts.setDefaultPitch(pitch);
|
|
57
|
+
|
|
58
|
+
Tts.speak(text);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.isSpeaking = false;
|
|
61
|
+
console.error('TTS Error:', error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stop speaking
|
|
68
|
+
*/
|
|
69
|
+
stop() {
|
|
70
|
+
Tts.stop();
|
|
71
|
+
this.isSpeaking = false;
|
|
72
|
+
this.isPaused = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if TTS is available
|
|
77
|
+
*/
|
|
78
|
+
async isAvailable() {
|
|
79
|
+
try {
|
|
80
|
+
const voices = await Tts.voices();
|
|
81
|
+
return voices && voices.length > 0;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get available voices
|
|
89
|
+
*/
|
|
90
|
+
async getVoices() {
|
|
91
|
+
try {
|
|
92
|
+
return await Tts.voices();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read entire page (utility function)
|
|
100
|
+
* Extracts text from components marked with accessibility labels
|
|
101
|
+
*/
|
|
102
|
+
readPage(textContent) {
|
|
103
|
+
if (!textContent) return;
|
|
104
|
+
|
|
105
|
+
// Clean and prepare text for reading
|
|
106
|
+
const cleanText = textContent
|
|
107
|
+
.replace(/\s+/g, ' ')
|
|
108
|
+
.replace(/[^\w\s.,!?-]/g, '')
|
|
109
|
+
.trim();
|
|
110
|
+
|
|
111
|
+
this.speak(cleanText, {
|
|
112
|
+
rate: 0.9, // Slightly slower for better comprehension
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default new TTSService();
|