I have a small app which is used to learn pronunciation , I have a rest api which i have text coming from it into the app , which is placed in text input , i am using react native tts to Speek out the text , now i need the cursor to follow the words , and when i select some text from that point it needs to speak.
When i have cursor following then text selection won’t work and vice versa.
import {
View,
Text,
StyleSheet,
TextInput,
ToastAndroid,
Dimensions,
Image,
ScrollView,
AppState,
ActivityIndicator,
NativeSyntheticEvent,
TextInputSelectionChangeEventData,
} from 'react-native';
import React, {useEffect, useRef, useState} from 'react';
import Tts from 'react-native-tts';
import {SelectList} from 'react-native-dropdown-select-list';
import {SingleItem} from '../../lib/types';
import {pitch, voices} from '../../store/Store';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {appColors} from '../../appColors/AppColors';
import LottieView from 'lottie-react-native';
const ICON_SIZE = 100;
const TextToVoice = ({route}: any) => {
const {item} = route.params as {item: SingleItem};
const [selectedVoice, setSelectedVoice] = useState('');
const [isPlaying, setIsPlaying] = useState(false);
const [_, setPitchRate] = useState<number>(1);
const [first, setFirst] = useState(true);
const [selectedVoiceType, setSelectedVoiceType] = useState<string>('Male');
const animationRef = useRef<LottieView>(null);
const [lastPosition, setLastPosition] = useState({
start: 0,
end: 0,
});
const [isImageLoading, setIsImageLoading] = useState(true);
const textInputRef = useRef<TextInput>(null);
const play = async () => {
if (!isPlaying) {
Tts.speak(item.text);
setIsPlaying(true);
}
};
const stop = async () => {
animationRef?.current?.play(0, 0);
await Tts.stop();
setLastPosition({start: 0, end: 0});
setIsPlaying(false);
};
const pause = async () => {
const isStopped = await Tts.stop();
console.log(isStopped, 'stopped');
setIsPlaying(isStopped);
};
const handleCursorPosition = async (event: any) => {
event.persist();
await stop();
const cursorPosition = event.nativeEvent.selection.start;
const textBeforeCursor = item.text.substring(0, cursorPosition);
const lastSpacePosition = textBeforeCursor.lastIndexOf(' ');
const slice1 = item.text.slice(lastSpacePosition + 1, cursorPosition);
const slice2 = item.text.slice(cursorPosition);
const text = slice1 + slice2;
const cursorPoint = item.text.indexOf(text.trim());
setLastPosition({start: cursorPoint, end: cursorPoint});
// Determine voice type based on the cursor position or selected text
const voiceIndex = cursorPosition % voices.length;
const selectedVoice = voices[voiceIndex];
setSelectedVoiceType(selectedVoice.value);
Tts.speak(text);
setIsPlaying(true);
};
const handleSelectChange = async (val: string) => {
try {
await stop();
const voiceChange = await Tts.setDefaultVoice(val);
if (voiceChange === 'success') {
setSelectedVoice(val);
const voiceIndex = voices.findIndex(voice => voice.key === val);
const selectedVoice = voices[voiceIndex];
setSelectedVoiceType(selectedVoice.value);
const sentenceToSpeak = item.text.substring(
lastPosition.start,
item.text.length,
);
if (!first) Tts.speak(sentenceToSpeak);
setFirst(false);
} else {
ToastAndroid.show('Failed to change voice', ToastAndroid.SHORT);
}
} catch (error) {
console.log(error, 'error at handleSelectChange');
}
};
const handleSelectPitchChange = async (val: number) => {
try {
await stop();
const rateChange = await Tts.setDefaultRate(val);
if (rateChange === 'success') {
setPitchRate(val);
const sentenceToSpeak = item.text.substring(
lastPosition.start,
item.text.length,
);
if (!first) Tts.speak(sentenceToSpeak);
setFirst(false);
} else {
ToastAndroid.show('Failed to change rate of voice', ToastAndroid.SHORT);
}
} catch (error) {
console.log(error, 'Error At Select Change');
}
};
const startEventListener = () => setIsPlaying(true);
const finishEventListener = () => setIsPlaying(false);
const progressEventListener = (event: any) => {
textInputRef?.current?.setSelection(event.start, event.end);
setLastPosition({start: event.start, end: event.end});
}; // setting the last know position that our voice is speaking
const errorEventListener = (event: any) => console.log(event);
const restartSpeech = async () => {
await stop();
Tts.speak(item.text);
};
const cleanUp = async () => {
try {
await stop();
Tts.removeAllListeners('tts-start');
Tts.removeAllListeners('tts-finish');
Tts.removeAllListeners('tts-progress');
Tts.removeAllListeners('tts-cancel');
Tts.removeAllListeners('tts-error');
} catch (error) {
console.error(error, 'error at cleanUp');
}
};
const backgroundHandler = () => {
const subscription = AppState.addEventListener('change', async appState => {
console.log(isPlaying, 'isPlaying');
if (appState.match(/inactive|background/)) {
// we need to stop the voice or pause it
await stop();
} else {
// we have it active , we need to restart
// if (!isPlaying) {
// await speakTextFromCurrentIndex();
// }
}
});
return subscription;
};
// attaching event listeners and AppState functions
useEffect(() => {
Tts.addEventListener('tts-finish', finishEventListener);
Tts.addEventListener('tts-start', startEventListener);
Tts.addEventListener('tts-progress', progressEventListener);
Tts.addEventListener('tts-cancel', finishEventListener);
Tts.addEventListener('tts-error', errorEventListener);
const appStateChanger = backgroundHandler();
return () => {
cleanUp();
appStateChanger.remove();
};
}, []);
return (
<View style={styles.container}>
<ScrollView style={styles.scrollView}>
<View style={styles.selectContainer}>
<View>
<Text style={styles.label}>Voices</Text>
<SelectList
setSelected={handleSelectChange}
data={voices}
save="key"
boxStyles={styles.selectList}
dropdownTextStyles={styles.dropdownText}
placeholder="Select Voice"
defaultOption={voices[0]}
inputStyles={styles.selectInput}
/>
</View>
<View>
<Text style={styles.label}>Speed</Text>
<SelectList
setSelected={handleSelectPitchChange}
data={pitch}
save="key"
boxStyles={styles.selectList}
dropdownTextStyles={styles.dropdownText}
placeholder="Select Speed"
defaultOption={pitch[2]}
searchPlaceholder="Search Speed"
inputStyles={styles.selectInput}
/>
</View>
</View>
<View style={styles.textParent}>
{item.image ? (
<View style={styles.imageBackground}>
{isImageLoading && <ActivityIndicator size="large" />}
<Image
source={{uri: item.image}}
style={styles.img}
onLoad={() => setIsImageLoading(false)}
onLoadStart={() => setIsImageLoading(true)}
resizeMode="cover"
/>
</View>
) : null}
<View style={styles.lottieAnimation2}>
<LottieView
autoPlay
ref={animationRef}
source={
selectedVoiceType === 'Male'
? require('../../assets/lottiefiles/boyReading2.0.json')
: require('../../assets/lottiefiles/girlTaking.json')
}
loop={false}
style={styles.lottieAnimation}
/>
<Text style={{color: 'black'}}>{selectedVoiceType}</Text>
</View>
<TextInput
ref={textInputRef}
value={item.text}
multiline
style={styles.text}
onSelectionChange={handleCursorPosition}
// autoFocus
selectTextOnFocus
showSoftInputOnFocus={false}
/>
</View>
<View style={styles.imgView}>
<Icon.Button
name="restart"
size={ICON_SIZE}
backgroundColor={appColors.primary}
onPress={restartSpeech}
/>
<Icon.Button
name={isPlaying ? 'pause-circle' : 'play-circle'}
size={ICON_SIZE}
backgroundColor={appColors.primary}
onPress={isPlaying ? pause : play}
/>
</View>
</ScrollView>
</View>
);
};
export default TextToVoice;
const styles = StyleSheet.create({
container: {
marginTop: 10,
},
scrollView: {
marginTop: 0,
},
selectContainer: {
marginHorizontal: 10,
},
label: {
color: appColors.primary,
fontSize: 20,
fontFamily: '600',
paddingLeft: 10,
fontWeight: 'bold',
},
selectList: {
margin: 10,
backgroundColor: '#eee',
borderRadius: 20,
borderWidth: 1,
borderColor: appColors.primary,
elevation: 5,
},
selectInput: {
color: 'black',
},
dropdownText: {
color: 'black',
},
textParent: {
minHeight: 300,
borderWidth: 2,
margin: 10,
borderColor: appColors.primary,
borderRadius: 10,
},
text: {
textAlign: 'justify',
color: 'black',
fontSize: 20,
overflow: 'scroll',
padding: 10,
fontFamily: '700',
},
imgView: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-evenly',
paddingBottom: 40,
},
iconRestart: {
backgroundColor: appColors.primary,
color: '#eee',
borderRadius: 70,
},
img: {
width: '100%',
height: 250,
borderRadius: 15,
},
imageBackground: {
alignItems: 'center',
borderWidth: 0.5,
borderColor: appColors.primary,
borderRadius: 10,
margin: 10,
},
lottieAnimation: {
height: 200,
width: 200,
},
lottieAnimation2: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-evenly',
borderWidth: 1,
borderColor: appColors.primary,
borderRadius: 10,
margin: 10,
},
});
A way to get these both working at same time