@internetarchive/bookreader 5.0.0-8 → 5.0.0-80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (313) hide show
  1. package/.eslintrc.js +17 -15
  2. package/.github/workflows/node.js.yml +73 -10
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +398 -1133
  6. package/BookReader/BookReader.js +1 -1
  7. package/BookReader/BookReader.js.LICENSE.txt +20 -20
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1782 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +7 -0
  11. package/BookReader/ia-bookreader-bundle.js.map +1 -0
  12. package/BookReader/icons/1up.svg +1 -1
  13. package/BookReader/icons/2up.svg +1 -1
  14. package/BookReader/icons/advance.svg +1 -1
  15. package/BookReader/icons/chevron-right.svg +1 -1
  16. package/BookReader/icons/close-circle-dark.svg +1 -1
  17. package/BookReader/icons/close-circle.svg +1 -1
  18. package/BookReader/icons/fullscreen.svg +1 -1
  19. package/BookReader/icons/fullscreen_exit.svg +1 -1
  20. package/BookReader/icons/hamburger.svg +1 -1
  21. package/BookReader/icons/left-arrow.svg +1 -1
  22. package/BookReader/icons/magnify-minus.svg +1 -1
  23. package/BookReader/icons/magnify-plus.svg +1 -1
  24. package/BookReader/icons/magnify.svg +1 -1
  25. package/BookReader/icons/pause.svg +1 -1
  26. package/BookReader/icons/play.svg +1 -1
  27. package/BookReader/icons/playback-speed.svg +1 -1
  28. package/BookReader/icons/read-aloud.svg +1 -1
  29. package/BookReader/icons/review.svg +1 -1
  30. package/BookReader/icons/thumbnails.svg +1 -1
  31. package/BookReader/icons/voice.svg +1 -0
  32. package/BookReader/icons/volume-full.svg +1 -1
  33. package/BookReader/images/BRicons.svg +3 -3
  34. package/BookReader/images/books_graphic.svg +1 -1
  35. package/BookReader/images/icon_book.svg +1 -1
  36. package/BookReader/images/icon_bookmark.svg +1 -1
  37. package/BookReader/images/icon_gear.svg +1 -1
  38. package/BookReader/images/icon_hamburger.svg +1 -1
  39. package/BookReader/images/icon_home.svg +1 -1
  40. package/BookReader/images/icon_info.svg +1 -1
  41. package/BookReader/images/icon_one_page.svg +1 -1
  42. package/BookReader/images/icon_pause.svg +1 -1
  43. package/BookReader/images/icon_play.svg +1 -1
  44. package/BookReader/images/icon_playback-rate.svg +1 -1
  45. package/BookReader/images/icon_search_button.svg +1 -1
  46. package/BookReader/images/icon_share.svg +1 -1
  47. package/BookReader/images/icon_skip-ahead.svg +1 -1
  48. package/BookReader/images/icon_skip-back.svg +1 -1
  49. package/BookReader/images/icon_speaker.svg +1 -1
  50. package/BookReader/images/icon_speaker_open.svg +1 -1
  51. package/BookReader/images/icon_thumbnails.svg +1 -1
  52. package/BookReader/images/icon_toc.svg +1 -1
  53. package/BookReader/images/icon_two_pages.svg +1 -1
  54. package/BookReader/images/marker_chap-off.svg +1 -1
  55. package/BookReader/images/marker_chap-on.svg +1 -1
  56. package/BookReader/images/marker_srch-on.svg +1 -1
  57. package/BookReader/jquery-3.js +2 -0
  58. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  59. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  60. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  61. package/BookReader/plugins/plugin.autoplay.js +1 -1
  62. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  63. package/BookReader/plugins/plugin.chapters.js +25 -1
  64. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  65. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  66. package/BookReader/plugins/plugin.iframe.js +1 -1
  67. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  68. package/BookReader/plugins/plugin.iiif.js +2 -0
  69. package/BookReader/plugins/plugin.iiif.js.map +1 -0
  70. package/BookReader/plugins/plugin.resume.js +1 -1
  71. package/BookReader/plugins/plugin.resume.js.map +1 -1
  72. package/BookReader/plugins/plugin.search.js +2 -1
  73. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  74. package/BookReader/plugins/plugin.search.js.map +1 -1
  75. package/BookReader/plugins/plugin.text_selection.js +2 -1
  76. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  77. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  78. package/BookReader/plugins/plugin.tts.js +1 -1
  79. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  80. package/BookReader/plugins/plugin.tts.js.map +1 -1
  81. package/BookReader/plugins/plugin.url.js +1 -1
  82. package/BookReader/plugins/plugin.url.js.map +1 -1
  83. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  84. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  85. package/BookReader/webcomponents-bundle.js +3 -0
  86. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  87. package/BookReader/webcomponents-bundle.js.map +1 -0
  88. package/BookReaderDemo/BookReaderDemo.css +18 -19
  89. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  90. package/BookReaderDemo/BookReaderJSAutoplay.js +4 -1
  91. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  92. package/BookReaderDemo/IADemoBr.js +147 -0
  93. package/BookReaderDemo/demo-advanced.html +2 -2
  94. package/BookReaderDemo/demo-autoplay.html +2 -3
  95. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  96. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  97. package/BookReaderDemo/demo-fullscreen.html +2 -4
  98. package/BookReaderDemo/demo-iiif.html +99 -12
  99. package/BookReaderDemo/demo-internetarchive.html +214 -18
  100. package/BookReaderDemo/demo-multiple.html +2 -1
  101. package/BookReaderDemo/demo-preview-pages.html +2 -1
  102. package/BookReaderDemo/demo-simple.html +2 -1
  103. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  104. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  105. package/BookReaderDemo/immersion-1up.html +2 -2
  106. package/BookReaderDemo/immersion-mode.html +2 -4
  107. package/BookReaderDemo/toggle_controls.html +3 -2
  108. package/BookReaderDemo/view_mode.html +2 -1
  109. package/BookReaderDemo/viewmode-cycle.html +2 -3
  110. package/CHANGELOG.md +536 -33
  111. package/README.md +14 -1
  112. package/babel.config.js +20 -0
  113. package/codecov.yml +6 -0
  114. package/index.html +4 -1
  115. package/jsconfig.json +19 -0
  116. package/netlify.toml +9 -0
  117. package/package.json +70 -60
  118. package/renovate.json +52 -0
  119. package/scripts/preversion.js +0 -1
  120. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  121. package/src/BookNavigator/assets/button-base.js +4 -2
  122. package/src/BookNavigator/assets/ia-logo.js +17 -0
  123. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  124. package/src/BookNavigator/assets/icon_close.js +1 -1
  125. package/src/BookNavigator/book-navigator.js +590 -0
  126. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  127. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  128. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  129. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  130. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  131. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  132. package/src/BookNavigator/delete-modal-actions.js +1 -1
  133. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  134. package/src/BookNavigator/downloads/downloads.js +41 -25
  135. package/src/BookNavigator/search/search-provider.js +49 -27
  136. package/src/BookNavigator/search/search-results.js +23 -9
  137. package/src/BookNavigator/sharing.js +27 -0
  138. package/src/BookNavigator/viewable-files.js +95 -0
  139. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -10
  140. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  141. package/src/BookReader/BookModel.js +64 -34
  142. package/src/BookReader/DragScrollable.js +233 -0
  143. package/src/BookReader/Mode1Up.js +56 -351
  144. package/src/BookReader/Mode1UpLit.js +388 -0
  145. package/src/BookReader/Mode2Up.js +73 -1318
  146. package/src/BookReader/Mode2UpLit.js +776 -0
  147. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  148. package/src/BookReader/ModeSmoothZoom.js +312 -0
  149. package/src/BookReader/ModeThumb.js +18 -12
  150. package/src/BookReader/Navbar/Navbar.js +14 -40
  151. package/src/BookReader/PageContainer.js +81 -6
  152. package/src/BookReader/ReduceSet.js +1 -1
  153. package/src/BookReader/Toolbar/Toolbar.js +10 -37
  154. package/src/BookReader/events.js +2 -3
  155. package/src/BookReader/options.js +27 -2
  156. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  157. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  158. package/src/BookReader/utils/SelectionObserver.js +45 -0
  159. package/src/BookReader/utils.js +118 -13
  160. package/src/BookReader.js +427 -1061
  161. package/src/assets/icons/magnify-minus.svg +3 -7
  162. package/src/assets/icons/magnify-plus.svg +3 -7
  163. package/src/assets/icons/voice.svg +1 -0
  164. package/src/css/BookReader.scss +1 -5
  165. package/src/css/_BRBookmarks.scss +1 -1
  166. package/src/css/_BRComponent.scss +1 -1
  167. package/src/css/_BRmain.scss +16 -3
  168. package/src/css/_BRnav.scss +12 -39
  169. package/src/css/_BRpages.scss +149 -40
  170. package/src/css/_BRsearch.scss +68 -25
  171. package/src/css/_BRtoolbar.scss +5 -5
  172. package/src/css/_TextSelection.scss +87 -27
  173. package/src/css/_colorbox.scss +2 -2
  174. package/src/css/_controls.scss +20 -7
  175. package/src/css/_icons.scss +1 -1
  176. package/src/ia-bookreader/ia-bookreader.js +224 -0
  177. package/src/plugins/plugin.archive_analytics.js +3 -3
  178. package/src/plugins/plugin.autoplay.js +5 -11
  179. package/src/plugins/plugin.chapters.js +237 -191
  180. package/src/plugins/plugin.iiif.js +151 -0
  181. package/src/plugins/plugin.resume.js +3 -3
  182. package/src/plugins/plugin.text_selection.js +464 -134
  183. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  184. package/src/plugins/search/plugin.search.js +142 -125
  185. package/src/plugins/search/utils.js +43 -0
  186. package/src/plugins/search/view.js +33 -58
  187. package/src/plugins/tts/AbstractTTSEngine.js +71 -40
  188. package/src/plugins/tts/FestivalTTSEngine.js +13 -14
  189. package/src/plugins/tts/PageChunk.js +15 -21
  190. package/src/plugins/tts/PageChunkIterator.js +8 -12
  191. package/src/plugins/tts/WebTTSEngine.js +87 -71
  192. package/src/plugins/tts/plugin.tts.js +96 -127
  193. package/src/plugins/tts/utils.js +15 -25
  194. package/src/plugins/url/UrlPlugin.js +191 -0
  195. package/src/plugins/{plugin.url.js → url/plugin.url.js} +45 -16
  196. package/src/util/browserSniffing.js +22 -0
  197. package/src/util/docCookies.js +21 -2
  198. package/tests/e2e/README.md +37 -0
  199. package/tests/e2e/autoplay.test.js +2 -2
  200. package/tests/e2e/base.test.js +8 -16
  201. package/tests/e2e/helpers/base.js +53 -48
  202. package/tests/e2e/helpers/debug.js +1 -1
  203. package/tests/e2e/helpers/params.js +17 -0
  204. package/tests/e2e/helpers/rightToLeft.js +8 -14
  205. package/tests/e2e/helpers/search.js +73 -0
  206. package/tests/e2e/models/Navigation.js +20 -37
  207. package/tests/e2e/rightToLeft.test.js +4 -5
  208. package/tests/e2e/viewmode.test.js +40 -33
  209. package/tests/jest/BookNavigator/book-navigator.test.js +661 -0
  210. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  211. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  212. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  213. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  214. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  215. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  216. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  217. package/tests/{karma → jest}/BookNavigator/search/search-results.test.js +109 -60
  218. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  219. package/tests/jest/BookNavigator/viewable-files/viewable-files-provider.test.js +80 -0
  220. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  221. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  222. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  223. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  224. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  225. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  226. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  227. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  228. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  229. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  230. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +10 -10
  231. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +88 -6
  232. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  233. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  234. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  235. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  236. package/tests/jest/BookReader/utils/SelectionObserver.test.js +57 -0
  237. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  238. package/tests/jest/BookReader/utils.test.js +229 -0
  239. package/tests/jest/BookReader.keyboard.test.js +190 -0
  240. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  241. package/tests/{BookReader.test.js → jest/BookReader.test.js} +26 -37
  242. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  243. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +4 -4
  244. package/tests/jest/plugins/plugin.chapters.test.js +195 -0
  245. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  246. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  247. package/tests/jest/plugins/plugin.text_selection.test.js +317 -0
  248. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  249. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +25 -47
  250. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +39 -6
  251. package/tests/jest/plugins/search/utils.js +25 -0
  252. package/tests/jest/plugins/search/utils.test.js +29 -0
  253. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +29 -9
  254. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  255. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  256. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  257. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  258. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  259. package/tests/jest/plugins/url/UrlPlugin.test.js +198 -0
  260. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +53 -14
  261. package/tests/jest/setup.js +3 -0
  262. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  263. package/tests/jest/util/docCookies.test.js +24 -0
  264. package/tests/{util → jest/util}/strings.test.js +1 -1
  265. package/tests/{utils.js → jest/utils.js} +38 -0
  266. package/webpack.config.js +12 -6
  267. package/.babelrc +0 -12
  268. package/.dependabot/config.yml +0 -6
  269. package/.testcaferc.json +0 -5
  270. package/BookReader/bookreader-component-bundle.js +0 -1450
  271. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  272. package/BookReader/bookreader-component-bundle.js.map +0 -1
  273. package/BookReader/jquery-1.10.1.js +0 -2
  274. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  275. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  276. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  277. package/BookReader/plugins/plugin.mobile_nav.js +0 -2
  278. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  279. package/BookReaderDemo/IIIFBookReader.js +0 -207
  280. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  281. package/BookReaderDemo/demo-iiif.js +0 -26
  282. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  283. package/karma.conf.js +0 -23
  284. package/src/BookNavigator/BookModel.js +0 -14
  285. package/src/BookNavigator/BookNavigator.js +0 -446
  286. package/src/BookNavigator/assets/book-loader.js +0 -27
  287. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  288. package/src/BookNavigator/search/a-search-result.js +0 -55
  289. package/src/BookReader/DebugConsole.js +0 -54
  290. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  291. package/src/ItemNavigator/ItemNavigator.js +0 -376
  292. package/src/ItemNavigator/providers/sharing.js +0 -29
  293. package/src/css/_MobileNav.scss +0 -194
  294. package/src/dragscrollable-br.js +0 -261
  295. package/src/lit-wrapper.js +0 -2
  296. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  297. package/src/plugins/plugin.mobile_nav.js +0 -287
  298. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  299. package/tests/BookReader/DebugConsole.test.js +0 -25
  300. package/tests/BookReader/Mode1Up.test.js +0 -164
  301. package/tests/BookReader/Mode2Up.test.js +0 -247
  302. package/tests/BookReader/utils.test.js +0 -109
  303. package/tests/e2e/helpers/desktopSearch.js +0 -72
  304. package/tests/e2e/helpers/mobileSearch.js +0 -85
  305. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  306. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  307. package/tests/karma/BookNavigator/search/search-provider.test.js +0 -23
  308. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  309. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  310. package/tests/plugins/plugin.chapters.test.js +0 -130
  311. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  312. package/tests/plugins/plugin.text_selection.test.js +0 -203
  313. package/tests/util/docCookies.test.js +0 -15
