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