@internetarchive/bookreader 5.0.0-9-multiple-files → 5.0.0-90

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.
Files changed (333) hide show
  1. package/.eslintrc.js +21 -19
  2. package/.github/workflows/node.js.yml +81 -7
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +505 -1442
  6. package/BookReader/BookReader.js +2 -21564
  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 -12
  13. package/BookReader/icons/2up.svg +1 -15
  14. package/BookReader/icons/advance.svg +3 -26
  15. package/BookReader/icons/chevron-right.svg +1 -1
  16. package/BookReader/icons/close-circle-dark.svg +1 -0
  17. package/BookReader/icons/close-circle.svg +1 -1
  18. package/BookReader/icons/fullscreen.svg +1 -17
  19. package/BookReader/icons/fullscreen_exit.svg +1 -17
  20. package/BookReader/icons/hamburger.svg +1 -15
  21. package/BookReader/icons/left-arrow.svg +1 -12
  22. package/BookReader/icons/magnify-minus.svg +1 -16
  23. package/BookReader/icons/magnify-plus.svg +1 -17
  24. package/BookReader/icons/magnify.svg +1 -15
  25. package/BookReader/icons/pause.svg +1 -23
  26. package/BookReader/icons/play.svg +1 -22
  27. package/BookReader/icons/playback-speed.svg +1 -34
  28. package/BookReader/icons/read-aloud.svg +1 -22
  29. package/BookReader/icons/review.svg +3 -22
  30. package/BookReader/icons/thumbnails.svg +1 -17
  31. package/BookReader/icons/voice.svg +1 -0
  32. package/BookReader/icons/volume-full.svg +1 -22
  33. package/BookReader/images/BRicons.svg +5 -94
  34. package/BookReader/images/books_graphic.svg +1 -177
  35. package/BookReader/images/icon_book.svg +1 -12
  36. package/BookReader/images/icon_bookmark.svg +1 -12
  37. package/BookReader/images/icon_gear.svg +1 -14
  38. package/BookReader/images/icon_hamburger.svg +1 -20
  39. package/BookReader/images/icon_home.svg +1 -21
  40. package/BookReader/images/icon_info.svg +1 -11
  41. package/BookReader/images/icon_one_page.svg +1 -8
  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 -15
  45. package/BookReader/images/icon_search_button.svg +1 -8
  46. package/BookReader/images/icon_share.svg +1 -9
  47. package/BookReader/images/icon_skip-ahead.svg +1 -6
  48. package/BookReader/images/icon_skip-back.svg +2 -13
  49. package/BookReader/images/icon_speaker.svg +1 -18
  50. package/BookReader/images/icon_speaker_open.svg +1 -10
  51. package/BookReader/images/icon_thumbnails.svg +1 -12
  52. package/BookReader/images/icon_toc.svg +1 -5
  53. package/BookReader/images/icon_two_pages.svg +1 -9
  54. package/BookReader/images/marker_chap-off.svg +1 -11
  55. package/BookReader/images/marker_chap-on.svg +1 -11
  56. package/BookReader/images/marker_srch-on.svg +1 -11
  57. package/BookReader/images/unviewable_page.png +0 -0
  58. package/BookReader/jquery-3.js +2 -0
  59. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  60. package/BookReader/plugins/plugin.archive_analytics.js +1 -172
  61. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  62. package/BookReader/plugins/plugin.autoplay.js +1 -165
  63. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  64. package/BookReader/plugins/plugin.chapters.js +22 -301
  65. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  66. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  67. package/BookReader/plugins/plugin.iframe.js +1 -74
  68. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  69. package/BookReader/plugins/plugin.iiif.js +2 -0
  70. package/BookReader/plugins/plugin.iiif.js.map +1 -0
  71. package/BookReader/plugins/plugin.resume.js +1 -368
  72. package/BookReader/plugins/plugin.resume.js.map +1 -1
  73. package/BookReader/plugins/plugin.search.js +2 -1420
  74. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  75. package/BookReader/plugins/plugin.search.js.map +1 -1
  76. package/BookReader/plugins/plugin.text_selection.js +2 -1080
  77. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  78. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  79. package/BookReader/plugins/plugin.tts.js +2 -9193
  80. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  81. package/BookReader/plugins/plugin.tts.js.map +1 -1
  82. package/BookReader/plugins/plugin.url.js +1 -269
  83. package/BookReader/plugins/plugin.url.js.map +1 -1
  84. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -379
  85. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  86. package/BookReader/webcomponents-bundle.js +3 -0
  87. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  88. package/BookReader/webcomponents-bundle.js.map +1 -0
  89. package/BookReaderDemo/BookReaderDemo.css +18 -19
  90. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  91. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  92. package/BookReaderDemo/IADemoBr.js +144 -0
  93. package/BookReaderDemo/demo-advanced.html +2 -2
  94. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  95. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  96. package/BookReaderDemo/demo-fullscreen.html +2 -4
  97. package/BookReaderDemo/demo-iiif.html +99 -12
  98. package/BookReaderDemo/demo-internetarchive.html +214 -18
  99. package/BookReaderDemo/demo-multiple.html +2 -1
  100. package/BookReaderDemo/demo-preview-pages.html +526 -525
  101. package/BookReaderDemo/demo-simple.html +2 -1
  102. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  103. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  104. package/BookReaderDemo/immersion-1up.html +2 -2
  105. package/BookReaderDemo/immersion-mode.html +2 -4
  106. package/BookReaderDemo/toggle_controls.html +3 -2
  107. package/BookReaderDemo/view_mode.html +2 -1
  108. package/BookReaderDemo/viewmode-cycle.html +2 -3
  109. package/CHANGELOG.md +595 -33
  110. package/README.md +14 -1
  111. package/babel.config.js +20 -0
  112. package/codecov.yml +6 -0
  113. package/index.html +5 -2
  114. package/jsconfig.json +19 -0
  115. package/netlify.toml +9 -0
  116. package/package.json +70 -62
  117. package/renovate.json +52 -0
  118. package/scripts/preversion.js +0 -1
  119. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  120. package/src/BookNavigator/assets/button-base.js +10 -2
  121. package/src/BookNavigator/assets/ia-logo.js +17 -0
  122. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  123. package/src/BookNavigator/assets/icon_close.js +1 -1
  124. package/src/BookNavigator/book-navigator.js +590 -0
  125. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  126. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  127. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  128. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +4 -9
  129. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  130. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  131. package/src/BookNavigator/delete-modal-actions.js +1 -1
  132. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  133. package/src/BookNavigator/downloads/downloads.js +29 -25
  134. package/src/BookNavigator/search/search-provider.js +80 -28
  135. package/src/BookNavigator/search/search-results.js +29 -26
  136. package/src/BookNavigator/sharing.js +27 -0
  137. package/src/BookNavigator/viewable-files.js +95 -0
  138. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +13 -12
  139. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +7 -7
  140. package/src/BookReader/BookModel.js +76 -41
  141. package/src/BookReader/DragScrollable.js +233 -0
  142. package/src/BookReader/ImageCache.js +48 -15
  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 +777 -0
  147. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  148. package/src/BookReader/ModeSmoothZoom.js +312 -0
  149. package/src/BookReader/ModeThumb.js +19 -13
  150. package/src/BookReader/Navbar/Navbar.js +70 -54
  151. package/src/BookReader/PageContainer.js +116 -22
  152. package/src/BookReader/ReduceSet.js +3 -3
  153. package/src/BookReader/Toolbar/Toolbar.js +14 -41
  154. package/src/BookReader/events.js +2 -3
  155. package/src/BookReader/options.js +73 -15
  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/classes.js +1 -1
  160. package/src/BookReader/utils.js +128 -13
  161. package/src/BookReader.js +562 -1078
  162. package/src/BookReaderPlugin.js +44 -0
  163. package/src/assets/icons/close-circle-dark.svg +1 -0
  164. package/src/assets/icons/magnify-minus.svg +3 -7
  165. package/src/assets/icons/magnify-plus.svg +3 -7
  166. package/src/assets/icons/voice.svg +1 -0
  167. package/src/assets/images/unviewable_page.png +0 -0
  168. package/src/css/BookReader.scss +1 -17
  169. package/src/css/_BRBookmarks.scss +1 -1
  170. package/src/css/_BRComponent.scss +1 -1
  171. package/src/css/_BRicon.scss +8 -2
  172. package/src/css/_BRmain.scss +33 -27
  173. package/src/css/_BRnav.scss +12 -42
  174. package/src/css/_BRpages.scss +170 -42
  175. package/src/css/_BRsearch.scss +68 -230
  176. package/src/css/_BRtoolbar.scss +5 -5
  177. package/src/css/_TextSelection.scss +87 -27
  178. package/src/css/_colorbox.scss +2 -2
  179. package/src/css/_controls.scss +24 -7
  180. package/src/css/_icons.scss +7 -1
  181. package/src/ia-bookreader/ia-bookreader.js +224 -0
  182. package/src/plugins/plugin.archive_analytics.js +84 -78
  183. package/src/plugins/plugin.autoplay.js +99 -104
  184. package/src/plugins/plugin.chapters.js +237 -191
  185. package/src/plugins/plugin.iframe.js +1 -1
  186. package/src/plugins/plugin.iiif.js +141 -0
  187. package/src/plugins/plugin.resume.js +53 -50
  188. package/src/plugins/plugin.text_selection.js +503 -175
  189. package/src/plugins/plugin.vendor-fullscreen.js +7 -7
  190. package/src/plugins/search/plugin.search.js +183 -121
  191. package/src/plugins/search/utils.js +43 -0
  192. package/src/plugins/search/view.js +67 -202
  193. package/src/plugins/tts/AbstractTTSEngine.js +75 -45
  194. package/src/plugins/tts/FestivalTTSEngine.js +21 -31
  195. package/src/plugins/tts/PageChunk.js +16 -23
  196. package/src/plugins/tts/PageChunkIterator.js +11 -17
  197. package/src/plugins/tts/WebTTSEngine.js +88 -72
  198. package/src/plugins/tts/plugin.tts.js +310 -350
  199. package/src/plugins/tts/utils.js +16 -26
  200. package/src/plugins/url/UrlPlugin.js +191 -0
  201. package/src/plugins/{plugin.url.js → url/plugin.url.js} +47 -18
  202. package/src/util/browserSniffing.js +22 -0
  203. package/src/util/docCookies.js +21 -2
  204. package/src/util/strings.js +1 -0
  205. package/tests/e2e/README.md +37 -0
  206. package/tests/e2e/autoplay.test.js +9 -6
  207. package/tests/e2e/base.test.js +8 -16
  208. package/tests/e2e/helpers/base.js +55 -50
  209. package/tests/e2e/helpers/debug.js +1 -1
  210. package/tests/e2e/helpers/mockSearch.js +19 -22
  211. package/tests/e2e/helpers/params.js +17 -0
  212. package/tests/e2e/helpers/rightToLeft.js +8 -14
  213. package/tests/e2e/helpers/search.js +73 -0
  214. package/tests/e2e/models/Navigation.js +20 -37
  215. package/tests/e2e/rightToLeft.test.js +4 -5
  216. package/tests/e2e/viewmode.test.js +40 -33
  217. package/tests/jest/BookNavigator/book-navigator.test.js +661 -0
  218. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  219. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  220. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  221. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  222. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  223. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  224. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  225. package/tests/{karma/BookNavigator → jest/BookNavigator/search}/search-results.test.js +109 -60
  226. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  227. package/tests/jest/BookNavigator/viewable-files/viewable-files-provider.test.js +80 -0
  228. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  229. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  230. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  231. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  232. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  233. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  234. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  235. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  236. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  237. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  238. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +42 -29
  239. package/tests/jest/BookReader/PageContainer.test.js +238 -0
  240. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  241. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +3 -3
  242. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  243. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  244. package/tests/jest/BookReader/utils/SelectionObserver.test.js +57 -0
  245. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  246. package/tests/jest/BookReader/utils.test.js +250 -0
  247. package/tests/jest/BookReader.keyboard.test.js +190 -0
  248. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +10 -2
  249. package/tests/{BookReader.test.js → jest/BookReader.test.js} +43 -53
  250. package/tests/jest/plugins/plugin.archive_analytics.test.js +20 -0
  251. package/tests/jest/plugins/plugin.autoplay.test.js +35 -0
  252. package/tests/jest/plugins/plugin.chapters.test.js +195 -0
  253. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +4 -4
  254. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +22 -35
  255. package/tests/jest/plugins/plugin.text_selection.test.js +316 -0
  256. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  257. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +26 -47
  258. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +42 -9
  259. package/tests/jest/plugins/search/utils.js +25 -0
  260. package/tests/jest/plugins/search/utils.test.js +29 -0
  261. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +30 -10
  262. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  263. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  264. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  265. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  266. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  267. package/tests/jest/plugins/url/UrlPlugin.test.js +198 -0
  268. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +57 -18
  269. package/tests/jest/setup.js +3 -0
  270. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  271. package/tests/jest/util/docCookies.test.js +24 -0
  272. package/tests/{util → jest/util}/strings.test.js +1 -1
  273. package/tests/{utils.js → jest/utils.js} +38 -0
  274. package/webpack.config.js +16 -10
  275. package/.babelrc +0 -12
  276. package/.dependabot/config.yml +0 -6
  277. package/.testcaferc.json +0 -5
  278. package/BookReader/bookreader-component-bundle.js +0 -14330
  279. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  280. package/BookReader/bookreader-component-bundle.js.map +0 -1
  281. package/BookReader/icons/sort-ascending.svg +0 -1
  282. package/BookReader/icons/sort-descending.svg +0 -1
  283. package/BookReader/jquery-1.10.1.js +0 -108
  284. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  285. package/BookReader/plugins/plugin.menu_toggle.js +0 -369
  286. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  287. package/BookReader/plugins/plugin.mobile_nav.js +0 -335
  288. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  289. package/BookReaderDemo/BookReaderJSAutoplay.js +0 -56
  290. package/BookReaderDemo/IIIFBookReader.js +0 -207
  291. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  292. package/BookReaderDemo/demo-autoplay.html +0 -38
  293. package/BookReaderDemo/demo-iiif.js +0 -26
  294. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  295. package/karma.conf.js +0 -23
  296. package/src/BookNavigator/BookModel.js +0 -14
  297. package/src/BookNavigator/BookNavigator.js +0 -452
  298. package/src/BookNavigator/assets/book-loader.js +0 -27
  299. package/src/BookNavigator/assets/icon_sort_ascending.js +0 -5
  300. package/src/BookNavigator/assets/icon_sort_descending.js +0 -5
  301. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  302. package/src/BookNavigator/search/a-search-result.js +0 -55
  303. package/src/BookNavigator/volumes/volumes-provider.js +0 -108
  304. package/src/BookNavigator/volumes/volumes.js +0 -162
  305. package/src/BookReader/DebugConsole.js +0 -54
  306. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  307. package/src/ItemNavigator/ItemNavigator.js +0 -372
  308. package/src/ItemNavigator/providers/sharing.js +0 -29
  309. package/src/assets/icons/sort-ascending.svg +0 -1
  310. package/src/assets/icons/sort-descending.svg +0 -1
  311. package/src/css/_MobileNav.scss +0 -194
  312. package/src/dragscrollable-br.js +0 -261
  313. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  314. package/src/plugins/plugin.mobile_nav.js +0 -287
  315. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  316. package/tests/BookReader/DebugConsole.test.js +0 -25
  317. package/tests/BookReader/Mode1Up.test.js +0 -164
  318. package/tests/BookReader/Mode2Up.test.js +0 -247
  319. package/tests/BookReader/PageContainer.test.js +0 -115
  320. package/tests/BookReader/utils.test.js +0 -109
  321. package/tests/e2e/helpers/desktopSearch.js +0 -72
  322. package/tests/e2e/helpers/mobileSearch.js +0 -85
  323. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  324. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  325. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  326. package/tests/karma/BookNavigator/volumes.test.js +0 -133
  327. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  328. package/tests/plugins/plugin.archive_analytics.test.js +0 -23
  329. package/tests/plugins/plugin.autoplay.test.js +0 -52
  330. package/tests/plugins/plugin.chapters.test.js +0 -130
  331. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  332. package/tests/plugins/plugin.text_selection.test.js +0 -203
  333. package/tests/util/docCookies.test.js +0 -15
