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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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