@internetarchive/bookreader 5.0.0-42 → 5.0.0-44-a2

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 (74) hide show
  1. package/.github/workflows/node.js.yml +14 -14
  2. package/.github/workflows/npm-publish.yml +4 -4
  3. package/BookReader/BookReader.css +14 -15
  4. package/BookReader/BookReader.js +1 -1
  5. package/BookReader/BookReader.js.map +1 -1
  6. package/BookReader/ia-bookreader-bundle.js +87 -87
  7. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  8. package/BookReader/icons/pause.svg +1 -1
  9. package/BookReader/icons/playback-speed.svg +1 -1
  10. package/BookReader/icons/read-aloud.svg +1 -1
  11. package/BookReader/images/BRicons.svg +2 -2
  12. package/BookReader/images/books_graphic.svg +1 -1
  13. package/BookReader/images/icon_book.svg +1 -1
  14. package/BookReader/images/icon_gear.svg +1 -1
  15. package/BookReader/images/icon_info.svg +1 -1
  16. package/BookReader/images/icon_playback-rate.svg +1 -1
  17. package/BookReader/images/icon_search_button.svg +1 -1
  18. package/BookReader/images/icon_share.svg +1 -1
  19. package/BookReader/images/icon_speaker.svg +1 -1
  20. package/BookReader/images/icon_speaker_open.svg +1 -1
  21. package/BookReader/images/marker_chap-off.svg +1 -1
  22. package/BookReader/images/marker_chap-on.svg +1 -1
  23. package/BookReader/images/marker_srch-on.svg +1 -1
  24. package/BookReader/jquery-1.10.1.js +1 -1
  25. package/BookReader/jquery-1.10.1.js.LICENSE.txt +6 -6
  26. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  27. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  28. package/BookReader/plugins/plugin.resume.js +1 -1
  29. package/BookReader/plugins/plugin.resume.js.map +1 -1
  30. package/BookReader/plugins/plugin.search.js +1 -1
  31. package/BookReader/plugins/plugin.search.js.map +1 -1
  32. package/BookReader/plugins/plugin.text_selection.js +1 -1
  33. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  34. package/BookReader/plugins/plugin.tts.js +1 -1
  35. package/BookReader/plugins/plugin.tts.js.map +1 -1
  36. package/BookReader/plugins/plugin.url.js +1 -1
  37. package/BookReader/plugins/plugin.url.js.map +1 -1
  38. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  39. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  40. package/BookReaderDemo/IADemoBr.js +22 -0
  41. package/BookReaderDemo/demo-internetarchive.html +3 -0
  42. package/CHANGELOG.md +5 -0
  43. package/babel.config.js +1 -1
  44. package/package.json +24 -29
  45. package/renovate.json +13 -4
  46. package/scripts/preversion.js +4 -1
  47. package/src/BookNavigator/book-navigator.js +4 -0
  48. package/src/BookNavigator/downloads/downloads-provider.js +14 -5
  49. package/src/BookNavigator/downloads/downloads.js +25 -1
  50. package/src/BookNavigator/search/a-search-result.js +4 -6
  51. package/src/BookNavigator/search/search-provider.js +1 -0
  52. package/src/BookNavigator/search/search-results.js +4 -0
  53. package/src/css/_controls.scss +1 -2
  54. package/src/plugins/search/plugin.search.js +25 -4
  55. package/src/plugins/search/view.js +1 -3
  56. package/src/plugins/tts/plugin.tts.js +15 -3
  57. package/tests/{karma → jest}/BookNavigator/book-navigator.test.js +119 -104
  58. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-button.test.js +13 -14
  59. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  60. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  61. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  62. package/tests/{karma → jest}/BookNavigator/downloads/downloads-provider.test.js +18 -18
  63. package/tests/{karma → jest}/BookNavigator/downloads/downloads.test.js +7 -8
  64. package/tests/{karma → jest}/BookNavigator/search/search-provider.test.js +29 -29
  65. package/tests/{karma → jest}/BookNavigator/search/search-results.test.js +57 -56
  66. package/tests/{karma → jest}/BookNavigator/sharing/sharing-provider.test.js +8 -8
  67. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  68. package/tests/{karma → jest}/BookNavigator/volumes/volumes-provider.test.js +38 -38
  69. package/tests/{karma → jest}/BookNavigator/volumes/volumes.test.js +15 -16
  70. package/tests/jest/plugins/search/plugin.search.test.js +40 -23
  71. package/tests/jest/plugins/tts/AbstractTTSEngine.test.js +3 -3
  72. package/karma.conf.js +0 -23
  73. package/tests/karma/BookNavigator/bookmarks/ia-bookmarks.test.js +0 -57
  74. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