@@ -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
 
@@ -28,6 +29,7 @@ import PageChunkIterator from './PageChunkIterator.js';
28
29
  * @property {() => void} resume
29
30
  * @property {() => void} finish force the sound to 'finish'
30
31
  * @property {number => void} setPlaybackRate
32
+ * @property {SpeechSynthesisVoice => void} setVoice
31
33
  **/
32
34
 
33
35
  /** Handling bookreader's text-to-speech */
@@ -50,9 +52,7 @@ export default class AbstractTTSEngine {
50
52
  /** @type {SpeechSynthesisVoice} */
51
53
  this.voice = null;
52
54
  // Listen for voice changes (fired by subclasses)
53
- this.events.on('voiceschanged', () => {
54
- this.voice = AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
55
- });
55
+ this.events.on('voiceschanged', this.updateBestVoice);
56
56
  this.events.trigger('voiceschanged');
57
57
  }
58
58
 
@@ -71,12 +71,17 @@ export default class AbstractTTSEngine {
71
71
  /** @abstract */
72
72
  init() { return null; }
73
73
 
74
+ updateBestVoice = () => {
75
+ this.voice = AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
76
+ }
77
+
74
78
  /**
75
79
  * @param {number} leafIndex
76
80
  * @param {number} numLeafs total number of leafs in the current book
77
81
  */
78
82
  start(leafIndex, numLeafs) {
79
83
  this.playing = true;
84
+ this.paused = false;
80
85
  this.opts.onLoadingStart();
81
86
 
82
87
  this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
@@ -92,6 +97,7 @@ export default class AbstractTTSEngine {
92
97
  stop() {
93
98
  if (this.activeSound) this.activeSound.stop();
94
99
  this.playing = false;
100
+ this.paused = true;
95
101
  this._chunkIterator = null;
96
102
  this.activeSound = null;
97
103
  this.events.trigger('stop');
@@ -124,13 +130,26 @@ export default class AbstractTTSEngine {
124
130
  }
125
131
 
126
132
  /** @public */
127
- jumpBackward() {
128
- Promise.all([
133
+ async jumpBackward() {
134
+ await Promise.all([
129
135
  this.activeSound.stop(),
130
136
  this._chunkIterator.decrement()
131
137
  .then(() => this._chunkIterator.decrement())
132
- ])
133
- .then(() => this.step());
138
+ ]);
139
+ this.step();
140
+ }
141
+
142
+ /** @param {string} voiceURI */
143
+ setVoice(voiceURI) {
144
+ // if the user actively selects a voice, don't re-choose best voice anymore
145
+ // MS Edge fires voices changed randomly very often
146
+ this.events.off('voiceschanged', this.updateBestVoice);
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
+ }
152
+ if (this.activeSound) this.activeSound.setVoice(this.voice);
134
153
  }
135
154
 
136
155
  /** @param {number} newRate */
@@ -140,36 +159,33 @@ export default class AbstractTTSEngine {
140
159
  }
141
160
 
142
161
  /** @private */
143
- step() {
144
- this._chunkIterator.next()
145
- .then(chunk => {
146
- if (chunk == PageChunkIterator.AT_END) {
147
- this.stop();
148
- this.opts.onDone();
149
- return;
150
- }
151
-
152
- this.opts.onLoadingStart();
153
- const sound = this.createSound(chunk);
154
- sound.chunk = chunk;
155
- sound.rate = this.playbackRate;
156
- sound.voice = this.voice;
157
- sound.load(() => this.opts.onLoadingComplete());
158
-
159
- this.opts.onLoadingComplete();
160
- return this.opts.beforeChunkPlay(chunk).then(() => sound);
161
- })
162
- .then(sound => {
163
- if (!this.playing) return;
164
-
165
- const playPromise = this.playSound(sound)
166
- .then(() => this.opts.afterChunkPlay(sound.chunk));
167
- if (this.paused) this.pause();
168
- return playPromise;
169
- })
170
- .then(() => {
171
- if (this.playing) return this.step();
172
- });
162
+ async step() {
163
+ const chunk = await this._chunkIterator.next();
164
+ if (chunk == PageChunkIterator.AT_END) {
165
+ this.stop();
166
+ this.opts.onDone();
167
+ return;
168
+ }
169
+ this.opts.onLoadingStart();
170
+ const sound = this.createSound(chunk);
171
+ sound.chunk = chunk;
172
+ sound.rate = this.playbackRate;
173
+ sound.voice = this.voice;
174
+ sound.load(() => this.opts.onLoadingComplete());
175
+
176
+ this.opts.onLoadingComplete();
177
+
178
+ await this.opts.beforeChunkPlay(chunk);
179
+
180
+ if (!this.playing) return;
181
+
182
+ const playPromise = await this.playSound(sound)
183
+ .then(()=> this.opts.afterChunkPlay(sound.chunk));
184
+
185
+ if (this.paused) this.pause();
186
+ await playPromise;
187
+
188
+ if (this.playing) return this.step();
173
189
  }
174
190
 
175
191
  /**
@@ -212,10 +228,12 @@ export default class AbstractTTSEngine {
212
228
  // user languages that match the book language
213
229
  const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
214
230
 
215
- // Try to find voices that intersect these two sets
216
- return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) ||
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)
217
235
  // no user languages match the books; let's return the best voice for the book language
218
- (bookLangVoices.find(v => v.default) || bookLangVoices[0])
236
+ || (bookLangVoices.find(v => v.default) || bookLangVoices[0])
219
237
  // No voices match the book language? let's find a voice in the user's language
220
238
  // and ignore book lang
221
239
  || AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
@@ -223,6 +241,19 @@ export default class AbstractTTSEngine {
223
241
  || (voices.find(v => v.default) || voices[0]);
224
242
  }
225
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
+
226
257
  /**
227
258
  * @private
228
259
  * Get the best voice that matches one of the BCP47 languages (order by preference)
@@ -1,5 +1,5 @@
1
1
  import AbstractTTSEngine from './AbstractTTSEngine.js';
2
- import { sleep } from './utils.js';
2
+ import { sleep } from '../../BookReader/utils.js';
3
3
  /* global soundManager */
4
4
  import 'soundmanager2';
5
5
  import 'jquery.browser';
@@ -23,7 +23,7 @@ export default class FestivalTTSEngine extends AbstractTTSEngine {
23
23
  // $.browsers is sometimes undefined on some Android browsers :/
24
24
  // Likely related to when $.browser was moved to npm
25
25
  /** @type {'mp3' | 'ogg'} format of audio to get */
26
- this.audioFormat = $.browser?.mozilla ? 'ogg' : 'mp3';
26
+ this.audioFormat = $.browser?.mozilla ? 'ogg' : 'mp3'; //eslint-disable-line no-jquery/no-browser
27
27
  }
28
28
 
29
29
  /** @override */
@@ -91,10 +91,10 @@ export default class FestivalTTSEngine extends AbstractTTSEngine {
91
91
  * See https://stackoverflow.com/questions/12206631/html5-audio-cant-play-through-javascript-unless-triggered-manually-once
92
92
  * @return {PromiseLike}
93
93
  */
94
- iOSCaptureUserIntentHack() {
94
+ async iOSCaptureUserIntentHack() {
95
95
  const sound = soundManager.createSound({ url: SILENCE_1MS[this.audioFormat] });
96
- return new Promise(res => sound.play({onfinish: res}))
97
- .then(() => sound.destruct());
96
+ await new Promise(res => sound.play({onfinish: res}));
97
+ sound.destruct();
98
98
  }
99
99
  }
100
100
 
@@ -107,7 +107,7 @@ class FestivalTTSSound {
107
107
  this.sound = null;
108
108
  this.rate = 1;
109
109
  /** @type {function} calling this resolves the "play" promise */
110
- this._finishResolver = null
110
+ this._finishResolver = null;
111
111
  }
112
112
 
113
113
  get loaded() {
@@ -122,21 +122,20 @@ class FestivalTTSSound {
122
122
  if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
123
123
  onload();
124
124
  },
125
- onresume: () => {
126
- sleep(25).then(() => {
127
- if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
128
- });
125
+ onresume: async () => {
126
+ await sleep(25);
127
+ if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
129
128
  }
130
129
  });
131
130
  return this.sound.load();
132
131
  }
133
132
 
134
- play() {
135
- return new Promise(res => {
133
+ async play() {
134
+ await new Promise(res => {
136
135
  this._finishResolver = res;
137
136
  this.sound.play({ onfinish: res });
138
- })
139
- .then(() => this.sound.destruct());
137
+ });
138
+ this.sound.destruct();
140
139
  }
141
140
 
142
141
  /** @override */
@@ -21,27 +21,18 @@ export default class PageChunk {
21
21
  * @param {number} leafIndex
22
22
  * @return {Promise<PageChunk[]>}
23
23
  */
24
- static fetch(server, bookPath, leafIndex) {
25
- // jquery's ajax "PromiseLike" implementation is inconsistent with
26
- // modern Promises, so convert it to a full promise (it doesn't forward
27
- // a returned promise to the next handler in the chain, which kind of
28
- // defeats the entire point of using promises to avoid "callback hell")
29
- return new Promise((res, rej) => {
30
- $.ajax({
31
- type: 'GET',
32
- url: `https://${server}/BookReader/BookReaderGetTextWrapper.php`,
33
- dataType:'jsonp',
34
- cache: true,
35
- data: {
36
- path: `${bookPath}_djvu.xml`,
37
- page: leafIndex
38
- },
39
- error: rej,
40
- })
41
- .then(chunks => {
42
- res(PageChunk._fromTextWrapperResponse(leafIndex, chunks));
43
- });
24
+ static async fetch(server, bookPath, leafIndex) {
25
+ const chunks = await $.ajax({
26
+ type: 'GET',
27
+ url: `https://${server}/BookReader/BookReaderGetTextWrapper.php`,
28
+ dataType:'jsonp',
29
+ cache: true,
30
+ data: {
31
+ path: `${bookPath}_djvu.xml`,
32
+ page: leafIndex
33
+ }
44
34
  });
35
+ return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
45
36
  }
46
37
 
47
38
  /**
@@ -97,7 +88,10 @@ export default class PageChunk {
97
88
  * @return {string}
98
89
  */
99
90
  static _removeDanglingHyphens(text) {
100
- return text.replace(/-\s+/g, '');
91
+ // Some books mis-OCR a dangling hyphen as a ¬ (mathematical not sign) . Since in math
92
+ // the not sign should not appear followed by a space, we think we can safely assume
93
+ // this should be replaced.
94
+ return text.replace(/[-¬]\s+/g, '');
101
95
  }
102
96
  }
103
97
 
@@ -53,22 +53,18 @@ export default class PageChunkIterator {
53
53
  * in the correct order.
54
54
  * @return {PromiseLike<"__PageChunkIterator.AT_END__" | PageChunk>}
55
55
  */
56
- _nextUncontrolled() {
56
+ async _nextUncontrolled() {
57
57
  if (this._cursor.page == this.pageCount) {
58
58
  return Promise.resolve(PageChunkIterator.AT_END);
59
59
  }
60
-
61
60
  this._recenterBuffer(this._cursor.page);
62
-
63
- return this._fetchPageChunks(this._cursor.page)
64
- .then(chunks => {
65
- if (this._cursor.chunk == chunks.length) {
66
- this._cursor.page++;
67
- this._cursor.chunk = 0;
68
- return this._nextUncontrolled();
69
- }
70
- return chunks[this._cursor.chunk++];
71
- });
61
+ const chunks = await this._fetchPageChunks(this._cursor.page);
62
+ if (this._cursor.chunk == chunks.length) {
63
+ this._cursor.page++;
64
+ this._cursor.chunk = 0;
65
+ return this._nextUncontrolled();
66
+ }
67
+ return chunks[this._cursor.chunk++];
72
68
  }
73
69
 
74
70
  /**
@@ -1,6 +1,7 @@
1
1
  /* global br */
2
2
  import { isChrome, isFirefox } from '../../util/browserSniffing.js';
3
- import { sleep, promisifyEvent, isAndroid } from './utils.js';
3
+ import { isAndroid } from './utils.js';
4
+ import { promisifyEvent, sleep } from '../../BookReader/utils.js';
4
5
  import AbstractTTSEngine from './AbstractTTSEngine.js';
5
6
  /** @typedef {import("./AbstractTTSEngine.js").PageChunk} PageChunk */
6
7
  /** @typedef {import("./AbstractTTSEngine.js").AbstractTTSSound} AbstractTTSSound */
@@ -70,7 +71,22 @@ export default class WebTTSEngine extends AbstractTTSEngine {
70
71
  }
71
72
 
72
73
  /** @override */
73
- getVoices() { return speechSynthesis.getVoices(); }
74
+ getVoices() {
75
+ const voices = speechSynthesis.getVoices();
76
+ if (voices.filter(v => v.default).length != 1) {
77
+ // iOS bug where the default system voice is sometimes
78
+ // missing from the list
79
+ voices.unshift({
80
+ voiceURI: 'bookreader.SystemDefault',
81
+ name: 'System Default',
82
+ // Not necessarily true, but very likely
83
+ lang: navigator.language,
84
+ default: true,
85
+ localService: true,
86
+ });
87
+ }
88
+ return voices;
89
+ }
74
90
 
75
91
  /** @override */
76
92
  createSound(chunk) {
@@ -121,7 +137,11 @@ export class WebTTSSound {
121
137
  this.started = false;
122
138
 
123
139
  this.utterance = new SpeechSynthesisUtterance(this.text.slice(this._charIndex));
124
- this.utterance.voice = this.voice;
140
+ // iOS bug where the default system voice is sometimes
141
+ // missing from the list
142
+ if (this.voice?.voiceURI !== 'bookreader.SystemDefault') {
143
+ this.utterance.voice = this.voice;
144
+ }
125
145
  // Need to also set lang (for some reason); won't set voice on Chrome@Android otherwise
126
146
  if (this.voice) this.utterance.lang = this.voice.lang;
127
147
  this.utterance.rate = this.rate;
@@ -167,7 +187,7 @@ export class WebTTSSound {
167
187
  * left off.
168
188
  * @return {Promise<void>}
169
189
  */
170
- reload() {
190
+ async reload() {
171
191
  // We'll restore the pause state, so copy it here
172
192
  const wasPaused = this.paused;
173
193
  // Use recent event to determine where we'll restart from
@@ -179,14 +199,12 @@ export class WebTTSSound {
179
199
  }
180
200
 
181
201
  // We can't modify the utterance object, so we have to make a new one
182
- return this.stop()
183
- .then(() => {
184
- this.load();
185
- // Instead of playing and immediately pausing, we don't start playing. Note
186
- // this is a requirement because pause doesn't work consistently across
187
- // browsers.
188
- if (!wasPaused) this.play();
189
- });
202
+ await this.stop();
203
+ this.load();
204
+ // Instead of playing and immediately pausing, we don't start playing. Note
205
+ // this is a requirement because pause doesn't work consistently across
206
+ // browsers.
207
+ if (!wasPaused) this.play();
190
208
  }
191
209
 
192
210
  play() {
@@ -222,15 +240,16 @@ export class WebTTSSound {
222
240
  return endPromise;
223
241
  }
224
242
 
225
- finish() {
226
- this.stop().then(() => this.utterance.dispatchEvent(new Event('finish')));
243
+ async finish() {
244
+ await this.stop();
245
+ this.utterance.dispatchEvent(new Event('finish'));
227
246
  }
228
247
 
229
248
  /**
230
249
  * @override
231
250
  * Will fire a pause event unless already paused
232
251
  **/
233
- pause() {
252
+ async pause() {
234
253
  if (this.paused) return;
235
254
 
236
255
  const pausePromise = promisifyEvent(this.utterance, 'pause');
@@ -246,19 +265,18 @@ export class WebTTSSound {
246
265
  if (pauseMightNotFire) {
247
266
  // wait for it just in case
248
267
  const timeoutPromise = sleep(100).then(() => 'timeout');
249
- Promise.race([pausePromise, timeoutPromise])
250
- .then(result => {
251
- // We got our pause event; nothing to do!
252
- if (result != 'timeout') return;
253
-
254
- this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
255
- // if pause might not work, then we'll stop entirely and restart later
256
- if (pauseMightNotWork) this.stop();
257
- });
268
+ const result = await Promise.race([pausePromise, timeoutPromise]);
269
+ // We got our pause event; nothing to do!
270
+ if (result != 'timeout') return;
271
+
272
+ this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
273
+
274
+ // if pause might not work, then we'll stop entirely and restart later
275
+ if (pauseMightNotWork) this.stop();
258
276
  }
259
277
  }
260
278
 
261
- resume() {
279
+ async resume() {
262
280
  if (!this.started) {
263
281
  this.play();
264
282
  return;
@@ -278,16 +296,15 @@ export class WebTTSSound {
278
296
  speechSynthesis.resume();
279
297
 
280
298
  if (resumeMightNotFire) {
281
- Promise.race([resumePromise, sleep(100).then(() => 'timeout')])
282
- .then(result => {
283
- if (result != 'timeout') return;
284
-
285
- this.utterance.dispatchEvent(new CustomEvent('resume', {}));
286
- if (resumeMightNotWork) {
287
- const reloadPromise = this.reload();
288
- reloadPromise.then(() => this.play());
289
- }
290
- });
299
+ const result = await Promise.race([resumePromise, sleep(100).then(() => 'timeout')]);
300
+
301
+ if (result != 'timeout') return;
302
+
303
+ this.utterance.dispatchEvent(new CustomEvent('resume', {}));
304
+ if (resumeMightNotWork) {
305
+ await this.reload();
306
+ this.play();
307
+ }
291
308
  }
292
309
  }
293
310
 
@@ -296,6 +313,11 @@ export class WebTTSSound {
296
313
  this.reload();
297
314
  }
298
315
 
316
+ /** @param {SpeechSynthesisVoice} voice */
317
+ setVoice(voice) {
318
+ this.voice = voice;
319
+ this.reload();
320
+ }
299
321
  /**
300
322
  * @private
301
323
  * Chrome has a bug where it only plays 15 seconds of TTS and then
@@ -303,45 +325,39 @@ export class WebTTSSound {
303
325
  * We avoid this (as described here: https://bugs.chromium.org/p/chromium/issues/detail?id=679437#c15 )
304
326
  * by pausing after 14 seconds and ~instantly resuming.
305
327
  */
306
- _chromePausingBugFix() {
328
+ async _chromePausingBugFix() {
307
329
  const timeoutPromise = sleep(14000).then(() => 'timeout');
308
330
  const pausePromise = promisifyEvent(this.utterance, 'pause').then(() => 'paused');
309
331
  const endPromise = promisifyEvent(this.utterance, 'end').then(() => 'ended');
310
- return Promise.race([timeoutPromise, pausePromise, endPromise])
311
- .then(result => {
312
- if (location.toString().indexOf('_debugReadAloud=true') != -1) {
313
- console.log(`CHROME-PAUSE-HACK: ${result}`);
314
- }
315
- switch (result) {
316
- case 'ended':
317
- // audio was stopped/finished; nothing to do
318
- break;
319
- case 'paused':
320
- // audio was paused; wait for resume
321
- // Chrome won't let you resume the audio if 14s have passed 🤷‍
322
- // We could do the same as before (but resume+pause instead of pause+resume),
323
- // but that means we'd _constantly_ be running in the background. So in that
324
- // case, let's just restart the chunk
325
- Promise.race([
326
- promisifyEvent(this.utterance, 'resume'),
327
- sleep(14000).then(() => 'timeout'),
328
- ])
329
- .then(result => {
330
- result == 'timeout' ? this.reload() : this._chromePausingBugFix();
331
- });
332
- break;
333
- case 'timeout':
334
- // We hit Chrome's secret cut off time. Pause/resume
335
- // to be able to keep TTS-ing
336
- speechSynthesis.pause();
337
- sleep(25)
338
- .then(() => {
339
- speechSynthesis.resume();
340
- this._chromePausingBugFix();
341
- });
342
- break;
343
- }
344
- });
332
+ const result = await Promise.race([timeoutPromise, pausePromise, endPromise]);
333
+ if (location.toString().indexOf('_debugReadAloud=true') != -1) {
334
+ console.log(`CHROME-PAUSE-HACK: ${result}`);
335
+ }
336
+ switch (result) {
337
+ case 'ended':
338
+ // audio was stopped/finished; nothing to do
339
+ break;
340
+ case 'paused':
341
+ // audio was paused; wait for resume
342
+ // Chrome won't let you resume the audio if 14s have passed 🤷‍
343
+ // We could do the same as before (but resume+pause instead of pause+resume),
344
+ // but that means we'd _constantly_ be running in the background. So in that
345
+ // case, let's just restart the chunk
346
+ await Promise.race([
347
+ promisifyEvent(this.utterance, 'resume'),
348
+ sleep(14000).then(() => 'timeout'),
349
+ ]);
350
+ result == 'timeout' ? this.reload() : this._chromePausingBugFix();
351
+ break;
352
+ case 'timeout':
353
+ // We hit Chrome's secret cut off time. Pause/resume
354
+ // to be able to keep TTS-ing
355
+ speechSynthesis.pause();
356
+ await sleep(25);
357
+ speechSynthesis.resume();
358
+ this._chromePausingBugFix();
359
+ break;
360
+ }
345
361
  }
346
362
  }
347
363