vidply 1.0.22 → 1.0.25
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/dist/dev/vidply.HLSRenderer-PNP5OPES.js +255 -0
- package/dist/dev/vidply.HLSRenderer-PNP5OPES.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-LXQ3I45Q.js +12 -0
- package/dist/dev/vidply.HTML5Renderer-LXQ3I45Q.js.map +7 -0
- package/dist/dev/vidply.TranscriptManager-GZKY44ON.js +1744 -0
- package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +7 -0
- package/dist/dev/vidply.VimeoRenderer-DCETT5IZ.js +213 -0
- package/dist/dev/vidply.VimeoRenderer-DCETT5IZ.js.map +7 -0
- package/dist/dev/vidply.YouTubeRenderer-QLMMD757.js +227 -0
- package/dist/dev/vidply.YouTubeRenderer-QLMMD757.js.map +7 -0
- package/dist/dev/vidply.chunk-UEIJOJH6.js +243 -0
- package/dist/dev/vidply.chunk-UEIJOJH6.js.map +7 -0
- package/dist/dev/vidply.chunk-UH5MTGKF.js +1630 -0
- package/dist/dev/vidply.chunk-UH5MTGKF.js.map +7 -0
- package/dist/dev/vidply.de-THBIMP4S.js +180 -0
- package/dist/dev/vidply.de-THBIMP4S.js.map +7 -0
- package/dist/dev/vidply.es-6VWDNNNL.js +180 -0
- package/dist/dev/vidply.es-6VWDNNNL.js.map +7 -0
- package/dist/{vidply.esm.js → dev/vidply.esm.js} +530 -5082
- package/dist/dev/vidply.esm.js.map +7 -0
- package/dist/dev/vidply.fr-WHTWCHWT.js +180 -0
- package/dist/dev/vidply.fr-WHTWCHWT.js.map +7 -0
- package/dist/dev/vidply.ja-BFQNPOFI.js +180 -0
- package/dist/dev/vidply.ja-BFQNPOFI.js.map +7 -0
- package/dist/{vidply.js → legacy/vidply.js} +7833 -7317
- package/dist/legacy/vidply.js.map +7 -0
- package/dist/legacy/vidply.min.js +6 -0
- package/dist/{vidply.min.meta.json → legacy/vidply.min.meta.json} +120 -94
- package/dist/prod/vidply.HLSRenderer-4PW35TCX.min.js +6 -0
- package/dist/prod/vidply.HTML5Renderer-XJCSUETP.min.js +6 -0
- package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +6 -0
- package/dist/prod/vidply.VimeoRenderer-P3PU27S7.min.js +6 -0
- package/dist/prod/vidply.YouTubeRenderer-DGKKWB5M.min.js +6 -0
- package/dist/prod/vidply.chunk-BQBGEJF7.min.js +6 -0
- package/dist/prod/vidply.chunk-MBUR3U5L.min.js +6 -0
- package/dist/prod/vidply.de-SWFW4HYT.min.js +6 -0
- package/dist/prod/vidply.es-7BJ2DJAY.min.js +6 -0
- package/dist/prod/vidply.esm.min.js +21 -0
- package/dist/prod/vidply.fr-DPVR5DFY.min.js +6 -0
- package/dist/prod/vidply.ja-PEBVWKVH.min.js +6 -0
- package/dist/vidply.css +184 -4
- package/dist/vidply.esm.min.meta.json +284 -102
- package/dist/vidply.min.css +1 -1
- package/package.json +4 -4
- package/src/controls/ControlBar.js +3341 -3246
- package/src/controls/TranscriptManager.js +2296 -2271
- package/src/core/Player.js +4807 -4730
- package/src/features/PlaylistManager.js +1203 -1039
- package/src/i18n/i18n.js +51 -7
- package/src/i18n/languages/de.js +5 -1
- package/src/i18n/languages/en.js +5 -1
- package/src/i18n/languages/es.js +5 -1
- package/src/i18n/languages/fr.js +5 -1
- package/src/i18n/languages/ja.js +5 -1
- package/src/i18n/translations.js +35 -18
- package/src/icons/Icons.js +2 -20
- package/src/renderers/HLSRenderer.js +7 -0
- package/src/styles/vidply.css +184 -4
- package/src/utils/DOMUtils.js +67 -0
- package/src/utils/MenuUtils.js +10 -4
- package/src/utils/SettingsMenuFactory.js +8 -4
- package/src/utils/WindowComponents.js +6 -4
- package/dist/vidply.esm.js.map +0 -7
- package/dist/vidply.esm.min.js +0 -18
- package/dist/vidply.js.map +0 -7
- package/dist/vidply.min.js +0 -18
|
@@ -1,1039 +1,1203 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VidPly Playlist Manager
|
|
3
|
-
* Manages playlists for audio and video content
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
-
import { createIconElement } from '../icons/Icons.js';
|
|
8
|
-
import { i18n } from '../i18n/i18n.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.player.on('
|
|
70
|
-
|
|
71
|
-
this.player.on('
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// data-playlist-auto-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
this.options.
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// data-playlist-
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
this.options.
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// data-playlist-
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
this.options.
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
controlBar.
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
controlBar.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
this.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
this.
|
|
466
|
-
|
|
467
|
-
this.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
'
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
this.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
this.
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
let
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (this.
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
//
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
this.
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1
|
+
/**
|
|
2
|
+
* VidPly Playlist Manager
|
|
3
|
+
* Manages playlists for audio and video content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
+
import { createIconElement } from '../icons/Icons.js';
|
|
8
|
+
import { i18n } from '../i18n/i18n.js';
|
|
9
|
+
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
10
|
+
|
|
11
|
+
// Static counter for unique IDs
|
|
12
|
+
let playlistInstanceCounter = 0;
|
|
13
|
+
|
|
14
|
+
export class PlaylistManager {
|
|
15
|
+
constructor(player, options = {}) {
|
|
16
|
+
this.player = player;
|
|
17
|
+
this.tracks = [];
|
|
18
|
+
this.initialTracks = Array.isArray(options.tracks) ? options.tracks : [];
|
|
19
|
+
this.currentIndex = -1;
|
|
20
|
+
|
|
21
|
+
// Generate unique instance ID for this playlist
|
|
22
|
+
this.instanceId = ++playlistInstanceCounter;
|
|
23
|
+
this.uniqueId = `vidply-playlist-${this.instanceId}`;
|
|
24
|
+
|
|
25
|
+
// Options
|
|
26
|
+
this.options = {
|
|
27
|
+
autoAdvance: options.autoAdvance !== false, // Default true
|
|
28
|
+
autoPlayFirst: options.autoPlayFirst !== false, // Default true - auto-play first track on load
|
|
29
|
+
loop: options.loop || false,
|
|
30
|
+
showPanel: options.showPanel !== false, // Default true
|
|
31
|
+
...options
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// UI elements
|
|
35
|
+
this.container = null;
|
|
36
|
+
this.playlistPanel = null;
|
|
37
|
+
this.trackInfoElement = null;
|
|
38
|
+
this.navigationFeedback = null; // Live region for keyboard navigation feedback
|
|
39
|
+
this.isPanelVisible = this.options.showPanel !== false;
|
|
40
|
+
|
|
41
|
+
// Track change guard to prevent cascade of next() calls
|
|
42
|
+
this.isChangingTrack = false;
|
|
43
|
+
|
|
44
|
+
// Bind methods
|
|
45
|
+
this.handleTrackEnd = this.handleTrackEnd.bind(this);
|
|
46
|
+
this.handleTrackError = this.handleTrackError.bind(this);
|
|
47
|
+
|
|
48
|
+
// Register this playlist manager with the player
|
|
49
|
+
this.player.playlistManager = this;
|
|
50
|
+
|
|
51
|
+
// Initialize
|
|
52
|
+
this.init();
|
|
53
|
+
|
|
54
|
+
// Update controls to add playlist buttons
|
|
55
|
+
this.updatePlayerControls();
|
|
56
|
+
|
|
57
|
+
// Load tracks if provided in options (after UI is ready)
|
|
58
|
+
if (this.initialTracks.length > 0) {
|
|
59
|
+
this.loadPlaylist(this.initialTracks);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
init() {
|
|
64
|
+
// Listen for track end
|
|
65
|
+
this.player.on('ended', this.handleTrackEnd);
|
|
66
|
+
this.player.on('error', this.handleTrackError);
|
|
67
|
+
|
|
68
|
+
// Listen for playback state changes to show/hide playlist in fullscreen
|
|
69
|
+
this.player.on('play', this.handlePlaybackStateChange.bind(this));
|
|
70
|
+
this.player.on('pause', this.handlePlaybackStateChange.bind(this));
|
|
71
|
+
this.player.on('ended', this.handlePlaybackStateChange.bind(this));
|
|
72
|
+
// Use fullscreenchange event which is what the player actually emits
|
|
73
|
+
this.player.on('fullscreenchange', this.handleFullscreenChange.bind(this));
|
|
74
|
+
|
|
75
|
+
// Listen for audio description state changes to update duration displays
|
|
76
|
+
this.player.on('audiodescriptionenabled', this.handleAudioDescriptionChange.bind(this));
|
|
77
|
+
this.player.on('audiodescriptiondisabled', this.handleAudioDescriptionChange.bind(this));
|
|
78
|
+
|
|
79
|
+
// Create UI if needed
|
|
80
|
+
if (this.options.showPanel) {
|
|
81
|
+
this.createUI();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for data-playlist attribute on player container (only if tracks weren't provided in options)
|
|
85
|
+
if (this.tracks.length === 0 && this.initialTracks.length === 0) {
|
|
86
|
+
this.loadPlaylistFromAttribute();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load playlist from data-playlist attribute if present
|
|
92
|
+
*/
|
|
93
|
+
loadPlaylistFromAttribute() {
|
|
94
|
+
// Check the original wrapper element for data-playlist
|
|
95
|
+
// Structure: #audio-player -> .vidply-player -> .vidply-video-wrapper -> <audio>
|
|
96
|
+
// So we need to go up 3 levels
|
|
97
|
+
if (!this.player.element || !this.player.element.parentElement) {
|
|
98
|
+
console.log('VidPly Playlist: No player element found');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const videoWrapper = this.player.element.parentElement; // .vidply-video-wrapper
|
|
103
|
+
const playerContainer = videoWrapper.parentElement; // .vidply-player
|
|
104
|
+
const originalElement = playerContainer ? playerContainer.parentElement : null; // #audio-player (original div)
|
|
105
|
+
|
|
106
|
+
if (!originalElement) {
|
|
107
|
+
console.log('VidPly Playlist: No original element found');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load playlist options from data attributes
|
|
112
|
+
this.loadOptionsFromAttributes(originalElement);
|
|
113
|
+
|
|
114
|
+
const playlistData = originalElement.getAttribute('data-playlist');
|
|
115
|
+
if (!playlistData) {
|
|
116
|
+
console.log('VidPly Playlist: No data-playlist attribute found');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('VidPly Playlist: Found data-playlist attribute, parsing...');
|
|
121
|
+
try {
|
|
122
|
+
const tracks = JSON.parse(playlistData);
|
|
123
|
+
if (Array.isArray(tracks) && tracks.length > 0) {
|
|
124
|
+
console.log(`VidPly Playlist: Loaded ${tracks.length} tracks from data-playlist`);
|
|
125
|
+
this.loadPlaylist(tracks);
|
|
126
|
+
} else {
|
|
127
|
+
console.warn('VidPly Playlist: data-playlist is not a valid array or is empty');
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('VidPly Playlist: Failed to parse data-playlist attribute', error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load playlist options from data attributes
|
|
136
|
+
* @param {HTMLElement} element - Element to read attributes from
|
|
137
|
+
*/
|
|
138
|
+
loadOptionsFromAttributes(element) {
|
|
139
|
+
// data-playlist-auto-advance
|
|
140
|
+
const autoAdvance = element.getAttribute('data-playlist-auto-advance');
|
|
141
|
+
if (autoAdvance !== null) {
|
|
142
|
+
this.options.autoAdvance = autoAdvance === 'true';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// data-playlist-auto-play-first
|
|
146
|
+
const autoPlayFirst = element.getAttribute('data-playlist-auto-play-first');
|
|
147
|
+
if (autoPlayFirst !== null) {
|
|
148
|
+
this.options.autoPlayFirst = autoPlayFirst === 'true';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// data-playlist-loop
|
|
152
|
+
const loop = element.getAttribute('data-playlist-loop');
|
|
153
|
+
if (loop !== null) {
|
|
154
|
+
this.options.loop = loop === 'true';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// data-playlist-show-panel
|
|
158
|
+
const showPanel = element.getAttribute('data-playlist-show-panel');
|
|
159
|
+
if (showPanel !== null) {
|
|
160
|
+
this.options.showPanel = showPanel === 'true';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('VidPly Playlist: Options from attributes:', this.options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update player controls to add playlist navigation buttons
|
|
168
|
+
*/
|
|
169
|
+
updatePlayerControls() {
|
|
170
|
+
if (!this.player.controlBar) return;
|
|
171
|
+
|
|
172
|
+
const controlBar = this.player.controlBar;
|
|
173
|
+
|
|
174
|
+
// Clear existing controls content (except the element itself)
|
|
175
|
+
controlBar.element.innerHTML = '';
|
|
176
|
+
|
|
177
|
+
// Recreate controls with playlist buttons now available
|
|
178
|
+
controlBar.createControls();
|
|
179
|
+
|
|
180
|
+
// Reattach events for the new controls
|
|
181
|
+
controlBar.attachEvents();
|
|
182
|
+
controlBar.setupAutoHide();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Load a playlist
|
|
187
|
+
* @param {Array} tracks - Array of track objects
|
|
188
|
+
*/
|
|
189
|
+
loadPlaylist(tracks) {
|
|
190
|
+
this.tracks = tracks;
|
|
191
|
+
this.currentIndex = -1;
|
|
192
|
+
|
|
193
|
+
// Add playlist class to container
|
|
194
|
+
if (this.container) {
|
|
195
|
+
this.container.classList.add('vidply-has-playlist');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Update UI
|
|
199
|
+
if (this.playlistPanel) {
|
|
200
|
+
this.renderPlaylist();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Auto-play first track (if enabled)
|
|
204
|
+
if (tracks.length > 0) {
|
|
205
|
+
if (this.options.autoPlayFirst) {
|
|
206
|
+
this.play(0);
|
|
207
|
+
} else {
|
|
208
|
+
// Load first track without playing
|
|
209
|
+
this.loadTrack(0);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Update visibility based on current state
|
|
214
|
+
this.updatePlaylistVisibilityInFullscreen();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load a track without playing
|
|
219
|
+
* @param {number} index - Track index
|
|
220
|
+
*/
|
|
221
|
+
loadTrack(index) {
|
|
222
|
+
if (index < 0 || index >= this.tracks.length) {
|
|
223
|
+
console.warn('VidPly Playlist: Invalid track index', index);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const track = this.tracks[index];
|
|
228
|
+
|
|
229
|
+
// Set guard flag to prevent cascade of next() calls during track change
|
|
230
|
+
this.isChangingTrack = true;
|
|
231
|
+
|
|
232
|
+
// Update current index
|
|
233
|
+
this.currentIndex = index;
|
|
234
|
+
|
|
235
|
+
// Load track into player
|
|
236
|
+
this.player.load({
|
|
237
|
+
src: track.src,
|
|
238
|
+
type: track.type,
|
|
239
|
+
poster: track.poster,
|
|
240
|
+
tracks: track.tracks || [],
|
|
241
|
+
audioDescriptionSrc: track.audioDescriptionSrc || null,
|
|
242
|
+
signLanguageSrc: track.signLanguageSrc || null
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Update UI
|
|
246
|
+
this.updateTrackInfo(track);
|
|
247
|
+
this.updatePlaylistUI();
|
|
248
|
+
|
|
249
|
+
// Emit event
|
|
250
|
+
this.player.emit('playlisttrackchange', {
|
|
251
|
+
index: index,
|
|
252
|
+
item: track,
|
|
253
|
+
total: this.tracks.length
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Clear guard flag after a short delay to ensure track is loaded
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
this.isChangingTrack = false;
|
|
259
|
+
}, 150);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Play a specific track
|
|
264
|
+
* @param {number} index - Track index
|
|
265
|
+
* @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
|
|
266
|
+
*/
|
|
267
|
+
play(index, userInitiated = false) {
|
|
268
|
+
if (index < 0 || index >= this.tracks.length) {
|
|
269
|
+
console.warn('VidPly Playlist: Invalid track index', index);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const track = this.tracks[index];
|
|
274
|
+
|
|
275
|
+
// Set guard flag to prevent cascade of next() calls during track change
|
|
276
|
+
this.isChangingTrack = true;
|
|
277
|
+
|
|
278
|
+
// Update current index
|
|
279
|
+
this.currentIndex = index;
|
|
280
|
+
|
|
281
|
+
// Load track into player
|
|
282
|
+
this.player.load({
|
|
283
|
+
src: track.src,
|
|
284
|
+
type: track.type,
|
|
285
|
+
poster: track.poster,
|
|
286
|
+
tracks: track.tracks || [],
|
|
287
|
+
audioDescriptionSrc: track.audioDescriptionSrc || null,
|
|
288
|
+
signLanguageSrc: track.signLanguageSrc || null
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Update UI
|
|
292
|
+
this.updateTrackInfo(track);
|
|
293
|
+
this.updatePlaylistUI();
|
|
294
|
+
|
|
295
|
+
// Emit event
|
|
296
|
+
this.player.emit('playlisttrackchange', {
|
|
297
|
+
index: index,
|
|
298
|
+
item: track,
|
|
299
|
+
total: this.tracks.length
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Auto-play and clear guard flag after playback starts
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
this.player.play();
|
|
305
|
+
// Clear guard flag after a short delay to ensure track has started
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
this.isChangingTrack = false;
|
|
308
|
+
}, 50);
|
|
309
|
+
}, 100);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Play next track
|
|
314
|
+
*/
|
|
315
|
+
next() {
|
|
316
|
+
let nextIndex = this.currentIndex + 1;
|
|
317
|
+
|
|
318
|
+
if (nextIndex >= this.tracks.length) {
|
|
319
|
+
if (this.options.loop) {
|
|
320
|
+
nextIndex = 0;
|
|
321
|
+
} else {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.play(nextIndex);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Play previous track
|
|
331
|
+
*/
|
|
332
|
+
previous() {
|
|
333
|
+
let prevIndex = this.currentIndex - 1;
|
|
334
|
+
|
|
335
|
+
if (prevIndex < 0) {
|
|
336
|
+
if (this.options.loop) {
|
|
337
|
+
prevIndex = this.tracks.length - 1;
|
|
338
|
+
} else {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.play(prevIndex);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Handle track end
|
|
348
|
+
*/
|
|
349
|
+
handleTrackEnd() {
|
|
350
|
+
// Don't auto-advance if we're already in the process of changing tracks
|
|
351
|
+
// This prevents a cascade of next() calls when loading a new track triggers an 'ended' event
|
|
352
|
+
if (this.isChangingTrack) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (this.options.autoAdvance) {
|
|
357
|
+
this.next();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Handle track error
|
|
363
|
+
*/
|
|
364
|
+
handleTrackError(e) {
|
|
365
|
+
console.error('VidPly Playlist: Track error', e);
|
|
366
|
+
|
|
367
|
+
// Try next track
|
|
368
|
+
if (this.options.autoAdvance) {
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
this.next();
|
|
371
|
+
}, 1000);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Handle playback state changes (for fullscreen playlist visibility)
|
|
377
|
+
*/
|
|
378
|
+
handlePlaybackStateChange() {
|
|
379
|
+
this.updatePlaylistVisibilityInFullscreen();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle fullscreen state changes
|
|
384
|
+
*/
|
|
385
|
+
handleFullscreenChange() {
|
|
386
|
+
// Use a small delay to ensure fullscreen state is fully applied
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
this.updatePlaylistVisibilityInFullscreen();
|
|
389
|
+
}, 50);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Handle audio description state changes
|
|
394
|
+
* Updates duration displays to show audio-described version duration when AD is enabled
|
|
395
|
+
*/
|
|
396
|
+
handleAudioDescriptionChange() {
|
|
397
|
+
const currentTrack = this.getCurrentTrack();
|
|
398
|
+
if (!currentTrack) return;
|
|
399
|
+
|
|
400
|
+
// Update the track info display with the appropriate duration
|
|
401
|
+
this.updateTrackInfo(currentTrack);
|
|
402
|
+
|
|
403
|
+
// Update the playlist UI to reflect duration changes (aria-labels)
|
|
404
|
+
this.updatePlaylistUI();
|
|
405
|
+
|
|
406
|
+
// Update visual duration elements in playlist panel
|
|
407
|
+
this.updatePlaylistDurations();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Update the visual duration displays in the playlist panel
|
|
412
|
+
* Called when audio description state changes
|
|
413
|
+
*/
|
|
414
|
+
updatePlaylistDurations() {
|
|
415
|
+
if (!this.playlistPanel) return;
|
|
416
|
+
|
|
417
|
+
const items = this.playlistPanel.querySelectorAll('.vidply-playlist-item');
|
|
418
|
+
|
|
419
|
+
items.forEach((item, index) => {
|
|
420
|
+
const track = this.tracks[index];
|
|
421
|
+
if (!track) return;
|
|
422
|
+
|
|
423
|
+
const effectiveDuration = this.getEffectiveDuration(track);
|
|
424
|
+
const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : '';
|
|
425
|
+
|
|
426
|
+
// Update duration badge on thumbnail (if exists)
|
|
427
|
+
const durationBadge = item.querySelector('.vidply-playlist-duration-badge');
|
|
428
|
+
if (durationBadge) {
|
|
429
|
+
durationBadge.textContent = trackDuration;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Update inline duration (if exists)
|
|
433
|
+
const inlineDuration = item.querySelector('.vidply-playlist-item-duration');
|
|
434
|
+
if (inlineDuration) {
|
|
435
|
+
inlineDuration.textContent = trackDuration;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get the effective duration for a track based on audio description state
|
|
442
|
+
* @param {Object} track - Track object
|
|
443
|
+
* @returns {number|null} - Duration in seconds or null if not available
|
|
444
|
+
*/
|
|
445
|
+
getEffectiveDuration(track) {
|
|
446
|
+
if (!track) return null;
|
|
447
|
+
|
|
448
|
+
const isAudioDescriptionEnabled = this.player.state.audioDescriptionEnabled;
|
|
449
|
+
|
|
450
|
+
// If audio description is enabled and track has audioDescriptionDuration, use it
|
|
451
|
+
if (isAudioDescriptionEnabled && track.audioDescriptionDuration) {
|
|
452
|
+
return track.audioDescriptionDuration;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Otherwise use regular duration
|
|
456
|
+
return track.duration || null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Update playlist visibility based on fullscreen and playback state
|
|
461
|
+
* In fullscreen: show when paused/not started, hide when playing
|
|
462
|
+
* Outside fullscreen: respect original panel visibility setting
|
|
463
|
+
*/
|
|
464
|
+
updatePlaylistVisibilityInFullscreen() {
|
|
465
|
+
if (!this.playlistPanel || !this.tracks.length) return;
|
|
466
|
+
|
|
467
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
468
|
+
const isPlaying = this.player.state.playing;
|
|
469
|
+
|
|
470
|
+
if (isFullscreen) {
|
|
471
|
+
// In fullscreen: show only when not playing (paused or not started)
|
|
472
|
+
// Check playing state explicitly since paused might not be set initially
|
|
473
|
+
if (!isPlaying) {
|
|
474
|
+
this.playlistPanel.classList.add('vidply-playlist-fullscreen-visible');
|
|
475
|
+
this.playlistPanel.style.display = 'block';
|
|
476
|
+
} else {
|
|
477
|
+
this.playlistPanel.classList.remove('vidply-playlist-fullscreen-visible');
|
|
478
|
+
// Add a smooth fade out with delay to match CSS transition
|
|
479
|
+
setTimeout(() => {
|
|
480
|
+
// Double-check state hasn't changed before hiding
|
|
481
|
+
if (this.player.state.playing && this.player.state.fullscreen) {
|
|
482
|
+
this.playlistPanel.style.display = 'none';
|
|
483
|
+
}
|
|
484
|
+
}, 300); // Match CSS transition duration
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
// Outside fullscreen: restore original behavior
|
|
488
|
+
this.playlistPanel.classList.remove('vidply-playlist-fullscreen-visible');
|
|
489
|
+
if (this.isPanelVisible && this.tracks.length > 0) {
|
|
490
|
+
this.playlistPanel.style.display = 'block';
|
|
491
|
+
} else {
|
|
492
|
+
this.playlistPanel.style.display = 'none';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create playlist UI
|
|
499
|
+
*/
|
|
500
|
+
createUI() {
|
|
501
|
+
// Find player container
|
|
502
|
+
this.container = this.player.container;
|
|
503
|
+
|
|
504
|
+
if (!this.container) {
|
|
505
|
+
console.warn('VidPly Playlist: No container found');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Create track artwork element (shows album art/poster for audio playlists)
|
|
510
|
+
// Only create for audio players
|
|
511
|
+
if (this.player.element.tagName === 'AUDIO') {
|
|
512
|
+
this.trackArtworkElement = DOMUtils.createElement('div', {
|
|
513
|
+
className: 'vidply-track-artwork',
|
|
514
|
+
attributes: {
|
|
515
|
+
'aria-hidden': 'true'
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
this.trackArtworkElement.style.display = 'none';
|
|
519
|
+
|
|
520
|
+
// Insert before video wrapper
|
|
521
|
+
const videoWrapper = this.container.querySelector('.vidply-video-wrapper');
|
|
522
|
+
if (videoWrapper) {
|
|
523
|
+
this.container.insertBefore(this.trackArtworkElement, videoWrapper);
|
|
524
|
+
} else {
|
|
525
|
+
this.container.appendChild(this.trackArtworkElement);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Create track info element (shows current track)
|
|
530
|
+
this.trackInfoElement = DOMUtils.createElement('div', {
|
|
531
|
+
className: 'vidply-track-info',
|
|
532
|
+
attributes: {
|
|
533
|
+
role: 'status'
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
this.trackInfoElement.style.display = 'none';
|
|
537
|
+
|
|
538
|
+
this.container.appendChild(this.trackInfoElement);
|
|
539
|
+
|
|
540
|
+
// Create navigation feedback live region
|
|
541
|
+
this.navigationFeedback = DOMUtils.createElement('div', {
|
|
542
|
+
className: 'vidply-sr-only',
|
|
543
|
+
attributes: {
|
|
544
|
+
role: 'status',
|
|
545
|
+
'aria-live': 'polite',
|
|
546
|
+
'aria-atomic': 'true'
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
this.container.appendChild(this.navigationFeedback);
|
|
550
|
+
|
|
551
|
+
// Create playlist panel with proper landmark
|
|
552
|
+
this.playlistPanel = DOMUtils.createElement('div', {
|
|
553
|
+
className: 'vidply-playlist-panel',
|
|
554
|
+
attributes: {
|
|
555
|
+
id: `${this.uniqueId}-panel`,
|
|
556
|
+
role: 'region',
|
|
557
|
+
'aria-label': i18n.t('playlist.title'),
|
|
558
|
+
'aria-labelledby': `${this.uniqueId}-heading`
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
this.playlistPanel.style.display = this.isPanelVisible ? 'none' : 'none'; // Will be shown when playlist is loaded
|
|
562
|
+
|
|
563
|
+
this.container.appendChild(this.playlistPanel);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Update track info display
|
|
568
|
+
*/
|
|
569
|
+
updateTrackInfo(track) {
|
|
570
|
+
if (!this.trackInfoElement) return;
|
|
571
|
+
|
|
572
|
+
const trackNumber = this.currentIndex + 1;
|
|
573
|
+
const totalTracks = this.tracks.length;
|
|
574
|
+
const trackTitle = track.title || i18n.t('playlist.untitled');
|
|
575
|
+
const trackArtist = track.artist || '';
|
|
576
|
+
|
|
577
|
+
// Use effective duration (audio description duration when AD is enabled)
|
|
578
|
+
const effectiveDuration = this.getEffectiveDuration(track);
|
|
579
|
+
const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : '';
|
|
580
|
+
const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : '';
|
|
581
|
+
|
|
582
|
+
// Screen reader announcement - include duration if available
|
|
583
|
+
const artistPart = trackArtist ? i18n.t('playlist.by') + trackArtist : '';
|
|
584
|
+
const durationPart = trackDurationReadable ? `. ${trackDurationReadable}` : '';
|
|
585
|
+
const announcement = i18n.t('playlist.nowPlaying', {
|
|
586
|
+
current: trackNumber,
|
|
587
|
+
total: totalTracks,
|
|
588
|
+
title: trackTitle,
|
|
589
|
+
artist: artistPart
|
|
590
|
+
}) + durationPart;
|
|
591
|
+
|
|
592
|
+
const trackOfText = i18n.t('playlist.trackOf', {
|
|
593
|
+
current: trackNumber,
|
|
594
|
+
total: totalTracks
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Build duration HTML if available
|
|
598
|
+
const durationHtml = trackDuration
|
|
599
|
+
? `<span class="vidply-track-duration" aria-hidden="true">${DOMUtils.escapeHTML(trackDuration)}</span>`
|
|
600
|
+
: '';
|
|
601
|
+
|
|
602
|
+
// Get description if available
|
|
603
|
+
const trackDescription = track.description || '';
|
|
604
|
+
|
|
605
|
+
this.trackInfoElement.innerHTML = `
|
|
606
|
+
<span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
|
|
607
|
+
<div class="vidply-track-header" aria-hidden="true">
|
|
608
|
+
<span class="vidply-track-number">${DOMUtils.escapeHTML(trackOfText)}</span>
|
|
609
|
+
${durationHtml}
|
|
610
|
+
</div>
|
|
611
|
+
<div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
|
|
612
|
+
${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ''}
|
|
613
|
+
${trackDescription ? `<div class="vidply-track-description" aria-hidden="true">${DOMUtils.escapeHTML(trackDescription)}</div>` : ''}
|
|
614
|
+
`;
|
|
615
|
+
|
|
616
|
+
this.trackInfoElement.style.display = 'block';
|
|
617
|
+
|
|
618
|
+
// Update track artwork if available (for audio playlists)
|
|
619
|
+
this.updateTrackArtwork(track);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Update track artwork display (for audio playlists)
|
|
624
|
+
*/
|
|
625
|
+
updateTrackArtwork(track) {
|
|
626
|
+
if (!this.trackArtworkElement) return;
|
|
627
|
+
|
|
628
|
+
// If track has a poster/artwork, show it
|
|
629
|
+
if (track.poster) {
|
|
630
|
+
this.trackArtworkElement.style.backgroundImage = `url(${track.poster})`;
|
|
631
|
+
this.trackArtworkElement.style.display = 'block';
|
|
632
|
+
} else {
|
|
633
|
+
// No artwork available, hide the element
|
|
634
|
+
this.trackArtworkElement.style.display = 'none';
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Render playlist
|
|
640
|
+
*/
|
|
641
|
+
renderPlaylist() {
|
|
642
|
+
if (!this.playlistPanel) return;
|
|
643
|
+
|
|
644
|
+
// Clear existing
|
|
645
|
+
this.playlistPanel.innerHTML = '';
|
|
646
|
+
|
|
647
|
+
// Create header
|
|
648
|
+
const header = DOMUtils.createElement('h2', {
|
|
649
|
+
className: 'vidply-playlist-header',
|
|
650
|
+
attributes: {
|
|
651
|
+
id: `${this.uniqueId}-heading`
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
header.textContent = `${i18n.t('playlist.title')} (${this.tracks.length})`;
|
|
655
|
+
this.playlistPanel.appendChild(header);
|
|
656
|
+
|
|
657
|
+
// Add keyboard instructions (visually hidden)
|
|
658
|
+
const instructions = DOMUtils.createElement('div', {
|
|
659
|
+
className: 'vidply-sr-only',
|
|
660
|
+
attributes: {
|
|
661
|
+
id: `${this.uniqueId}-keyboard-instructions`
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
instructions.textContent = 'Playlist navigation: Use Up and Down arrow keys to move between tracks. Press Page Up or Page Down to skip 5 tracks. Press Home to go to first track, End to go to last track. Press Enter or Space to play the selected track.';
|
|
665
|
+
this.playlistPanel.appendChild(instructions);
|
|
666
|
+
|
|
667
|
+
// Create list (proper ul element)
|
|
668
|
+
const list = DOMUtils.createElement('ul', {
|
|
669
|
+
className: 'vidply-playlist-list',
|
|
670
|
+
attributes: {
|
|
671
|
+
role: 'listbox',
|
|
672
|
+
'aria-labelledby': `${this.uniqueId}-heading`,
|
|
673
|
+
'aria-describedby': `${this.uniqueId}-keyboard-instructions`
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
this.tracks.forEach((track, index) => {
|
|
678
|
+
const item = this.createPlaylistItem(track, index);
|
|
679
|
+
list.appendChild(item);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
this.playlistPanel.appendChild(list);
|
|
683
|
+
|
|
684
|
+
// Show panel if it should be visible
|
|
685
|
+
if (this.isPanelVisible) {
|
|
686
|
+
this.playlistPanel.style.display = 'block';
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Create playlist item element
|
|
692
|
+
*/
|
|
693
|
+
createPlaylistItem(track, index) {
|
|
694
|
+
const trackPosition = i18n.t('playlist.trackOf', {
|
|
695
|
+
current: index + 1,
|
|
696
|
+
total: this.tracks.length
|
|
697
|
+
});
|
|
698
|
+
const trackTitle = track.title || i18n.t('playlist.trackUntitled', { number: index + 1 });
|
|
699
|
+
const trackArtist = track.artist ? i18n.t('playlist.by') + track.artist : '';
|
|
700
|
+
|
|
701
|
+
// Use effective duration (audio description duration when AD is enabled)
|
|
702
|
+
const effectiveDuration = this.getEffectiveDuration(track);
|
|
703
|
+
const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : '';
|
|
704
|
+
const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : '';
|
|
705
|
+
const isActive = index === this.currentIndex;
|
|
706
|
+
|
|
707
|
+
// Build accessible label for screen readers
|
|
708
|
+
// With role="option" and aria-checked, screen reader will announce selection state
|
|
709
|
+
// Position is already announced via aria-posinset/aria-setsize
|
|
710
|
+
// Format: "Title by Artist. 3 minutes, 45 seconds."
|
|
711
|
+
let ariaLabel = `${trackTitle}${trackArtist}`;
|
|
712
|
+
if (trackDurationReadable) {
|
|
713
|
+
ariaLabel += `. ${trackDurationReadable}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Create list item container (semantic HTML)
|
|
717
|
+
const item = DOMUtils.createElement('li', {
|
|
718
|
+
className: isActive ? 'vidply-playlist-item vidply-playlist-item-active' : 'vidply-playlist-item',
|
|
719
|
+
attributes: {
|
|
720
|
+
'data-playlist-index': index
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Create button wrapper for interactive content
|
|
725
|
+
const button = DOMUtils.createElement('button', {
|
|
726
|
+
className: 'vidply-playlist-item-button',
|
|
727
|
+
attributes: {
|
|
728
|
+
type: 'button',
|
|
729
|
+
role: 'option',
|
|
730
|
+
tabIndex: index === 0 ? 0 : -1, // Only first item is in tab order initially
|
|
731
|
+
'aria-label': ariaLabel,
|
|
732
|
+
'aria-posinset': index + 1,
|
|
733
|
+
'aria-setsize': this.tracks.length,
|
|
734
|
+
'aria-checked': isActive ? 'true' : 'false'
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Add aria-current if active
|
|
739
|
+
if (isActive) {
|
|
740
|
+
button.setAttribute('aria-current', 'true');
|
|
741
|
+
button.setAttribute('tabIndex', '0'); // Active item should always be tabbable
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Thumbnail container with optional duration badge
|
|
745
|
+
const thumbnailContainer = DOMUtils.createElement('span', {
|
|
746
|
+
className: 'vidply-playlist-thumbnail-container',
|
|
747
|
+
attributes: {
|
|
748
|
+
'aria-hidden': 'true'
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Thumbnail or icon
|
|
753
|
+
const thumbnail = DOMUtils.createElement('span', {
|
|
754
|
+
className: 'vidply-playlist-thumbnail'
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
if (track.poster) {
|
|
758
|
+
thumbnail.style.backgroundImage = `url(${track.poster})`;
|
|
759
|
+
} else {
|
|
760
|
+
// Show music/speaker icon for audio tracks
|
|
761
|
+
const icon = createIconElement('music');
|
|
762
|
+
icon.classList.add('vidply-playlist-thumbnail-icon');
|
|
763
|
+
thumbnail.appendChild(icon);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
thumbnailContainer.appendChild(thumbnail);
|
|
767
|
+
|
|
768
|
+
// Duration badge on thumbnail (like YouTube) - only show if there's a poster
|
|
769
|
+
if (trackDuration && track.poster) {
|
|
770
|
+
const durationBadge = DOMUtils.createElement('span', {
|
|
771
|
+
className: 'vidply-playlist-duration-badge'
|
|
772
|
+
});
|
|
773
|
+
durationBadge.textContent = trackDuration;
|
|
774
|
+
thumbnailContainer.appendChild(durationBadge);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
button.appendChild(thumbnailContainer);
|
|
778
|
+
|
|
779
|
+
// Info section (title, artist, description)
|
|
780
|
+
const info = DOMUtils.createElement('span', {
|
|
781
|
+
className: 'vidply-playlist-item-info',
|
|
782
|
+
attributes: {
|
|
783
|
+
'aria-hidden': 'true'
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Title row with optional inline duration (for when no thumbnail)
|
|
788
|
+
const titleRow = DOMUtils.createElement('span', {
|
|
789
|
+
className: 'vidply-playlist-item-title-row'
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const title = DOMUtils.createElement('span', {
|
|
793
|
+
className: 'vidply-playlist-item-title'
|
|
794
|
+
});
|
|
795
|
+
title.textContent = trackTitle;
|
|
796
|
+
titleRow.appendChild(title);
|
|
797
|
+
|
|
798
|
+
// Inline duration (shown when no poster/thumbnail)
|
|
799
|
+
if (trackDuration && !track.poster) {
|
|
800
|
+
const inlineDuration = DOMUtils.createElement('span', {
|
|
801
|
+
className: 'vidply-playlist-item-duration'
|
|
802
|
+
});
|
|
803
|
+
inlineDuration.textContent = trackDuration;
|
|
804
|
+
titleRow.appendChild(inlineDuration);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
info.appendChild(titleRow);
|
|
808
|
+
|
|
809
|
+
// Artist
|
|
810
|
+
if (track.artist) {
|
|
811
|
+
const artist = DOMUtils.createElement('span', {
|
|
812
|
+
className: 'vidply-playlist-item-artist'
|
|
813
|
+
});
|
|
814
|
+
artist.textContent = track.artist;
|
|
815
|
+
info.appendChild(artist);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Description (truncated)
|
|
819
|
+
if (track.description) {
|
|
820
|
+
const description = DOMUtils.createElement('span', {
|
|
821
|
+
className: 'vidply-playlist-item-description'
|
|
822
|
+
});
|
|
823
|
+
description.textContent = track.description;
|
|
824
|
+
info.appendChild(description);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
button.appendChild(info);
|
|
828
|
+
|
|
829
|
+
// Play icon
|
|
830
|
+
const playIcon = createIconElement('play');
|
|
831
|
+
playIcon.classList.add('vidply-playlist-item-icon');
|
|
832
|
+
playIcon.setAttribute('aria-hidden', 'true');
|
|
833
|
+
button.appendChild(playIcon);
|
|
834
|
+
|
|
835
|
+
// Click handler
|
|
836
|
+
button.addEventListener('click', () => {
|
|
837
|
+
this.play(index, true); // User-initiated
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Keyboard handler
|
|
841
|
+
button.addEventListener('keydown', (e) => {
|
|
842
|
+
this.handlePlaylistItemKeydown(e, index);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// Append button to list item
|
|
846
|
+
item.appendChild(button);
|
|
847
|
+
|
|
848
|
+
return item;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Handle keyboard navigation in playlist items
|
|
853
|
+
*/
|
|
854
|
+
handlePlaylistItemKeydown(e, index) {
|
|
855
|
+
const buttons = Array.from(this.playlistPanel.querySelectorAll('.vidply-playlist-item-button'));
|
|
856
|
+
let newIndex = -1;
|
|
857
|
+
let announcement = '';
|
|
858
|
+
|
|
859
|
+
switch(e.key) {
|
|
860
|
+
case 'Enter':
|
|
861
|
+
case ' ':
|
|
862
|
+
e.preventDefault();
|
|
863
|
+
e.stopPropagation();
|
|
864
|
+
this.play(index, true); // User-initiated
|
|
865
|
+
return; // No need to move focus
|
|
866
|
+
|
|
867
|
+
case 'ArrowDown':
|
|
868
|
+
e.preventDefault();
|
|
869
|
+
e.stopPropagation();
|
|
870
|
+
// Move to next item
|
|
871
|
+
if (index < buttons.length - 1) {
|
|
872
|
+
newIndex = index + 1;
|
|
873
|
+
} else {
|
|
874
|
+
// At the end, announce boundary
|
|
875
|
+
announcement = `End of playlist. ${buttons.length} of ${buttons.length}.`;
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
|
|
879
|
+
case 'ArrowUp':
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
e.stopPropagation();
|
|
882
|
+
// Move to previous item
|
|
883
|
+
if (index > 0) {
|
|
884
|
+
newIndex = index - 1;
|
|
885
|
+
} else {
|
|
886
|
+
// At the beginning, announce boundary
|
|
887
|
+
announcement = 'Beginning of playlist. 1 of ' + buttons.length + '.';
|
|
888
|
+
}
|
|
889
|
+
break;
|
|
890
|
+
|
|
891
|
+
case 'PageDown':
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
e.stopPropagation();
|
|
894
|
+
// Move 5 items down (or to end)
|
|
895
|
+
newIndex = Math.min(index + 5, buttons.length - 1);
|
|
896
|
+
if (newIndex === buttons.length - 1 && index !== newIndex) {
|
|
897
|
+
announcement = `Jumped to last track. ${newIndex + 1} of ${buttons.length}.`;
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
|
|
901
|
+
case 'PageUp':
|
|
902
|
+
e.preventDefault();
|
|
903
|
+
e.stopPropagation();
|
|
904
|
+
// Move 5 items up (or to beginning)
|
|
905
|
+
newIndex = Math.max(index - 5, 0);
|
|
906
|
+
if (newIndex === 0 && index !== newIndex) {
|
|
907
|
+
announcement = `Jumped to first track. 1 of ${buttons.length}.`;
|
|
908
|
+
}
|
|
909
|
+
break;
|
|
910
|
+
|
|
911
|
+
case 'Home':
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
e.stopPropagation();
|
|
914
|
+
// Move to first item
|
|
915
|
+
newIndex = 0;
|
|
916
|
+
if (index !== 0) {
|
|
917
|
+
announcement = `First track. 1 of ${buttons.length}.`;
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
920
|
+
|
|
921
|
+
case 'End':
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
e.stopPropagation();
|
|
924
|
+
// Move to last item
|
|
925
|
+
newIndex = buttons.length - 1;
|
|
926
|
+
if (index !== buttons.length - 1) {
|
|
927
|
+
announcement = `Last track. ${buttons.length} of ${buttons.length}.`;
|
|
928
|
+
}
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Update tab indices for roving tabindex pattern
|
|
933
|
+
if (newIndex !== -1 && newIndex !== index) {
|
|
934
|
+
buttons[index].setAttribute('tabIndex', '-1');
|
|
935
|
+
buttons[newIndex].setAttribute('tabIndex', '0');
|
|
936
|
+
buttons[newIndex].focus({ preventScroll: false });
|
|
937
|
+
|
|
938
|
+
// Scroll the focused item into view (same behavior as mouse interaction)
|
|
939
|
+
const item = buttons[newIndex].closest('.vidply-playlist-item');
|
|
940
|
+
if (item) {
|
|
941
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Announce navigation feedback
|
|
946
|
+
if (announcement && this.navigationFeedback) {
|
|
947
|
+
this.navigationFeedback.textContent = announcement;
|
|
948
|
+
// Clear after a short delay to allow for repeated announcements
|
|
949
|
+
setTimeout(() => {
|
|
950
|
+
if (this.navigationFeedback) {
|
|
951
|
+
this.navigationFeedback.textContent = '';
|
|
952
|
+
}
|
|
953
|
+
}, 1000);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Update playlist UI (highlight current track)
|
|
959
|
+
*/
|
|
960
|
+
updatePlaylistUI() {
|
|
961
|
+
if (!this.playlistPanel) return;
|
|
962
|
+
|
|
963
|
+
const items = this.playlistPanel.querySelectorAll('.vidply-playlist-item');
|
|
964
|
+
const buttons = this.playlistPanel.querySelectorAll('.vidply-playlist-item-button');
|
|
965
|
+
|
|
966
|
+
items.forEach((item, index) => {
|
|
967
|
+
const button = buttons[index];
|
|
968
|
+
if (!button) return;
|
|
969
|
+
|
|
970
|
+
const track = this.tracks[index];
|
|
971
|
+
const trackPosition = i18n.t('playlist.trackOf', {
|
|
972
|
+
current: index + 1,
|
|
973
|
+
total: this.tracks.length
|
|
974
|
+
});
|
|
975
|
+
const trackTitle = track.title || i18n.t('playlist.trackUntitled', { number: index + 1 });
|
|
976
|
+
const trackArtist = track.artist ? i18n.t('playlist.by') + track.artist : '';
|
|
977
|
+
|
|
978
|
+
// Use effective duration (audio description duration when AD is enabled)
|
|
979
|
+
const effectiveDuration = this.getEffectiveDuration(track);
|
|
980
|
+
const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : '';
|
|
981
|
+
|
|
982
|
+
if (index === this.currentIndex) {
|
|
983
|
+
// Update list item styling
|
|
984
|
+
item.classList.add('vidply-playlist-item-active');
|
|
985
|
+
|
|
986
|
+
// Update button ARIA attributes
|
|
987
|
+
button.setAttribute('aria-current', 'true');
|
|
988
|
+
button.setAttribute('aria-checked', 'true');
|
|
989
|
+
button.setAttribute('tabIndex', '0'); // Active item should be tabbable
|
|
990
|
+
|
|
991
|
+
// Simplified aria-label - status and actions are announced via ARIA roles
|
|
992
|
+
let ariaLabel = `${trackTitle}${trackArtist}`;
|
|
993
|
+
if (trackDurationReadable) {
|
|
994
|
+
ariaLabel += `. ${trackDurationReadable}`;
|
|
995
|
+
}
|
|
996
|
+
button.setAttribute('aria-label', ariaLabel);
|
|
997
|
+
|
|
998
|
+
// Scroll into view within playlist panel (uses 'nearest' to minimize page scroll)
|
|
999
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1000
|
+
} else {
|
|
1001
|
+
// Update list item styling
|
|
1002
|
+
item.classList.remove('vidply-playlist-item-active');
|
|
1003
|
+
|
|
1004
|
+
// Update button ARIA attributes
|
|
1005
|
+
button.removeAttribute('aria-current');
|
|
1006
|
+
button.setAttribute('aria-checked', 'false');
|
|
1007
|
+
button.setAttribute('tabIndex', '-1'); // Remove from tab order (use arrow keys)
|
|
1008
|
+
|
|
1009
|
+
// Simplified aria-label - status and actions are announced via ARIA roles
|
|
1010
|
+
let ariaLabel = `${trackTitle}${trackArtist}`;
|
|
1011
|
+
if (trackDurationReadable) {
|
|
1012
|
+
ariaLabel += `. ${trackDurationReadable}`;
|
|
1013
|
+
}
|
|
1014
|
+
button.setAttribute('aria-label', ariaLabel);
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Get current track
|
|
1021
|
+
*/
|
|
1022
|
+
getCurrentTrack() {
|
|
1023
|
+
return this.tracks[this.currentIndex] || null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Get playlist info
|
|
1028
|
+
*/
|
|
1029
|
+
getPlaylistInfo() {
|
|
1030
|
+
return {
|
|
1031
|
+
currentIndex: this.currentIndex,
|
|
1032
|
+
totalTracks: this.tracks.length,
|
|
1033
|
+
currentTrack: this.getCurrentTrack(),
|
|
1034
|
+
hasNext: this.hasNext(),
|
|
1035
|
+
hasPrevious: this.hasPrevious()
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Check if there is a next track
|
|
1041
|
+
*/
|
|
1042
|
+
hasNext() {
|
|
1043
|
+
if (this.options.loop) return true;
|
|
1044
|
+
return this.currentIndex < this.tracks.length - 1;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Check if there is a previous track
|
|
1049
|
+
*/
|
|
1050
|
+
hasPrevious() {
|
|
1051
|
+
if (this.options.loop) return true;
|
|
1052
|
+
return this.currentIndex > 0;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Add track to playlist
|
|
1057
|
+
*/
|
|
1058
|
+
addTrack(track) {
|
|
1059
|
+
this.tracks.push(track);
|
|
1060
|
+
|
|
1061
|
+
if (this.playlistPanel) {
|
|
1062
|
+
this.renderPlaylist();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Remove track from playlist
|
|
1068
|
+
*/
|
|
1069
|
+
removeTrack(index) {
|
|
1070
|
+
if (index < 0 || index >= this.tracks.length) return;
|
|
1071
|
+
|
|
1072
|
+
this.tracks.splice(index, 1);
|
|
1073
|
+
|
|
1074
|
+
// Adjust current index if needed
|
|
1075
|
+
if (index < this.currentIndex) {
|
|
1076
|
+
this.currentIndex--;
|
|
1077
|
+
} else if (index === this.currentIndex) {
|
|
1078
|
+
// Current track was removed, play next or stop
|
|
1079
|
+
if (this.currentIndex >= this.tracks.length) {
|
|
1080
|
+
this.currentIndex = this.tracks.length - 1;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (this.currentIndex >= 0) {
|
|
1084
|
+
this.play(this.currentIndex);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (this.playlistPanel) {
|
|
1089
|
+
this.renderPlaylist();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Clear playlist
|
|
1095
|
+
*/
|
|
1096
|
+
clear() {
|
|
1097
|
+
this.tracks = [];
|
|
1098
|
+
this.currentIndex = -1;
|
|
1099
|
+
|
|
1100
|
+
if (this.playlistPanel) {
|
|
1101
|
+
this.playlistPanel.innerHTML = '';
|
|
1102
|
+
this.playlistPanel.style.display = 'none';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (this.trackInfoElement) {
|
|
1106
|
+
this.trackInfoElement.innerHTML = '';
|
|
1107
|
+
this.trackInfoElement.style.display = 'none';
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (this.trackArtworkElement) {
|
|
1111
|
+
this.trackArtworkElement.style.backgroundImage = '';
|
|
1112
|
+
this.trackArtworkElement.style.display = 'none';
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Toggle playlist panel visibility
|
|
1118
|
+
* @param {boolean} show - Optional: force show (true) or hide (false)
|
|
1119
|
+
* @returns {boolean} - New visibility state
|
|
1120
|
+
*/
|
|
1121
|
+
togglePanel(show) {
|
|
1122
|
+
if (!this.playlistPanel) return false;
|
|
1123
|
+
|
|
1124
|
+
// Determine new state
|
|
1125
|
+
const shouldShow = show !== undefined ? show : this.playlistPanel.style.display === 'none';
|
|
1126
|
+
|
|
1127
|
+
if (shouldShow) {
|
|
1128
|
+
this.playlistPanel.style.display = 'block';
|
|
1129
|
+
this.isPanelVisible = true;
|
|
1130
|
+
|
|
1131
|
+
// Focus first item if playlist has tracks
|
|
1132
|
+
if (this.tracks.length > 0) {
|
|
1133
|
+
setTimeout(() => {
|
|
1134
|
+
const firstItem = this.playlistPanel.querySelector('.vidply-playlist-item[tabindex="0"]');
|
|
1135
|
+
if (firstItem) {
|
|
1136
|
+
firstItem.focus({ preventScroll: true });
|
|
1137
|
+
}
|
|
1138
|
+
}, 100);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Update toggle button state if it exists
|
|
1142
|
+
if (this.player.controlBar && this.player.controlBar.controls.playlistToggle) {
|
|
1143
|
+
this.player.controlBar.controls.playlistToggle.setAttribute('aria-expanded', 'true');
|
|
1144
|
+
this.player.controlBar.controls.playlistToggle.setAttribute('aria-pressed', 'true');
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
this.playlistPanel.style.display = 'none';
|
|
1148
|
+
this.isPanelVisible = false;
|
|
1149
|
+
|
|
1150
|
+
// Update toggle button state if it exists
|
|
1151
|
+
if (this.player.controlBar && this.player.controlBar.controls.playlistToggle) {
|
|
1152
|
+
this.player.controlBar.controls.playlistToggle.setAttribute('aria-expanded', 'false');
|
|
1153
|
+
this.player.controlBar.controls.playlistToggle.setAttribute('aria-pressed', 'false');
|
|
1154
|
+
|
|
1155
|
+
// Return focus to toggle button
|
|
1156
|
+
this.player.controlBar.controls.playlistToggle.focus({ preventScroll: true });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return this.isPanelVisible;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Show playlist panel
|
|
1165
|
+
*/
|
|
1166
|
+
showPanel() {
|
|
1167
|
+
return this.togglePanel(true);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Hide playlist panel
|
|
1172
|
+
*/
|
|
1173
|
+
hidePanel() {
|
|
1174
|
+
return this.togglePanel(false);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Destroy playlist manager
|
|
1179
|
+
*/
|
|
1180
|
+
destroy() {
|
|
1181
|
+
// Remove event listeners
|
|
1182
|
+
this.player.off('ended', this.handleTrackEnd);
|
|
1183
|
+
this.player.off('error', this.handleTrackError);
|
|
1184
|
+
|
|
1185
|
+
// Remove UI
|
|
1186
|
+
if (this.trackArtworkElement) {
|
|
1187
|
+
this.trackArtworkElement.remove();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (this.trackInfoElement) {
|
|
1191
|
+
this.trackInfoElement.remove();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (this.playlistPanel) {
|
|
1195
|
+
this.playlistPanel.remove();
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Clear data
|
|
1199
|
+
this.clear();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export default PlaylistManager;
|