@@ -1,75 +1,29 @@
1
1
  class SearchView {
2
2
  /**
3
3
  * @param {object} params
4
- * @param {string} params.selector A selector for the element that the search tray will be rendered in
5
- * @param {string} params.query An existing query string
6
- * @param {object} params.br The BookReader instance
4
+ * @param {object} params.br The BookReader instance
5
+ * @param {function} params.cancelSearch callback when a user wants to cancel search
7
6
  *
8
7
  * @event BookReader:SearchResultsCleared - when the search results nav gets cleared
9
8
  * @event BookReader:ToggleSearchMenu - when search results menu should toggle
10
9
  */
11
- constructor(params) {
12
- if (!params.selector) {
13
- console.warn('BookReader::Search - SearchView must be passed a valid CSS selector');
14
- return;
15
- }
16
-
17
- this.br = params.br;
18
-
19
- // Search results are returned as a text blob with the hits wrapped in
20
- // triple mustaches. Hits occasionally include text beyond the search
21
- // term, so everything within the staches is captured and wrapped.
22
- this.matcher = new RegExp('{{{(.+?)}}}', 'g');
10
+ constructor({ br, searchCancelledCallback = () => {} }) {
11
+ this.br = br;
23
12
  this.matches = [];
24
- this.cacheDOMElements(params.selector);
13
+ this.cacheDOMElements();
14
+ this.cancelSearch = searchCancelledCallback;
15
+ }
16
+
17
+ init() {
25
18
  this.bindEvents();
26
19
  }
27
20
 
28
- /**
29
- * @param {string} selector A selector for the element that the search tray will be rendered in
30
- */
31
- cacheDOMElements(selector) {
21
+ cacheDOMElements() {
32
22
  this.dom = {};
33
-
34
- // The parent search tray in mobile menu
35
- this.dom.searchTray = this.renderSearchTray(selector);
36
- // Container for rendered search results
37
- this.dom.results = this.dom.searchTray.querySelector('[data-id="results"]');
38
- // Element used to display number of results
39
- this.dom.resultsCount = this.dom.searchTray.querySelector('[data-id="results_count"]');
40
- // Search input within the mobile search tray
41
- this.dom.searchField = this.dom.searchTray.querySelector('[name="query"]');
42
- // Waiting indicator displayed while waiting for a search request
43
- this.dom.searchPending = this.dom.searchTray.querySelector('[data-id="searchPending"]');
44
- // The element added to the mobile menu that is animated into view when
45
- // the "search" nav item is clicked
46
- this.dom.mobileSearch = this.buildMobileDrawer();
47
23
  // Search input within the top toolbar. Will be removed once the mobile menu is replaced.
48
24
  this.dom.toolbarSearch = this.buildToolbarSearch();
49
25
  }
50
26
 
51
- /**
52
- * @param {boolean} bool
53
- */
54
- toggleSearchTray(bool = this.dom.searchTray.classList.contains('hidden')) {
55
- this.dom.searchTray.classList.toggle('hidden', !bool);
56
- }
57
-
58
- /**
59
- * @param {boolean} bool
60
- */
61
- toggleResultsCount(bool) {
62
- this.dom.resultsCount.classList.toggle('visible', bool);
63
- }
64
-
65
- /**
66
- * @param {SearchInsideResults} results
67
- */
68
- updateResultsCount(results) {
69
- this.dom.resultsCount.innerText = `(${results} result${results != 1 ? 's' : ''})`;
70
- this.toggleResultsCount(true);
71
- }
72
-
73
27
  /**
74
28
  * @param {string} query
75
29
  */
@@ -78,7 +32,6 @@ class SearchView {
78
32
  }
79
33
 
80
34
  emptyMatches() {
81
- this.dom.results.innerHTML = '';
82
35
  this.matches = [];
83
36
  }
84
37
 
@@ -86,48 +39,20 @@ class SearchView {
86
39
  this.br.$('.BRnavpos .BRsearch').remove();
87
40
  }
88
41
 
89
- clearSearchFieldAndResults() {
42
+ clearSearchFieldAndResults(dispatchEventWhenComplete = true) {
90
43
  this.br.removeSearchResults();
91
- this.toggleResultsCount(false);
92
44
  this.removeResultPins();
93
45
  this.emptyMatches();
94
46
  this.setQuery('');
95
47
  this.teardownSearchNavigation();
96
- this.br.trigger('SearchResultsCleared');
48
+ if (dispatchEventWhenComplete) {
49
+ this.br.trigger('SearchResultsCleared');
50
+ }
97
51
  }
98
52
 
99
53
  toggleSidebar() {
100
54
  this.br.trigger('ToggleSearchMenu');
101
55
  }
102
- /**
103
- * @param {string} selector The ID attribute to be used for the search tray
104
- */
105
- renderSearchTray(selector) {
106
- const searchTray = document.createElement('div');
107
- searchTray.setAttribute('id', selector.replace(/^#/, ''));
108
- searchTray.innerHTML = `
109
- <header>
110
- <div>
111
- <h3>Search inside</h3>
112
- <p data-id="results_count"></p>
113
- </div>
114
- <a href="#" class="close"></a>
115
- </header>
116
- <form action="" method="get">
117
- <fieldset>
118
- <input name="all_files" id="all_files" type="checkbox" />
119
- <label class="checkbox" for="all_files">Search all files</label>
120
- <input type="search" name="query" placeholder="Enter a search term" />
121
- </fieldset>
122
- </form>
123
- <div data-id="searchPending" id="search_pending">
124
- <p>Your search results will appear below</p>
125
- <div class="loader tc mt20"></div>
126
- </div>
127
- <ul data-id="results"></ul>
128
- `;
129
- return searchTray;
130
- }
131
56
 
132
57
  renderSearchNavigation() {
133
58
  const selector = 'BRsearch-navigation';
@@ -215,17 +140,19 @@ class SearchView {
215
140
  const start = pool.slice(0, pool.length / 2);
216
141
  const end = pool.slice(pool.length / 2);
217
142
  return closestTo((comparisonFn(start, end, comparator) ? start : end), comparator);
218
- }
143
+ };
219
144
 
220
145
  const closestPage = closestTo(matchPages, currentPage);
221
146
  return this.matches.indexOf(this.matches.find((m) => m.par[0].page === closestPage));
222
147
  }
223
148
 
224
149
  updateResultsPosition() {
150
+ if (!this.dom.searchNavigation) return;
225
151
  this.dom.searchNavigation.find('[data-id=resultsCount]').text(this.resultsPosition());
226
152
  }
227
153
 
228
154
  updateSearchNavigationButtons() {
155
+ if (!this.dom.searchNavigation) return;
229
156
  this.dom.searchNavigation.find('.prev').attr('disabled', !this.currentMatchIndex);
230
157
  this.dom.searchNavigation.find('.next').attr('disabled', this.currentMatchIndex + 1 === this.matches.length);
231
158
  }
@@ -272,19 +199,6 @@ class SearchView {
272
199
  this.updateSearchNavigationButtons();
273
200
  }
274
201
 
275
- /**
276
- * @param {array} matches
277
- */
278
- renderMatches(matches) {
279
- const items = matches.map((match) => `
280
- <li data-page="${match.par[0].page}" data-page-index="${this.br.leafNumToIndex(match.par[0].page)}">
281
- <h4>Page ${match.par[0].page}</h4>
282
- <p>${match.text.replace(this.matcher, '<mark>$1</mark>')}</p>
283
- </li>
284
- `);
285
- this.dom.results.innerHTML = items.join('');
286
- }
287
-
288
202
  /**
289
203
  * @param {boolean} bool
290
204
  */
@@ -293,23 +207,6 @@ class SearchView {
293
207
  this.br.refs.$BRfooter.find('.BRsearch').css({ visibility: pinsVisibleState });
294
208
  }
295
209
 
296
- buildMobileDrawer() {
297
- const mobileSearch = document.createElement('li');
298
- mobileSearch.innerHTML = `
299
- <span>
300
- <span class="DrawerIconWrapper">
301
- <img class="DrawerIcon" src="${this.br.imagesBaseURL}icon_search_button.svg" />
302
- </span>
303
- Search
304
- </span>
305
- <div data-id="search_slot">
306
- </div>
307
- `;
308
- mobileSearch.querySelector('[data-id="search_slot"]').appendChild(this.dom.searchTray);
309
- mobileSearch.classList.add('BRmobileMenu__search');
310
- return mobileSearch;
311
- }
312
-
313
210
  buildToolbarSearch() {
314
211
  const toolbarSearch = document.createElement('span');
315
212
  toolbarSearch.classList.add('BRtoolbarSection', 'BRtoolbarSectionSearch');
@@ -329,25 +226,20 @@ class SearchView {
329
226
  */
330
227
  renderPins(matches) {
331
228
  matches.forEach((match) => {
332
- const queryString = match.text;
333
- const pageIndex = this.br.leafNumToIndex(match.par[0].page);
334
- const pageNumber = this.br.getPageNum(pageIndex);
229
+ const pageIndex = this.br.book.leafNumToIndex(match.par[0].page);
335
230
  const uiStringSearch = "Search result"; // i18n
336
- const uiStringPage = "Page"; // i18n
337
-
338
- const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.getNumLeafs() - 1);
339
-
340
- const queryStringWithB = queryString.replace(this.matcher, '<b>$1</b>');
341
-
342
- let queryStringWithBTruncated = '';
343
-
344
- if (queryString.length > 100) {
345
- queryStringWithBTruncated = queryString
346
- .replace(/^(.{100}[^\s]*).*/, "$1")
347
- .replace(this.matcher, '<b>$1</b>')
348
- + '...';
231
+ const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.book.getNumLeafs() - 1);
232
+
233
+ let html = match.html;
234
+ if (html.length > 200) {
235
+ const start = Math.max(0, html.indexOf('<mark>') - 100);
236
+ if (start != 0) {
237
+ html = '' + match.html
238
+ .substring(start)
239
+ // Make sure at word boundary though
240
+ .replace(/^\S+/, '');
241
+ }
349
242
  }
350
-
351
243
  // draw marker
352
244
  $('<div>')
353
245
  .addClass('BRsearch')
@@ -357,35 +249,27 @@ class SearchView {
357
249
  .attr('title', uiStringSearch)
358
250
  .append(`
359
251
  <div class="BRquery">
360
- <div>${queryStringWithBTruncated || queryStringWithB}</div>
361
- <div>${uiStringPage} ${pageNumber}</div>
252
+ <main>${html}</main>
253
+ <footer>Page ${match.displayPageNumber}</footer>
362
254
  </div>
363
255
  `)
364
- .data({ pageIndex })
365
256
  .appendTo(this.br.$('.BRnavline'))
366
- .hover(
367
- (event) => {
368
- // remove from other markers then turn on just for this
369
- // XXX should be done when nav slider moves
370
- const marker = event.currentTarget;
371
- const tooltip = marker.querySelector('.BRquery');
372
- const tooltipOffset = tooltip.getBoundingClientRect();
373
- const targetOffset = marker.getBoundingClientRect();
374
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
375
- if (tooltipOffset.x - boxSizeAdjust < 0) {
376
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
377
- }
378
- $('.BRsearch,.BRchapter').removeClass('front');
379
- $(event.target).addClass('front');
380
- },
381
- (event) => $(event.target).removeClass('front'))
382
- .click(function (event) {
383
- // closures are nested and deep, using an arrow function breaks references.
384
- // Todo: update to arrow function & clean up closures
385
- // to remove `bind` dependency
386
- this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
387
- this.br.updateSearchHilites();
388
- }.bind(this));
257
+ .on("mouseenter", (event) => {
258
+ // remove from other markers then turn on just for this
259
+ // XXX should be done when nav slider moves
260
+ const marker = event.currentTarget;
261
+ const tooltip = marker.querySelector('.BRquery');
262
+ const tooltipOffset = tooltip.getBoundingClientRect();
263
+ const targetOffset = marker.getBoundingClientRect();
264
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
265
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
266
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
267
+ }
268
+ $('.BRsearch,.BRchapter').removeClass('front');
269
+ $(event.target).addClass('front');
270
+ })
271
+ .on("mouseleave", (event) => $(event.target).removeClass('front'))
272
+ .on("click", () => { this.br._searchPluginGoToResult(match.matchIndex); });
389
273
  });
390
274
  }
391
275
 
@@ -393,20 +277,28 @@ class SearchView {
393
277
  * @param {boolean} bool
394
278
  */
395
279
  toggleSearchPending(bool) {
396
- this.dom.searchPending.classList.toggle('visible', bool);
397
280
  if (bool) {
398
- this.br.showProgressPopup("Search results will appear below...");
281
+ this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
399
282
  }
400
283
  else {
401
284
  this.br.removeProgressPopup();
402
285
  }
403
286
  }
404
287
 
405
- renderErrorModal() {
288
+ /**
289
+ * Primary callback when user cancels search popup
290
+ */
291
+ progressPopupClosed() {
292
+ this.toggleSearchPending();
293
+ this.cancelSearch();
294
+ }
295
+
296
+ renderErrorModal(textIsProcessing = false) {
297
+ const errorDetails = `${!textIsProcessing ? 'The text may still be processing. ' : ''}Please try again.`;
406
298
  this.renderModalMessage(`
407
299
  Sorry, there was an error with your search.
408
300
  <br />
409
- The text may still be processing.
301
+ ${errorDetails}
410
302
  `);
411
303
  this.delayModalRemovalFor(4000);
412
304
  }
@@ -445,14 +337,6 @@ class SearchView {
445
337
  setTimeout(this.br.removeProgressPopup.bind(this.br), timeoutMS);
446
338
  }
447
339
 
448
- openMobileMenu() {
449
- this.br.refs.$mmenu.data('mmenu').open();
450
- }
451
-
452
- closeMobileMenu() {
453
- this.br.refs.$mmenu.data('mmenu').close();
454
- }
455
-
456
340
  /**
457
341
  * @param {Event} e
458
342
  */
@@ -461,7 +345,6 @@ class SearchView {
461
345
  const query = e.target.querySelector('[name="query"]').value;
462
346
  if (!query.length) { return false; }
463
347
  this.br.search(query);
464
- this.dom.searchField.blur();
465
348
  this.emptyMatches();
466
349
  this.toggleSearchPending(true);
467
350
  return false;
@@ -479,9 +362,7 @@ class SearchView {
479
362
  this.teardownSearchNavigation();
480
363
  this.renderSearchNavigation();
481
364
  this.bindSearchNavigationEvents();
482
- this.renderMatches(results.matches);
483
365
  this.renderPins(results.matches);
484
- this.updateResultsCount(results.matches.length);
485
366
  this.toggleSearchPending(false);
486
367
  if (options.goToFirstResult) {
487
368
  $(document).one('BookReader:pageChanged', () => {
@@ -492,15 +373,6 @@ class SearchView {
492
373
  }
493
374
  }
494
375
 
495
- /**
496
- * @param {Event} e
497
- */
498
- handleNavToggledCallback(e) {
499
- const is_visible = this.br.navigationIsVisible();
500
- this.togglePinsFor(is_visible);
501
- this.toggleSearchTray(is_visible ? !!this.dom.results.querySelector('li') : false);
502
- }
503
-
504
376
  handleSearchStarted() {
505
377
  this.emptyMatches();
506
378
  this.br.removeSearchHilites();
@@ -510,9 +382,14 @@ class SearchView {
510
382
  this.setQuery(this.br.searchTerm);
511
383
  }
512
384
 
513
- handleSearchCallbackError() {
385
+ /**
386
+ * Event listener for: `BookReader:SearchCallbackError`
387
+ * @param {CustomEvent} event
388
+ */
389
+ handleSearchCallbackError(event = {}) {
514
390
  this.toggleSearchPending(false);
515
- this.renderErrorModal();
391
+ const isIndexed = event?.detail?.props?.results?.indexed;
392
+ this.renderErrorModal(isIndexed);
516
393
  }
517
394
 
518
395
  handleSearchCallbackBookNotIndexed() {
@@ -528,26 +405,14 @@ class SearchView {
528
405
  bindEvents() {
529
406
  const namespace = 'BookReader:';
530
407
 
408
+ window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
531
409
  $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
532
- .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
533
410
  .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
534
- .on(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this))
535
411
  .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
536
412
  .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))
537
413
  .on(`${namespace}pageChanged`, this.updateSearchNavigation.bind(this));
538
414
 
539
- this.dom.searchTray.addEventListener('submit', this.submitHandler.bind(this));
540
415
  this.dom.toolbarSearch.querySelector('form').addEventListener('submit', this.submitHandler.bind(this));
541
- this.dom.searchField.addEventListener('search', () => {
542
- if (this.dom.searchField.value) { return; }
543
- this.clearSearchFieldAndResults();
544
- });
545
-
546
- $(this.dom.results).on('click', 'li', (e) => {
547
- this.br._searchPluginGoToResult(+e.currentTarget.dataset.pageIndex);
548
- this.br.updateSearchHilites();
549
- this.closeMobileMenu();
550
- });
551
416
  }
552
417
  }
553
418
 
@@ -1,12 +1,12 @@
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
 
5
6
  /**
6
7
  * @export
7
8
  * @typedef {Object} TTSEngineOptions
8
- * @property {String} server
9
- * @property {String} bookPath
9
+ * @property {import('@/src/util/strings.js').StringWithVars} pageChunkUrl
10
10
  * @property {ISO6391} bookLanguage
11
11
  * @property {Function} onLoadingStart
12
12
  * @property {Function} onLoadingComplete
@@ -28,6 +28,7 @@ import PageChunkIterator from './PageChunkIterator.js';
28
28
  * @property {() => void} resume
29
29
  * @property {() => void} finish force the sound to 'finish'
30
30
  * @property {number => void} setPlaybackRate
31
+ * @property {SpeechSynthesisVoice => void} setVoice
31
32
  **/
32
33
 
33
34
  /** Handling bookreader's text-to-speech */
@@ -39,6 +40,7 @@ export default class AbstractTTSEngine {
39
40
  constructor(options) {
40
41
  this.playing = false;
41
42
  this.paused = false;
43
+ /** @type {TTSEngineOptions} */
42
44
  this.opts = options;
43
45
  /** @type {PageChunkIterator} */
44
46
  this._chunkIterator = null;
@@ -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,17 +71,21 @@ 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, {
83
- server: this.opts.server,
84
- bookPath: this.opts.bookPath,
88
+ pageChunkUrl: this.opts.pageChunkUrl,
85
89
  pageBufferSize: 5,
86
90
  });
87
91
 
@@ -92,6 +96,7 @@ export default class AbstractTTSEngine {
92
96
  stop() {
93
97
  if (this.activeSound) this.activeSound.stop();
94
98
  this.playing = false;
99
+ this.paused = true;
95
100
  this._chunkIterator = null;
96
101
  this.activeSound = null;
97
102
  this.events.trigger('stop');
@@ -124,13 +129,26 @@ export default class AbstractTTSEngine {
124
129
  }
125
130
 
126
131
  /** @public */
127
- jumpBackward() {
128
- Promise.all([
132
+ async jumpBackward() {
133
+ await Promise.all([
129
134
  this.activeSound.stop(),
130
135
  this._chunkIterator.decrement()
131
- .then(() => this._chunkIterator.decrement())
132
- ])
133
- .then(() => this.step());
136
+ .then(() => this._chunkIterator.decrement()),
137
+ ]);
138
+ this.step();
139
+ }
140
+
141
+ /** @param {string} voiceURI */
142
+ setVoice(voiceURI) {
143
+ // if the user actively selects a voice, don't re-choose best voice anymore
144
+ // MS Edge fires voices changed randomly very often
145
+ this.events.off('voiceschanged', this.updateBestVoice);
146
+ this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI);
147
+ // if the current book has a language set, store the selected voice with the book language as a suffix
148
+ if (this.opts.bookLanguage && hasLocalStorage()) {
149
+ localStorage.setItem(`BRtts-voice-${this.opts.bookLanguage}`, this.voice.voiceURI);
150
+ }
151
+ if (this.activeSound) this.activeSound.setVoice(this.voice);
134
152
  }
135
153
 
136
154
  /** @param {number} newRate */
@@ -140,36 +158,33 @@ export default class AbstractTTSEngine {
140
158
  }
141
159
 
142
160
  /** @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
- });
161
+ async step() {
162
+ const chunk = await this._chunkIterator.next();
163
+ if (chunk == PageChunkIterator.AT_END) {
164
+ this.stop();
165
+ this.opts.onDone();
166
+ return;
167
+ }
168
+ this.opts.onLoadingStart();
169
+ const sound = this.createSound(chunk);
170
+ sound.chunk = chunk;
171
+ sound.rate = this.playbackRate;
172
+ sound.voice = this.voice;
173
+ sound.load(() => this.opts.onLoadingComplete());
174
+
175
+ this.opts.onLoadingComplete();
176
+
177
+ await this.opts.beforeChunkPlay(chunk);
178
+
179
+ if (!this.playing) return;
180
+
181
+ const playPromise = await this.playSound(sound)
182
+ .then(()=> this.opts.afterChunkPlay(sound.chunk));
183
+
184
+ if (this.paused) this.pause();
185
+ await playPromise;
186
+
187
+ if (this.playing) return this.step();
173
188
  }
174
189
 
175
190
  /**
@@ -212,10 +227,12 @@ export default class AbstractTTSEngine {
212
227
  // user languages that match the book language
213
228
  const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
214
229
 
215
- // Try to find voices that intersect these two sets
216
- return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) ||
230
+ // First try to find the last chosen voice from localStorage for the current book language
231
+ return AbstractTTSEngine.getMatchingStoredVoice(bookLangVoices, bookLanguage)
232
+ // Try to find voices that intersect these two sets
233
+ || AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices)
217
234
  // no user languages match the books; let's return the best voice for the book language
218
- (bookLangVoices.find(v => v.default) || bookLangVoices[0])
235
+ || (bookLangVoices.find(v => v.default) || bookLangVoices[0])
219
236
  // No voices match the book language? let's find a voice in the user's language
220
237
  // and ignore book lang
221
238
  || AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
@@ -223,6 +240,19 @@ export default class AbstractTTSEngine {
223
240
  || (voices.find(v => v.default) || voices[0]);
224
241
  }
225
242
 
243
+ /**
244
+ * @private
245
+ * Get the voice last selected by the user for the book language from localStorage.
246
+ * Returns undefined if no voice is stored or found.
247
+ * @param {SpeechSynthesisVoice[]} voices browser voices to choose from
248
+ * @param {ISO6391} bookLanguage book language to look for
249
+ * @return {SpeechSynthesisVoice | undefined}
250
+ */
251
+ static getMatchingStoredVoice(voices, bookLanguage) {
252
+ const storedVoice = hasLocalStorage() && localStorage.getItem(`BRtts-voice-${bookLanguage}`);
253
+ return (storedVoice ? voices.find(v => v.voiceURI === storedVoice) : undefined);
254
+ }
255
+
226
256
  /**
227
257
  * @private
228
258
  * Get the best voice that matches one of the BCP47 languages (order by preference)