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

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  import { html } from 'lit';
2
2
 
3
- import { viewableFilesIcon } from '@internetarchive/ia-item-navigator';
4
- import '@internetarchive/ia-item-navigator';
3
+ import { viewableFilesIcon } from '@internetarchive/ia-item-navigator/dist/src/menus/viewable-files';
4
+ import '@internetarchive/ia-item-navigator/dist/src/menus/viewable-files';
5
5
 
6
6
  /**
7
7
  * * @typedef { 'title_asc' | 'title_desc' | 'default'} SortTypesT
@@ -11,12 +11,12 @@ const sortTypes = {
11
11
  title_desc: 'title_desc',
12
12
  default: 'default'
13
13
  };
14
- export default class VolumesProvider {
14
+ export default class ViewableFilesProvider {
15
15
  /**
16
- * @param {import('../../BookReader').default} bookreader
16
+ * @param {import('../BookReader').default} bookreader
17
17
  */
18
18
  constructor({ baseHost, bookreader, onProviderChange }) {
19
- /** @type {import('../../BookReader').default} */
19
+ /** @type {import('../BookReader').default} */
20
20
  this.bookreader = bookreader;
21
21
  this.onProviderChange = onProviderChange;
22
22
  this.baseHost = baseHost;
@@ -30,13 +30,13 @@ export default class VolumesProvider {
30
30
  this.icon = html`${viewableFilesIcon}`;
31
31
  this.sortOrderBy = sortTypes.default;
32
32
 
33
- this.component = document.createElement("iaux-viewable-files");
33
+ this.component = document.createElement("iaux-in-viewable-files-panel");
34
34
  this.component.addSortToUrl = true;
35
35
  this.component.subPrefix = bookreader.options.subPrefix || "";
36
36
  this.component.baseHost = baseHost;
37
37
  this.component.fileList = [...this.viewableFiles];
38
38
 
39
- this.sortFilesComponent = document.createElement("iaux-sort-viewable-files");
39
+ this.sortFilesComponent = document.createElement("iaux-in-sort-files-button");
40
40
  this.sortFilesComponent.fileListRaw = this.viewableFiles;
41
41
  this.sortFilesComponent.addEventListener('fileListSorted', (e) => this.handleFileListSorted(e));
42
42
  this.actionButton = this.sortFilesComponent;
package/src/BookReader.js CHANGED
@@ -1788,7 +1788,7 @@ BookReader.prototype.paramsFromFragment = function(fragment) {
1788
1788
  // Index and page
1789
1789
  if ('undefined' != typeof(urlHash['page'])) {
1790
1790
  // page was set -- may not be int
1791
- params.page = urlHash['page'];
1791
+ params.page = decodeURIComponent(urlHash['page']);
1792
1792
  }
1793
1793
 
1794
1794
  // $$$ process /region
@@ -1820,11 +1820,10 @@ BookReader.prototype.paramsFromFragment = function(fragment) {
1820
1820
  * @return {string}
1821
1821
  */
1822
1822
  BookReader.prototype.fragmentFromParams = function(params, urlMode = 'hash') {
1823
- const separator = '/';
1824
1823
  const fragments = [];
1825
1824
 
1826
1825
  if ('undefined' != typeof(params.page)) {
1827
- fragments.push('page', params.page);
1826
+ fragments.push('page', encodeURIComponent(params.page));
1828
1827
  } else {
1829
1828
  if ('undefined' != typeof(params.index)) {
1830
1829
  // Don't have page numbering but we do have the index
@@ -1850,10 +1849,10 @@ BookReader.prototype.fragmentFromParams = function(params, urlMode = 'hash') {
1850
1849
 
1851
1850
  // search
1852
1851
  if (params.search && urlMode === 'hash') {
1853
- fragments.push('search', params.search);
1852
+ fragments.push('search', utils.encodeURIComponentPlus(params.search));
1854
1853
  }
1855
1854
 
1856
- return utils.encodeURIComponentPlus(fragments.join(separator)).replace(/%2F/g, '/');
1855
+ return fragments.join('/');
1857
1856
  };
1858
1857
 
1859
1858
  /**
@@ -30,7 +30,7 @@ export class IaBookReader extends LitElement {
30
30
  super();
31
31
  this.item = undefined;
32
32
  this.bookreader = undefined;
33
- this.baseHost = 'https://archive.org';
33
+ this.baseHost = 'archive.org';
34
34
  this.fullscreen = false;
35
35
  this.signedIn = false;
36
36
  /** @type {ModalManager} */
@@ -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
@@ -9,7 +9,7 @@ import DownloadsProvider from '@/src/BookNavigator/downloads/downloads-provider.
9
9
  import SearchProvider from '@/src/BookNavigator/search/search-provider.js';
10
10
  import SharingProvider from '@/src/BookNavigator/sharing.js';
11
11
  import VisualAdjustmentsProvider from '@/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js';
12
- import VolumesProvider from '@/src/BookNavigator/volumes/volumes-provider.js';
12
+ import ViewableFilesProvider from '@/src/BookNavigator/viewable-files.js';
13
13
  import { ModalManager } from '@internetarchive/modal-manager';
14
14
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
15
15
  import '@/src/BookNavigator/book-navigator.js';
@@ -214,7 +214,7 @@ describe('<book-navigator>', () => {
214
214
  await el.elementUpdated;
215
215
 
216
216
  expect(el.menuProviders.volumes).toBeDefined();
217
- expect(el.menuProviders.volumes).toBeInstanceOf(VolumesProvider);
217
+ expect(el.menuProviders.volumes).toBeInstanceOf(ViewableFilesProvider);
218
218
 
219
219
  // also adds a menu shortcut
220
220
  expect(el.menuShortcuts.find(m => m.id === 'volumes')).toBeDefined();
@@ -30,7 +30,7 @@ describe('Sharing Provider', () => {
30
30
  expect(provider.id).toEqual('share');
31
31
  expect(provider.icon).toBeDefined();
32
32
  expect(provider.label).toEqual('Share this book');
33
- expect(fixtureSync(provider.component).tagName).toContain('IAUX-SHARING-OPTIONS');
33
+ expect(fixtureSync(provider.component).tagName).toContain('IAUX-IN-SHARE-PANEL');
34
34
  });
35
35
 
36
36
  describe('Handles being a sub file/volume', () => {
@@ -1,6 +1,6 @@
1
1
  import { fixtureCleanup, fixtureSync } from '@open-wc/testing-helpers';
2
2
  import sinon from 'sinon';
3
- import volumesProvider from '@/src/BookNavigator/volumes/volumes-provider';
3
+ import ViewableFilesProvider from '@/src/BookNavigator/viewable-files';
4
4
 
5
5
  const brOptions = {
6
6
  "options": {
@@ -44,7 +44,7 @@ describe('Volumes Provider', () => {
44
44
  const onProviderChange = sinon.fake();
45
45
 
46
46
  const baseHost = "https://archive.org";
47
- const provider = new volumesProvider({
47
+ const provider = new ViewableFilesProvider({
48
48
  baseHost,
49
49
  bookreader: brOptions,
50
50
  onProviderChange
@@ -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