blob: 59d45544d025e0f4b2be6e123d6aaa11266e584f [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Manages getting and storing user preferences.
*/
export class PrefsManager {
constructor() {
/** @private {?string} */
this.voiceNameFromPrefs_ = null;
/** @private {?string} */
this.voiceNameFromLocale_ = null;
/** @private {Set<string>} */
this.validVoiceNames_ = new Set();
/** @private {number} */
this.speechRate_ = 1.0;
/** @private {number} */
this.speechPitch_ = 1.0;
/** @private {boolean} */
this.wordHighlight_ = true;
/** @const {string} */
this.color_ = '#da36e8';
/** @private {string} */
this.highlightColor_ = '#5e9bff';
/** @private {boolean} */
this.migrationInProgress_ = false;
/** @private {boolean} */
this.backgroundShadingEnabled_ = false;
/** @private {boolean} */
this.navigationControlsEnabled_ = true;
/** @private {boolean} */
this.enhancedNetworkVoicesEnabled_ = true;
}
/**
* Get the list of TTS voices, and set the default voice if not already set.
* @private
*/
updateDefaultVoice_() {
var uiLocale = chrome.i18n.getMessage('@@ui_locale');
uiLocale = uiLocale.replace('_', '-').toLowerCase();
chrome.tts.getVoices((voices) => {
this.validVoiceNames_ = new Set();
if (voices.length === 0) {
return;
}
voices.forEach((voice) => {
if (!voice.eventTypes.includes(chrome.tts.EventType.START) ||
!voice.eventTypes.includes(chrome.tts.EventType.END) ||
!voice.eventTypes.includes(chrome.tts.EventType.WORD) ||
!voice.eventTypes.includes(chrome.tts.EventType.CANCELLED)) {
return;
}
if (voice.voiceName) {
this.validVoiceNames_.add(voice.voiceName);
}
});
voices.sort(function(a, b) {
function score(voice) {
if (voice.lang === undefined) {
return -1;
}
var lang = voice.lang.toLowerCase();
var s = 0;
if (lang === uiLocale) {
s += 2;
}
if (lang.substr(0, 2) === uiLocale.substr(0, 2)) {
s += 1;
}
return s;
}
return score(b) - score(a);
});
const firstVoiceName = voices[0].voiceName;
if (firstVoiceName) {
this.voiceNameFromLocale_ = firstVoiceName;
}
chrome.storage.sync.get(['voice'], (prefs) => {
if (!prefs['voice']) {
chrome.storage.sync.set({'voice': PrefsManager.SYSTEM_VOICE});
}
});
});
}
/**
* Migrates Select-to-Speak rate and pitch settings to global Text-to-Speech
* settings. This is a one-time migration that happens on upgrade to M70.
* See http://crbug.com/866550.
* @param {string} rateStr
* @param {string} pitchStr
* @private
*/
migrateToGlobalTtsSettings_(rateStr, pitchStr) {
if (this.migrationInProgress_) {
return;
}
this.migrationInProgress_ = true;
let stsRate = PrefsManager.DEFAULT_RATE;
let stsPitch = PrefsManager.DEFAULT_PITCH;
let globalRate = PrefsManager.DEFAULT_RATE;
let globalPitch = PrefsManager.DEFAULT_PITCH;
if (rateStr !== undefined) {
stsRate = parseFloat(rateStr);
}
if (pitchStr !== undefined) {
stsPitch = parseFloat(pitchStr);
}
// Get global prefs using promises so that we can receive both pitch and
// rate before doing migration logic.
const getPrefsPromises = [];
getPrefsPromises.push(new Promise((resolve, reject) => {
chrome.settingsPrivate.getPref('settings.tts.speech_rate', (pref) => {
if (pref === undefined) {
reject();
}
globalRate = pref.value;
resolve();
});
}));
getPrefsPromises.push(new Promise((resolve, reject) => {
chrome.settingsPrivate.getPref('settings.tts.speech_pitch', (pref) => {
if (pref === undefined) {
reject();
}
globalPitch = pref.value;
resolve();
});
}));
Promise.all(getPrefsPromises)
.then(
() => {
const stsOptionsModified =
stsRate !== PrefsManager.DEFAULT_RATE ||
stsPitch !== PrefsManager.DEFAULT_PITCH;
const globalOptionsModified =
globalRate !== PrefsManager.DEFAULT_RATE ||
globalPitch !== PrefsManager.DEFAULT_PITCH;
const optionsEqual =
stsRate === globalRate && stsPitch === globalPitch;
if (optionsEqual) {
// No need to write global prefs if all the prefs are the same
// as defaults. Just remove STS rate and pitch.
this.onTtsSettingsMigrationSuccess_();
return;
}
if (stsOptionsModified && !globalOptionsModified) {
// Set global prefs using promises so we can set both rate and
// pitch successfully before removing the preferences from
// chrome.storage.sync.
const setPrefsPromises = [];
setPrefsPromises.push(new Promise((resolve, reject) => {
chrome.settingsPrivate.setPref(
'settings.tts.speech_rate', stsRate,
'' /* unused, see crbug.com/866161 */, (success) => {
if (success) {
resolve();
} else {
reject();
}
});
}));
setPrefsPromises.push(new Promise((resolve, reject) => {
chrome.settingsPrivate.setPref(
'settings.tts.speech_pitch', stsPitch,
'' /* unused, see crbug.com/866161 */, (success) => {
if (success) {
resolve();
} else {
reject();
}
});
}));
Promise.all(setPrefsPromises)
.then(
this.onTtsSettingsMigrationSuccess_.bind(this),
(error) => {
console.log(error);
this.migrationInProgress_ = false;
});
} else if (globalOptionsModified) {
// Global options were already modified, so STS will use global
// settings regardless of whether STS was modified yet or not.
this.onTtsSettingsMigrationSuccess_();
}
},
(error) => {
console.log(error);
this.migrationInProgress_ = false;
});
}
/**
* When TTS settings are successfully migrated, removes rate and pitch from
* chrome.storage.sync.
* @private
*/
onTtsSettingsMigrationSuccess_() {
chrome.storage.sync.remove('rate');
chrome.storage.sync.remove('pitch');
this.migrationInProgress_ = false;
}
/**
* Loads preferences from chrome.storage, sets default values if
* necessary, and registers a listener to update prefs when they
* change.
*/
initPreferences() {
var updatePrefs = () => {
chrome.storage.sync.get(
[
'voice', 'rate', 'pitch', 'wordHighlight', 'highlightColor',
'backgroundShading', 'navigationControls', 'enhancedNetworkVoices'
],
(prefs) => {
if (prefs['voice']) {
this.voiceNameFromPrefs_ = prefs['voice'];
}
if (prefs['wordHighlight'] !== undefined) {
this.wordHighlight_ = prefs['wordHighlight'];
} else {
chrome.storage.sync.set({'wordHighlight': this.wordHighlight_});
}
if (prefs['highlightColor']) {
this.highlightColor_ = prefs['highlightColor'];
} else {
chrome.storage.sync.set({'highlightColor': this.highlightColor_});
}
if (prefs['backgroundShading'] !== undefined) {
this.backgroundShadingEnabled_ = prefs['backgroundShading'];
} else {
chrome.storage.sync.set(
{'backgroundShading': this.backgroundShadingEnabled_});
}
if (prefs['navigationControls'] !== undefined) {
this.navigationControlsEnabled_ = prefs['navigationControls'];
} else {
chrome.storage.sync.set(
{'navigationControls': this.navigationControlsEnabled_});
}
if (prefs['enhancedNetworkVoices'] !== undefined) {
this.enhancedNetworkVoicesEnabled_ =
prefs['enhancedNetworkVoices'];
} else {
chrome.storage.sync.set({
'enhancedNetworkVoices': this.enhancedNetworkVoicesEnabled_
});
}
if (prefs['rate'] && prefs['pitch']) {
// Removes 'rate' and 'pitch' prefs after migrating data to global
// TTS settings if appropriate.
this.migrateToGlobalTtsSettings_(prefs['rate'], prefs['pitch']);
}
});
};
updatePrefs();
chrome.storage.onChanged.addListener(updatePrefs);
this.updateDefaultVoice_();
window.speechSynthesis.onvoiceschanged = () => {
this.updateDefaultVoice_();
};
}
/**
* Generates the basic speech options for Select-to-Speak based on user
* preferences. Call for each chrome.tts.speak.
* @return {!chrome.tts.TtsOptions} options The TTS options.
*/
speechOptions() {
const options = /** @type {!chrome.tts.TtsOptions} */ ({});
// To use the default (system) voice: don't specify options['voiceName'].
if (this.voiceNameFromPrefs_ === PrefsManager.SYSTEM_VOICE) {
return options;
}
// Pick the voice name from prefs first, or the one that matches
// the locale next, but don't pick a voice that isn't currently
// loaded. If no voices are found, leave the voiceName option
// unset to let the browser try to route the speech request
// anyway if possible.
var valid = '';
this.validVoiceNames_.forEach(function(voiceName) {
if (valid) {
valid += ',';
}
valid += voiceName;
});
if (this.voiceNameFromPrefs_ &&
this.validVoiceNames_.has(this.voiceNameFromPrefs_)) {
options['voiceName'] = this.voiceNameFromPrefs_;
} else if (
this.voiceNameFromLocale_ &&
this.validVoiceNames_.has(this.voiceNameFromLocale_)) {
options['voiceName'] = this.voiceNameFromLocale_;
}
return options;
}
/**
* Gets the user's word highlighting enabled preference.
* @return {boolean} True if word highlighting is enabled.
*/
wordHighlightingEnabled() {
return this.wordHighlight_;
}
/**
* Gets the user's word highlighting color preference.
* @return {string} Highlight color.
*/
highlightColor() {
return this.highlightColor_;
}
/**
* Gets the focus ring color. This is not currently a user preference but it
* could be in the future; stored here for similarity to highlight color.
* @return {string} Highlight color.
*/
focusRingColor() {
return this.color_;
}
/**
* Gets the user's focus ring background color. If the user disabled greying
* out the background, alpha will be set to fully transparent.
* @return {boolean} True if the background shade should be drawn.
*/
backgroundShadingEnabled() {
return this.backgroundShadingEnabled_;
}
/**
* Gets the user's preference for showing navigation controls that allow them
* to navigate to next/previous sentences, paragraphs, and more.
* @return {boolean} True if navigation controls should be shown when STS is
* active.
*/
navigationControlsEnabled() {
return this.navigationControlsEnabled_;
}
/**
* Gets the user's preference for whether enhanced network TTS voices are
* enabled.
* @return {boolean} True if enhanced TTS voices are enabled.
*/
enhancedNetworkVoicesEnabled() {
return this.enhancedNetworkVoicesEnabled_;
}
}
/**
* Constant representing the system TTS voice.
* @type {string}
*/
PrefsManager.SYSTEM_VOICE = 'select_to_speak_system_voice';
/**
* Default speech rate for both Select-to-Speak and global prefs.
* @type {number}
*/
PrefsManager.DEFAULT_RATE = 1.0;
/**
* Default speech pitch for both Select-to-Speak and global prefs.
* @type {number}
*/
PrefsManager.DEFAULT_PITCH = 1.0;