@internetarchive/radio-player 0.0.3 → 0.0.4-alpha.1

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 (93) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +142 -142
  3. package/index.d.ts +15 -15
  4. package/index.js +28 -28
  5. package/lib/demo/full-text-search-service.d.ts +14 -14
  6. package/lib/demo/full-text-search-service.js +33 -33
  7. package/lib/demo/radio-player-controller.d.ts +41 -41
  8. package/lib/demo/radio-player-controller.js +199 -199
  9. package/lib/src/assets/img/left.d.ts +2 -2
  10. package/lib/src/assets/img/left.js +15 -15
  11. package/lib/src/assets/img/right.d.ts +2 -2
  12. package/lib/src/assets/img/right.js +15 -15
  13. package/lib/src/models/music-zone.d.ts +12 -12
  14. package/lib/src/models/music-zone.js +15 -15
  15. package/lib/src/models/radio-player-config.d.ts +26 -26
  16. package/lib/src/models/radio-player-config.js +27 -27
  17. package/lib/src/radio-player.d.ts +589 -589
  18. package/lib/src/radio-player.js +1338 -1338
  19. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-backend.d.ts +82 -82
  20. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-backend.js +107 -107
  21. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-response.d.ts +48 -48
  22. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-response.js +58 -58
  23. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-service-interface.d.ts +16 -16
  24. package/lib/src/search-handler/search-backends/full-text-search-backend/full-text-search-service-interface.js +1 -1
  25. package/lib/src/search-handler/search-backends/local-search-backend/local-search-backend.d.ts +27 -27
  26. package/lib/src/search-handler/search-backends/local-search-backend/local-search-backend.js +42 -42
  27. package/lib/src/search-handler/search-backends/search-backend-interface.d.ts +9 -9
  28. package/lib/src/search-handler/search-backends/search-backend-interface.js +1 -1
  29. package/lib/src/search-handler/search-handler-interface.d.ts +17 -17
  30. package/lib/src/search-handler/search-handler-interface.js +1 -1
  31. package/lib/src/search-handler/search-handler.d.ts +83 -83
  32. package/lib/src/search-handler/search-handler.js +177 -177
  33. package/lib/src/search-handler/search-helper.d.ts +12 -12
  34. package/lib/src/search-handler/search-helper.js +22 -22
  35. package/lib/src/search-handler/search-models.d.ts +45 -45
  36. package/lib/src/search-handler/search-models.js +49 -49
  37. package/lib/src/search-handler/transcript-index-interface.d.ts +38 -38
  38. package/lib/src/search-handler/transcript-index-interface.js +1 -1
  39. package/lib/src/search-handler/transcript-index.d.ts +53 -53
  40. package/lib/src/search-handler/transcript-index.js +80 -80
  41. package/lib/src/search-results-switcher.d.ts +58 -58
  42. package/lib/src/search-results-switcher.js +133 -133
  43. package/package.json +93 -93
  44. package/src/assets/img/left.ts +17 -17
  45. package/src/assets/img/right.ts +17 -17
  46. package/src/models/music-zone.ts +17 -17
  47. package/src/models/radio-player-config.ts +47 -47
  48. package/src/radio-player.ts +1402 -1402
  49. package/src/search-handler/search-backends/full-text-search-backend/full-text-search-backend.ts +140 -140
  50. package/src/search-handler/search-backends/full-text-search-backend/full-text-search-response.ts +110 -110
  51. package/src/search-handler/search-backends/full-text-search-backend/full-text-search-service-interface.ts +17 -17
  52. package/src/search-handler/search-backends/local-search-backend/local-search-backend.ts +50 -50
  53. package/src/search-handler/search-backends/search-backend-interface.ts +10 -10
  54. package/src/search-handler/search-handler-interface.ts +18 -18
  55. package/src/search-handler/search-handler.ts +217 -217
  56. package/src/search-handler/search-helper.ts +25 -25
  57. package/src/search-handler/search-models.ts +69 -69
  58. package/src/search-handler/transcript-index-interface.ts +42 -42
  59. package/src/search-handler/transcript-index.ts +93 -93
  60. package/src/search-results-switcher.ts +131 -131
  61. package/.eslintrc +0 -37
  62. package/.gitignore +0 -11
  63. package/.storybook/.babelrc +0 -15
  64. package/.storybook/addons.js +0 -7
  65. package/.storybook/config.js +0 -11
  66. package/.storybook/webpack.config.js +0 -5
  67. package/demo/full-text-search-service.ts +0 -32
  68. package/demo/index.html +0 -53
  69. package/demo/radio-player-controller.ts +0 -237
  70. package/karma.bs.config.js +0 -16
  71. package/karma.conf.js +0 -66
  72. package/package-lock.json +0 -51719
  73. package/stories/index.stories.js +0 -40
  74. package/stories/transcript.js +0 -683
  75. package/test/assets/arrow.mp3 +0 -0
  76. package/test/music-zone.test.js +0 -13
  77. package/test/promised-sleep.js +0 -3
  78. package/test/radio-player-config.test.js +0 -42
  79. package/test/radio-player.test.js +0 -1072
  80. package/test/sample_transcript.js +0 -1493
  81. package/test/search-handler/search-backends/full-text-sample-response-brackets.js +0 -60
  82. package/test/search-handler/search-backends/full-text-sample-response-em.js +0 -60
  83. package/test/search-handler/search-backends/full-text-search-backend.test.js +0 -74
  84. package/test/search-handler/search-backends/full-text-search-response.test.js +0 -14
  85. package/test/search-handler/search-backends/local-search-backend.test.js +0 -39
  86. package/test/search-handler/search-handler.test.js +0 -550
  87. package/test/search-handler/search-helper.test.js +0 -63
  88. package/test/search-handler/search-model.test.js +0 -56
  89. package/test/search-handler/transcript-index.test.js +0 -66
  90. package/test/search-results-switcher.test.js +0 -97
  91. package/tsconfig.build.json +0 -7
  92. package/tsconfig.json +0 -21
  93. package/yarn.lock +0 -14450
