UNPKG

apphouse

Version:

Component library for React that uses observable state management and theme-able components.

282 lines (254 loc) 7.46 kB
import { makeAutoObservable, runInAction } from 'mobx'; import { getErrorMessage, keys } from '../..'; const defaultVoice: SpeechSynthesisVoice = { name: 'Alex', lang: 'en-US', localService: false, voiceURI: 'Alex', default: true }; export interface IUtterance extends SpeechSynthesisUtterance { id: string; } export type IUtteranceStatus = 'idle' | 'speaking' | 'ended' | 'error'; export interface IUtteranceOptions { volume?: number; rate?: number; pitch?: number; lang?: string; voice?: SpeechSynthesisVoice; onstart?: () => void; onend?: () => void; onerror?: () => void; onpause?: () => void; onresume?: () => void; onmark?: () => void; onboundary?: () => void; } /** * A class that represents a speech synthesis utterance * It is a wrapper around the SpeechSynthesisUtterance class */ export class Utterance { id: string; utterance?: SpeechSynthesisUtterance; retryCount: number; error: string | null; status: IUtteranceStatus; userOptions: IUtteranceOptions; text: string; rate: number; volume: number; pitch: number; lang: string; voice: SpeechSynthesisVoice; mark: SpeechSynthesisEvent | null; /** * It marks the character position that has been read by the speech * synthesis engine. */ readerIndex: number; read: string; unread: string; constructor(id: string, text: string, options: IUtteranceOptions = {}) { this.utterance = new SpeechSynthesisUtterance(); this.id = id; this.retryCount = 0; this.error = null; this.status = 'idle'; this.userOptions = options; this.text = ''; this.rate = options?.rate || 1; this.volume = options?.volume || 1; this.pitch = options?.pitch || 1; this.lang = options?.lang || 'en-US'; this.voice = options?.voice || defaultVoice; this.mark = null; this.readerIndex = 0; this.read = ''; this.unread = text; this.setUtteranceOptions(options); this.setUtterance(options); this.setText(text); makeAutoObservable(this); } restart = () => { this.utterance = new SpeechSynthesisUtterance(); const options = this.userOptions; this.setUtteranceOptions(options); this.setUtterance(options); this.setReaderIndex(0); }; setVolume = (volume: number) => { if (this.utterance) { this.utterance.volume = volume; this.volume = volume; } }; setRate = (rate: number) => { runInAction(() => { if (this.utterance) { this.utterance.rate = rate; this.rate = rate; } }); }; setMark = (mark: string) => { if (this.utterance) { this.utterance.text = mark; this.text = mark; } }; setVoice = (voice: SpeechSynthesisVoice) => { if (this.utterance) { this.utterance.voice = voice; this.voice = voice; } }; setPitch = (pitch: number) => { if (this.utterance) { this.utterance.pitch = pitch; this.pitch = pitch; } }; setReaderIndex = (index: number) => { runInAction(() => { this.readerIndex = index; }); }; /** * Increments the retry count */ setRetry = () => { this.retryCount = this.retryCount + 1; }; private setUtteranceOptions = (options: IUtteranceOptions) => { try { keys(options).forEach((key) => { if (this.utterance) { // do not set the option if it is one of the options we set internally if (this.isNotOfOverwrittenValues(key)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const value = options[key]; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.utterance[key] = value; } } }); } catch (error) { console.log({ error }); } }; private isNotOfOverwrittenValues = (option: string) => { return ( !option.includes('onstart') && !option.includes('onend') && !option.includes('onerror') && !option.includes('voice') ); }; private setUtterance = (options: IUtteranceOptions) => { const setError = this.setError; const setStatus = this.setStatus; const setReaderIndex = this.setReaderIndex; const setRead = this.setRead; const setUnread = this.setUnread; const read = this.read; if ('speechSynthesis' in window) { // Create new SpeechSynthesisUtterance object const utterance = this.utterance; if (utterance) { utterance.volume = options?.volume || 1; utterance.rate = options?.rate || 1; utterance.pitch = options?.pitch || 1; utterance.lang = options?.lang || 'en-US'; // utterance.voice = options?.voice || defaultVoice; if (utterance) { utterance.onstart = () => { // console.log('onstart', { id: this.id }); options.onstart && options.onstart(); setStatus('speaking'); }; utterance.onerror = (error) => { // console.log('error', { id: this.id, error }); options.onerror && options.onerror(); setError(getErrorMessage(error)); setStatus('error'); }; utterance.onend = () => { // console.log('ended', { id: this.id }); options.onend && options.onend(); setStatus('ended'); }; utterance.onmark = (event) => { // console.log('onmark', { id: this.id }); options.onmark && options.onmark(); console.log('onmark', event); }; utterance.onboundary = (event) => { // console.log('onboundary', { id: this.id }); options.onboundary && options.onboundary(); const text = this.text; if (event.name === 'word') { setReaderIndex(event.charIndex + event.charLength); // mark the word as read const _read = text.slice(0, event.charIndex + event.charLength); const unreadText = text.slice(event.charIndex + event.charLength); setRead(read + _read); setUnread(unreadText); } }; } } return utterance; } }; setRead = (read: string) => { runInAction(() => { this.read = read; }); }; setUnread = (unread: string) => { runInAction(() => { this.unread = unread; }); }; /** * Sets the status of the utterance. * Possible values: idle, active, ended, error. * Use idle when the utterance has been created but not started. * Use active when the utterance is being spoken. * Use ended when the utterance has finished being spoken. * Use error when the utterance has encountered an error. * @param status IUtteranceStatus */ private setStatus = (status: IUtteranceStatus) => { runInAction(() => { this.status = status; }); }; /** * Sets the error property in this utterance * @param error the error */ private setError = (error: string) => { runInAction(() => { this.error = error; }); }; /** * Sets the text property in this utterance. * This is the text that will be spoken. * @param text the text to speak */ setText = (text: string) => { runInAction(() => { this.text = text; if (this.utterance) { this.utterance.text = text; } }); }; }