@internetarchive/bookreader 5.0.0-70-a3 → 5.0.0-72
Sign up to get free protection for your applications and to get access to all the features.
- package/.github/workflows/node.js.yml +16 -16
- package/.github/workflows/npm-publish.yml +6 -6
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/CHANGELOG.md +14 -0
- package/netlify.toml +1 -1
- package/package.json +6 -7
- package/scripts/preversion.js +0 -4
- package/src/BookReader/utils/SelectionObserver.js +3 -1
- package/src/BookReader.js +4 -5
- package/src/plugins/tts/AbstractTTSEngine.js +25 -3
- package/src/plugins/tts/plugin.tts.js +1 -1
- package/src/plugins/tts/utils.js +15 -0
- package/src/plugins/url/UrlPlugin.js +5 -7
- package/tests/jest/BookReader/utils/SelectionObserver.test.js +14 -0
- package/tests/jest/plugins/tts/AbstractTTSEngine.test.js +20 -0
- package/tests/jest/plugins/url/UrlPlugin.test.js +8 -0
- /package/tests/jest/BookNavigator/{volumes/volumes-provider.test.js → viewable-files/viewable-files-provider.test.js} +0 -0
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
|
1855
|
+
return fragments.join('/');
|
1857
1856
|
};
|
1858
1857
|
|
1859
1858
|
/**
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import PageChunkIterator from './PageChunkIterator.js';
|
2
|
+
import { hasLocalStorage } from './utils.js';
|
2
3
|
/** @typedef {import('./utils.js').ISO6391} ISO6391 */
|
3
4
|
/** @typedef {import('./PageChunk.js')} PageChunk */
|
4
5
|
|
@@ -80,6 +81,7 @@ export default class AbstractTTSEngine {
|
|
80
81
|
*/
|
81
82
|
start(leafIndex, numLeafs) {
|
82
83
|
this.playing = true;
|
84
|
+
this.paused = false;
|
83
85
|
this.opts.onLoadingStart();
|
84
86
|
|
85
87
|
this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
|
@@ -95,6 +97,7 @@ export default class AbstractTTSEngine {
|
|
95
97
|
stop() {
|
96
98
|
if (this.activeSound) this.activeSound.stop();
|
97
99
|
this.playing = false;
|
100
|
+
this.paused = true;
|
98
101
|
this._chunkIterator = null;
|
99
102
|
this.activeSound = null;
|
100
103
|
this.events.trigger('stop');
|
@@ -142,6 +145,10 @@ export default class AbstractTTSEngine {
|
|
142
145
|
// MS Edge fires voices changed randomly very often
|
143
146
|
this.events.off('voiceschanged', this.updateBestVoice);
|
144
147
|
this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI);
|
148
|
+
// if the current book has a language set, store the selected voice with the book language as a suffix
|
149
|
+
if (this.opts.bookLanguage && hasLocalStorage()) {
|
150
|
+
localStorage.setItem(`BRtts-voice-${this.opts.bookLanguage}`, this.voice.voiceURI);
|
151
|
+
}
|
145
152
|
if (this.activeSound) this.activeSound.setVoice(this.voice);
|
146
153
|
}
|
147
154
|
|
@@ -221,10 +228,12 @@ export default class AbstractTTSEngine {
|
|
221
228
|
// user languages that match the book language
|
222
229
|
const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
|
223
230
|
|
224
|
-
//
|
225
|
-
return AbstractTTSEngine.
|
231
|
+
// First try to find the last chosen voice from localStorage for the current book language
|
232
|
+
return AbstractTTSEngine.getMatchingStoredVoice(bookLangVoices, bookLanguage)
|
233
|
+
// Try to find voices that intersect these two sets
|
234
|
+
|| AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices)
|
226
235
|
// no user languages match the books; let's return the best voice for the book language
|
227
|
-
(bookLangVoices.find(v => v.default) || bookLangVoices[0])
|
236
|
+
|| (bookLangVoices.find(v => v.default) || bookLangVoices[0])
|
228
237
|
// No voices match the book language? let's find a voice in the user's language
|
229
238
|
// and ignore book lang
|
230
239
|
|| AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
|
@@ -232,6 +241,19 @@ export default class AbstractTTSEngine {
|
|
232
241
|
|| (voices.find(v => v.default) || voices[0]);
|
233
242
|
}
|
234
243
|
|
244
|
+
/**
|
245
|
+
* @private
|
246
|
+
* Get the voice last selected by the user for the book language from localStorage.
|
247
|
+
* Returns undefined if no voice is stored or found.
|
248
|
+
* @param {SpeechSynthesisVoice[]} voices browser voices to choose from
|
249
|
+
* @param {ISO6391} bookLanguage book language to look for
|
250
|
+
* @return {SpeechSynthesisVoice | undefined}
|
251
|
+
*/
|
252
|
+
static getMatchingStoredVoice(voices, bookLanguage) {
|
253
|
+
const storedVoice = hasLocalStorage() && localStorage.getItem(`BRtts-voice-${bookLanguage}`);
|
254
|
+
return (storedVoice ? voices.find(v => v.voiceURI === storedVoice) : undefined);
|
255
|
+
}
|
256
|
+
|
235
257
|
/**
|
236
258
|
* @private
|
237
259
|
* Get the best voice that matches one of the BCP47 languages (order by preference)
|
package/src/plugins/tts/utils.js
CHANGED
@@ -64,3 +64,18 @@ function searchForISO6391(language, columnsToSearch) {
|
|
64
64
|
}
|
65
65
|
return null;
|
66
66
|
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Checks whether the current browser supports localStorage or
|
70
|
+
* if the current context has access to it.
|
71
|
+
* @return {boolean}
|
72
|
+
*/
|
73
|
+
export function hasLocalStorage() {
|
74
|
+
try {
|
75
|
+
return !!window.localStorage;
|
76
|
+
} catch (e) {
|
77
|
+
// Will throw in sandboxed iframe
|
78
|
+
// DOMException: Window.localStorage getter: Forbidden in a sandboxed document without the 'allow-same-origin' flag.
|
79
|
+
return false;
|
80
|
+
}
|
81
|
+
}
|
@@ -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
|
-
|
87
|
-
|
86
|
+
// Not in the URL
|
87
|
+
if (!hasPropertyKey && !hasDeprecatedKey) {
|
88
88
|
return;
|
89
89
|
}
|
90
90
|
|
91
|
-
|
92
|
-
|
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
|
@@ -40,4 +40,18 @@ describe("SelectionObserver", () => {
|
|
40
40
|
observer._onSelectionChange();
|
41
41
|
expect(handler.callCount).toBe(2);
|
42
42
|
});
|
43
|
+
|
44
|
+
test('Only fires when selection started in selector', () => {
|
45
|
+
const handler = sinon.spy();
|
46
|
+
const observer = new SelectionObserver(".text-layer", handler);
|
47
|
+
const target = document.createElement("div");
|
48
|
+
target.classList.add("text-layer");
|
49
|
+
|
50
|
+
// stub window.getSelection
|
51
|
+
const getSelectionStub = sinon.stub(window, "getSelection");
|
52
|
+
getSelectionStub.returns({ toString: () => "test", anchorNode: document.body });
|
53
|
+
observer._onSelectionChange();
|
54
|
+
expect(handler.callCount).toBe(0);
|
55
|
+
expect(observer.selecting).toBe(false);
|
56
|
+
});
|
43
57
|
});
|
@@ -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
|
|