@@ -77,6 +77,9 @@
77
77
  <p>Features behind signed in gate: Bookmarks</p>
78
78
  <p>Logged In Status: <span id="logged-in-status">Logged Out</span></p>
79
79
  </div>
80
+ <div class="demo">
81
+ <button id="show-lcp">LCP Download option</button>
82
+ </div>
80
83
  <div class="demo">
81
84
  <button id="multi-volume">Multiple books</button>
82
85
  </div>
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 5.0.0-43
2
+ Fix: search results panel display asserted page numbers @cdrini
3
+ Dev: dependency updates @renovate
4
+ Dev: node-fetch update @cdrini
5
+
1
6
  # 5.0.0-42
2
7
  Dev: update testing dependencies @renovate
3
8
  Dev: update `<ia-item-navigator>` @iisa
package/babel.config.js CHANGED
@@ -2,7 +2,7 @@ module.exports = {
2
2
  presets: [
3
3
  [
4
4
  "@babel/preset-env",
5
- {
5
+ process.env.NODE_ENV == 'test' ? { targets: {esmodules: true} } : {
6
6
  targets: "> 2%, ie 11, edge 14, samsung > 9, OperaMini all, UCAndroid > 12, Safari >= 9",
7
7
  useBuiltIns: "usage",
8
8
  corejs: 3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-42",
3
+ "version": "5.0.0-44-a2",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,50 +41,49 @@
41
41
  "lit": "^2.2.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@babel/core": "7.17.5",
44
+ "@babel/core": "7.17.9",
45
45
  "@babel/eslint-parser": "7.17.0",
46
46
  "@babel/plugin-proposal-class-properties": "7.16.7",
47
- "@babel/plugin-proposal-decorators": "7.17.2",
47
+ "@babel/plugin-proposal-decorators": "7.17.9",
48
48
  "@babel/preset-env": "7.16.11",
49
- "@open-wc/testing": "^3.1.3",
50
- "@open-wc/testing-karma": "^4.0.9",
51
- "@types/jest": "^27.4.1",
49
+ "@open-wc/testing-helpers": "^2.1.2",
50
+ "@types/jest": "^27.5.1",
52
51
  "@webcomponents/webcomponentsjs": "^2.6.0",
53
- "babel-loader": "8.2.3",
52
+ "babel-loader": "8.2.5",
54
53
  "codecov": "^3.8.3",
55
- "concurrently": "7.0.0",
56
- "core-js": "3.16.2",
54
+ "concurrently": "7.2.2",
55
+ "core-js": "3.22.3",
57
56
  "cpx2": "4.2.0",
58
57
  "eslint": "^7.32.0",
59
58
  "eslint-plugin-no-jquery": "^2.7.0",
60
59
  "eslint-plugin-testcafe": "^0.2.1",
61
60
  "hammerjs": "^2.0.8",
62
- "http-server": "14.1.0",
61
+ "http-server": "14.1.1",
63
62
  "iso-language-codes": "1.1.0",
64
- "jest": "^27.5.1",
65
- "jquery": "1.11.3",
63
+ "jest": "^28.1.0",
64
+ "jest-environment-jsdom": "^28.1.0",
65
+ "jquery": "1.12.4",
66
66
  "jquery-colorbox": "1.6.4",
67
67
  "jquery-ui": "1.12.1",
68
68
  "jquery-ui-touch-punch": "0.2.3",
69
69
  "jquery.browser": "0.1.0",
70
70
  "jquery.mmenu": "5.6.5",
71
- "karma-coverage": "^2.1.0",
72
- "live-server": "1.2.1",
73
- "node-fetch": "2.6.7",
71
+ "live-server": "1.2.2",
72
+ "node-fetch": "3.2.10",
74
73
  "regenerator-runtime": "0.13.9",
75
- "sass": "1.38.2",
76
- "sinon": "^12.0.1",
74
+ "sass": "1.52.1",
75
+ "sinon": "^14.0.0",
77
76
  "soundmanager2": "2.97.20170602",
78
- "svgo": "2.4.0",
79
- "testcafe": "^1.18.6",
77
+ "svgo": "2.8.0",
78
+ "testcafe": "^1.19.0",
80
79
  "testcafe-browser-provider-browserstack": "^1.13.2-alpha.1",
81
80
  "webpack": "5.51.1",
82
- "webpack-cli": "4.8.0"
81
+ "webpack-cli": "4.9.2"
83
82
  },
84
83
  "jest": {
85
84
  "testEnvironment": "jsdom",
86
85
  "transformIgnorePatterns": [
87
- "node_modules/(?!(lit-html|lit-element|lit|@lit|@internetarchive)/)"
86
+ "node_modules/(?!(lit-html|lit-element|lit|@lit|@internetarchive|@open-wc)/)"
88
87
  ],
89
88
  "moduleNameMapper": {
90
89
  "^@/(.*)$": "<rootDir>/$1"
@@ -95,8 +94,7 @@
95
94
  "roots": [
96
95
  "<rootDir>/src/",
97
96
  "<rootDir>/tests/jest/"
98
- ],
99
- "coverageDirectory": "<rootDir>/coverage-jest"
97
+ ]
100
98
  },
101
99
  "scripts": {
102
100
  "preversion": "npm run test && node scripts/preversion.js",
@@ -115,15 +113,12 @@
115
113
  "serve": "npx http-server . --port 8000",
116
114
  "serve-live": "npx live-server . --port 8000 --watch=index.html,BookReader,BookReaderDemo",
117
115
  "serve-dev": "npm run build-css && npx concurrently --kill-others npm:serve-live npm:build-*:watch",
118
- "test": "npx concurrently --group npm:test-jest npm:test-karma",
116
+ "test": "npx jest --coverage --colors",
117
+ "test:watch": "npx jest --watch",
119
118
  "test:e2e": "npm run build && npx testcafe",
120
119
  "test:e2e:dev": "npx testcafe --live --dev",
121
- "test-jest:watch": "npx jest --watch",
122
- "test-jest": "npx jest --coverage --colors",
123
- "test-karma": "npx karma start --coverage",
124
- "test-karma:watch": "npx karma start --auto-watch=true --single-run=false",
125
120
  "DOCS:update:test-deps": "If CI succeeds, these should be good to update",
126
- "update:test-deps": "npm i @babel/eslint-parser@latest @open-wc/testing@latest @open-wc/testing-karma@latest @types/jest@latest codecov@latest eslint@latest eslint-plugin-testcafe@latest jest@latest karma-coverage@latest sinon@latest testcafe@latest",
121
+ "update:test-deps": "npm i @babel/eslint-parser@latest @open-wc/testing-helpers@latest @types/jest@latest codecov@latest eslint@latest eslint-plugin-testcafe@latest jest@latest sinon@latest testcafe@latest",
127
122
  "DOCS:update:build-deps": "These can cause strange changes, so do an npm run build + check file size (git diff --stat), and check the site is as expected",
128
123
  "update:build-deps": "npm i @babel/core@latest @babel/preset-env@latest babel-loader@latest core-js@latest regenerator-runtime@latest sass@latest svgo@latest webpack@latest webpack-cli@latest",
129
124
  "codecov": "npx codecov"
package/renovate.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "extends": [
3
- "config:base"
3
+ "config:base",
4
+ "schedule:monthly"
4
5
  ],
5
6
  "packageRules": [
6
7
  {
7
8
  "matchPackageNames": [
8
9
  "@babel/eslint-parser",
9
- "@open-wc/testing",
10
- "@open-wc/testing-karma",
10
+ "@open-wc/testing-helpers",
11
11
  "@types/jest",
12
12
  "codecov",
13
13
  "eslint",
14
14
  "eslint-plugin-no-jquery",
15
15
  "eslint-plugin-testcafe",
16
16
  "jest",
17
- "karma-coverage",
17
+ "jest-environment-jsdom",
18
18
  "sinon",
19
19
  "testcafe"
20
20
  ],
@@ -38,6 +38,15 @@
38
38
  {
39
39
  "matchPackagePatterns": ["*"],
40
40
  "rangeStrategy": "bump"
41
+ },
42
+ {
43
+ "matchPackagePatterns": ["^actions/"],
44
+ "groupName": "GitHub Actions",
45
+ "automerge": true
46
+ },
47
+ {
48
+ "matchPackagePatterns": ["^@internetarchive"],
49
+ "schedule": ["at any time"]
41
50
  }
42
51
  ]
43
52
  }
@@ -1,8 +1,11 @@
1
1
  const { version: OLD_VERSION } = require('../package.json');
2
- const fetch = require('node-fetch');
3
2
  const OLD_RELEASE_URL = `https://api.github.com/repos/internetarchive/bookreader/releases/tags/v${OLD_VERSION}`;
4
3
 
5
4
  async function main() {
5
+ // Need this because fetch is ESM-only, and we're on Node 16. Someday we should
6
+ // be able to move this up to the top without renaming this file to a .mjs or whatever
7
+ const {default: fetch} = await import('node-fetch');
8
+
6
9
  const {created_at} = await fetch(OLD_RELEASE_URL).then(r => r.json());
7
10
  const today = new Date().toISOString().slice(0, -5);
8
11
  const searchUrl = 'https://github.com/internetarchive/bookreader/pulls?' + new URLSearchParams({
@@ -106,6 +106,10 @@ export class BookNavigator extends LitElement {
106
106
  this.loadSharedObserver();
107
107
  this.initializeBookSubmenus();
108
108
  }
109
+
110
+ if (changed.has('downloadableTypes')) {
111
+ this.initializeBookSubmenus();
112
+ }
109
113
  }
110
114
 
111
115
  /**
@@ -8,16 +8,28 @@ const menuBase = {
8
8
  url: '#',
9
9
  note: 'PDF files contain high quality images of pages.',
10
10
  },
11
+ lcppdf: {
12
+ type: 'Get LCP PDF',
13
+ url: '#',
14
+ note: 'PDF files contain high quality images of pages.',
15
+ },
16
+ lcpepub: {
17
+ type: 'Get LCP ePub',
18
+ url: '#',
19
+ note: 'ePub files are smaller in size, but may contain errors.',
20
+ },
11
21
  epub: {
12
22
  type: 'Encrypted Adobe ePub',
13
23
  url: '#',
14
24
  note: 'ePub files are smaller in size, but may contain errors.',
15
- }
25
+ },
16
26
  };
17
27
 
18
28
  const publicMenuBase = {
19
29
  pdf: "PDF",
20
- epub: "ePub"
30
+ epub: "ePub",
31
+ lcppdf: "LCP PDF",
32
+ lcpepub: "LCP ePub",
21
33
  };
22
34
 
23
35
  export default class DownloadsProvider {
@@ -30,9 +42,6 @@ export default class DownloadsProvider {
30
42
  this.id = 'downloads';
31
43
  this.component = '';
32
44
  this.isBookProtected = bookreader?.options?.isProtected || false;
33
-
34
- this.computeAvailableTypes = this.computeAvailableTypes.bind(this);
35
- this.update = this.update.bind(this);
36
45
  }
37
46
 
38
47
  update(downloadTypes) {
@@ -40,6 +40,16 @@ export class IABookDownloads extends LitElement {
40
40
  ));
41
41
  }
42
42
 
43
+ /**
44
+ * checks if downloads list contains an LCP option
45
+ * @return {boolean}
46
+ */
47
+ get hasLCPOption() {
48
+ const regex = /^(LCP)/g;
49
+ const lcpAvailable = this.downloads.some(option => option.type?.match(regex));
50
+ return lcpAvailable;
51
+ }
52
+
43
53
  get header() {
44
54
  if (!this.renderHeader) {
45
55
  return nothing;
@@ -59,12 +69,26 @@ export class IABookDownloads extends LitElement {
59
69
  `;
60
70
  }
61
71
 
72
+ get installSimplyEAldikoThoriumMsg() {
73
+ return html`
74
+ <p>For LCP downloads, make sure you have SimplyE or Aldiko Next installed on mobile or Thorium on desktop.</p>
75
+ <ul>
76
+ <li><a href="https://librarysimplified.org/simplye/" rel="noopener noreferrer nofollow" target="_blank">Install SimplyE</a></li>
77
+ <li><a href="https://www.demarque.com/en-aldiko" rel="noopener noreferrer nofollow" target="_blank">Install Aldiko</a></li>
78
+ <li><a href="https://www.edrlab.org/software/thorium-reader/" rel="noopener noreferrer nofollow" target="_blank">Install Thorium</a></li>
79
+ </ul>
80
+ `;
81
+ }
82
+
62
83
  render() {
63
84
  return html`
64
85
  ${this.header}
65
86
  ${this.loanExpiryMessage}
66
87
  <ul>${this.renderDownloadOptions()}</ul>
67
- ${this.isBookProtected ? this.accessProtectedBook : nothing}
88
+ ${this.hasLCPOption
89
+ ? this.installSimplyEAldikoThoriumMsg
90
+ : (this.isBookProtected ? this.accessProtectedBook : nothing)
91
+ }
68
92
  `;
69
93
  }
70
94
 
@@ -1,5 +1,6 @@
1
1
  import { html, LitElement, nothing } from 'lit';
2
2
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
3
+ /** @typedef {import('@/src/plugins/search/plugin.search.js').SearchInsideMatch} SearchInsideMatch */
3
4
 
4
5
  export class BookSearchResult extends LitElement {
5
6
  static get properties() {
@@ -35,17 +36,14 @@ export class BookSearchResult extends LitElement {
35
36
  }
36
37
 
37
38
  render() {
38
- const { match } = this;
39
- const { par = [] } = match;
40
- const [resultDetails = {}] = par;
41
- const pageNumber = Number.isInteger(resultDetails.page)
42
- ? html`<p class="page-num">Page -${resultDetails.page}-</p>` : nothing;
39
+ /** @type {SearchInsideMatch} */
40
+ const match = this.match;
43
41
  const coverImage = html`<img src="${match.cover}" />`;
44
42
  return html`
45
43
  <li @click=${this.resultSelected}>
46
44
  ${match.cover ? coverImage : nothing}
47
45
  <h4>${match.title || nothing}</h4>
48
- ${pageNumber}
46
+ <p class="page-num">Page ${match.displayPageNumber}</p>
49
47
  ${this.highlightedHit(match.text)}
50
48
  </li>
51
49
  `;
@@ -146,6 +146,7 @@ export default class SearchProvider {
146
146
  };
147
147
  this.updateMenu({ openMenu: false });
148
148
  this.bookreader?.searchView?.clearSearchFieldAndResults(false);
149
+ this.bookreader?.urlPlugin?.removeUrlParam('q');
149
150
  }
150
151
 
151
152
  /**
@@ -60,6 +60,9 @@ export class IABookSearchResults extends LitElement {
60
60
 
61
61
  setQuery(e) {
62
62
  this.query = e.currentTarget.value;
63
+ if (!this.query) {
64
+ this.cancelSearch();
65
+ }
63
66
  }
64
67
 
65
68
  performSearch(e) {
@@ -148,6 +151,7 @@ export class IABookSearchResults extends LitElement {
148
151
  name="query"
149
152
  alt="Search inside this book."
150
153
  @keyup=${this.setQuery}
154
+ @search=${this.setQuery}
151
155
  .value=${this.query}
152
156
  />
153
157
  </fieldset>
@@ -34,12 +34,11 @@
34
34
  -webkit-appearance: none;
35
35
  appearance: none;
36
36
  font-size: 10px;
37
- text-align: center;
38
37
  text-align-last: center;
39
38
  color: $controlsText;
40
39
  border: none;
41
40
  cursor: pointer;
42
- option {
41
+ option, optgroup {
43
42
  background: $controlsBG;
44
43
  }
45
44
  }
@@ -29,6 +29,8 @@ import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.
29
29
  import SearchView from './view.js';
30
30
  /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
31
31
  /** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
32
+ /** @typedef {import('../../BookReader/BookModel').LeafNum} LeafNum */
33
+ /** @typedef {import('../../BookReader/BookModel').PageNumString} PageNumString */
32
34
 
33
35
  jQuery.extend(BookReader.defaultOptions, {
34
36
  server: 'ia600609.us.archive.org',
@@ -215,6 +217,12 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
215
217
  cache: true,
216
218
  beforeSend: xhr => { this.searchXHR = xhr; },
217
219
  }));
220
+
221
+ if (!this.searchTerm) {
222
+ this.bookreader?.urlPlugin?.removeUrlParam('q');
223
+ } else {
224
+ this.bookreader?.urlPlugin?.setUrlParam('q', this.searchTerm);
225
+ }
218
226
  };
219
227
 
220
228
  /**
@@ -228,6 +236,7 @@ BookReader.prototype._cancelSearch = function () {
228
236
  this.searchXHR = null;
229
237
  this.searchCancelled = true;
230
238
  this.searchResults = [];
239
+ this.bookreader?.urlPlugin?.removeUrlParam('q');
231
240
  };
232
241
 
233
242
  /**
@@ -241,6 +250,7 @@ BookReader.prototype.cancelSearchRequest = function () {
241
250
  this.searchView.toggleSearchPending();
242
251
  this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
243
252
  }
253
+ this.bookreader?.urlPlugin?.removeUrlParam('q');
244
254
  };
245
255
 
246
256
  /**
@@ -257,6 +267,7 @@ BookReader.prototype.cancelSearchRequest = function () {
257
267
  /**
258
268
  * @typedef {object} SearchInsideMatch
259
269
  * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
270
+ * @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
260
271
  * @property {string} text
261
272
  * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
262
273
  */
@@ -269,22 +280,32 @@ BookReader.prototype.cancelSearchRequest = function () {
269
280
  */
270
281
 
271
282
  /**
272
- * Search Results return handler
283
+ * Attach some fields to search inside results
273
284
  * @param {SearchInsideResults} results
274
- * @param {object} options
275
- * @param {boolean} options.goToFirstResult
285
+ * @param {(pageNum: LeafNum) => PageNumString} displayPageNumberFn
276
286
  */
277
- BookReader.prototype.BRSearchCallback = function(results, options) {
287
+ export function marshallSearchResults(results, displayPageNumberFn) {
278
288
  // Attach matchIndex to a few things to make it easier to identify
279
289
  // an active/selected match
280
290
  for (const [index, match] of results.matches.entries()) {
281
291
  match.matchIndex = index;
292
+ match.displayPageNumber = displayPageNumberFn(match.par[0].page);
282
293
  for (const par of match.par) {
283
294
  for (const box of par.boxes) {
284
295
  box.matchIndex = index;
285
296
  }
286
297
  }
287
298
  }
299
+ }
300
+
301
+ /**
302
+ * Search Results return handler
303
+ * @param {SearchInsideResults} results
304
+ * @param {object} options
305
+ * @param {boolean} options.goToFirstResult
306
+ */
307
+ BookReader.prototype.BRSearchCallback = function(results, options) {
308
+ marshallSearchResults(results, pageNum => this.getPageNum(this.leafNumToIndex(pageNum)));
288
309
  this.searchResults = results || [];
289
310
 
290
311
  this.updateSearchHilites();
@@ -230,9 +230,7 @@ class SearchView {
230
230
  matches.forEach((match) => {
231
231
  const queryString = match.text;
232
232
  const pageIndex = this.br.leafNumToIndex(match.par[0].page);
233
- const pageNumber = this.br.getPageNum(pageIndex);
234
233
  const uiStringSearch = "Search result"; // i18n
235
- const uiStringPage = "Page"; // i18n
236
234
 
237
235
  const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.getNumLeafs() - 1);
238
236
 
@@ -257,7 +255,7 @@ class SearchView {
257
255
  .append(`
258
256
  <div class="BRquery">
259
257
  <div>${queryStringWithBTruncated || queryStringWithB}</div>
260
- <div>${uiStringPage} ${pageNumber}</div>
258
+ <div>Page ${match.displayPageNumber}</div>
261
259
  </div>
262
260
  `)
263
261
  .appendTo(this.br.$('.BRnavline'))
@@ -158,18 +158,30 @@ BookReader.prototype.initNavbar = (function (super_) {
158
158
 
159
159
  $el.find('.BRcontrols').prepend(this.refs.$BRReadAloudToolbar);
160
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
+
161
168
  const renderVoicesMenu = (voicesMenu) => {
162
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
+
163
174
  if (this.ttsEngine.getVoices().length > 1) {
164
- voicesMenu.append(this.ttsEngine.getVoices().map(
165
- voice =>
166
- $(`<option value="${voice.voiceURI}">${voice.lang} - ${voice.name}</option>`)));
175
+ voicesMenu.append($(`<optgroup label="Book Language (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
176
+ voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages)} </optgroup>`));
177
+
167
178
  voicesMenu.val(this.ttsEngine.voice.voiceURI);
168
179
  voicesMenu.show();
169
180
  } else {
170
181
  voicesMenu.hide();
171
182
  }
172
183
  };
184
+
173
185
  const voicesMenu = this.refs.$BRReadAloudToolbar.find('[name=playback-voice]');
174
186
  renderVoicesMenu(voicesMenu);
175
187
  voicesMenu.on("change", ev => this.ttsEngine.setVoice(voicesMenu.val()));