@internetarchive/bookreader 5.0.0-70-a3 → 5.0.0-71

Sign up to get free protection for your applications and to get access to all the features.
@@ -142,6 +142,10 @@ export default class AbstractTTSEngine {
142
142
  // MS Edge fires voices changed randomly very often
143
143
  this.events.off('voiceschanged', this.updateBestVoice);
144
144
  this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI);
145
+ // if the current book has a language set, store the selected voice with the book language as a suffix
146
+ if (this.opts.bookLanguage) {
147
+ localStorage.setItem(`BRtts-voice-${this.opts.bookLanguage}`, this.voice.voiceURI);
148
+ }
145
149
  if (this.activeSound) this.activeSound.setVoice(this.voice);
146
150
  }
147
151
 
@@ -221,10 +225,12 @@ export default class AbstractTTSEngine {
221
225
  // user languages that match the book language
222
226
  const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
223
227
 
224
- // Try to find voices that intersect these two sets
225
- return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) ||
228
+ // First try to find the last chosen voice from localStorage for the current book language
229
+ return AbstractTTSEngine.getMatchingStoredVoice(bookLangVoices, bookLanguage)
230
+ // Try to find voices that intersect these two sets
231
+ || AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices)
226
232
  // no user languages match the books; let's return the best voice for the book language
227
- (bookLangVoices.find(v => v.default) || bookLangVoices[0])
233
+ || (bookLangVoices.find(v => v.default) || bookLangVoices[0])
228
234
  // No voices match the book language? let's find a voice in the user's language
229
235
  // and ignore book lang
230
236
  || AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
@@ -232,6 +238,19 @@ export default class AbstractTTSEngine {
232
238
  || (voices.find(v => v.default) || voices[0]);
233
239
  }
234
240
 
241
+ /**
242
+ * @private
243
+ * Get the voice last selected by the user for the book language from localStorage.
244
+ * Returns undefined if no voice is stored or found.
245
+ * @param {SpeechSynthesisVoice[]} voices browser voices to choose from
246
+ * @param {ISO6391} bookLanguage book language to look for
247
+ * @return {SpeechSynthesisVoice | undefined}
248
+ */
249
+ static getMatchingStoredVoice(voices, bookLanguage) {
250
+ const storedVoice = localStorage.getItem(`BRtts-voice-${bookLanguage}`);
251
+ return (storedVoice ? voices.find(v => v.voiceURI === storedVoice) : undefined);
252
+ }
253
+
235
254
  /**
236
255
  * @private
237
256
  * Get the best voice that matches one of the BCP47 languages (order by preference)
@@ -45,7 +45,7 @@ export class UrlPlugin {
45
45
 
46
46
  const strPathParams = this.urlSchema
47
47
  .filter(s => s.position == 'path')
48
- .map(schema => pathParams[schema.name] ? `${schema.name}/${pathParams[schema.name]}` : '')
48
+ .map(schema => pathParams[schema.name] ? `${schema.name}/${encodeURIComponent(pathParams[schema.name])}` : '')
49
49
  .join('/');
50
50
 
51
51
  // replace consecutive slashes with a single slash + remove trailing slashes
@@ -83,15 +83,13 @@ export class UrlPlugin {
83
83
  const hasPropertyKey = doesKeyExists(urlStrSplitSlashObj, schema.name);
84
84
  const hasDeprecatedKey = doesKeyExists(schema, 'deprecated_for') && hasPropertyKey;
85
85
 
86
- if (hasDeprecatedKey) {
87
- urlState[schema.deprecated_for] = urlStrSplitSlashObj[schema.name];
86
+ // Not in the URL
87
+ if (!hasPropertyKey && !hasDeprecatedKey) {
88
88
  return;
89
89
  }
90
90
 
91
- if (hasPropertyKey) {
92
- urlState[schema.name] = urlStrSplitSlashObj[schema.name];
93
- return;
94
- }
91
+ const urlStateParam = hasDeprecatedKey ? schema.deprecated_for : schema.name;
92
+ urlState[urlStateParam] = decodeURIComponent(urlStrSplitSlashObj[schema.name]);
95
93
  });
96
94
 
97
95
  // Add searchParams to urlState
@@ -111,6 +111,26 @@ for (const dummyVoice of [dummyVoiceHyphens, dummyVoiceUnderscores]) {
111
111
 
112
112
  expect(getBestBookVoice(voices, 'en', ['en-CA', 'en'])).toBe(voices[0]);
113
113
  });
114
+
115
+ test('choose stored language from localStorage', () => {
116
+ const voices = [
117
+ dummyVoice({lang: "en-US", voiceURI: "English US", default: true}),
118
+ dummyVoice({lang: "en-GB", voiceURI: "English GB"}),
119
+ dummyVoice({lang: "en-CA", voiceURI: "English CA"}),
120
+ ];
121
+ class DummyEngine extends AbstractTTSEngine {
122
+ getVoices() { return voices; }
123
+ }
124
+ const ttsEngine = new DummyEngine({...DUMMY_TTS_ENGINE_OPTS, bookLanguage: 'en'});
125
+ // simulates setting default voice on tts startup
126
+ ttsEngine.updateBestVoice();
127
+ // simulates user choosing a voice that matches the bookLanguage
128
+ // voice will be stored in localStorage
129
+ ttsEngine.setVoice(voices[2].voiceURI);
130
+
131
+ // expecting the voice to be selected by getMatchingStoredVoice and returned as best voice
132
+ expect(getBestBookVoice(voices, 'en', [])).toBe(voices[2]);
133
+ });
114
134
  });
115
135
  }
116
136
 
@@ -19,6 +19,10 @@ describe('UrlPlugin tests', () => {
19
19
  expect(urlPlugin.urlStateToUrlString(urlStateWithQueries)).toBe(expectedUrlFromStateWithQueries);
20
20
  });
21
21
 
22
+ test('encodes page number', () => {
23
+ expect(urlPlugin.urlStateToUrlString({ page: '12/46' })).toBe(`page/12%2F46`);
24
+ });
25
+
22
26
  test('urlStateToUrlString with unknown states in schema', () => {
23
27
  const urlState = { page: 'n7', mode: '1up' };
24
28
  const urlStateWithQueries = { page: 'n7', mode: '1up', q: 'hello', viewer: 'theater', sortBy: 'title_asc' };
@@ -47,6 +51,10 @@ describe('UrlPlugin tests', () => {
47
51
  expect(urlPlugin.urlStringToUrlState(url1)).toEqual({page: 'n7', mode: '1up'});
48
52
  });
49
53
 
54
+ test('decodes page number', () => {
55
+ expect(urlPlugin.urlStringToUrlState('/page/12%2F46')).toStrictEqual({ page: '12/46' });
56
+ });
57
+
50
58
  test('urlStringToUrlState with deprecated_for', () => {
51
59
  const url = '/page/n7/mode/2up/search/hello';
52
60