@internetarchive/bookreader 5.0.0-88 → 5.0.0-89
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.
- package/BookReader/BookReader.css +14 -0
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.iiif.js.map +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.resume.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 +10 -0
- package/codecov.yml +1 -1
- package/package.json +1 -1
- package/src/BookReader/ImageCache.js +48 -15
- package/src/BookReader/Mode2UpLit.js +3 -2
- package/src/BookReader/PageContainer.js +41 -22
- package/src/BookReader/options.js +24 -3
- package/src/BookReader/utils.js +10 -0
- package/src/BookReader.js +89 -38
- package/src/BookReaderPlugin.js +16 -0
- package/src/css/_BRpages.scss +21 -2
- package/src/plugins/plugin.autoplay.js +98 -102
- package/src/plugins/plugin.iiif.js +16 -30
- package/src/plugins/plugin.resume.js +54 -51
- package/src/plugins/plugin.text_selection.js +68 -76
- package/src/plugins/tts/AbstractTTSEngine.js +2 -4
- package/src/plugins/tts/PageChunk.js +5 -9
- package/src/plugins/tts/PageChunkIterator.js +3 -5
- package/src/plugins/tts/plugin.tts.js +309 -329
- package/src/plugins/url/plugin.url.js +1 -1
- package/src/util/strings.js +1 -0
- package/tests/e2e/autoplay.test.js +8 -5
- package/tests/e2e/helpers/base.js +2 -2
- package/tests/e2e/helpers/mockSearch.js +6 -9
- package/tests/jest/BookReader/PageContainer.test.js +96 -55
- package/tests/jest/BookReader/utils.test.js +21 -0
- package/tests/jest/BookReader.test.js +13 -12
- package/tests/jest/plugins/plugin.autoplay.test.js +9 -22
- package/tests/jest/plugins/plugin.resume.test.js +19 -32
- package/tests/jest/plugins/plugin.text_selection.test.js +23 -24
@@ -1,361 +1,341 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
* Plugin for Text to Speech in BookReader
|
4
|
-
*/
|
1
|
+
// @ts-check
|
2
|
+
|
5
3
|
import FestivalTTSEngine from './FestivalTTSEngine.js';
|
6
4
|
import WebTTSEngine from './WebTTSEngine.js';
|
7
5
|
import { toISO6391, approximateWordCount } from './utils.js';
|
8
6
|
import { en as tooltips } from './tooltip_dict.js';
|
9
7
|
import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
|
8
|
+
import { BookReaderPlugin } from '../../BookReaderPlugin.js';
|
9
|
+
import { applyVariables } from '../../util/strings.js';
|
10
10
|
/** @typedef {import('./PageChunk.js').default} PageChunk */
|
11
11
|
/** @typedef {import("./AbstractTTSEngine.js").default} AbstractTTSEngine */
|
12
12
|
|
13
|
-
|
14
|
-
jQuery.extend(BookReader.defaultOptions, {
|
15
|
-
server: 'ia600609.us.archive.org',
|
16
|
-
bookPath: '',
|
17
|
-
enableTtsPlugin: true,
|
18
|
-
});
|
19
|
-
|
20
|
-
// Extend the constructor to add TTS properties
|
21
|
-
BookReader.prototype.setup = (function (super_) {
|
22
|
-
return function (options) {
|
23
|
-
super_.call(this, options);
|
24
|
-
|
25
|
-
if (this.options.enableTtsPlugin) {
|
26
|
-
/** @type { {[pageIndex: number]: Array<{ l: number, r: number, t: number, b: number }>} } */
|
27
|
-
this._ttsBoxesByIndex = {};
|
28
|
-
|
29
|
-
let TTSEngine = WebTTSEngine.isSupported() ? WebTTSEngine :
|
30
|
-
FestivalTTSEngine.isSupported() ? FestivalTTSEngine :
|
31
|
-
null;
|
32
|
-
|
33
|
-
if (/_forceTTSEngine=(festival|web)/.test(location.toString())) {
|
34
|
-
const engineName = location.toString().match(/_forceTTSEngine=(festival|web)/)[1];
|
35
|
-
TTSEngine = { festival: FestivalTTSEngine, web: WebTTSEngine }[engineName];
|
36
|
-
}
|
13
|
+
const BookReader = /** @type {typeof import('../../BookReader').default} */(window.BookReader);
|
37
14
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
15
|
+
/**
|
16
|
+
* Plugin for Text to Speech in BookReader
|
17
|
+
*/
|
18
|
+
export class TtsPlugin extends BookReaderPlugin {
|
19
|
+
options = {
|
20
|
+
enabled: true,
|
21
|
+
/**
|
22
|
+
* @type {import('@/src/util/strings.js').StringWithVars}
|
23
|
+
* The URL where to get PageChunk objects for a given page. Expects a var `pageIndex`
|
24
|
+
**/
|
25
|
+
pageChunkUrl: 'https://{{server}}/BookReader/BookReaderGetTextWrapper.php?path={{bookPath|urlencode}}_djvu.xml&page={{pageIndex}}&callback=false',
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* @override
|
30
|
+
* @param {Partial<TtsPlugin['options']>} options
|
31
|
+
**/
|
32
|
+
setup(options) {
|
33
|
+
super.setup(Object.assign({
|
34
|
+
// @deprecated support options specified in global options
|
35
|
+
server: this.br.options.server,
|
36
|
+
bookPath: this.br.options.bookPath,
|
37
|
+
}, options));
|
38
|
+
|
39
|
+
if (!this.options.enabled) return;
|
40
|
+
|
41
|
+
/** @type { {[pageIndex: number]: Array<{ l: number, r: number, t: number, b: number }>} } */
|
42
|
+
this._ttsBoxesByIndex = {};
|
43
|
+
|
44
|
+
let TTSEngine = WebTTSEngine.isSupported() ? WebTTSEngine :
|
45
|
+
FestivalTTSEngine.isSupported() ? FestivalTTSEngine :
|
46
|
+
null;
|
47
|
+
|
48
|
+
if (/_forceTTSEngine=(festival|web)/.test(location.toString())) {
|
49
|
+
const engineName = location.toString().match(/_forceTTSEngine=(festival|web)/)[1];
|
50
|
+
TTSEngine = { festival: FestivalTTSEngine, web: WebTTSEngine }[engineName];
|
51
51
|
}
|
52
|
-
};
|
53
|
-
})(BookReader.prototype.setup);
|
54
|
-
|
55
|
-
BookReader.prototype.init = (function(super_) {
|
56
|
-
return function() {
|
57
|
-
if (this.options.enableTtsPlugin) {
|
58
|
-
// Bind to events
|
59
|
-
|
60
|
-
this.bind(BookReader.eventNames.PostInit, () => {
|
61
|
-
this.$('.BRicon.read').click(() => {
|
62
|
-
this.ttsToggle();
|
63
|
-
return false;
|
64
|
-
});
|
65
|
-
if (this.ttsEngine) {
|
66
|
-
this.ttsEngine.init();
|
67
|
-
if (/[?&]_autoReadAloud=show/.test(location.toString())) {
|
68
|
-
this.ttsStart(false); // false flag is to initiate read aloud controls
|
69
|
-
}
|
70
|
-
}
|
71
|
-
});
|
72
52
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
53
|
+
if (TTSEngine) {
|
54
|
+
/** @type {AbstractTTSEngine} */
|
55
|
+
this.ttsEngine = new TTSEngine({
|
56
|
+
pageChunkUrl: applyVariables(this.options.pageChunkUrl, this.br.options.vars),
|
57
|
+
bookLanguage: toISO6391(this.br.options.bookLanguage),
|
58
|
+
onLoadingStart: this.br.showProgressPopup.bind(this.br, 'Loading audio...'),
|
59
|
+
onLoadingComplete: this.br.removeProgressPopup.bind(this.br),
|
60
|
+
onDone: this.stop.bind(this),
|
61
|
+
beforeChunkPlay: this.beforeChunkPlay.bind(this),
|
62
|
+
afterChunkPlay: this.sendChunkFinishedAnalyticsEvent.bind(this),
|
63
|
+
});
|
78
64
|
}
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
65
|
+
}
|
66
|
+
|
67
|
+
/** @override */
|
68
|
+
init() {
|
69
|
+
if (!this.options.enabled) return;
|
70
|
+
|
71
|
+
this.br.bind(BookReader.eventNames.PostInit, () => {
|
72
|
+
this.br.$('.BRicon.read').click(() => {
|
73
|
+
this.toggle();
|
74
|
+
return false;
|
75
|
+
});
|
76
|
+
if (this.ttsEngine) {
|
77
|
+
this.ttsEngine.init();
|
78
|
+
if (/[?&]_autoReadAloud=show/.test(location.toString())) {
|
79
|
+
this.start(false); // false flag is to initiate read aloud controls
|
80
|
+
}
|
81
|
+
}
|
82
|
+
});
|
83
|
+
|
84
|
+
// This is fired when the hash changes by one of the other plugins!
|
85
|
+
// i.e. it will fire every time the page changes -_-
|
86
|
+
// this.br.bind(BookReader.eventNames.stop, function(e, br) {
|
87
|
+
// this.ttsStop();
|
88
|
+
// }.bind(this));
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* @override
|
93
|
+
* @param {import ("@/src/BookReader/PageContainer.js").PageContainer} pageContainer
|
94
|
+
*/
|
95
|
+
_configurePageContainer(pageContainer) {
|
96
|
+
if (this.options.enabled && pageContainer.page && pageContainer.page.index in this._ttsBoxesByIndex) {
|
88
97
|
const pageIndex = pageContainer.page.index;
|
89
98
|
renderBoxesInPageContainerLayer('ttsHiliteLayer', this._ttsBoxesByIndex[pageIndex], pageContainer.page, pageContainer.$container[0]);
|
90
99
|
}
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* @override
|
104
|
+
* @param {JQuery<HTMLElement>} $navBar
|
105
|
+
*/
|
106
|
+
extendNavBar($navBar) {
|
107
|
+
if (!this.options.enabled || !this.ttsEngine) return;
|
108
|
+
|
109
|
+
this.br.refs.$BRReadAloudToolbar = $(`
|
110
|
+
<ul class="read-aloud">
|
101
111
|
<li>
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
</
|
106
|
-
<
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
</
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
// Extend initNavbar
|
118
|
-
BookReader.prototype.initNavbar = (function (super_) {
|
119
|
-
return function () {
|
120
|
-
const $el = super_.call(this);
|
121
|
-
if (this.options.enableTtsPlugin && this.ttsEngine) {
|
122
|
-
this.refs.$BRReadAloudToolbar = $(`
|
123
|
-
<ul class="read-aloud">
|
124
|
-
<li>
|
125
|
-
<select class="playback-speed" name="playback-speed" title="${tooltips.playbackSpeed}">
|
126
|
-
<option value="0.25">0.25x</option>
|
127
|
-
<option value="0.5">0.5x</option>
|
128
|
-
<option value="0.75">0.75x</option>
|
129
|
-
<option value="1.0" selected>1.0x</option>
|
130
|
-
<option value="1.25">1.25x</option>
|
131
|
-
<option value="1.5">1.5x</option>
|
132
|
-
<option value="1.75">1.75x</option>
|
133
|
-
<option value="2">2x</option>
|
134
|
-
</select>
|
135
|
-
</li>
|
136
|
-
<li>
|
137
|
-
<button type="button" name="review" title="${tooltips.review}">
|
138
|
-
<div class="icon icon-review"></div>
|
139
|
-
</button>
|
140
|
-
</li>
|
141
|
-
<li>
|
142
|
-
<button type="button" name="play" title="${tooltips.play}">
|
143
|
-
<div class="icon icon-play"></div>
|
144
|
-
<div class="icon icon-pause"></div>
|
145
|
-
</button>
|
146
|
-
</li>
|
147
|
-
<li>
|
148
|
-
<button type="button" name="advance" title="${tooltips.advance}">
|
149
|
-
<div class="icon icon-advance"></div>
|
150
|
-
</button>
|
151
|
-
</li>
|
152
|
-
<li>
|
153
|
-
<select class="playback-voices" name="playback-voice" style="display: none" title="Change read aloud voices">
|
154
|
-
</select>
|
155
|
-
</li>
|
156
|
-
</ul>
|
157
|
-
`);
|
158
|
-
|
159
|
-
$el.find('.BRcontrols').prepend(this.refs.$BRReadAloudToolbar);
|
160
|
-
|
161
|
-
const renderVoiceOption = (voices) => {
|
162
|
-
return voices.map(voice =>
|
163
|
-
`<option value="${voice.voiceURI}">${voice.lang} - ${voice.name}</option>`).join('');
|
164
|
-
};
|
165
|
-
|
166
|
-
const voiceSortOrder = (a,b) => `${a.lang} - ${a.name}`.localeCompare(`${b.lang} - ${b.name}`);
|
167
|
-
|
168
|
-
const renderVoicesMenu = (voicesMenu) => {
|
169
|
-
voicesMenu.empty();
|
170
|
-
const bookLanguage = this.ttsEngine.opts.bookLanguage;
|
171
|
-
const bookLanguages = this.ttsEngine.getVoices().filter(v => v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
172
|
-
const otherLanguages = this.ttsEngine.getVoices().filter(v => !v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
173
|
-
|
174
|
-
if (this.ttsEngine.getVoices().length > 1) {
|
175
|
-
voicesMenu.append($(`<optgroup label="Book Language (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
|
176
|
-
voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages)} </optgroup>`));
|
177
|
-
|
178
|
-
voicesMenu.val(this.ttsEngine.voice.voiceURI);
|
179
|
-
voicesMenu.show();
|
180
|
-
} else {
|
181
|
-
voicesMenu.hide();
|
182
|
-
}
|
183
|
-
};
|
184
|
-
|
185
|
-
const voicesMenu = this.refs.$BRReadAloudToolbar.find('[name=playback-voice]');
|
186
|
-
renderVoicesMenu(voicesMenu);
|
187
|
-
voicesMenu.on("change", ev => this.ttsEngine.setVoice(voicesMenu.val()));
|
188
|
-
this.ttsEngine.events.on('pause resume start', () => this.ttsUpdateState());
|
189
|
-
this.ttsEngine.events.on('voiceschanged', () => renderVoicesMenu(voicesMenu));
|
190
|
-
this.refs.$BRReadAloudToolbar.find('[name=play]').on("click", this.ttsPlayPause.bind(this));
|
191
|
-
this.refs.$BRReadAloudToolbar.find('[name=advance]').on("click", this.ttsJumpForward.bind(this));
|
192
|
-
this.refs.$BRReadAloudToolbar.find('[name=review]').on("click", this.ttsJumpBackward.bind(this));
|
193
|
-
const $rateSelector = this.refs.$BRReadAloudToolbar.find('select[name="playback-speed"]');
|
194
|
-
$rateSelector.on("change", ev => this.ttsEngine.setPlaybackRate(parseFloat($rateSelector.val())));
|
195
|
-
$(`<li>
|
196
|
-
<button class="BRicon read js-tooltip" title="${tooltips.readAloud}">
|
197
|
-
<div class="icon icon-read-aloud"></div>
|
198
|
-
<span class="BRtooltip">${tooltips.readAloud}</span>
|
112
|
+
<select class="playback-speed" name="playback-speed" title="${tooltips.playbackSpeed}">
|
113
|
+
<option value="0.25">0.25x</option>
|
114
|
+
<option value="0.5">0.5x</option>
|
115
|
+
<option value="0.75">0.75x</option>
|
116
|
+
<option value="1.0" selected>1.0x</option>
|
117
|
+
<option value="1.25">1.25x</option>
|
118
|
+
<option value="1.5">1.5x</option>
|
119
|
+
<option value="1.75">1.75x</option>
|
120
|
+
<option value="2">2x</option>
|
121
|
+
</select>
|
122
|
+
</li>
|
123
|
+
<li>
|
124
|
+
<button type="button" name="review" title="${tooltips.review}">
|
125
|
+
<div class="icon icon-review"></div>
|
199
126
|
</button>
|
200
|
-
</li
|
127
|
+
</li>
|
128
|
+
<li>
|
129
|
+
<button type="button" name="play" title="${tooltips.play}">
|
130
|
+
<div class="icon icon-play"></div>
|
131
|
+
<div class="icon icon-pause"></div>
|
132
|
+
</button>
|
133
|
+
</li>
|
134
|
+
<li>
|
135
|
+
<button type="button" name="advance" title="${tooltips.advance}">
|
136
|
+
<div class="icon icon-advance"></div>
|
137
|
+
</button>
|
138
|
+
</li>
|
139
|
+
<li>
|
140
|
+
<select class="playback-voices" name="playback-voice" style="display: none" title="Change read aloud voices">
|
141
|
+
</select>
|
142
|
+
</li>
|
143
|
+
</ul>
|
144
|
+
`);
|
145
|
+
|
146
|
+
$navBar.find('.BRcontrols').prepend(this.br.refs.$BRReadAloudToolbar);
|
147
|
+
|
148
|
+
const renderVoiceOption = (voices) => {
|
149
|
+
return voices.map(voice =>
|
150
|
+
`<option value="${voice.voiceURI}">${voice.lang} - ${voice.name}</option>`).join('');
|
151
|
+
};
|
152
|
+
|
153
|
+
const voiceSortOrder = (a,b) => `${a.lang} - ${a.name}`.localeCompare(`${b.lang} - ${b.name}`);
|
154
|
+
|
155
|
+
const renderVoicesMenu = (voicesMenu) => {
|
156
|
+
voicesMenu.empty();
|
157
|
+
const bookLanguage = this.ttsEngine.opts.bookLanguage;
|
158
|
+
const bookLanguages = this.ttsEngine.getVoices().filter(v => v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
159
|
+
const otherLanguages = this.ttsEngine.getVoices().filter(v => !v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
160
|
+
|
161
|
+
if (this.ttsEngine.getVoices().length > 1) {
|
162
|
+
voicesMenu.append($(`<optgroup label="Book Language (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
|
163
|
+
voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages)} </optgroup>`));
|
164
|
+
|
165
|
+
voicesMenu.val(this.ttsEngine.voice.voiceURI);
|
166
|
+
voicesMenu.show();
|
167
|
+
} else {
|
168
|
+
voicesMenu.hide();
|
169
|
+
}
|
170
|
+
};
|
171
|
+
|
172
|
+
const voicesMenu = this.br.refs.$BRReadAloudToolbar.find('[name=playback-voice]');
|
173
|
+
renderVoicesMenu(voicesMenu);
|
174
|
+
voicesMenu.on("change", ev => this.ttsEngine.setVoice(voicesMenu.val()));
|
175
|
+
this.ttsEngine.events.on('pause resume start', () => this.updateState());
|
176
|
+
this.ttsEngine.events.on('voiceschanged', () => renderVoicesMenu(voicesMenu));
|
177
|
+
this.br.refs.$BRReadAloudToolbar.find('[name=play]').on("click", this.playPause.bind(this));
|
178
|
+
this.br.refs.$BRReadAloudToolbar.find('[name=advance]').on("click", this.jumpForward.bind(this));
|
179
|
+
this.br.refs.$BRReadAloudToolbar.find('[name=review]').on("click", this.jumpBackward.bind(this));
|
180
|
+
const $rateSelector = this.br.refs.$BRReadAloudToolbar.find('select[name="playback-speed"]');
|
181
|
+
$rateSelector.on("change", ev => this.ttsEngine.setPlaybackRate(parseFloat($rateSelector.val())));
|
182
|
+
$(`<li>
|
183
|
+
<button class="BRicon read js-tooltip" title="${tooltips.readAloud}">
|
184
|
+
<div class="icon icon-read-aloud"></div>
|
185
|
+
<span class="BRtooltip">${tooltips.readAloud}</span>
|
186
|
+
</button>
|
187
|
+
</li>`).insertBefore($navBar.find('.BRcontrols .BRicon.zoom_out').closest('li'));
|
188
|
+
}
|
189
|
+
|
190
|
+
toggle() {
|
191
|
+
this.br._plugins.autoplay?.stop();
|
192
|
+
if (this.ttsEngine.playing) {
|
193
|
+
this.stop();
|
194
|
+
} else {
|
195
|
+
this.start();
|
201
196
|
}
|
202
|
-
return $el;
|
203
|
-
};
|
204
|
-
})(BookReader.prototype.initNavbar);
|
205
|
-
|
206
|
-
// ttsToggle()
|
207
|
-
//______________________________________________________________________________
|
208
|
-
BookReader.prototype.ttsToggle = function () {
|
209
|
-
if (this.autoStop) this.autoStop();
|
210
|
-
if (this.ttsEngine.playing) {
|
211
|
-
this.ttsStop();
|
212
|
-
} else {
|
213
|
-
this.ttsStart();
|
214
197
|
}
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
this.
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
this.ttsSendAnalyticsEvent('Start');
|
226
|
-
if (startTTSEngine)
|
227
|
-
this.ttsEngine.start(this.currentIndex(), this.book.getNumLeafs());
|
228
|
-
};
|
229
|
-
|
230
|
-
BookReader.prototype.ttsJumpForward = function () {
|
231
|
-
if (this.ttsEngine.paused) {
|
232
|
-
this.ttsEngine.resume();
|
198
|
+
|
199
|
+
start(startTTSEngine = true) {
|
200
|
+
if (this.br.constModeThumb == this.br.mode)
|
201
|
+
this.br.switchMode(this.br.constMode1up);
|
202
|
+
|
203
|
+
this.br.refs.$BRReadAloudToolbar.addClass('visible');
|
204
|
+
this.br.$('.BRicon.read').addClass('unread active');
|
205
|
+
this.sendAnalyticsEvent('Start');
|
206
|
+
if (startTTSEngine)
|
207
|
+
this.ttsEngine.start(this.br.currentIndex(), this.br.book.getNumLeafs());
|
233
208
|
}
|
234
|
-
this.ttsEngine.jumpForward();
|
235
|
-
};
|
236
209
|
|
237
|
-
|
238
|
-
|
239
|
-
|
210
|
+
jumpForward() {
|
211
|
+
if (this.ttsEngine.paused) {
|
212
|
+
this.ttsEngine.resume();
|
213
|
+
}
|
214
|
+
this.ttsEngine.jumpForward();
|
240
215
|
}
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
};
|
248
|
-
|
249
|
-
BookReader.prototype.ttsPlayPause = function() {
|
250
|
-
if (!this.ttsEngine.playing) {
|
251
|
-
this.ttsToggle();
|
252
|
-
} else {
|
253
|
-
this.ttsEngine.togglePlayPause();
|
254
|
-
this.ttsUpdateState();
|
216
|
+
|
217
|
+
jumpBackward() {
|
218
|
+
if (this.ttsEngine.paused) {
|
219
|
+
this.ttsEngine.resume();
|
220
|
+
}
|
221
|
+
this.ttsEngine.jumpBackward();
|
255
222
|
}
|
256
|
-
};
|
257
|
-
|
258
|
-
// ttsStop()
|
259
|
-
//______________________________________________________________________________
|
260
|
-
BookReader.prototype.ttsStop = function () {
|
261
|
-
this.refs.$BRReadAloudToolbar.removeClass('visible');
|
262
|
-
this.$('.BRicon.read').removeClass('unread active');
|
263
|
-
this.ttsSendAnalyticsEvent('Stop');
|
264
|
-
this.ttsEngine.stop();
|
265
|
-
this.ttsRemoveHilites();
|
266
|
-
this.removeProgressPopup();
|
267
|
-
};
|
268
223
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
BookReader.prototype.ttsBeforeChunkPlay = async function(chunk) {
|
274
|
-
await this.ttsMaybeFlipToIndex(chunk.leafIndex);
|
275
|
-
this.ttsHighlightChunk(chunk);
|
276
|
-
this.ttsScrollToChunk(chunk);
|
277
|
-
};
|
224
|
+
updateState() {
|
225
|
+
const isPlaying = !(this.ttsEngine.paused || !this.ttsEngine.playing);
|
226
|
+
this.br.$('.read-aloud [name=play]').toggleClass('playing', isPlaying);
|
227
|
+
}
|
278
228
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
229
|
+
playPause() {
|
230
|
+
if (!this.ttsEngine.playing) {
|
231
|
+
this.toggle();
|
232
|
+
} else {
|
233
|
+
this.ttsEngine.togglePlayPause();
|
234
|
+
this.updateState();
|
235
|
+
}
|
236
|
+
}
|
285
237
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
this.
|
293
|
-
|
294
|
-
await this._modes.mode2Up.mode2UpLit.jumpToIndex(leafIndex);
|
238
|
+
|
239
|
+
stop() {
|
240
|
+
this.br.refs.$BRReadAloudToolbar.removeClass('visible');
|
241
|
+
this.br.$('.BRicon.read').removeClass('unread active');
|
242
|
+
this.sendAnalyticsEvent('Stop');
|
243
|
+
this.ttsEngine.stop();
|
244
|
+
this.removeHilites();
|
245
|
+
this.br.removeProgressPopup();
|
295
246
|
}
|
296
|
-
};
|
297
247
|
|
298
|
-
/**
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
// group by index; currently only possible to have chunks on one page :/
|
308
|
-
this._ttsBoxesByIndex = {
|
309
|
-
[pageIndex]: chunk.lineRects.map(([l, b, r, t]) => ({l, r, b, t})),
|
310
|
-
};
|
311
|
-
|
312
|
-
// update any already created pages
|
313
|
-
for (const [pageIndexString, boxes] of Object.entries(this._ttsBoxesByIndex)) {
|
314
|
-
const pageIndex = parseFloat(pageIndexString);
|
315
|
-
const page = this.book.getPage(pageIndex);
|
316
|
-
const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
|
317
|
-
pageContainers.forEach(container => renderBoxesInPageContainerLayer('ttsHiliteLayer', boxes, page, container));
|
248
|
+
/**
|
249
|
+
* @param {PageChunk} chunk
|
250
|
+
* Returns once the flip is done
|
251
|
+
*/
|
252
|
+
async beforeChunkPlay(chunk) {
|
253
|
+
await this.maybeFlipToIndex(chunk.leafIndex);
|
254
|
+
this.highlightChunk(chunk);
|
255
|
+
this.scrollToChunk(chunk);
|
318
256
|
}
|
319
|
-
};
|
320
257
|
|
321
|
-
/**
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
$(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]?.scrollIntoView({
|
329
|
-
// Only vertically center the highlight if we're in 1up or in full screen. In
|
330
|
-
// 2up, if we're not fullscreen, the whole body gets scrolled around to try to
|
331
|
-
// center the highlight 🙄 See:
|
332
|
-
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
|
333
|
-
// Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
|
334
|
-
// full-width, and covers up the last line of the highlight.
|
335
|
-
block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
|
336
|
-
inline: 'center',
|
337
|
-
behavior: 'smooth',
|
338
|
-
});
|
339
|
-
};
|
340
|
-
|
341
|
-
// ttsRemoveHilites()
|
342
|
-
//______________________________________________________________________________
|
343
|
-
BookReader.prototype.ttsRemoveHilites = function () {
|
344
|
-
$(this.getActivePageContainerElements()).find('.ttsHiliteLayer').remove();
|
345
|
-
this._ttsBoxesByIndex = {};
|
346
|
-
};
|
258
|
+
/**
|
259
|
+
* @param {PageChunk} chunk
|
260
|
+
*/
|
261
|
+
sendChunkFinishedAnalyticsEvent(chunk) {
|
262
|
+
this.sendAnalyticsEvent('ChunkFinished-Words', approximateWordCount(chunk.text));
|
263
|
+
}
|
347
264
|
|
348
|
-
/**
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
265
|
+
/**
|
266
|
+
* Flip the page if the provided leaf index is not visible
|
267
|
+
* @param {Number} leafIndex
|
268
|
+
*/
|
269
|
+
async maybeFlipToIndex(leafIndex) {
|
270
|
+
if (this.br.constMode2up != this.br.mode) {
|
271
|
+
this.br.jumpToIndex(leafIndex);
|
272
|
+
} else {
|
273
|
+
await this.br._modes.mode2Up.mode2UpLit.jumpToIndex(leafIndex);
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
/**
|
278
|
+
* @param {PageChunk} chunk
|
279
|
+
*/
|
280
|
+
highlightChunk(chunk) {
|
281
|
+
// The poorly-named variable leafIndex
|
282
|
+
const pageIndex = chunk.leafIndex;
|
283
|
+
|
284
|
+
this.removeHilites();
|
285
|
+
|
286
|
+
// group by index; currently only possible to have chunks on one page :/
|
287
|
+
this._ttsBoxesByIndex = {
|
288
|
+
[pageIndex]: chunk.lineRects.map(([l, b, r, t]) => ({l, r, b, t})),
|
289
|
+
};
|
290
|
+
|
291
|
+
// update any already created pages
|
292
|
+
for (const [pageIndexString, boxes] of Object.entries(this._ttsBoxesByIndex)) {
|
293
|
+
const pageIndex = parseFloat(pageIndexString);
|
294
|
+
const page = this.br.book.getPage(pageIndex);
|
295
|
+
const pageContainers = this.br.getActivePageContainerElementsForIndex(pageIndex);
|
296
|
+
pageContainers.forEach(container => renderBoxesInPageContainerLayer('ttsHiliteLayer', boxes, page, container));
|
297
|
+
}
|
360
298
|
}
|
361
|
-
|
299
|
+
|
300
|
+
/**
|
301
|
+
* @param {PageChunk} chunk
|
302
|
+
*/
|
303
|
+
scrollToChunk(chunk) {
|
304
|
+
// It behaves weird if used in thumb mode
|
305
|
+
if (this.br.constModeThumb == this.br.mode) return;
|
306
|
+
|
307
|
+
$(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]?.scrollIntoView({
|
308
|
+
// Only vertically center the highlight if we're in 1up or in full screen. In
|
309
|
+
// 2up, if we're not fullscreen, the whole body gets scrolled around to try to
|
310
|
+
// center the highlight 🙄 See:
|
311
|
+
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
|
312
|
+
// Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
|
313
|
+
// full-width, and covers up the last line of the highlight.
|
314
|
+
block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
|
315
|
+
inline: 'center',
|
316
|
+
behavior: 'smooth',
|
317
|
+
});
|
318
|
+
}
|
319
|
+
|
320
|
+
removeHilites() {
|
321
|
+
$(this.br.getActivePageContainerElements()).find('.ttsHiliteLayer').remove();
|
322
|
+
this._ttsBoxesByIndex = {};
|
323
|
+
}
|
324
|
+
|
325
|
+
/**
|
326
|
+
* @private
|
327
|
+
* Send an analytics event with an optional value. Also attaches the book's language.
|
328
|
+
* @param {string} action
|
329
|
+
* @param {number} [value]
|
330
|
+
*/
|
331
|
+
sendAnalyticsEvent(action, value) {
|
332
|
+
if (this.br._plugins.archiveAnalytics) {
|
333
|
+
const extraValues = {};
|
334
|
+
const mediaLanguage = this.ttsEngine.opts.bookLanguage;
|
335
|
+
if (mediaLanguage) extraValues.mediaLanguage = mediaLanguage;
|
336
|
+
this.br._plugins.archiveAnalytics.sendEvent('BRReadAloud', action, value, extraValues);
|
337
|
+
}
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
BookReader?.registerPlugin('tts', TtsPlugin);
|