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