@internetarchive/bookreader 5.0.0-70-a2 → 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.
@@ -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