@@ -1,1339 +1,1339 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
8
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
9
- return new (P || (P = Promise))(function (resolve, reject) {
10
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
11
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
12
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
13
- step((generator = generator.apply(thisArg, _arguments || [])).next());
14
- });
15
- };
16
- /* eslint-disable import/no-duplicates */
17
- import { LitElement, html, css, } from 'lit';
18
- import { customElement, property, } from 'lit/decorators.js';
19
- import '@internetarchive/waveform-progress';
20
- import '@internetarchive/playback-controls';
21
- import '@internetarchive/scrubber-bar';
22
- import { QuickSearchEntry } from '@internetarchive/expandable-search-bar';
23
- import '@internetarchive/expandable-search-bar';
24
- import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
25
- import './search-results-switcher';
26
- import { ZoneOfSilence } from '@internetarchive/waveform-progress';
27
- import { PlaybackMode } from '@internetarchive/playback-controls';
28
- import MusicZone from './models/music-zone';
29
- /**
30
- * A Radio Player element to play back transcribed audio.
31
- *
32
- * It is responsible for orchestrating all of the other components like:
33
- * - Waveform Progress
34
- * - Scrubber Bar
35
- * - Playback Controls
36
- * - Transcript View
37
- * - Audio Element
38
- * - Search Bar
39
- *
40
- * @export
41
- * @class RadioPlayer
42
- * @extends {LitElement}
43
- */
44
- let RadioPlayer = class RadioPlayer extends LitElement {
45
- constructor() {
46
- super(...arguments);
47
- /**
48
- * Radio player configuration
49
- *
50
- * @type {(RadioPlayerConfig | undefined)}
51
- * @memberof RadioPlayer
52
- */
53
- this.config = undefined;
54
- /**
55
- * The Search Handler
56
- *
57
- * @type {(SearchHandlerInterface | undefined)}
58
- * @memberof RadioPlayer
59
- */
60
- this.searchHandler = undefined;
61
- /**
62
- * Transcript configuration
63
- *
64
- * @type {(TranscriptConfig | undefined)}
65
- * @memberof RadioPlayer
66
- */
67
- this.transcriptConfig = undefined;
68
- /**
69
- * Search result transcript. It's also a `TranscriptConfig` object, but is broken up by
70
- * transcript entries as well as search results.
71
- *
72
- * If this is set, the transcript view, uses this, otherwise it uses `transcriptConfig`
73
- *
74
- * @type {(TranscriptConfig | undefined)}
75
- * @memberof RadioPlayer
76
- */
77
- this.searchResultsTranscript = undefined;
78
- /**
79
- * Current playback time
80
- *
81
- * @type {number}
82
- * @memberof RadioPlayer
83
- */
84
- this.currentTime = 0;
85
- /**
86
- * Current search term
87
- *
88
- * @type {string}
89
- * @memberof RadioPlayer
90
- */
91
- this.searchTerm = '';
92
- /**
93
- * Flag to determine whether or not to skip the music sections
94
- *
95
- * @type {boolean}
96
- * @memberof RadioPlayer
97
- */
98
- this.skipMusicSections = false;
99
- /**
100
- * Percent of playback that is complete
101
- *
102
- * @private
103
- * @type {number}
104
- * @memberof RadioPlayer
105
- */
106
- this.percentComplete = 0;
107
- /**
108
- * Flag for whether or not the radio player is playing
109
- *
110
- * @private
111
- * @type {boolean}
112
- * @memberof RadioPlayer
113
- */
114
- this.isPlaying = false;
115
- /**
116
- * Duration of the audio file
117
- *
118
- * @private
119
- * @type {number}
120
- * @memberof RadioPlayer
121
- */
122
- this.duration = 0;
123
- /**
124
- * The playback rate where 1 is "normal", 0 is paused, 2 is twice as fase
125
- *
126
- * @private
127
- * @type {number}
128
- * @memberof RadioPlayer
129
- */
130
- this.playbackRate = 1;
131
- /**
132
- * Volume from 0 to 1
133
- *
134
- * @private
135
- * @type {number}
136
- * @memberof RadioPlayer
137
- */
138
- this.volume = 1;
139
- /**
140
- * Flag for whether we should show the search results switcher
141
- *
142
- * @private
143
- * @type {boolean}
144
- * @memberof RadioPlayer
145
- */
146
- this.shouldShowSearchResultSwitcher = false;
147
- /**
148
- * Flag for whether we should show the 'no results' message
149
- *
150
- * @private
151
- * @type {boolean}
152
- * @memberof RadioPlayer
153
- */
154
- this.shouldShowNoSearchResultMessage = false;
155
- /**
156
- * Are we searching or not?
157
- *
158
- * @private
159
- * @type {boolean}
160
- * @memberof RadioPlayer
161
- */
162
- this.isSearching = false;
163
- this.musicZones = [];
164
- }
165
- /**
166
- * LitElement lifecycle main render method
167
- *
168
- * @returns {TemplateResult}
169
- * @memberof RadioPlayer
170
- */
171
- render() {
172
- return html `
173
- ${this.audioElementTemplate}
174
- <section role="main">
175
- ${this.titleDateTemplate} ${this.collectionLogoTemplate} ${this.playbackControlsTemplate}
176
- <div class="waveform-scrubber-container">
177
- ${this.waveFormProgressTemplate} ${this.scrubberBarTemplate}
178
- </div>
179
- ${this.searchSectionTemplate} ${this.transcriptViewTemplate}
180
- </section>
181
- `;
182
- }
183
- /**
184
- * Start playback
185
- *
186
- * @memberof RadioPlayer
187
- */
188
- play() {
189
- /* istanbul ignore else */
190
- if (this.audioElement)
191
- this.audioElement.play();
192
- }
193
- /**
194
- * Pause playback
195
- *
196
- * @memberof RadioPlayer
197
- */
198
- pause() {
199
- /* istanbul ignore else */
200
- if (this.audioElement)
201
- this.audioElement.pause();
202
- }
203
- /**
204
- * Seek audio to given number of seconds
205
- *
206
- * @param {number} seconds
207
- * @memberof RadioPlayer
208
- */
209
- seekTo(seconds) {
210
- /* istanbul ignore else */
211
- if (this.audioElement)
212
- this.audioElement.seekTo(seconds);
213
- }
214
- /**
215
- * Generate markup for the title and date view sections
216
- *
217
- * @readonly
218
- * @private
219
- * @type {TemplateResult}
220
- * @memberof RadioPlayer
221
- */
222
- get titleDateTemplate() {
223
- return html `
224
- <div class="title-date">
225
- <div class="title">
226
- ${this.config ? this.config.title : ''}
227
- </div>
228
-
229
- <div class="date">
230
- ${this.config ? this.config.date : ''}
231
- </div>
232
- </div>
233
- `;
234
- }
235
- /**
236
- * Generate markup for the logo element
237
- *
238
- * @readonly
239
- * @private
240
- * @type {TemplateResult}
241
- * @memberof RadioPlayer
242
- */
243
- get collectionLogoTemplate() {
244
- return html `
245
- <img class="collection-logo" src=${this.logoUrl} alt="collection logo" />
246
- `;
247
- }
248
- /**
249
- * Return the logoUrl from the config if provided
250
- *
251
- * @readonly
252
- * @private
253
- * @type {string}
254
- * @memberof RadioPlayer
255
- */
256
- get logoUrl() {
257
- return this.config ? this.config.logoUrl : '';
258
- }
259
- /**
260
- * Generate the markup for the waveform progress element
261
- *
262
- * @readonly
263
- * @private
264
- * @type {TemplateResult}
265
- * @memberof RadioPlayer
266
- */
267
- get waveFormProgressTemplate() {
268
- return this.waveformUrl
269
- ? html `
270
- <waveform-progress
271
- interactive="true"
272
- .waveformUrl=${this.waveformUrl}
273
- .percentComplete=${this.percentComplete}
274
- @valuechange=${this.valueChangedFromScrub}
275
- >
276
- </waveform-progress>
277
- `
278
- : undefined;
279
- }
280
- /**
281
- * Generate an array of `ZoneOfSilence` models for the waveform progress element
282
- *
283
- * It does this by parsing the transcript for music sections and converting
284
- * those music sections to zones of silence.
285
- *
286
- * @readonly
287
- * @private
288
- * @type {ZoneOfSilence[]}
289
- * @memberof RadioPlayer
290
- */
291
- get zonesOfSilence() {
292
- /* istanbul ignore else */
293
- if (this.duration === 0) {
294
- return [];
295
- }
296
- // eslint-disable-next-line max-len
297
- const musicEntries = this.transcriptEntries.filter((entry) => entry.isMusic === true);
298
- const zonesOfSilence = musicEntries.map((entry) => {
299
- const startPercent = (entry.start / this.duration) * 100;
300
- const endPercent = (entry.end / this.duration) * 100;
301
- return new ZoneOfSilence(startPercent, endPercent);
302
- });
303
- return zonesOfSilence;
304
- }
305
- /**
306
- * Return the waveform url from the configuration
307
- *
308
- * @readonly
309
- * @private
310
- * @type {string}
311
- * @memberof RadioPlayer
312
- */
313
- get waveformUrl() {
314
- return this.config ? this.config.waveformUrl : '';
315
- }
316
- /**
317
- * Generate the audio element markup
318
- *
319
- * @readonly
320
- * @private
321
- * @type {TemplateResult}
322
- * @memberof RadioPlayer
323
- */
324
- get audioElementTemplate() {
325
- return html `
326
- <audio-element
327
- .sources=${this.audioSources}
328
- .playbackRate=${this.playbackRate}
329
- .volume=${this.volume}
330
- @timeupdate=${this.handleTimeChange}
331
- @durationchange=${this.handleDurationChange}
332
- @playbackStarted=${this.playbackStarted}
333
- @playbackPaused=${this.playbackPaused}
334
- @canplay=${this.canplay}
335
- >
336
- </audio-element>
337
- `;
338
- }
339
- /**
340
- * Return the audio sources from the configuration
341
- *
342
- * @readonly
343
- * @private
344
- * @type {AudioSource[]}
345
- * @memberof RadioPlayer
346
- */
347
- get audioSources() {
348
- return this.config ? this.config.audioSources : [];
349
- }
350
- /**
351
- * Generate markup for the playback controls
352
- *
353
- * @readonly
354
- * @private
355
- * @type {TemplateResult}
356
- * @memberof RadioPlayer
357
- */
358
- get playbackControlsTemplate() {
359
- return html `
360
- <playback-controls
361
- @back-button-pressed=${this.backButtonHandler}
362
- @play-pause-button-pressed=${this.playPauseButtonHandler}
363
- @forward-button-pressed=${this.forwardButtonHandler}
364
- @volumeChange=${this.volumeChanged}
365
- @playbackRateChange=${this.changePlaybackRate}
366
- @next-section-button-pressed=${this.nextSectionButtonHandler}
367
- @prev-section-button-pressed=${this.prevSectionButtonHandler}
368
- >
369
- </playback-controls>
370
- `;
371
- }
372
- /**
373
- * Generate an array of start and end points for each of the music zones.
374
- * This allows us to show the markers on the scrubber bar and optionally skip those sections.
375
- *
376
- * @readonly
377
- * @private
378
- * @type {number[]}
379
- * @memberof RadioPlayer
380
- */
381
- get scrubberBarMarkerPercentages() {
382
- const percentages = [0];
383
- this.zonesOfSilence.forEach((zone) => {
384
- percentages.push(zone.startPercent);
385
- percentages.push(zone.endPercent);
386
- });
387
- percentages.push(100);
388
- return percentages;
389
- }
390
- /**
391
- * Generate markup for the scrubber bar
392
- *
393
- * @readonly
394
- * @private
395
- * @type {TemplateResult}
396
- * @memberof RadioPlayer
397
- */
398
- get scrubberBarTemplate() {
399
- return html `
400
- <scrubber-bar
401
- .sectionMarkerPercentages=${this.scrubberBarMarkerPercentages}
402
- .value=${this.percentComplete}
403
- @valuechange=${this.valueChangedFromScrub}
404
- >
405
- </scrubber-bar>
406
- `;
407
- }
408
- /**
409
- * Generate markup for the transcript view
410
- *
411
- * @readonly
412
- * @private
413
- * @type {TemplateResult}
414
- * @memberof RadioPlayer
415
- */
416
- get transcriptViewTemplate() {
417
- return html `
418
- <div class="transcript-container">
419
- <transcript-view
420
- .config=${this.currentTranscript}
421
- .currentTime=${this.currentTime}
422
- @transcriptEntrySelected=${this.transcriptEntrySelected}
423
- >
424
- </transcript-view>
425
- </div>
426
- `;
427
- }
428
- get currentTranscript() {
429
- return this.searchResultsTranscript || this.transcriptConfig;
430
- }
431
- /**
432
- * Generate markup for search section
433
- *
434
- * @readonly
435
- * @private
436
- * @type {TemplateResult}
437
- * @memberof RadioPlayer
438
- */
439
- get searchSectionTemplate() {
440
- return html `
441
- <div class="search-section">
442
- <expandable-search-bar
443
- searchTerm=${this.searchTerm}
444
- .quickSearches=${this.quickSearches}
445
- @inputchange=${this.updateSearchTerm}
446
- @enterKeyPressed=${this.searchEnterKeyPressed}
447
- @searchCleared=${this.searchCleared}
448
- >
449
- </expandable-search-bar>
450
- <div class="search-results-info">
451
- ${this.searchActivityIndicator}
452
- ${this.searchResultsSwitcherTemplate}
453
- ${this.noSearchResultsTemplate}
454
- </div>
455
- </div>
456
- `;
457
- }
458
- /**
459
- * Generate markup for the search results switcher
460
- *
461
- * @readonly
462
- * @private
463
- * @type {TemplateResult}
464
- * @memberof RadioPlayer
465
- */
466
- get searchResultsSwitcherTemplate() {
467
- return html `
468
- <search-results-switcher
469
- class="${this.shouldShowSearchResultSwitcher && !this.isSearching ? '' : 'hidden'}"
470
- @searchResultIndexChanged=${this.searchResultIndexChanged}
471
- >
472
- </search-results-switcher>
473
- `;
474
- }
475
- /**
476
- * Search activity indicator template
477
- *
478
- * @readonly
479
- * @private
480
- * @type {TemplateResult}
481
- * @memberof RadioPlayer
482
- */
483
- get searchActivityIndicator() {
484
- return html `
485
- <ia-activity-indicator
486
- class="${this.isSearching ? '' : 'hidden'}">
487
- </ia-activity-indicator>
488
- `;
489
- }
490
- /**
491
- * Generate markup for the "No Search Results" message
492
- *
493
- * @readonly
494
- * @private
495
- * @type {TemplateResult}
496
- * @memberof RadioPlayer
497
- */
498
- get noSearchResultsTemplate() {
499
- return html `
500
- <div
501
- class="
502
- no-search-results-message
503
- ${this.shouldShowNoSearchResultMessage && !this.isSearching ? '' : 'hidden'}"
504
- >
505
- No search results.
506
- </div>
507
- `;
508
- }
509
- /**
510
- * Generate an array of `QuickSearchEntry` objects based on the quick search
511
- * configuration passed in.
512
- *
513
- * @readonly
514
- * @private
515
- * @type {QuickSearchEntry[]}
516
- * @memberof RadioPlayer
517
- */
518
- get quickSearches() {
519
- if (!this.config) {
520
- return [];
521
- }
522
- const entries = this.config.quickSearches.map(entry => new QuickSearchEntry(entry));
523
- return entries;
524
- }
525
- /**
526
- * Update the local search term when the SearchBar widget gets updated and emits the event
527
- *
528
- * @private
529
- * @param {CustomEvent} e
530
- * @returns {void}
531
- * @memberof RadioPlayer
532
- */
533
- updateSearchTerm(e) {
534
- const detail = e.detail || {};
535
- if (!detail.value) {
536
- return;
537
- }
538
- const searchTerm = detail.value;
539
- this.searchTerm = searchTerm;
540
- this.emitSearchTermChangedEvent(searchTerm);
541
- }
542
- /**
543
- * Triggered when the user clicks the clear search button in the search bar.
544
- * Clean up all of the search info and reset to the base transcript.
545
- *
546
- * @private
547
- * @memberof RadioPlayer
548
- */
549
- searchCleared() {
550
- this.searchTerm = '';
551
- this.searchResultsTranscript = undefined;
552
- this.emitSearchClearedEvent();
553
- this.emitSearchTermChangedEvent('');
554
- /* istanbul ignore else */
555
- if (this.transcriptView) {
556
- this.transcriptView.selectedSearchResultIndex = 0;
557
- }
558
- /* istanbul ignore else */
559
- if (this.searchResultsSwitcher) {
560
- this.searchResultsSwitcher.currentResultIndex = 0;
561
- }
562
- }
563
- /**
564
- * Triggered when the user scrubs through search result indices so we can coordinate
565
- * the UI updates like scrolling the transcript view
566
- *
567
- * @private
568
- * @param {CustomEvent} e
569
- * @returns {void}
570
- * @memberof RadioPlayer
571
- */
572
- searchResultIndexChanged(e) {
573
- const detail = e.detail || {};
574
- const { searchResultIndex } = detail;
575
- if (searchResultIndex === undefined || !this.transcriptView) {
576
- return;
577
- }
578
- this.transcriptView.selectedSearchResultIndex = searchResultIndex;
579
- this.transcriptView.scrollToSelectedSearchResult();
580
- const event = new CustomEvent('highlightedSearchResultChanged', {
581
- detail: { searchResultIndex },
582
- });
583
- this.dispatchEvent(event);
584
- }
585
- /**
586
- * Triggered when the user presses enter in the search bar so we can trigger a search.
587
- *
588
- * @private
589
- * @param {CustomEvent} e
590
- * @memberof RadioPlayer
591
- */
592
- searchEnterKeyPressed(e) {
593
- const detail = e.detail || {};
594
- if (!detail.value) {
595
- return;
596
- }
597
- this.executeSearch(detail.value);
598
- }
599
- executeSearch(term) {
600
- return __awaiter(this, void 0, void 0, function* () {
601
- if (!this.searchHandler || term.length < 2) {
602
- this.searchResultsTranscript = undefined;
603
- return;
604
- }
605
- this.searchTerm = term;
606
- this.isSearching = true;
607
- const event = new Event('searchExecuted');
608
- this.dispatchEvent(event);
609
- this.searchResultsTranscript = yield this.searchHandler.search(term);
610
- this.isSearching = false;
611
- });
612
- }
613
- /**
614
- * Return transcript entries from the transcript configuration
615
- *
616
- * @readonly
617
- * @private
618
- * @type {TranscriptEntryConfig[]}
619
- * @memberof RadioPlayer
620
- */
621
- get transcriptEntries() {
622
- return this.currentTranscript ? this.currentTranscript.entries : [];
623
- }
624
- /**
625
- * Return the transcript view DOM element
626
- *
627
- * @readonly
628
- * @private
629
- * @type {(TranscriptView | null)}
630
- * @memberof RadioPlayer
631
- */
632
- get transcriptView() {
633
- /* istanbul ignore next */
634
- return this.shadowRoot
635
- ? this.shadowRoot.querySelector('transcript-view')
636
- : null;
637
- }
638
- /**
639
- * Return the audio element DOM element
640
- *
641
- * @readonly
642
- * @private
643
- * @type {(AudioElement | null)}
644
- * @memberof RadioPlayer
645
- */
646
- get audioElement() {
647
- /* istanbul ignore next */
648
- return this.shadowRoot
649
- ? this.shadowRoot.querySelector('audio-element')
650
- : null;
651
- }
652
- /**
653
- * Return the playback controls DOM element
654
- *
655
- * @readonly
656
- * @private
657
- * @type {(PlaybackControls | null)}
658
- * @memberof RadioPlayer
659
- */
660
- get playbackControls() {
661
- /* istanbul ignore next */
662
- return this.shadowRoot
663
- ? this.shadowRoot.querySelector('playback-controls')
664
- : null;
665
- }
666
- /**
667
- * Return the search results switcher DOM element
668
- *
669
- * @readonly
670
- * @private
671
- * @type {(SearchResultsSwitcher | null)}
672
- * @memberof RadioPlayer
673
- */
674
- get searchResultsSwitcher() {
675
- /* istanbul ignore next */
676
- return this.shadowRoot
677
- ? this.shadowRoot.querySelector('search-results-switcher')
678
- : null;
679
- }
680
- /**
681
- * Triggered when the user changes the playback rate so we can update the audio element
682
- *
683
- * @private
684
- * @param {CustomEvent} e
685
- * @returns {void}
686
- * @memberof RadioPlayer
687
- */
688
- changePlaybackRate(e) {
689
- const detail = e.detail || {};
690
- if (!detail.playbackRate) {
691
- return;
692
- }
693
- this.playbackRate = detail.playbackRate;
694
- const event = new CustomEvent('playbackRateChanged', {
695
- detail: { playbackRate: this.playbackRate },
696
- });
697
- this.dispatchEvent(event);
698
- }
699
- /**
700
- * Triggered when the user changes the volume from the playback controls
701
- *
702
- * @private
703
- * @param {CustomEvent} e
704
- * @returns {void}
705
- * @memberof RadioPlayer
706
- */
707
- volumeChanged(e) {
708
- const detail = e.detail || {};
709
- if (detail.volume === undefined) {
710
- return;
711
- }
712
- this.volume = e.detail.volume;
713
- const event = new CustomEvent('volumeChanged', {
714
- detail: { volume: this.volume },
715
- });
716
- this.dispatchEvent(event);
717
- }
718
- /**
719
- * Triggered when the user presses the back button in the playback controls
720
- *
721
- * @private
722
- * @memberof RadioPlayer
723
- */
724
- backButtonHandler() {
725
- /* istanbul ignore else */
726
- if (this.audioElement) {
727
- this.audioElement.seekBy(-10);
728
- }
729
- const event = new Event('jumpBackButtonPressed');
730
- this.dispatchEvent(event);
731
- }
732
- /**
733
- * Triggered when the user presses the play/pause button
734
- *
735
- * @private
736
- * @returns {void}
737
- * @memberof RadioPlayer
738
- */
739
- playPauseButtonHandler() {
740
- this.isPlaying = !this.isPlaying;
741
- /* istanbul ignore if */
742
- if (!this.audioElement) {
743
- return;
744
- }
745
- if (this.isPlaying) {
746
- this.audioElement.play();
747
- }
748
- else {
749
- this.audioElement.pause();
750
- }
751
- const event = new CustomEvent('playPauseButtonPressed', {
752
- detail: { isPlaying: this.isPlaying },
753
- });
754
- this.dispatchEvent(event);
755
- }
756
- /**
757
- * Triggered when the user presses the forward button in the playback controls
758
- *
759
- * @private
760
- * @memberof RadioPlayer
761
- */
762
- forwardButtonHandler() {
763
- /* istanbul ignore else */
764
- if (this.audioElement) {
765
- this.audioElement.seekBy(10);
766
- }
767
- const event = new Event('jumpForwardButtonPressed');
768
- this.dispatchEvent(event);
769
- }
770
- /**
771
- * Skip to the next section
772
- *
773
- * @private
774
- * @returns {void}
775
- * @memberof RadioPlayer
776
- */
777
- nextSectionButtonHandler() {
778
- /* istanbul ignore if */
779
- if (!this.audioElement) {
780
- return;
781
- }
782
- const percentsGreaterThanValue = this.scrubberBarMarkerPercentages.filter(value => value > this.percentComplete + 0.1);
783
- const closestUpper = Math.min(...percentsGreaterThanValue);
784
- const seekTo = this.duration * (closestUpper / 100) + 0.1;
785
- this.audioElement.seekTo(seekTo);
786
- const event = new Event('nextSectionButtonPressed');
787
- this.dispatchEvent(event);
788
- }
789
- /**
790
- * Jump to the previous section
791
- *
792
- * @private
793
- * @returns {void}
794
- * @memberof RadioPlayer
795
- */
796
- prevSectionButtonHandler() {
797
- /* istanbul ignore if */
798
- if (!this.audioElement) {
799
- return;
800
- }
801
- const percentsLessThanValue = this.scrubberBarMarkerPercentages.filter(value => value < this.percentComplete - 0.1);
802
- const closestLower = Math.max(...percentsLessThanValue);
803
- const seekTo = this.duration * (closestLower / 100) - 0.1;
804
- this.audioElement.seekTo(seekTo);
805
- const event = new Event('prevSectionButtonPressed');
806
- this.dispatchEvent(event);
807
- }
808
- /**
809
- * Handle the duration change event like when the audio file first loads
810
- *
811
- * @private
812
- * @param {CustomEvent} e
813
- * @returns {void}
814
- * @memberof RadioPlayer
815
- */
816
- handleDurationChange(e) {
817
- const detail = e.detail || {};
818
- if (!detail.duration) {
819
- return;
820
- }
821
- this.duration = detail.duration;
822
- }
823
- /**
824
- * Triggered as the playback progresses in the audio element
825
- *
826
- * @private
827
- * @param {CustomEvent} e
828
- * @returns {void}
829
- * @memberof RadioPlayer
830
- */
831
- handleTimeChange(e) {
832
- const detail = e.detail || {};
833
- if (!detail.currentTime) {
834
- return;
835
- }
836
- this.currentTime = detail.currentTime;
837
- const percent = this.currentTime / this.duration;
838
- this.percentComplete = percent * 100;
839
- }
840
- /**
841
- * Update any consumers that the current time has changed
842
- *
843
- * @private
844
- * @memberof RadioPlayer
845
- */
846
- emitCurrentTimeChangedEvent() {
847
- const event = new CustomEvent('currentTimeChanged', {
848
- detail: { currentTime: this.currentTime },
849
- });
850
- this.dispatchEvent(event);
851
- }
852
- /**
853
- * When the user clears the search, we want to bubble up the event to other consumers.
854
- *
855
- * @private
856
- * @memberof RadioPlayer
857
- */
858
- emitSearchClearedEvent() {
859
- const event = new Event('searchCleared');
860
- this.dispatchEvent(event);
861
- }
862
- emitSearchTermChangedEvent(term) {
863
- const event = new CustomEvent('searchTermChanged', {
864
- detail: { searchTerm: term },
865
- });
866
- this.dispatchEvent(event);
867
- }
868
- /**
869
- * Handle the playback paused event
870
- *
871
- * @private
872
- * @memberof RadioPlayer
873
- */
874
- playbackPaused() {
875
- this.isPlaying = false;
876
- /* istanbul ignore else */
877
- if (this.playbackControls) {
878
- this.playbackControls.playbackMode = PlaybackMode.paused;
879
- }
880
- const event = new Event('playbackPaused');
881
- this.dispatchEvent(event);
882
- }
883
- /**
884
- * Handle the playback started event
885
- *
886
- * @private
887
- * @memberof RadioPlayer
888
- */
889
- playbackStarted() {
890
- this.isPlaying = true;
891
- /* istanbul ignore else */
892
- if (this.playbackControls) {
893
- this.playbackControls.playbackMode = PlaybackMode.playing;
894
- }
895
- const event = new Event('playbackStarted');
896
- this.dispatchEvent(event);
897
- }
898
- /**
899
- * Emits a `canplay` event when the media is ready to be played
900
- *
901
- * @private
902
- * @memberof RadioPlayer
903
- */
904
- canplay() {
905
- const event = new Event('canplay');
906
- this.dispatchEvent(event);
907
- }
908
- /**
909
- * Triggered when the user scrubs the scrubber bar or waveform progress bar
910
- *
911
- * Handles updating the audioElement to the new time and emitting an event
912
- * that the time changed
913
- *
914
- * @private
915
- * @param {CustomEvent} e
916
- * @returns {void}
917
- * @memberof RadioPlayer
918
- */
919
- valueChangedFromScrub(e) {
920
- const detail = e.detail || {};
921
- if (!detail.value) {
922
- return;
923
- }
924
- const percentage = detail.value;
925
- const newTime = this.duration * (percentage / 100);
926
- this.currentTime = newTime;
927
- /* istanbul ignore else */
928
- if (this.audioElement) {
929
- this.audioElement.seekTo(newTime);
930
- }
931
- this.percentComplete = percentage;
932
- const event = new CustomEvent('timeChangedFromScrub', {
933
- detail: { newTime: this.currentTime },
934
- });
935
- this.dispatchEvent(event);
936
- }
937
- /**
938
- * Triggered when the user selects a transcript entry.
939
- * Allows it to jump to that spot in the playback.
940
- *
941
- * @private
942
- * @param {CustomEvent} e
943
- * @returns {void}
944
- * @memberof RadioPlayer
945
- */
946
- transcriptEntrySelected(e) {
947
- const detail = e.detail || {};
948
- const entry = detail.entry || {};
949
- if (!entry.start) {
950
- return;
951
- }
952
- const newTime = entry.start;
953
- this.currentTime = newTime;
954
- /* istanbul ignore else */
955
- if (this.audioElement) {
956
- this.audioElement.seekTo(newTime);
957
- this.audioElement.play();
958
- }
959
- const event = new CustomEvent('transcriptEntrySelected', {
960
- detail: { newTime: this.currentTime },
961
- });
962
- this.dispatchEvent(event);
963
- }
964
- /**
965
- * Creates the music zones based on the transcript so we know when to skip past sections
966
- *
967
- * @private
968
- * @memberof RadioPlayer
969
- */
970
- updateMusicZones() {
971
- // eslint-disable-next-line max-len
972
- const musicEntries = this.transcriptEntries.filter((entry) => entry.isMusic === true);
973
- // eslint-disable-next-line max-len
974
- const musicZones = musicEntries.map((entry) => new MusicZone(entry.start, entry.end));
975
- this.musicZones = musicZones;
976
- }
977
- /**
978
- * Skips a music zone if enabled
979
- *
980
- * @private
981
- * @memberof RadioPlayer
982
- */
983
- skipMusicZone() {
984
- // eslint-disable-next-line max-len
985
- const activeMusicZone = this.musicZones.find((zone) => this.currentTime > zone.start && this.currentTime < zone.end);
986
- if (activeMusicZone && this.audioElement) {
987
- this.audioElement.seekTo(activeMusicZone.end + 0.1);
988
- }
989
- }
990
- /**
991
- * Updates the search results switcher numbers
992
- *
993
- * @private
994
- * @returns {void}
995
- * @memberof RadioPlayer
996
- */
997
- updateSearchResultSwitcher() {
998
- this.shouldShowNoSearchResultMessage = false;
999
- this.shouldShowSearchResultSwitcher = false;
1000
- if (this.searchTerm.length === 0) {
1001
- return;
1002
- }
1003
- const resultCount = this.searchResults.length;
1004
- if (resultCount === 0) {
1005
- this.shouldShowNoSearchResultMessage = true;
1006
- }
1007
- else {
1008
- this.shouldShowSearchResultSwitcher = true;
1009
- /* istanbul ignore else */
1010
- if (this.searchResultsSwitcher) {
1011
- this.searchResultsSwitcher.numberOfResults = resultCount;
1012
- }
1013
- }
1014
- }
1015
- /**
1016
- * Returns the search result template entries from the transcript config
1017
- *
1018
- * @readonly
1019
- * @private
1020
- * @type {TranscriptEntryConfig[]}
1021
- * @memberof RadioPlayer
1022
- */
1023
- get searchResults() {
1024
- return this.searchResultsTranscript ? this.searchResultsTranscript.searchResults : [];
1025
- }
1026
- /**
1027
- * LitElement life cycle event when any reactive property gets updated
1028
- *
1029
- * @param {PropertyValues} changedProperties
1030
- * @memberof RadioPlayer
1031
- */
1032
- updated(changedProperties) {
1033
- // when the transcriptConfig gets changed, reload the music zones and search results switcher
1034
- if (changedProperties.has('transcriptConfig')) {
1035
- this.updateMusicZones();
1036
- }
1037
- if (changedProperties.has('searchResultsTranscript')) {
1038
- this.updateSearchResultSwitcher();
1039
- }
1040
- if (changedProperties.has('searchHandler') && this.searchTerm) {
1041
- this.executeSearch(this.searchTerm);
1042
- }
1043
- // as the currentTime gets updated, emit an event and skip the music zone if enabled
1044
- if (changedProperties.has('currentTime')) {
1045
- this.emitCurrentTimeChangedEvent();
1046
- if (this.skipMusicSections) {
1047
- this.skipMusicZone();
1048
- }
1049
- }
1050
- }
1051
- /**
1052
- * Component styles
1053
- *
1054
- * @readonly
1055
- * @static
1056
- * @type {CSSResult}
1057
- * @memberof RadioPlayer
1058
- */
1059
- static get styles() {
1060
- const collectionLogoMaxHeightCss = css `var(--collectionLogoMaxHeight, 8rem)`;
1061
- const titleColorCss = css `var(--titleColor, white)`;
1062
- const titleFontCss = css `var(--titleFont, 1.5em sans-serif)`;
1063
- const dateColorCss = css `var(--dateColor, white)`;
1064
- const dateFontCss = css `var(--dateFont, 1em sans-serif)`;
1065
- const waveformProgressHeightCss = css `var(--waveformProgressHeight, 5rem)`;
1066
- return css `
1067
- section[role='main'] {
1068
- display: -ms-grid;
1069
- display: grid;
1070
- grid-gap: 0.5rem;
1071
- }
1072
-
1073
- ia-activity-indicator {
1074
- width: 1.5em;
1075
- height: 1.5em;
1076
- display: block;
1077
- margin: auto;
1078
- --activityIndicatorLoadingRingColor: #999;
1079
- --activityIndicatorLoadingDotColor: #999;
1080
- }
1081
-
1082
- /* mobile view */
1083
- @media (max-width: 770px) {
1084
- section[role='main'] {
1085
- -ms-grid-columns: 25% 0.5rem 1fr;
1086
- -ms-grid-rows: auto 0.5rem auto 0.5rem auto 0.5rem auto 0.5rem auto;
1087
- grid-template-columns: 25% 1fr;
1088
- grid-template-areas:
1089
- 'collection-logo title-date'
1090
- 'waveform-scrubber waveform-scrubber'
1091
- 'playback-controls playback-controls'
1092
- 'search-section search-section'
1093
- 'transcript-container transcript-container';
1094
- }
1095
- .date {
1096
- text-align: left;
1097
- }
1098
- transcript-view {
1099
- --timeDisplay: none;
1100
- }
1101
- playback-controls {
1102
- width: 75%;
1103
- margin: auto;
1104
- grid-row: 5;
1105
- grid-column: 1;
1106
- -ms-grid-row: 5;
1107
- -ms-grid-column: 1;
1108
- -ms-grid-column-span: 3;
1109
- }
1110
- .title-date {
1111
- grid-row: 1;
1112
- grid-column: 3;
1113
- -ms-grid-row: 1;
1114
- -ms-grid-column: 3;
1115
- }
1116
- .transcript-container {
1117
- grid-row: 9;
1118
- grid-column: 1;
1119
- -ms-grid-row: 9;
1120
- -ms-grid-column: 1;
1121
- -ms-grid-column-span: 3;
1122
- }
1123
- .collection-logo {
1124
- grid-row: 1;
1125
- grid-column: 1;
1126
- -ms-grid-row: 1;
1127
- -ms-grid-column: 1;
1128
- }
1129
- .waveform-scrubber-container {
1130
- grid-row: 3;
1131
- grid-column: 1;
1132
- -ms-grid-row: 3;
1133
- -ms-grid-column: 1;
1134
- -ms-grid-column-span: 3;
1135
- }
1136
- .search-section {
1137
- grid-row: 7;
1138
- grid-column: 1;
1139
- -ms-grid-row: 7;
1140
- -ms-grid-column: 1;
1141
- -ms-grid-column-span: 3;
1142
- width: 75%;
1143
- margin: auto;
1144
- }
1145
- expandable-search-bar {
1146
- width: 100%;
1147
- }
1148
- }
1149
-
1150
- /* wide view */
1151
- @media (min-width: 770px) {
1152
- section[role='main'] {
1153
- -ms-grid-columns: 192px 0.5rem 0 0.5rem 250px 0.5rem 1fr;
1154
- -ms-grid-rows: auto 0.5rem auto 0.5rem auto;
1155
- grid-template-columns: 192px 0 250px 1fr;
1156
- grid-template-areas:
1157
- 'title-date title-date title-date title-date'
1158
- 'collection-logo 1 playback-controls waveform-scrubber'
1159
- 'search-section transcript-container transcript-container transcript-container';
1160
- }
1161
- .title-date {
1162
- display: flex;
1163
- justify-content: space-between;
1164
- align-items: baseline;
1165
- grid-row: 1;
1166
- grid-column: 1;
1167
- -ms-grid-row: 1;
1168
- -ms-grid-column: 1;
1169
- -ms-grid-column-span: 7;
1170
- }
1171
- transcript-view {
1172
- --timeDisplay: block;
1173
- }
1174
- playback-controls {
1175
- grid-row: 3;
1176
- grid-column: 5;
1177
- -ms-grid-row: 3;
1178
- -ms-grid-column: 5;
1179
- -ms-grid-column-span: 1;
1180
- }
1181
- .transcript-container {
1182
- grid-row: 5;
1183
- grid-column: 3;
1184
- -ms-grid-row: 5;
1185
- -ms-grid-column: 3;
1186
- -ms-grid-column-span: 5;
1187
- }
1188
- .collection-logo {
1189
- grid-row: 3;
1190
- grid-column: 1;
1191
- -ms-grid-row: 3;
1192
- -ms-grid-column: 1;
1193
- }
1194
- .waveform-scrubber-container {
1195
- grid-row: 3;
1196
- grid-column: 7;
1197
- -ms-grid-row: 3;
1198
- -ms-grid-column: 7;
1199
- -ms-grid-column-span: 1;
1200
- }
1201
- .search-section {
1202
- grid-row: 5;
1203
- grid-column: 1;
1204
- -ms-grid-row: 5;
1205
- -ms-grid-column: 1;
1206
- -ms-grid-column-span: 1;
1207
- }
1208
- }
1209
-
1210
- .title-date {
1211
- grid-area: title-date;
1212
- }
1213
-
1214
- .title {
1215
- color: ${titleColorCss};
1216
- font: ${titleFontCss};
1217
- }
1218
-
1219
- .date {
1220
- color: ${dateColorCss};
1221
- font: ${dateFontCss};
1222
- }
1223
-
1224
- waveform-progress {
1225
- width: 100%;
1226
- height: ${waveformProgressHeightCss};
1227
- }
1228
-
1229
- playback-controls {
1230
- grid-area: playback-controls;
1231
- }
1232
-
1233
- .transcript-container {
1234
- grid-area: transcript-container;
1235
- }
1236
-
1237
- transcript-view {
1238
- max-width: 600px;
1239
- display: block;
1240
- }
1241
-
1242
- .collection-logo {
1243
- width: 100%;
1244
- max-height: ${collectionLogoMaxHeightCss};
1245
- object-fit: contain;
1246
- grid-area: collection-logo;
1247
- align-self: center;
1248
- }
1249
-
1250
- .waveform-scrubber-container {
1251
- width: 100%;
1252
- height: 100%;
1253
- grid-area: waveform-scrubber;
1254
- }
1255
-
1256
- .search-section {
1257
- grid-area: search-section;
1258
- }
1259
-
1260
- .search-results-info {
1261
- margin-top: 0.5em;
1262
- }
1263
-
1264
- .quick-search-container {
1265
- max-height: 150px;
1266
- overflow-y: scroll;
1267
- scrollbar-width: none;
1268
- margin: 0 0.5em;
1269
- }
1270
-
1271
- .quick-search-container::-webkit-scrollbar {
1272
- display: none;
1273
- }
1274
-
1275
- expandable-search-bar {
1276
- display: block;
1277
- margin: auto;
1278
- }
1279
-
1280
- .no-search-results-message {
1281
- text-align: center;
1282
- }
1283
-
1284
- .hidden {
1285
- display: none;
1286
- }
1287
- `;
1288
- }
1289
- };
1290
- __decorate([
1291
- property({ type: Object })
1292
- ], RadioPlayer.prototype, "config", void 0);
1293
- __decorate([
1294
- property({ type: Object })
1295
- ], RadioPlayer.prototype, "searchHandler", void 0);
1296
- __decorate([
1297
- property({ type: Object })
1298
- ], RadioPlayer.prototype, "transcriptConfig", void 0);
1299
- __decorate([
1300
- property({ type: Object })
1301
- ], RadioPlayer.prototype, "searchResultsTranscript", void 0);
1302
- __decorate([
1303
- property({ type: Number })
1304
- ], RadioPlayer.prototype, "currentTime", void 0);
1305
- __decorate([
1306
- property({ type: String })
1307
- ], RadioPlayer.prototype, "searchTerm", void 0);
1308
- __decorate([
1309
- property({ type: Boolean })
1310
- ], RadioPlayer.prototype, "skipMusicSections", void 0);
1311
- __decorate([
1312
- property({ type: Number })
1313
- ], RadioPlayer.prototype, "percentComplete", void 0);
1314
- __decorate([
1315
- property({ type: Boolean })
1316
- ], RadioPlayer.prototype, "isPlaying", void 0);
1317
- __decorate([
1318
- property({ type: Number })
1319
- ], RadioPlayer.prototype, "duration", void 0);
1320
- __decorate([
1321
- property({ type: Number })
1322
- ], RadioPlayer.prototype, "playbackRate", void 0);
1323
- __decorate([
1324
- property({ type: Number })
1325
- ], RadioPlayer.prototype, "volume", void 0);
1326
- __decorate([
1327
- property({ type: Boolean })
1328
- ], RadioPlayer.prototype, "shouldShowSearchResultSwitcher", void 0);
1329
- __decorate([
1330
- property({ type: Boolean })
1331
- ], RadioPlayer.prototype, "shouldShowNoSearchResultMessage", void 0);
1332
- __decorate([
1333
- property({ type: Boolean })
1334
- ], RadioPlayer.prototype, "isSearching", void 0);
1335
- RadioPlayer = __decorate([
1336
- customElement('radio-player')
1337
- ], RadioPlayer);
1338
- export default RadioPlayer;
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
8
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
9
+ return new (P || (P = Promise))(function (resolve, reject) {
10
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
11
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
12
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
13
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
14
+ });
15
+ };
16
+ /* eslint-disable import/no-duplicates */
17
+ import { LitElement, html, css, } from 'lit';
18
+ import { customElement, property, } from 'lit/decorators.js';
19
+ import '@internetarchive/waveform-progress';
20
+ import '@internetarchive/playback-controls';
21
+ import '@internetarchive/scrubber-bar';
22
+ import { QuickSearchEntry } from '@internetarchive/expandable-search-bar';
23
+ import '@internetarchive/expandable-search-bar';
24
+ import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
25
+ import './search-results-switcher';
26
+ import { ZoneOfSilence } from '@internetarchive/waveform-progress';
27
+ import { PlaybackMode } from '@internetarchive/playback-controls';
28
+ import MusicZone from './models/music-zone';
29
+ /**
30
+ * A Radio Player element to play back transcribed audio.
31
+ *
32
+ * It is responsible for orchestrating all of the other components like:
33
+ * - Waveform Progress
34
+ * - Scrubber Bar
35
+ * - Playback Controls
36
+ * - Transcript View
37
+ * - Audio Element
38
+ * - Search Bar
39
+ *
40
+ * @export
41
+ * @class RadioPlayer
42
+ * @extends {LitElement}
43
+ */
44
+ let RadioPlayer = class RadioPlayer extends LitElement {
45
+ constructor() {
46
+ super(...arguments);
47
+ /**
48
+ * Radio player configuration
49
+ *
50
+ * @type {(RadioPlayerConfig | undefined)}
51
+ * @memberof RadioPlayer
52
+ */
53
+ this.config = undefined;
54
+ /**
55
+ * The Search Handler
56
+ *
57
+ * @type {(SearchHandlerInterface | undefined)}
58
+ * @memberof RadioPlayer
59
+ */
60
+ this.searchHandler = undefined;
61
+ /**
62
+ * Transcript configuration
63
+ *
64
+ * @type {(TranscriptConfig | undefined)}
65
+ * @memberof RadioPlayer
66
+ */
67
+ this.transcriptConfig = undefined;
68
+ /**
69
+ * Search result transcript. It's also a `TranscriptConfig` object, but is broken up by
70
+ * transcript entries as well as search results.
71
+ *
72
+ * If this is set, the transcript view, uses this, otherwise it uses `transcriptConfig`
73
+ *
74
+ * @type {(TranscriptConfig | undefined)}
75
+ * @memberof RadioPlayer
76
+ */
77
+ this.searchResultsTranscript = undefined;
78
+ /**
79
+ * Current playback time
80
+ *
81
+ * @type {number}
82
+ * @memberof RadioPlayer
83
+ */
84
+ this.currentTime = 0;
85
+ /**
86
+ * Current search term
87
+ *
88
+ * @type {string}
89
+ * @memberof RadioPlayer
90
+ */
91
+ this.searchTerm = '';
92
+ /**
93
+ * Flag to determine whether or not to skip the music sections
94
+ *
95
+ * @type {boolean}
96
+ * @memberof RadioPlayer
97
+ */
98
+ this.skipMusicSections = false;
99
+ /**
100
+ * Percent of playback that is complete
101
+ *
102
+ * @private
103
+ * @type {number}
104
+ * @memberof RadioPlayer
105
+ */
106
+ this.percentComplete = 0;
107
+ /**
108
+ * Flag for whether or not the radio player is playing
109
+ *
110
+ * @private
111
+ * @type {boolean}
112
+ * @memberof RadioPlayer
113
+ */
114
+ this.isPlaying = false;
115
+ /**
116
+ * Duration of the audio file
117
+ *
118
+ * @private
119
+ * @type {number}
120
+ * @memberof RadioPlayer
121
+ */
122
+ this.duration = 0;
123
+ /**
124
+ * The playback rate where 1 is "normal", 0 is paused, 2 is twice as fase
125
+ *
126
+ * @private
127
+ * @type {number}
128
+ * @memberof RadioPlayer
129
+ */
130
+ this.playbackRate = 1;
131
+ /**
132
+ * Volume from 0 to 1
133
+ *
134
+ * @private
135
+ * @type {number}
136
+ * @memberof RadioPlayer
137
+ */
138
+ this.volume = 1;
139
+ /**
140
+ * Flag for whether we should show the search results switcher
141
+ *
142
+ * @private
143
+ * @type {boolean}
144
+ * @memberof RadioPlayer
145
+ */
146
+ this.shouldShowSearchResultSwitcher = false;
147
+ /**
148
+ * Flag for whether we should show the 'no results' message
149
+ *
150
+ * @private
151
+ * @type {boolean}
152
+ * @memberof RadioPlayer
153
+ */
154
+ this.shouldShowNoSearchResultMessage = false;
155
+ /**
156
+ * Are we searching or not?
157
+ *
158
+ * @private
159
+ * @type {boolean}
160
+ * @memberof RadioPlayer
161
+ */
162
+ this.isSearching = false;
163
+ this.musicZones = [];
164
+ }
165
+ /**
166
+ * LitElement lifecycle main render method
167
+ *
168
+ * @returns {TemplateResult}
169
+ * @memberof RadioPlayer
170
+ */
171
+ render() {
172
+ return html `
173
+ ${this.audioElementTemplate}
174
+ <section role="main">
175
+ ${this.titleDateTemplate} ${this.collectionLogoTemplate} ${this.playbackControlsTemplate}
176
+ <div class="waveform-scrubber-container">
177
+ ${this.waveFormProgressTemplate} ${this.scrubberBarTemplate}
178
+ </div>
179
+ ${this.searchSectionTemplate} ${this.transcriptViewTemplate}
180
+ </section>
181
+ `;
182
+ }
183
+ /**
184
+ * Start playback
185
+ *
186
+ * @memberof RadioPlayer
187
+ */
188
+ play() {
189
+ /* istanbul ignore else */
190
+ if (this.audioElement)
191
+ this.audioElement.play();
192
+ }
193
+ /**
194
+ * Pause playback
195
+ *
196
+ * @memberof RadioPlayer
197
+ */
198
+ pause() {
199
+ /* istanbul ignore else */
200
+ if (this.audioElement)
201
+ this.audioElement.pause();
202
+ }
203
+ /**
204
+ * Seek audio to given number of seconds
205
+ *
206
+ * @param {number} seconds
207
+ * @memberof RadioPlayer
208
+ */
209
+ seekTo(seconds) {
210
+ /* istanbul ignore else */
211
+ if (this.audioElement)
212
+ this.audioElement.seekTo(seconds);
213
+ }
214
+ /**
215
+ * Generate markup for the title and date view sections
216
+ *
217
+ * @readonly
218
+ * @private
219
+ * @type {TemplateResult}
220
+ * @memberof RadioPlayer
221
+ */
222
+ get titleDateTemplate() {
223
+ return html `
224
+ <div class="title-date">
225
+ <div class="title">
226
+ ${this.config ? this.config.title : ''}
227
+ </div>
228
+
229
+ <div class="date">
230
+ ${this.config ? this.config.date : ''}
231
+ </div>
232
+ </div>
233
+ `;
234
+ }
235
+ /**
236
+ * Generate markup for the logo element
237
+ *
238
+ * @readonly
239
+ * @private
240
+ * @type {TemplateResult}
241
+ * @memberof RadioPlayer
242
+ */
243
+ get collectionLogoTemplate() {
244
+ return html `
245
+ <img class="collection-logo" src=${this.logoUrl} alt="collection logo" />
246
+ `;
247
+ }
248
+ /**
249
+ * Return the logoUrl from the config if provided
250
+ *
251
+ * @readonly
252
+ * @private
253
+ * @type {string}
254
+ * @memberof RadioPlayer
255
+ */
256
+ get logoUrl() {
257
+ return this.config ? this.config.logoUrl : '';
258
+ }
259
+ /**
260
+ * Generate the markup for the waveform progress element
261
+ *
262
+ * @readonly
263
+ * @private
264
+ * @type {TemplateResult}
265
+ * @memberof RadioPlayer
266
+ */
267
+ get waveFormProgressTemplate() {
268
+ return this.waveformUrl
269
+ ? html `
270
+ <waveform-progress
271
+ interactive="true"
272
+ .waveformUrl=${this.waveformUrl}
273
+ .percentComplete=${this.percentComplete}
274
+ @valuechange=${this.valueChangedFromScrub}
275
+ >
276
+ </waveform-progress>
277
+ `
278
+ : undefined;
279
+ }
280
+ /**
281
+ * Generate an array of `ZoneOfSilence` models for the waveform progress element
282
+ *
283
+ * It does this by parsing the transcript for music sections and converting
284
+ * those music sections to zones of silence.
285
+ *
286
+ * @readonly
287
+ * @private
288
+ * @type {ZoneOfSilence[]}
289
+ * @memberof RadioPlayer
290
+ */
291
+ get zonesOfSilence() {
292
+ /* istanbul ignore else */
293
+ if (this.duration === 0) {
294
+ return [];
295
+ }
296
+ // eslint-disable-next-line max-len
297
+ const musicEntries = this.transcriptEntries.filter((entry) => entry.isMusic === true);
298
+ const zonesOfSilence = musicEntries.map((entry) => {
299
+ const startPercent = (entry.start / this.duration) * 100;
300
+ const endPercent = (entry.end / this.duration) * 100;
301
+ return new ZoneOfSilence(startPercent, endPercent);
302
+ });
303
+ return zonesOfSilence;
304
+ }
305
+ /**
306
+ * Return the waveform url from the configuration
307
+ *
308
+ * @readonly
309
+ * @private
310
+ * @type {string}
311
+ * @memberof RadioPlayer
312
+ */
313
+ get waveformUrl() {
314
+ return this.config ? this.config.waveformUrl : '';
315
+ }
316
+ /**
317
+ * Generate the audio element markup
318
+ *
319
+ * @readonly
320
+ * @private
321
+ * @type {TemplateResult}
322
+ * @memberof RadioPlayer
323
+ */
324
+ get audioElementTemplate() {
325
+ return html `
326
+ <audio-element
327
+ .sources=${this.audioSources}
328
+ .playbackRate=${this.playbackRate}
329
+ .volume=${this.volume}
330
+ @timeupdate=${this.handleTimeChange}
331
+ @durationchange=${this.handleDurationChange}
332
+ @playbackStarted=${this.playbackStarted}
333
+ @playbackPaused=${this.playbackPaused}
334
+ @canplay=${this.canplay}
335
+ >
336
+ </audio-element>
337
+ `;
338
+ }
339
+ /**
340
+ * Return the audio sources from the configuration
341
+ *
342
+ * @readonly
343
+ * @private
344
+ * @type {AudioSource[]}
345
+ * @memberof RadioPlayer
346
+ */
347
+ get audioSources() {
348
+ return this.config ? this.config.audioSources : [];
349
+ }
350
+ /**
351
+ * Generate markup for the playback controls
352
+ *
353
+ * @readonly
354
+ * @private
355
+ * @type {TemplateResult}
356
+ * @memberof RadioPlayer
357
+ */
358
+ get playbackControlsTemplate() {
359
+ return html `
360
+ <playback-controls
361
+ @back-button-pressed=${this.backButtonHandler}
362
+ @play-pause-button-pressed=${this.playPauseButtonHandler}
363
+ @forward-button-pressed=${this.forwardButtonHandler}
364
+ @volumeChange=${this.volumeChanged}
365
+ @playbackRateChange=${this.changePlaybackRate}
366
+ @next-section-button-pressed=${this.nextSectionButtonHandler}
367
+ @prev-section-button-pressed=${this.prevSectionButtonHandler}
368
+ >
369
+ </playback-controls>
370
+ `;
371
+ }
372
+ /**
373
+ * Generate an array of start and end points for each of the music zones.
374
+ * This allows us to show the markers on the scrubber bar and optionally skip those sections.
375
+ *
376
+ * @readonly
377
+ * @private
378
+ * @type {number[]}
379
+ * @memberof RadioPlayer
380
+ */
381
+ get scrubberBarMarkerPercentages() {
382
+ const percentages = [0];
383
+ this.zonesOfSilence.forEach((zone) => {
384
+ percentages.push(zone.startPercent);
385
+ percentages.push(zone.endPercent);
386
+ });
387
+ percentages.push(100);
388
+ return percentages;
389
+ }
390
+ /**
391
+ * Generate markup for the scrubber bar
392
+ *
393
+ * @readonly
394
+ * @private
395
+ * @type {TemplateResult}
396
+ * @memberof RadioPlayer
397
+ */
398
+ get scrubberBarTemplate() {
399
+ return html `
400
+ <scrubber-bar
401
+ .sectionMarkerPercentages=${this.scrubberBarMarkerPercentages}
402
+ .value=${this.percentComplete}
403
+ @valuechange=${this.valueChangedFromScrub}
404
+ >
405
+ </scrubber-bar>
406
+ `;
407
+ }
408
+ /**
409
+ * Generate markup for the transcript view
410
+ *
411
+ * @readonly
412
+ * @private
413
+ * @type {TemplateResult}
414
+ * @memberof RadioPlayer
415
+ */
416
+ get transcriptViewTemplate() {
417
+ return html `
418
+ <div class="transcript-container">
419
+ <transcript-view
420
+ .config=${this.currentTranscript}
421
+ .currentTime=${this.currentTime}
422
+ @transcriptEntrySelected=${this.transcriptEntrySelected}
423
+ >
424
+ </transcript-view>
425
+ </div>
426
+ `;
427
+ }
428
+ get currentTranscript() {
429
+ return this.searchResultsTranscript || this.transcriptConfig;
430
+ }
431
+ /**
432
+ * Generate markup for search section
433
+ *
434
+ * @readonly
435
+ * @private
436
+ * @type {TemplateResult}
437
+ * @memberof RadioPlayer
438
+ */
439
+ get searchSectionTemplate() {
440
+ return html `
441
+ <div class="search-section">
442
+ <expandable-search-bar
443
+ searchTerm=${this.searchTerm}
444
+ .quickSearches=${this.quickSearches}
445
+ @inputchange=${this.updateSearchTerm}
446
+ @enterKeyPressed=${this.searchEnterKeyPressed}
447
+ @searchCleared=${this.searchCleared}
448
+ >
449
+ </expandable-search-bar>
450
+ <div class="search-results-info">
451
+ ${this.searchActivityIndicator}
452
+ ${this.searchResultsSwitcherTemplate}
453
+ ${this.noSearchResultsTemplate}
454
+ </div>
455
+ </div>
456
+ `;
457
+ }
458
+ /**
459
+ * Generate markup for the search results switcher
460
+ *
461
+ * @readonly
462
+ * @private
463
+ * @type {TemplateResult}
464
+ * @memberof RadioPlayer
465
+ */
466
+ get searchResultsSwitcherTemplate() {
467
+ return html `
468
+ <search-results-switcher
469
+ class="${this.shouldShowSearchResultSwitcher && !this.isSearching ? '' : 'hidden'}"
470
+ @searchResultIndexChanged=${this.searchResultIndexChanged}
471
+ >
472
+ </search-results-switcher>
473
+ `;
474
+ }
475
+ /**
476
+ * Search activity indicator template
477
+ *
478
+ * @readonly
479
+ * @private
480
+ * @type {TemplateResult}
481
+ * @memberof RadioPlayer
482
+ */
483
+ get searchActivityIndicator() {
484
+ return html `
485
+ <ia-activity-indicator
486
+ class="${this.isSearching ? '' : 'hidden'}">
487
+ </ia-activity-indicator>
488
+ `;
489
+ }
490
+ /**
491
+ * Generate markup for the "No Search Results" message
492
+ *
493
+ * @readonly
494
+ * @private
495
+ * @type {TemplateResult}
496
+ * @memberof RadioPlayer
497
+ */
498
+ get noSearchResultsTemplate() {
499
+ return html `
500
+ <div
501
+ class="
502
+ no-search-results-message
503
+ ${this.shouldShowNoSearchResultMessage && !this.isSearching ? '' : 'hidden'}"
504
+ >
505
+ No search results.
506
+ </div>
507
+ `;
508
+ }
509
+ /**
510
+ * Generate an array of `QuickSearchEntry` objects based on the quick search
511
+ * configuration passed in.
512
+ *
513
+ * @readonly
514
+ * @private
515
+ * @type {QuickSearchEntry[]}
516
+ * @memberof RadioPlayer
517
+ */
518
+ get quickSearches() {
519
+ if (!this.config) {
520
+ return [];
521
+ }
522
+ const entries = this.config.quickSearches.map(entry => new QuickSearchEntry(entry));
523
+ return entries;
524
+ }
525
+ /**
526
+ * Update the local search term when the SearchBar widget gets updated and emits the event
527
+ *
528
+ * @private
529
+ * @param {CustomEvent} e
530
+ * @returns {void}
531
+ * @memberof RadioPlayer
532
+ */
533
+ updateSearchTerm(e) {
534
+ const detail = e.detail || {};
535
+ if (!detail.value) {
536
+ return;
537
+ }
538
+ const searchTerm = detail.value;
539
+ this.searchTerm = searchTerm;
540
+ this.emitSearchTermChangedEvent(searchTerm);
541
+ }
542
+ /**
543
+ * Triggered when the user clicks the clear search button in the search bar.
544
+ * Clean up all of the search info and reset to the base transcript.
545
+ *
546
+ * @private
547
+ * @memberof RadioPlayer
548
+ */
549
+ searchCleared() {
550
+ this.searchTerm = '';
551
+ this.searchResultsTranscript = undefined;
552
+ this.emitSearchClearedEvent();
553
+ this.emitSearchTermChangedEvent('');
554
+ /* istanbul ignore else */
555
+ if (this.transcriptView) {
556
+ this.transcriptView.selectedSearchResultIndex = 0;
557
+ }
558
+ /* istanbul ignore else */
559
+ if (this.searchResultsSwitcher) {
560
+ this.searchResultsSwitcher.currentResultIndex = 0;
561
+ }
562
+ }
563
+ /**
564
+ * Triggered when the user scrubs through search result indices so we can coordinate
565
+ * the UI updates like scrolling the transcript view
566
+ *
567
+ * @private
568
+ * @param {CustomEvent} e
569
+ * @returns {void}
570
+ * @memberof RadioPlayer
571
+ */
572
+ searchResultIndexChanged(e) {
573
+ const detail = e.detail || {};
574
+ const { searchResultIndex } = detail;
575
+ if (searchResultIndex === undefined || !this.transcriptView) {
576
+ return;
577
+ }
578
+ this.transcriptView.selectedSearchResultIndex = searchResultIndex;
579
+ this.transcriptView.scrollToSelectedSearchResult();
580
+ const event = new CustomEvent('highlightedSearchResultChanged', {
581
+ detail: { searchResultIndex },
582
+ });
583
+ this.dispatchEvent(event);
584
+ }
585
+ /**
586
+ * Triggered when the user presses enter in the search bar so we can trigger a search.
587
+ *
588
+ * @private
589
+ * @param {CustomEvent} e
590
+ * @memberof RadioPlayer
591
+ */
592
+ searchEnterKeyPressed(e) {
593
+ const detail = e.detail || {};
594
+ if (!detail.value) {
595
+ return;
596
+ }
597
+ this.executeSearch(detail.value);
598
+ }
599
+ executeSearch(term) {
600
+ return __awaiter(this, void 0, void 0, function* () {
601
+ if (!this.searchHandler || term.length < 2) {
602
+ this.searchResultsTranscript = undefined;
603
+ return;
604
+ }
605
+ this.searchTerm = term;
606
+ this.isSearching = true;
607
+ const event = new Event('searchExecuted');
608
+ this.dispatchEvent(event);
609
+ this.searchResultsTranscript = yield this.searchHandler.search(term);
610
+ this.isSearching = false;
611
+ });
612
+ }
613
+ /**
614
+ * Return transcript entries from the transcript configuration
615
+ *
616
+ * @readonly
617
+ * @private
618
+ * @type {TranscriptEntryConfig[]}
619
+ * @memberof RadioPlayer
620
+ */
621
+ get transcriptEntries() {
622
+ return this.currentTranscript ? this.currentTranscript.entries : [];
623
+ }
624
+ /**
625
+ * Return the transcript view DOM element
626
+ *
627
+ * @readonly
628
+ * @private
629
+ * @type {(TranscriptView | null)}
630
+ * @memberof RadioPlayer
631
+ */
632
+ get transcriptView() {
633
+ /* istanbul ignore next */
634
+ return this.shadowRoot
635
+ ? this.shadowRoot.querySelector('transcript-view')
636
+ : null;
637
+ }
638
+ /**
639
+ * Return the audio element DOM element
640
+ *
641
+ * @readonly
642
+ * @private
643
+ * @type {(AudioElement | null)}
644
+ * @memberof RadioPlayer
645
+ */
646
+ get audioElement() {
647
+ /* istanbul ignore next */
648
+ return this.shadowRoot
649
+ ? this.shadowRoot.querySelector('audio-element')
650
+ : null;
651
+ }
652
+ /**
653
+ * Return the playback controls DOM element
654
+ *
655
+ * @readonly
656
+ * @private
657
+ * @type {(PlaybackControls | null)}
658
+ * @memberof RadioPlayer
659
+ */
660
+ get playbackControls() {
661
+ /* istanbul ignore next */
662
+ return this.shadowRoot
663
+ ? this.shadowRoot.querySelector('playback-controls')
664
+ : null;
665
+ }
666
+ /**
667
+ * Return the search results switcher DOM element
668
+ *
669
+ * @readonly
670
+ * @private
671
+ * @type {(SearchResultsSwitcher | null)}
672
+ * @memberof RadioPlayer
673
+ */
674
+ get searchResultsSwitcher() {
675
+ /* istanbul ignore next */
676
+ return this.shadowRoot
677
+ ? this.shadowRoot.querySelector('search-results-switcher')
678
+ : null;
679
+ }
680
+ /**
681
+ * Triggered when the user changes the playback rate so we can update the audio element
682
+ *
683
+ * @private
684
+ * @param {CustomEvent} e
685
+ * @returns {void}
686
+ * @memberof RadioPlayer
687
+ */
688
+ changePlaybackRate(e) {
689
+ const detail = e.detail || {};
690
+ if (!detail.playbackRate) {
691
+ return;
692
+ }
693
+ this.playbackRate = detail.playbackRate;
694
+ const event = new CustomEvent('playbackRateChanged', {
695
+ detail: { playbackRate: this.playbackRate },
696
+ });
697
+ this.dispatchEvent(event);
698
+ }
699
+ /**
700
+ * Triggered when the user changes the volume from the playback controls
701
+ *
702
+ * @private
703
+ * @param {CustomEvent} e
704
+ * @returns {void}
705
+ * @memberof RadioPlayer
706
+ */
707
+ volumeChanged(e) {
708
+ const detail = e.detail || {};
709
+ if (detail.volume === undefined) {
710
+ return;
711
+ }
712
+ this.volume = e.detail.volume;
713
+ const event = new CustomEvent('volumeChanged', {
714
+ detail: { volume: this.volume },
715
+ });
716
+ this.dispatchEvent(event);
717
+ }
718
+ /**
719
+ * Triggered when the user presses the back button in the playback controls
720
+ *
721
+ * @private
722
+ * @memberof RadioPlayer
723
+ */
724
+ backButtonHandler() {
725
+ /* istanbul ignore else */
726
+ if (this.audioElement) {
727
+ this.audioElement.seekBy(-10);
728
+ }
729
+ const event = new Event('jumpBackButtonPressed');
730
+ this.dispatchEvent(event);
731
+ }
732
+ /**
733
+ * Triggered when the user presses the play/pause button
734
+ *
735
+ * @private
736
+ * @returns {void}
737
+ * @memberof RadioPlayer
738
+ */
739
+ playPauseButtonHandler() {
740
+ this.isPlaying = !this.isPlaying;
741
+ /* istanbul ignore if */
742
+ if (!this.audioElement) {
743
+ return;
744
+ }
745
+ if (this.isPlaying) {
746
+ this.audioElement.play();
747
+ }
748
+ else {
749
+ this.audioElement.pause();
750
+ }
751
+ const event = new CustomEvent('playPauseButtonPressed', {
752
+ detail: { isPlaying: this.isPlaying },
753
+ });
754
+ this.dispatchEvent(event);
755
+ }
756
+ /**
757
+ * Triggered when the user presses the forward button in the playback controls
758
+ *
759
+ * @private
760
+ * @memberof RadioPlayer
761
+ */
762
+ forwardButtonHandler() {
763
+ /* istanbul ignore else */
764
+ if (this.audioElement) {
765
+ this.audioElement.seekBy(10);
766
+ }
767
+ const event = new Event('jumpForwardButtonPressed');
768
+ this.dispatchEvent(event);
769
+ }
770
+ /**
771
+ * Skip to the next section
772
+ *
773
+ * @private
774
+ * @returns {void}
775
+ * @memberof RadioPlayer
776
+ */
777
+ nextSectionButtonHandler() {
778
+ /* istanbul ignore if */
779
+ if (!this.audioElement) {
780
+ return;
781
+ }
782
+ const percentsGreaterThanValue = this.scrubberBarMarkerPercentages.filter(value => value > this.percentComplete + 0.1);
783
+ const closestUpper = Math.min(...percentsGreaterThanValue);
784
+ const seekTo = this.duration * (closestUpper / 100) + 0.1;
785
+ this.audioElement.seekTo(seekTo);
786
+ const event = new Event('nextSectionButtonPressed');
787
+ this.dispatchEvent(event);
788
+ }
789
+ /**
790
+ * Jump to the previous section
791
+ *
792
+ * @private
793
+ * @returns {void}
794
+ * @memberof RadioPlayer
795
+ */
796
+ prevSectionButtonHandler() {
797
+ /* istanbul ignore if */
798
+ if (!this.audioElement) {
799
+ return;
800
+ }
801
+ const percentsLessThanValue = this.scrubberBarMarkerPercentages.filter(value => value < this.percentComplete - 0.1);
802
+ const closestLower = Math.max(...percentsLessThanValue);
803
+ const seekTo = this.duration * (closestLower / 100) - 0.1;
804
+ this.audioElement.seekTo(seekTo);
805
+ const event = new Event('prevSectionButtonPressed');
806
+ this.dispatchEvent(event);
807
+ }
808
+ /**
809
+ * Handle the duration change event like when the audio file first loads
810
+ *
811
+ * @private
812
+ * @param {CustomEvent} e
813
+ * @returns {void}
814
+ * @memberof RadioPlayer
815
+ */
816
+ handleDurationChange(e) {
817
+ const detail = e.detail || {};
818
+ if (!detail.duration) {
819
+ return;
820
+ }
821
+ this.duration = detail.duration;
822
+ }
823
+ /**
824
+ * Triggered as the playback progresses in the audio element
825
+ *
826
+ * @private
827
+ * @param {CustomEvent} e
828
+ * @returns {void}
829
+ * @memberof RadioPlayer
830
+ */
831
+ handleTimeChange(e) {
832
+ const detail = e.detail || {};
833
+ if (!detail.currentTime) {
834
+ return;
835
+ }
836
+ this.currentTime = detail.currentTime;
837
+ const percent = this.currentTime / this.duration;
838
+ this.percentComplete = percent * 100;
839
+ }
840
+ /**
841
+ * Update any consumers that the current time has changed
842
+ *
843
+ * @private
844
+ * @memberof RadioPlayer
845
+ */
846
+ emitCurrentTimeChangedEvent() {
847
+ const event = new CustomEvent('currentTimeChanged', {
848
+ detail: { currentTime: this.currentTime },
849
+ });
850
+ this.dispatchEvent(event);
851
+ }
852
+ /**
853
+ * When the user clears the search, we want to bubble up the event to other consumers.
854
+ *
855
+ * @private
856
+ * @memberof RadioPlayer
857
+ */
858
+ emitSearchClearedEvent() {
859
+ const event = new Event('searchCleared');
860
+ this.dispatchEvent(event);
861
+ }
862
+ emitSearchTermChangedEvent(term) {
863
+ const event = new CustomEvent('searchTermChanged', {
864
+ detail: { searchTerm: term },
865
+ });
866
+ this.dispatchEvent(event);
867
+ }
868
+ /**
869
+ * Handle the playback paused event
870
+ *
871
+ * @private
872
+ * @memberof RadioPlayer
873
+ */
874
+ playbackPaused() {
875
+ this.isPlaying = false;
876
+ /* istanbul ignore else */
877
+ if (this.playbackControls) {
878
+ this.playbackControls.playbackMode = PlaybackMode.paused;
879
+ }
880
+ const event = new Event('playbackPaused');
881
+ this.dispatchEvent(event);
882
+ }
883
+ /**
884
+ * Handle the playback started event
885
+ *
886
+ * @private
887
+ * @memberof RadioPlayer
888
+ */
889
+ playbackStarted() {
890
+ this.isPlaying = true;
891
+ /* istanbul ignore else */
892
+ if (this.playbackControls) {
893
+ this.playbackControls.playbackMode = PlaybackMode.playing;
894
+ }
895
+ const event = new Event('playbackStarted');
896
+ this.dispatchEvent(event);
897
+ }
898
+ /**
899
+ * Emits a `canplay` event when the media is ready to be played
900
+ *
901
+ * @private
902
+ * @memberof RadioPlayer
903
+ */
904
+ canplay() {
905
+ const event = new Event('canplay');
906
+ this.dispatchEvent(event);
907
+ }
908
+ /**
909
+ * Triggered when the user scrubs the scrubber bar or waveform progress bar
910
+ *
911
+ * Handles updating the audioElement to the new time and emitting an event
912
+ * that the time changed
913
+ *
914
+ * @private
915
+ * @param {CustomEvent} e
916
+ * @returns {void}
917
+ * @memberof RadioPlayer
918
+ */
919
+ valueChangedFromScrub(e) {
920
+ const detail = e.detail || {};
921
+ if (!detail.value) {
922
+ return;
923
+ }
924
+ const percentage = detail.value;
925
+ const newTime = this.duration * (percentage / 100);
926
+ this.currentTime = newTime;
927
+ /* istanbul ignore else */
928
+ if (this.audioElement) {
929
+ this.audioElement.seekTo(newTime);
930
+ }
931
+ this.percentComplete = percentage;
932
+ const event = new CustomEvent('timeChangedFromScrub', {
933
+ detail: { newTime: this.currentTime },
934
+ });
935
+ this.dispatchEvent(event);
936
+ }
937
+ /**
938
+ * Triggered when the user selects a transcript entry.
939
+ * Allows it to jump to that spot in the playback.
940
+ *
941
+ * @private
942
+ * @param {CustomEvent} e
943
+ * @returns {void}
944
+ * @memberof RadioPlayer
945
+ */
946
+ transcriptEntrySelected(e) {
947
+ const detail = e.detail || {};
948
+ const entry = detail.entry || {};
949
+ if (!entry.start) {
950
+ return;
951
+ }
952
+ const newTime = entry.start;
953
+ this.currentTime = newTime;
954
+ /* istanbul ignore else */
955
+ if (this.audioElement) {
956
+ this.audioElement.seekTo(newTime);
957
+ this.audioElement.play();
958
+ }
959
+ const event = new CustomEvent('transcriptEntrySelected', {
960
+ detail: { newTime: this.currentTime },
961
+ });
962
+ this.dispatchEvent(event);
963
+ }
964
+ /**
965
+ * Creates the music zones based on the transcript so we know when to skip past sections
966
+ *
967
+ * @private
968
+ * @memberof RadioPlayer
969
+ */
970
+ updateMusicZones() {
971
+ // eslint-disable-next-line max-len
972
+ const musicEntries = this.transcriptEntries.filter((entry) => entry.isMusic === true);
973
+ // eslint-disable-next-line max-len
974
+ const musicZones = musicEntries.map((entry) => new MusicZone(entry.start, entry.end));
975
+ this.musicZones = musicZones;
976
+ }
977
+ /**
978
+ * Skips a music zone if enabled
979
+ *
980
+ * @private
981
+ * @memberof RadioPlayer
982
+ */
983
+ skipMusicZone() {
984
+ // eslint-disable-next-line max-len
985
+ const activeMusicZone = this.musicZones.find((zone) => this.currentTime > zone.start && this.currentTime < zone.end);
986
+ if (activeMusicZone && this.audioElement) {
987
+ this.audioElement.seekTo(activeMusicZone.end + 0.1);
988
+ }
989
+ }
990
+ /**
991
+ * Updates the search results switcher numbers
992
+ *
993
+ * @private
994
+ * @returns {void}
995
+ * @memberof RadioPlayer
996
+ */
997
+ updateSearchResultSwitcher() {
998
+ this.shouldShowNoSearchResultMessage = false;
999
+ this.shouldShowSearchResultSwitcher = false;
1000
+ if (this.searchTerm.length === 0) {
1001
+ return;
1002
+ }
1003
+ const resultCount = this.searchResults.length;
1004
+ if (resultCount === 0) {
1005
+ this.shouldShowNoSearchResultMessage = true;
1006
+ }
1007
+ else {
1008
+ this.shouldShowSearchResultSwitcher = true;
1009
+ /* istanbul ignore else */
1010
+ if (this.searchResultsSwitcher) {
1011
+ this.searchResultsSwitcher.numberOfResults = resultCount;
1012
+ }
1013
+ }
1014
+ }
1015
+ /**
1016
+ * Returns the search result template entries from the transcript config
1017
+ *
1018
+ * @readonly
1019
+ * @private
1020
+ * @type {TranscriptEntryConfig[]}
1021
+ * @memberof RadioPlayer
1022
+ */
1023
+ get searchResults() {
1024
+ return this.searchResultsTranscript ? this.searchResultsTranscript.searchResults : [];
1025
+ }
1026
+ /**
1027
+ * LitElement life cycle event when any reactive property gets updated
1028
+ *
1029
+ * @param {PropertyValues} changedProperties
1030
+ * @memberof RadioPlayer
1031
+ */
1032
+ updated(changedProperties) {
1033
+ // when the transcriptConfig gets changed, reload the music zones and search results switcher
1034
+ if (changedProperties.has('transcriptConfig')) {
1035
+ this.updateMusicZones();
1036
+ }
1037
+ if (changedProperties.has('searchResultsTranscript')) {
1038
+ this.updateSearchResultSwitcher();
1039
+ }
1040
+ if (changedProperties.has('searchHandler') && this.searchTerm) {
1041
+ this.executeSearch(this.searchTerm);
1042
+ }
1043
+ // as the currentTime gets updated, emit an event and skip the music zone if enabled
1044
+ if (changedProperties.has('currentTime')) {
1045
+ this.emitCurrentTimeChangedEvent();
1046
+ if (this.skipMusicSections) {
1047
+ this.skipMusicZone();
1048
+ }
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Component styles
1053
+ *
1054
+ * @readonly
1055
+ * @static
1056
+ * @type {CSSResult}
1057
+ * @memberof RadioPlayer
1058
+ */
1059
+ static get styles() {
1060
+ const collectionLogoMaxHeightCss = css `var(--collectionLogoMaxHeight, 8rem)`;
1061
+ const titleColorCss = css `var(--titleColor, white)`;
1062
+ const titleFontCss = css `var(--titleFont, 1.5em sans-serif)`;
1063
+ const dateColorCss = css `var(--dateColor, white)`;
1064
+ const dateFontCss = css `var(--dateFont, 1em sans-serif)`;
1065
+ const waveformProgressHeightCss = css `var(--waveformProgressHeight, 5rem)`;
1066
+ return css `
1067
+ section[role='main'] {
1068
+ display: -ms-grid;
1069
+ display: grid;
1070
+ grid-gap: 0.5rem;
1071
+ }
1072
+
1073
+ ia-activity-indicator {
1074
+ width: 1.5em;
1075
+ height: 1.5em;
1076
+ display: block;
1077
+ margin: auto;
1078
+ --activityIndicatorLoadingRingColor: #999;
1079
+ --activityIndicatorLoadingDotColor: #999;
1080
+ }
1081
+
1082
+ /* mobile view */
1083
+ @media (max-width: 770px) {
1084
+ section[role='main'] {
1085
+ -ms-grid-columns: 25% 0.5rem 1fr;
1086
+ -ms-grid-rows: auto 0.5rem auto 0.5rem auto 0.5rem auto 0.5rem auto;
1087
+ grid-template-columns: 25% 1fr;
1088
+ grid-template-areas:
1089
+ 'collection-logo title-date'
1090
+ 'waveform-scrubber waveform-scrubber'
1091
+ 'playback-controls playback-controls'
1092
+ 'search-section search-section'
1093
+ 'transcript-container transcript-container';
1094
+ }
1095
+ .date {
1096
+ text-align: left;
1097
+ }
1098
+ transcript-view {
1099
+ --timeDisplay: none;
1100
+ }
1101
+ playback-controls {
1102
+ width: 75%;
1103
+ margin: auto;
1104
+ grid-row: 5;
1105
+ grid-column: 1;
1106
+ -ms-grid-row: 5;
1107
+ -ms-grid-column: 1;
1108
+ -ms-grid-column-span: 3;
1109
+ }
1110
+ .title-date {
1111
+ grid-row: 1;
1112
+ grid-column: 3;
1113
+ -ms-grid-row: 1;
1114
+ -ms-grid-column: 3;
1115
+ }
1116
+ .transcript-container {
1117
+ grid-row: 9;
1118
+ grid-column: 1;
1119
+ -ms-grid-row: 9;
1120
+ -ms-grid-column: 1;
1121
+ -ms-grid-column-span: 3;
1122
+ }
1123
+ .collection-logo {
1124
+ grid-row: 1;
1125
+ grid-column: 1;
1126
+ -ms-grid-row: 1;
1127
+ -ms-grid-column: 1;
1128
+ }
1129
+ .waveform-scrubber-container {
1130
+ grid-row: 3;
1131
+ grid-column: 1;
1132
+ -ms-grid-row: 3;
1133
+ -ms-grid-column: 1;
1134
+ -ms-grid-column-span: 3;
1135
+ }
1136
+ .search-section {
1137
+ grid-row: 7;
1138
+ grid-column: 1;
1139
+ -ms-grid-row: 7;
1140
+ -ms-grid-column: 1;
1141
+ -ms-grid-column-span: 3;
1142
+ width: 75%;
1143
+ margin: auto;
1144
+ }
1145
+ expandable-search-bar {
1146
+ width: 100%;
1147
+ }
1148
+ }
1149
+
1150
+ /* wide view */
1151
+ @media (min-width: 770px) {
1152
+ section[role='main'] {
1153
+ -ms-grid-columns: 192px 0.5rem 0 0.5rem 250px 0.5rem 1fr;
1154
+ -ms-grid-rows: auto 0.5rem auto 0.5rem auto;
1155
+ grid-template-columns: 192px 0 250px 1fr;
1156
+ grid-template-areas:
1157
+ 'title-date title-date title-date title-date'
1158
+ 'collection-logo 1 playback-controls waveform-scrubber'
1159
+ 'search-section transcript-container transcript-container transcript-container';
1160
+ }
1161
+ .title-date {
1162
+ display: flex;
1163
+ justify-content: space-between;
1164
+ align-items: baseline;
1165
+ grid-row: 1;
1166
+ grid-column: 1;
1167
+ -ms-grid-row: 1;
1168
+ -ms-grid-column: 1;
1169
+ -ms-grid-column-span: 7;
1170
+ }
1171
+ transcript-view {
1172
+ --timeDisplay: block;
1173
+ }
1174
+ playback-controls {
1175
+ grid-row: 3;
1176
+ grid-column: 5;
1177
+ -ms-grid-row: 3;
1178
+ -ms-grid-column: 5;
1179
+ -ms-grid-column-span: 1;
1180
+ }
1181
+ .transcript-container {
1182
+ grid-row: 5;
1183
+ grid-column: 3;
1184
+ -ms-grid-row: 5;
1185
+ -ms-grid-column: 3;
1186
+ -ms-grid-column-span: 5;
1187
+ }
1188
+ .collection-logo {
1189
+ grid-row: 3;
1190
+ grid-column: 1;
1191
+ -ms-grid-row: 3;
1192
+ -ms-grid-column: 1;
1193
+ }
1194
+ .waveform-scrubber-container {
1195
+ grid-row: 3;
1196
+ grid-column: 7;
1197
+ -ms-grid-row: 3;
1198
+ -ms-grid-column: 7;
1199
+ -ms-grid-column-span: 1;
1200
+ }
1201
+ .search-section {
1202
+ grid-row: 5;
1203
+ grid-column: 1;
1204
+ -ms-grid-row: 5;
1205
+ -ms-grid-column: 1;
1206
+ -ms-grid-column-span: 1;
1207
+ }
1208
+ }
1209
+
1210
+ .title-date {
1211
+ grid-area: title-date;
1212
+ }
1213
+
1214
+ .title {
1215
+ color: ${titleColorCss};
1216
+ font: ${titleFontCss};
1217
+ }
1218
+
1219
+ .date {
1220
+ color: ${dateColorCss};
1221
+ font: ${dateFontCss};
1222
+ }
1223
+
1224
+ waveform-progress {
1225
+ width: 100%;
1226
+ height: ${waveformProgressHeightCss};
1227
+ }
1228
+
1229
+ playback-controls {
1230
+ grid-area: playback-controls;
1231
+ }
1232
+
1233
+ .transcript-container {
1234
+ grid-area: transcript-container;
1235
+ }
1236
+
1237
+ transcript-view {
1238
+ max-width: 600px;
1239
+ display: block;
1240
+ }
1241
+
1242
+ .collection-logo {
1243
+ width: 100%;
1244
+ max-height: ${collectionLogoMaxHeightCss};
1245
+ object-fit: contain;
1246
+ grid-area: collection-logo;
1247
+ align-self: center;
1248
+ }
1249
+
1250
+ .waveform-scrubber-container {
1251
+ width: 100%;
1252
+ height: 100%;
1253
+ grid-area: waveform-scrubber;
1254
+ }
1255
+
1256
+ .search-section {
1257
+ grid-area: search-section;
1258
+ }
1259
+
1260
+ .search-results-info {
1261
+ margin-top: 0.5em;
1262
+ }
1263
+
1264
+ .quick-search-container {
1265
+ max-height: 150px;
1266
+ overflow-y: scroll;
1267
+ scrollbar-width: none;
1268
+ margin: 0 0.5em;
1269
+ }
1270
+
1271
+ .quick-search-container::-webkit-scrollbar {
1272
+ display: none;
1273
+ }
1274
+
1275
+ expandable-search-bar {
1276
+ display: block;
1277
+ margin: auto;
1278
+ }
1279
+
1280
+ .no-search-results-message {
1281
+ text-align: center;
1282
+ }
1283
+
1284
+ .hidden {
1285
+ display: none;
1286
+ }
1287
+ `;
1288
+ }
1289
+ };
1290
+ __decorate([
1291
+ property({ type: Object })
1292
+ ], RadioPlayer.prototype, "config", void 0);
1293
+ __decorate([
1294
+ property({ type: Object })
1295
+ ], RadioPlayer.prototype, "searchHandler", void 0);
1296
+ __decorate([
1297
+ property({ type: Object })
1298
+ ], RadioPlayer.prototype, "transcriptConfig", void 0);
1299
+ __decorate([
1300
+ property({ type: Object })
1301
+ ], RadioPlayer.prototype, "searchResultsTranscript", void 0);
1302
+ __decorate([
1303
+ property({ type: Number })
1304
+ ], RadioPlayer.prototype, "currentTime", void 0);
1305
+ __decorate([
1306
+ property({ type: String })
1307
+ ], RadioPlayer.prototype, "searchTerm", void 0);
1308
+ __decorate([
1309
+ property({ type: Boolean })
1310
+ ], RadioPlayer.prototype, "skipMusicSections", void 0);
1311
+ __decorate([
1312
+ property({ type: Number })
1313
+ ], RadioPlayer.prototype, "percentComplete", void 0);
1314
+ __decorate([
1315
+ property({ type: Boolean })
1316
+ ], RadioPlayer.prototype, "isPlaying", void 0);
1317
+ __decorate([
1318
+ property({ type: Number })
1319
+ ], RadioPlayer.prototype, "duration", void 0);
1320
+ __decorate([
1321
+ property({ type: Number })
1322
+ ], RadioPlayer.prototype, "playbackRate", void 0);
1323
+ __decorate([
1324
+ property({ type: Number })
1325
+ ], RadioPlayer.prototype, "volume", void 0);
1326
+ __decorate([
1327
+ property({ type: Boolean })
1328
+ ], RadioPlayer.prototype, "shouldShowSearchResultSwitcher", void 0);
1329
+ __decorate([
1330
+ property({ type: Boolean })
1331
+ ], RadioPlayer.prototype, "shouldShowNoSearchResultMessage", void 0);
1332
+ __decorate([
1333
+ property({ type: Boolean })
1334
+ ], RadioPlayer.prototype, "isSearching", void 0);
1335
+ RadioPlayer = __decorate([
1336
+ customElement('radio-player')
1337
+ ], RadioPlayer);
1338
+ export default RadioPlayer;
1339
1339
  //# sourceMappingURL=radio-player.js.map