diff --git a/src/components/Settings/Language/Language.container.js b/src/components/Settings/Language/Language.container.js index 5cfc1056b..a29456916 100644 --- a/src/components/Settings/Language/Language.container.js +++ b/src/components/Settings/Language/Language.container.js @@ -60,10 +60,6 @@ export class LanguageContainer extends Component { * TTS default engine */ ttsDefaultEngine: PropTypes.object, - /** - * list of available voices - */ - voices: PropTypes.array, /** * Callback fired when language changes */ @@ -78,7 +74,28 @@ export class LanguageContainer extends Component { onClose: PropTypes.func, history: PropTypes.object.isRequired, getVoices: PropTypes.func.isRequired, - updateLangSpeechStatus: PropTypes.func.isRequired + updateLangSpeechStatus: PropTypes.func.isRequired, + /** + * Object that contain properties refered to "download a language feature" + */ + downloadingLang: PropTypes.shape({ + // the indicator that the user is trying to download a language. + isdownloading: PropTypes.bool, + // the indicator that availables voices have been updated. + isUpdated: PropTypes.bool, + // the indicator that the user pressed the 'configure a local voice' button. + isDiferentTts: PropTypes.bool, + // the tts engine name that the user is trying to download. + engineName: PropTypes.string, + // the tts Engine market ID that the user is trying to download. + marketId: PropTypes.string, + // the lang that the user is trying to download. + selectedLang: PropTypes.string, + // the indicator that the lang is already available online and the app would change the lang after starting downloading. + continueOnline: PropTypes.bool, + // the indicator that the user clicks to download and the tts is already available. is Used to show a different message. + firstClick: PropTypes.bool + }) }; state = { @@ -88,10 +105,132 @@ export class LanguageContainer extends Component { avaliableAndDownloadablesLangs: [], downloadablesOnly: [] }, - downloadLangLoading: true, + downloadLangLoading: false, downloadingLangError: { ttsError: false, langError: false } }; + componentDidMount = () => { + if (isAndroid()) { + const { isdownloading } = this.props.downloadingLang; + this.refreshAndroidLanguageList(); + this.setState({ + downloadLangLoading: isdownloading + }); + document.addEventListener( + 'backbutton', + this.handleAndroidBackButton, + false + ); + } + }; + + componentWillUnmount = () => { + if (isAndroid()) + document.removeEventListener( + 'backbutton', + this.handleAndroidBackButton, + false + ); + }; + + componentDidUpdate = async () => { + if (isAndroid()) { + const { isdownloading, isUpdated } = this.props.downloadingLang; + if (isdownloading && isUpdated) { + await this.lookDownloadingLang(); + } + } + }; + + refreshAndroidLanguageList = () => { + if (!isAndroid()) return; + const prepareDownloadablesLenguages = () => { + const getDownloadablesLenguages = downloadablesTts => { + const formatLangObject = downloadablesLangs => { + return downloadablesLangs.map(langObject => { + //const code = ISO6391.getCode(langObject.lang); + const { lang } = langObject; + const code = lang.slice(0, 2).toLowerCase(); + langObject.langCode = code; + langObject.nativeName = ISO6391.getNativeName(code); + const showLangCode = + downloadablesLangs.filter(language => language.langCode === code) + .length > 1; + + const langFullCode = showLangCode ? `(${lang})` : ''; + langObject.nativeName = `${langObject.nativeName} ${langFullCode}`; + return langObject; + }); + }; + const downloadablesLangsArray = downloadablesTts.map(tts => { + return { + langs: tts.langs, + marketId: tts.marketId, + ttsName: tts.name + }; + }); + const identifiedLangsArray = downloadablesLangsArray.map(langObject => + langObject.langs.map(language => { + return { + lang: language, + marketId: langObject.marketId, + ttsName: langObject.ttsName + }; + }) + ); + const INITIAL_VALUE = []; + const downloadablesLangs = identifiedLangsArray.reduce( + (accumulator, currentValue) => accumulator.concat(currentValue), + INITIAL_VALUE + ); + return formatLangObject(downloadablesLangs); + }; + + const filterAvailablesAndDownloadablesLangs = downloadablesLangs => { + const { ttsEngines } = this.props; + const { langs } = this.props; + const ttsEnginesNames = ttsEngines.map(tts => tts.name); + + const availableAndDownloadableLang = downloadablesLangs.filter( + ({ lang, ttsName }) => langs.includes(lang) + ); + + return availableAndDownloadableLang.map(item => { + item.ttsAvailable = ttsEnginesNames.includes(item.ttsName); + return item; + }); + }; + + const filterDownloadablesOnlyLangs = ( + downloadables, + availableAndDownloadables + ) => { + return downloadables.filter( + downloadableLang => + !availableAndDownloadables.includes(downloadableLang) + ); + }; + + const downloadablesLangsList = getDownloadablesLenguages( + downloadablesTts + ); + const avaliableAndDownloadablesLangs = filterAvailablesAndDownloadablesLangs( + downloadablesLangsList + ); + const downloadablesOnly = filterDownloadablesOnlyLangs( + downloadablesLangsList, + avaliableAndDownloadablesLangs + ); + return { + avaliableAndDownloadablesLangs, + downloadablesOnly + }; + }; + this.setState({ + downloadablesLangs: prepareDownloadablesLenguages() + }); + }; + handleSubmit = async (optionalLang = null) => { const { onLangChange } = this.props; const selectedLang = optionalLang ? optionalLang : this.state.selectedLang; @@ -132,6 +271,7 @@ export class LanguageContainer extends Component { await setTtsEngine(engineName); const voices = await getVoices(); await updateLangSpeechStatus(voices); + this.refreshAndroidLanguageList(); } catch (err) { throw new Error('TTS engine selection error on handleSetTtsEngine'); } @@ -202,6 +342,7 @@ export class LanguageContainer extends Component { isDiferentTts: false, engineName: ttsName, marketId: marketId, + isUpdated: false, selectedLang: lang }; this.props.setDownloadingLang(downloadingLangState); @@ -213,83 +354,6 @@ export class LanguageContainer extends Component { this.setState({ openDialog: { open: false, downloadingLangData: {} } }); }; - prepareDownloadablesLenguages = () => { - const getDownloadablesLenguages = downloadablesTts => { - const formatLangObject = downloadablesLangs => { - return downloadablesLangs.map(langObject => { - //const code = ISO6391.getCode(langObject.lang); - const { lang } = langObject; - const code = lang.slice(0, 2).toLowerCase(); - langObject.langCode = code; - langObject.nativeName = ISO6391.getNativeName(code); - const showLangCode = - downloadablesLangs.filter(language => language.langCode === code) - .length > 1; - - const langFullCode = showLangCode ? `(${lang})` : ''; - langObject.nativeName = `${langObject.nativeName} ${langFullCode}`; - return langObject; - }); - }; - const downloadablesLangsArray = downloadablesTts.map(tts => { - return { langs: tts.langs, marketId: tts.marketId, ttsName: tts.name }; - }); - const identifiedLangsArray = downloadablesLangsArray.map(langObject => - langObject.langs.map(language => { - return { - lang: language, - marketId: langObject.marketId, - ttsName: langObject.ttsName - }; - }) - ); - const INITIAL_VALUE = []; - const downloadablesLangs = identifiedLangsArray.reduce( - (accumulator, currentValue) => accumulator.concat(currentValue), - INITIAL_VALUE - ); - return formatLangObject(downloadablesLangs); - }; - - const filterAvailablesAndDownloadablesLangs = downloadablesLangs => { - const { ttsEngines } = this.props; - const { langs } = this.props; - const ttsEnginesNames = ttsEngines.map(tts => tts.name); - - const availableAndDownloadableLang = downloadablesLangs.filter( - ({ lang, ttsName }) => langs.includes(lang) - ); - - return availableAndDownloadableLang.map(item => { - item.ttsAvailable = ttsEnginesNames.includes(item.ttsName); - return item; - }); - }; - - const filterDownloadablesOnlyLangs = ( - downloadables, - availableAndDownloadables - ) => { - return downloadables.filter( - downloadableLang => - !availableAndDownloadables.includes(downloadableLang) - ); - }; - - const downloadablesLangsList = getDownloadablesLenguages(downloadablesTts); - const avaliableAndDownloadablesLangs = filterAvailablesAndDownloadablesLangs( - downloadablesLangsList - ); - const downloadablesOnly = filterDownloadablesOnlyLangs( - downloadablesLangsList, - avaliableAndDownloadablesLangs - ); - return { - avaliableAndDownloadablesLangs, - downloadablesOnly - }; - }; - pauseCallback = () => { const downloadingLangState = { ...this.props.downloadingLang, @@ -305,12 +369,7 @@ export class LanguageContainer extends Component { downloadingLangData, firstClick = false ) => { - const { - setDownloadingLang, - ttsEngine, - localLangs, - lang: appLang - } = this.props; + const { setDownloadingLang, localLangs, lang: appLang } = this.props; const { ttsName, lang, marketId } = downloadingLangData; const { avaliableAndDownloadablesLangs } = this.state.downloadablesLangs; @@ -321,39 +380,34 @@ export class LanguageContainer extends Component { .includes(downloadingLangData.lang) && appLang !== lang ? true : false; - - if (ttsEngine.name === ttsName) { - const downloadingLangState = { - isdownloading: true, - isDiferentTts: false, - engineName: ttsName, - marketId: marketId, - selectedLang: lang, - continueOnline, - firstClick - }; - setDownloadingLang(downloadingLangState); - this.setState({ - downloadingLangError: { - ttsError: false, - langError: true - } - }); - return; - } + event.stopPropagation(); this.setState({ downloadLangLoading: true }); + try { + await this.handleSetTtsEngine(ttsName); + } catch (err) { + console.error(err); + } const downloadingLangState = { isdownloading: true, isDiferentTts: true, engineName: ttsName, marketId: marketId, selectedLang: lang, + isUpdated: true, continueOnline, firstClick }; setDownloadingLang(downloadingLangState); }; + handleAndroidBackButton = () => { + if (this.props.downloadingLang.isdownloading) { + this.onErrorDialogCancel(); + return; + } + this.onClose(); + }; + onErrorDialogAcepted = () => { const { ttsError } = this.state.downloadingLangError; const { marketId, continueOnline, lang } = this.props.downloadingLang; @@ -402,45 +456,42 @@ export class LanguageContainer extends Component { }; lookDownloadingLang = async () => { - const { - isDiferentTts, - engineName, - selectedLang - } = this.props.downloadingLang; - - const { - setDownloadingLang, - - ttsEngines, - ttsEngine, - history, - showNotification - } = this.props; + const { setDownloadingLang, downloadingLang } = this.props; + + const processUpdates = async () => { + const { isDiferentTts, engineName, selectedLang } = downloadingLang; + + const { + localLangs, + ttsEngines, + ttsEngine, + history, + showNotification + } = this.props; + + if (engineName === ttsEngine.name && localLangs.includes(selectedLang)) { + setDownloadingLang({ isdownloading: false, isUpdated: false }); + this.setState({ selectedLang: selectedLang }); + if (isDiferentTts) return; + await this.handleSubmit(selectedLang); + showNotification( + + ); + history.push('/settings'); + return; + } - const ttsEnginesNames = ttsEngines.map(tts => tts.name); - if (!ttsEnginesNames.includes(engineName)) { - this.setState({ - downloadingLangError: { - ...this.state.downloadingLangError, - ttsError: true - } - }); - return; - } - if (ttsEngine.name !== engineName) { - try { - await this.handleSetTtsEngine(engineName); - } catch { + const ttsEnginesNames = ttsEngines.map(tts => tts.name); + if (!ttsEnginesNames.includes(engineName)) { this.setState({ downloadingLangError: { - ttsError: false, - langError: true + ...this.state.downloadingLangError, + ttsError: true } }); + return; } - } - const localLangs = this.props.localLangs; - if (!localLangs.includes(selectedLang)) { + this.setState({ downloadingLangError: { ttsError: false, @@ -448,66 +499,13 @@ export class LanguageContainer extends Component { } }); return; - } - const downloadingLangState = { - isdownloading: false }; - setDownloadingLang(downloadingLangState); - this.setState({ - downloadingLangError: { - ttsError: false, - langError: false - }, - selectedLang: selectedLang - }); - this.refreshLanguageList(); - if (isDiferentTts) return; - await this.handleSubmit(selectedLang); - showNotification( - - ); - history.push('/settings'); - }; - - refreshLanguageList = () => { - this.setState({ - downloadablesLangs: isAndroid() - ? this.prepareDownloadablesLenguages() - : { - //downloadablesLangsList: [] - avaliableAndDownloadablesLangs: [], - downloadablesOnly: [] - } - }); - }; - - refreshDownloadLanguage = async () => { - const { isdownloading } = this.props.downloadingLang; - - this.refreshLanguageList(); - if (isdownloading) await this.lookDownloadingLang(); + setDownloadingLang({ ...downloadingLang, isUpdated: false }); + await processUpdates(); this.setState({ downloadLangLoading: false }); }; - componentDidMount = async () => { - if (this.props.langsFetched) this.refreshDownloadLanguage(); - }; - - componentDidUpdate = async prevProps => { - const isdownloading = this.props.downloadingLang?.isdownloading; - const langsFetched = this.props.langsFetched; - - if (!prevProps.langsFetched && langsFetched) { - this.setState({ downloadLangLoading: true }); - await this.refreshDownloadLanguage(); - } - if (!isdownloading) return; - if (prevProps.downloadingLang.isdownloading === false) { - await this.refreshDownloadLanguage(); - } - }; - render() { const { lang, langs, localLangs, ttsEngines, ttsEngine } = this.props; @@ -563,13 +561,11 @@ export class LanguageContainer extends Component { const mapStateToProps = state => ({ lang: state.language.lang, - langsFetched: state.language.langsFetched, langs: state.language.langs, localLangs: state.language.localLangs, ttsEngines: state.speech.ttsEngines, ttsEngine: state.speech.ttsEngine, - downloadingLang: state.language.downloadingLang, - voices: state.speech.voices + downloadingLang: state.language.downloadingLang }); const mapDispatchToProps = { diff --git a/src/providers/LanguageProvider/LanguageProvider.reducer.js b/src/providers/LanguageProvider/LanguageProvider.reducer.js index 68a17418d..3b22429ad 100644 --- a/src/providers/LanguageProvider/LanguageProvider.reducer.js +++ b/src/providers/LanguageProvider/LanguageProvider.reducer.js @@ -16,7 +16,6 @@ const initialState = { dir: 'ltr', langs: [], localLangs: [], - langsFetched: false, downloadingLang: { isdownloading: false } }; @@ -45,8 +44,7 @@ function languageProviderReducer(state = initialState, action) { return { ...state, langs: action.langs.sort(), - localLangs: action.localLangs || [], - langsFetched: true + localLangs: action.localLangs || [] }; case SET_DOWNLOADING_LANG: return { ...state, downloadingLang: action.downloadingLangData }; diff --git a/src/providers/SpeechProvider/SpeechProvider.actions.js b/src/providers/SpeechProvider/SpeechProvider.actions.js index 7c99b9346..e63afd439 100644 --- a/src/providers/SpeechProvider/SpeechProvider.actions.js +++ b/src/providers/SpeechProvider/SpeechProvider.actions.js @@ -53,26 +53,19 @@ export function receiveTtsEngine(ttsEngineName) { } export function getTtsEngines() { - const ttsEngines = tts?.getTtsEngines(); + const ttsEngines = tts.getTtsEngines(); return { type: RECEIVE_TTS_ENGINES, ttsEngines }; } -export function setTtsEngine(selectedTtsEngineName) { +export function setTtsEngine(ttsEngineName) { return async dispatch => { dispatch(requestTtsEngine()); try { - const engineAvailable = tts - .getTtsEngines() - .map(tts => tts.name) - .includes(selectedTtsEngineName); - const engineName = engineAvailable - ? selectedTtsEngineName - : tts.getTtsDefaultEngine().name; - const voices = await tts.setTtsEngine(engineName); - dispatch(receiveTtsEngine(engineName)); + const voices = await tts.setTtsEngine(ttsEngineName); + dispatch(receiveTtsEngine(ttsEngineName)); if (!voices.length) { throw new Error('TTS engine does not have a language.'); } @@ -82,7 +75,7 @@ export function setTtsEngine(selectedTtsEngineName) { }; } -export function updateLangSpeechStatus(voices) { +export function updateLangSpeechStatus(voices, forceChangeVoice = false) { return async (dispatch, getState) => { try { const supportedLangs = getSupportedLangs(voices); @@ -103,7 +96,9 @@ export function updateLangSpeechStatus(voices) { // last step is to change voice in case it is available if ( - getState().speech.options.lang.substring(0, 2) !== lang.substring(0, 2) + getState().speech.options.lang.substring(0, 2) !== + lang.substring(0, 2) || + forceChangeVoice ) { const uris = voices.map(v => { return v.voiceURI; diff --git a/src/providers/SpeechProvider/SpeechProvider.container.js b/src/providers/SpeechProvider/SpeechProvider.container.js index 413b4b157..c61a805a9 100644 --- a/src/providers/SpeechProvider/SpeechProvider.container.js +++ b/src/providers/SpeechProvider/SpeechProvider.container.js @@ -11,13 +11,17 @@ import { setTtsEngine, setCurrentVoiceSource } from './SpeechProvider.actions'; +import { setDownloadingLang } from '../LanguageProvider/LanguageProvider.actions'; + import { isAndroid } from '../../cordova-util'; export class SpeechProvider extends Component { static propTypes = { children: PropTypes.node.isRequired, ttsEngine: PropTypes.object, - setTtsEngine: PropTypes.func + setTtsEngine: PropTypes.func, + downloadingLang: PropTypes.object, + setDownloadingLang: PropTypes.func }; async componentDidMount() { @@ -26,30 +30,68 @@ export class SpeechProvider extends Component { updateLangSpeechStatus, getTtsEngines, getTtsDefaultEngine, + ttsEngines, ttsEngine, + ttsDefaultEngine, setTtsEngine, - setCurrentVoiceSource + setCurrentVoiceSource, + downloadingLang, + setDownloadingLang } = this.props; + const getTtsEngineName = () => { + const ttsEnginesNames = ttsEngines?.map(tts => tts.name); + if (!ttsEnginesNames) return null; + if ( + downloadingLang?.isdownloading && + downloadingLang.engineName && + ttsEnginesNames.includes(downloadingLang.engineName) + ) { + forceChangeVoice = true; + return downloadingLang.engineName; + } + + if ( + ttsEngine && + ttsEngine.name && + ttsEnginesNames.includes(ttsEngine.name) + ) + return ttsEngine.name; + + const defaultEngineName = ttsDefaultEngine?.name; + if (ttsEnginesNames.includes(defaultEngineName)) return defaultEngineName; + return null; + }; + + let forceChangeVoice = false; if (tts.isSupported()) { //if android we have to set the tts engine first - if (isAndroid()) { - getTtsEngines(); - getTtsDefaultEngine(); + try { + if (isAndroid()) { + getTtsEngines(); + getTtsDefaultEngine(); + } + } catch (error) { + console.error(error); } - if (ttsEngine && ttsEngine.name) { + + const ttsEngineName = getTtsEngineName(); + + if (ttsEngineName) { try { - await setTtsEngine(ttsEngine.name); + await setTtsEngine(ttsEngineName); } catch (err) { console.error(err.message); + forceChangeVoice = false; } } try { const voices = await getVoices(); - await updateLangSpeechStatus(voices); + await updateLangSpeechStatus(voices, forceChangeVoice); } catch (err) { console.error(err.message); } + setDownloadingLang({ ...downloadingLang, isUpdated: true }); } setCurrentVoiceSource(); } @@ -62,7 +104,11 @@ export class SpeechProvider extends Component { } const mapStateToProps = state => ({ - ttsEngine: state.speech.ttsEngine + ttsEngines: state.speech.ttsEngines, + ttsEngine: state.speech.ttsEngine, + ttsDefaultEngine: state.speech.ttsDefaultEngine, + //todo: downloadingVoices + downloadingLang: state.language.downloadingLang }); const mapDispatchToProps = { @@ -71,7 +117,9 @@ const mapDispatchToProps = { getTtsDefaultEngine, setTtsEngine, updateLangSpeechStatus, - setCurrentVoiceSource + setCurrentVoiceSource, + //todo: setDownloadingVoices + setDownloadingLang }; export default connect( diff --git a/src/reducers.js b/src/reducers.js index 26e891a95..ad32e6ecb 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -1,8 +1,4 @@ -import { - persistCombineReducers, - persistReducer, - createMigrate -} from 'redux-persist'; +import { persistCombineReducers, createMigrate } from 'redux-persist'; import appReducer from './components/App/App.reducer'; import languageProviderReducer from './providers/LanguageProvider/LanguageProvider.reducer'; @@ -30,21 +26,14 @@ const boardMigrations = { const config = { key: 'root', storage, - blacklist: ['language'], version: 0, migrate: createMigrate(boardMigrations, { debug: false }) }; -const languagePersistConfig = { - key: 'language', - storage: storage, - blacklist: ['langsFetched'] -}; - export default function createReducer() { return persistCombineReducers(config, { app: appReducer, - language: persistReducer(languagePersistConfig, languageProviderReducer), + language: languageProviderReducer, speech: speechProviderReducer, board: boardReducer, communicator: communicatorReducer,