vidply 1.0.5 → 1.0.7
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 +22 -22
- package/README.md +608 -593
- package/dist/vidply.css +2422 -1807
- package/dist/vidply.esm.js +1480 -93
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +48 -25
- package/dist/vidply.js +1480 -93
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +48 -25
- package/package.json +2 -2
- package/src/controls/CaptionManager.js +278 -248
- package/src/controls/ControlBar.js +2033 -2026
- package/src/controls/KeyboardManager.js +233 -233
- package/src/controls/SettingsDialog.js +417 -417
- package/src/controls/TranscriptManager.js +1803 -728
- package/src/core/Player.js +1616 -1134
- package/src/i18n/i18n.js +66 -66
- package/src/i18n/translations.js +616 -561
- package/src/icons/Icons.js +187 -183
- package/src/index.js +95 -95
- package/src/renderers/HLSRenderer.js +302 -302
- package/src/renderers/HTML5Renderer.js +298 -298
- package/src/renderers/VimeoRenderer.js +257 -257
- package/src/renderers/YouTubeRenderer.js +274 -274
- package/src/styles/vidply.css +2422 -1807
- package/src/utils/DOMUtils.js +154 -154
- package/src/utils/EventEmitter.js +53 -53
- package/src/utils/StorageManager.js +156 -0
- package/src/utils/TimeUtils.js +87 -87
|
@@ -1,728 +1,1803 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcript Manager Component
|
|
3
|
-
* Manages transcript display and interaction
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
-
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
8
|
-
import { createIconElement } from '../icons/Icons.js';
|
|
9
|
-
import { i18n } from '../i18n/i18n.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
this.
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
this.
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
*
|
|
630
|
-
*/
|
|
631
|
-
|
|
632
|
-
this.
|
|
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
|
-
this.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
this.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Transcript Manager Component
|
|
3
|
+
* Manages transcript display and interaction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
+
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
8
|
+
import { createIconElement } from '../icons/Icons.js';
|
|
9
|
+
import { i18n } from '../i18n/i18n.js';
|
|
10
|
+
import { StorageManager } from '../utils/StorageManager.js';
|
|
11
|
+
|
|
12
|
+
export class TranscriptManager {
|
|
13
|
+
constructor(player) {
|
|
14
|
+
this.player = player;
|
|
15
|
+
this.transcriptWindow = null;
|
|
16
|
+
this.transcriptEntries = [];
|
|
17
|
+
this.metadataCues = [];
|
|
18
|
+
this.currentActiveEntry = null;
|
|
19
|
+
this.isVisible = false;
|
|
20
|
+
|
|
21
|
+
// Storage manager
|
|
22
|
+
this.storage = new StorageManager('vidply');
|
|
23
|
+
|
|
24
|
+
// Dragging state
|
|
25
|
+
this.isDragging = false;
|
|
26
|
+
this.dragOffsetX = 0;
|
|
27
|
+
this.dragOffsetY = 0;
|
|
28
|
+
|
|
29
|
+
// Resizing state
|
|
30
|
+
this.isResizing = false;
|
|
31
|
+
this.resizeDirection = null;
|
|
32
|
+
this.resizeStartX = 0;
|
|
33
|
+
this.resizeStartY = 0;
|
|
34
|
+
this.resizeStartWidth = 0;
|
|
35
|
+
this.resizeStartHeight = 0;
|
|
36
|
+
this.resizeEnabled = false;
|
|
37
|
+
|
|
38
|
+
// Settings menu state
|
|
39
|
+
this.settingsMenuVisible = false;
|
|
40
|
+
this.settingsMenu = null;
|
|
41
|
+
this.settingsButton = null;
|
|
42
|
+
this.settingsMenuJustOpened = false;
|
|
43
|
+
|
|
44
|
+
// Keyboard drag mode
|
|
45
|
+
this.keyboardDragMode = false;
|
|
46
|
+
|
|
47
|
+
// Style dialog state
|
|
48
|
+
this.styleDialog = null;
|
|
49
|
+
this.styleDialogVisible = false;
|
|
50
|
+
this.styleDialogJustOpened = false;
|
|
51
|
+
|
|
52
|
+
// Load saved preferences from localStorage
|
|
53
|
+
const savedPreferences = this.storage.getTranscriptPreferences();
|
|
54
|
+
|
|
55
|
+
// Transcript styling options (with defaults, then player options, then saved preferences)
|
|
56
|
+
this.transcriptStyle = {
|
|
57
|
+
fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || '100%',
|
|
58
|
+
fontFamily: savedPreferences?.fontFamily || this.player.options.transcriptFontFamily || 'sans-serif',
|
|
59
|
+
color: savedPreferences?.color || this.player.options.transcriptColor || '#ffffff',
|
|
60
|
+
backgroundColor: savedPreferences?.backgroundColor || this.player.options.transcriptBackgroundColor || '#1e1e1e',
|
|
61
|
+
opacity: savedPreferences?.opacity ?? this.player.options.transcriptOpacity ?? 0.98
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Store event handlers for cleanup
|
|
65
|
+
this.handlers = {
|
|
66
|
+
timeupdate: () => this.updateActiveEntry(),
|
|
67
|
+
resize: null,
|
|
68
|
+
mousemove: null,
|
|
69
|
+
mouseup: null,
|
|
70
|
+
touchmove: null,
|
|
71
|
+
touchend: null,
|
|
72
|
+
mousedown: null,
|
|
73
|
+
touchstart: null,
|
|
74
|
+
keydown: null,
|
|
75
|
+
settingsClick: null,
|
|
76
|
+
settingsKeydown: null,
|
|
77
|
+
documentClick: null,
|
|
78
|
+
styleDialogKeydown: null
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.init();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
init() {
|
|
85
|
+
// Listen for time updates to highlight active transcript entry
|
|
86
|
+
this.player.on('timeupdate', this.handlers.timeupdate);
|
|
87
|
+
|
|
88
|
+
// Reposition transcript when entering/exiting fullscreen
|
|
89
|
+
this.player.on('fullscreenchange', () => {
|
|
90
|
+
if (this.isVisible) {
|
|
91
|
+
// Add a small delay to ensure DOM has updated after fullscreen transition
|
|
92
|
+
setTimeout(() => this.positionTranscript(), 100);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Toggle transcript window visibility
|
|
99
|
+
*/
|
|
100
|
+
toggleTranscript() {
|
|
101
|
+
if (this.isVisible) {
|
|
102
|
+
this.hideTranscript();
|
|
103
|
+
} else {
|
|
104
|
+
this.showTranscript();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show transcript window
|
|
110
|
+
*/
|
|
111
|
+
showTranscript() {
|
|
112
|
+
if (this.transcriptWindow) {
|
|
113
|
+
this.transcriptWindow.style.display = 'flex';
|
|
114
|
+
this.isVisible = true;
|
|
115
|
+
|
|
116
|
+
// Focus the settings button for keyboard accessibility
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (this.settingsButton) {
|
|
119
|
+
this.settingsButton.focus();
|
|
120
|
+
}
|
|
121
|
+
}, 150);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create transcript window
|
|
126
|
+
this.createTranscriptWindow();
|
|
127
|
+
this.loadTranscriptData();
|
|
128
|
+
|
|
129
|
+
// Show the window
|
|
130
|
+
if (this.transcriptWindow) {
|
|
131
|
+
this.transcriptWindow.style.display = 'flex';
|
|
132
|
+
// Re-position after showing (in case window was resized while hidden)
|
|
133
|
+
setTimeout(() => this.positionTranscript(), 0);
|
|
134
|
+
|
|
135
|
+
// Focus the settings button for keyboard accessibility
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
if (this.settingsButton) {
|
|
138
|
+
this.settingsButton.focus();
|
|
139
|
+
}
|
|
140
|
+
}, 150);
|
|
141
|
+
}
|
|
142
|
+
this.isVisible = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Hide transcript window
|
|
147
|
+
*/
|
|
148
|
+
hideTranscript() {
|
|
149
|
+
if (this.transcriptWindow) {
|
|
150
|
+
this.transcriptWindow.style.display = 'none';
|
|
151
|
+
this.isVisible = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create the transcript window UI
|
|
157
|
+
*/
|
|
158
|
+
createTranscriptWindow() {
|
|
159
|
+
this.transcriptWindow = DOMUtils.createElement('div', {
|
|
160
|
+
className: `${this.player.options.classPrefix}-transcript-window`,
|
|
161
|
+
attributes: {
|
|
162
|
+
'role': 'dialog',
|
|
163
|
+
'aria-label': 'Video Transcript',
|
|
164
|
+
'tabindex': '-1'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Header (draggable)
|
|
169
|
+
this.transcriptHeader = DOMUtils.createElement('div', {
|
|
170
|
+
className: `${this.player.options.classPrefix}-transcript-header`,
|
|
171
|
+
attributes: {
|
|
172
|
+
'aria-label': 'Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.',
|
|
173
|
+
'tabindex': '0'
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Header left side (settings button + title)
|
|
178
|
+
this.headerLeft = DOMUtils.createElement('div', {
|
|
179
|
+
className: `${this.player.options.classPrefix}-transcript-header-left`
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Settings button
|
|
183
|
+
this.settingsButton = DOMUtils.createElement('button', {
|
|
184
|
+
className: `${this.player.options.classPrefix}-transcript-settings`,
|
|
185
|
+
attributes: {
|
|
186
|
+
'type': 'button',
|
|
187
|
+
'aria-label': i18n.t('transcript.settings'),
|
|
188
|
+
'aria-expanded': 'false'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
this.settingsButton.appendChild(createIconElement('settings'));
|
|
192
|
+
this.handlers.settingsClick = (e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
if (this.settingsMenuVisible) {
|
|
196
|
+
this.hideSettingsMenu();
|
|
197
|
+
} else {
|
|
198
|
+
this.showSettingsMenu();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.settingsButton.addEventListener('click', this.handlers.settingsClick);
|
|
202
|
+
|
|
203
|
+
// Keyboard handler for settings button
|
|
204
|
+
this.handlers.settingsKeydown = (e) => {
|
|
205
|
+
// D key to toggle keyboard drag mode
|
|
206
|
+
if (e.key === 'd' || e.key === 'D') {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
this.toggleKeyboardDragMode();
|
|
210
|
+
}
|
|
211
|
+
// R key to toggle resize mode
|
|
212
|
+
else if (e.key === 'r' || e.key === 'R') {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
this.toggleResizeMode();
|
|
216
|
+
}
|
|
217
|
+
// Escape to close menu if open
|
|
218
|
+
else if (e.key === 'Escape' && this.settingsMenuVisible) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
this.hideSettingsMenu();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
this.settingsButton.addEventListener('keydown', this.handlers.settingsKeydown);
|
|
225
|
+
|
|
226
|
+
const title = DOMUtils.createElement('h3', {
|
|
227
|
+
textContent: i18n.t('transcript.title')
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.headerLeft.appendChild(this.settingsButton);
|
|
231
|
+
this.headerLeft.appendChild(title);
|
|
232
|
+
|
|
233
|
+
const closeButton = DOMUtils.createElement('button', {
|
|
234
|
+
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
235
|
+
attributes: {
|
|
236
|
+
'type': 'button',
|
|
237
|
+
'aria-label': i18n.t('transcript.close')
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
closeButton.appendChild(createIconElement('close'));
|
|
241
|
+
closeButton.addEventListener('click', () => this.hideTranscript());
|
|
242
|
+
|
|
243
|
+
this.transcriptHeader.appendChild(this.headerLeft);
|
|
244
|
+
this.transcriptHeader.appendChild(closeButton);
|
|
245
|
+
|
|
246
|
+
// Content container
|
|
247
|
+
this.transcriptContent = DOMUtils.createElement('div', {
|
|
248
|
+
className: `${this.player.options.classPrefix}-transcript-content`
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.transcriptWindow.appendChild(this.transcriptHeader);
|
|
252
|
+
this.transcriptWindow.appendChild(this.transcriptContent);
|
|
253
|
+
|
|
254
|
+
// Append to player container
|
|
255
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
256
|
+
|
|
257
|
+
// Position it next to the video wrapper
|
|
258
|
+
this.positionTranscript();
|
|
259
|
+
|
|
260
|
+
// Setup drag functionality
|
|
261
|
+
this.setupDragAndDrop();
|
|
262
|
+
|
|
263
|
+
// Setup document click handler to close settings menu and style dialog
|
|
264
|
+
// DON'T add it yet - it will be added when the menu is first opened
|
|
265
|
+
this.handlers.documentClick = (e) => {
|
|
266
|
+
// Ignore if menu was just opened (prevents immediate closing)
|
|
267
|
+
if (this.settingsMenuJustOpened) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Ignore if style dialog was just opened (prevents immediate closing)
|
|
272
|
+
if (this.styleDialogJustOpened) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Ignore clicks on the settings button itself
|
|
277
|
+
if (this.settingsButton && this.settingsButton.contains(e.target)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Ignore clicks on the settings menu items
|
|
282
|
+
if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Close settings menu if clicking outside
|
|
287
|
+
if (this.settingsMenuVisible) {
|
|
288
|
+
this.hideSettingsMenu();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Close style dialog if clicking outside (but not on settings button)
|
|
292
|
+
if (this.styleDialogVisible && this.styleDialog &&
|
|
293
|
+
!this.styleDialog.contains(e.target)) {
|
|
294
|
+
this.hideStyleDialog();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// Store flag to track if handler has been added
|
|
298
|
+
this.documentClickHandlerAdded = false;
|
|
299
|
+
|
|
300
|
+
// Re-position on window resize (debounced)
|
|
301
|
+
let resizeTimeout;
|
|
302
|
+
this.handlers.resize = () => {
|
|
303
|
+
clearTimeout(resizeTimeout);
|
|
304
|
+
resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
|
|
305
|
+
};
|
|
306
|
+
window.addEventListener('resize', this.handlers.resize);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Position transcript window next to video
|
|
311
|
+
*/
|
|
312
|
+
positionTranscript() {
|
|
313
|
+
if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
|
|
314
|
+
|
|
315
|
+
const isMobile = window.innerWidth < 640;
|
|
316
|
+
const videoRect = this.player.videoWrapper.getBoundingClientRect();
|
|
317
|
+
|
|
318
|
+
// Check if player is in fullscreen mode
|
|
319
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
320
|
+
|
|
321
|
+
if (isMobile && !isFullscreen) {
|
|
322
|
+
// Mobile: Position underneath the video and controls as part of the layout
|
|
323
|
+
this.transcriptWindow.style.position = 'relative';
|
|
324
|
+
this.transcriptWindow.style.left = '0';
|
|
325
|
+
this.transcriptWindow.style.right = '0';
|
|
326
|
+
this.transcriptWindow.style.bottom = 'auto';
|
|
327
|
+
this.transcriptWindow.style.top = 'auto';
|
|
328
|
+
this.transcriptWindow.style.width = '100%';
|
|
329
|
+
this.transcriptWindow.style.maxWidth = '100%';
|
|
330
|
+
this.transcriptWindow.style.maxHeight = '400px';
|
|
331
|
+
this.transcriptWindow.style.height = 'auto';
|
|
332
|
+
this.transcriptWindow.style.borderRadius = '0';
|
|
333
|
+
this.transcriptWindow.style.transform = 'none';
|
|
334
|
+
this.transcriptWindow.style.border = 'none';
|
|
335
|
+
this.transcriptWindow.style.borderTop = '1px solid var(--vidply-border-light)';
|
|
336
|
+
this.transcriptWindow.style.boxShadow = 'none';
|
|
337
|
+
// Disable dragging on mobile
|
|
338
|
+
if (this.transcriptHeader) {
|
|
339
|
+
this.transcriptHeader.style.cursor = 'default';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Ensure transcript is at the container level for proper stacking
|
|
343
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
344
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
345
|
+
}
|
|
346
|
+
} else if (isFullscreen) {
|
|
347
|
+
// In fullscreen: position in bottom right corner inside the video
|
|
348
|
+
this.transcriptWindow.style.position = 'fixed';
|
|
349
|
+
this.transcriptWindow.style.left = 'auto';
|
|
350
|
+
this.transcriptWindow.style.right = '20px';
|
|
351
|
+
this.transcriptWindow.style.bottom = '80px'; // Above controls
|
|
352
|
+
this.transcriptWindow.style.top = 'auto';
|
|
353
|
+
this.transcriptWindow.style.maxHeight = 'calc(100vh - 180px)'; // Leave space for controls
|
|
354
|
+
this.transcriptWindow.style.height = 'auto';
|
|
355
|
+
this.transcriptWindow.style.width = '400px';
|
|
356
|
+
this.transcriptWindow.style.maxWidth = '400px';
|
|
357
|
+
this.transcriptWindow.style.borderRadius = '8px';
|
|
358
|
+
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
359
|
+
this.transcriptWindow.style.borderTop = '';
|
|
360
|
+
|
|
361
|
+
// Move back to container for fullscreen
|
|
362
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
363
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
// Desktop mode: position next to video
|
|
367
|
+
this.transcriptWindow.style.position = 'absolute';
|
|
368
|
+
this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
|
|
369
|
+
this.transcriptWindow.style.right = 'auto';
|
|
370
|
+
this.transcriptWindow.style.bottom = 'auto';
|
|
371
|
+
this.transcriptWindow.style.top = '0';
|
|
372
|
+
this.transcriptWindow.style.height = `${videoRect.height}px`;
|
|
373
|
+
this.transcriptWindow.style.maxHeight = 'none';
|
|
374
|
+
this.transcriptWindow.style.width = '400px';
|
|
375
|
+
this.transcriptWindow.style.maxWidth = '400px';
|
|
376
|
+
this.transcriptWindow.style.borderRadius = '8px';
|
|
377
|
+
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
378
|
+
this.transcriptWindow.style.borderTop = '';
|
|
379
|
+
// Enable dragging on desktop
|
|
380
|
+
if (this.transcriptHeader) {
|
|
381
|
+
this.transcriptHeader.style.cursor = 'move';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Move back to container for desktop
|
|
385
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
386
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Load transcript data from caption/subtitle tracks
|
|
393
|
+
*/
|
|
394
|
+
loadTranscriptData() {
|
|
395
|
+
this.transcriptEntries = [];
|
|
396
|
+
this.transcriptContent.innerHTML = '';
|
|
397
|
+
|
|
398
|
+
// Get all text tracks
|
|
399
|
+
const textTracks = Array.from(this.player.element.textTracks);
|
|
400
|
+
|
|
401
|
+
// Find different track types
|
|
402
|
+
const captionTrack = textTracks.find(
|
|
403
|
+
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
404
|
+
);
|
|
405
|
+
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
406
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
407
|
+
|
|
408
|
+
// We need at least one track type
|
|
409
|
+
if (!captionTrack && !descriptionTrack && !metadataTrack) {
|
|
410
|
+
this.showNoTranscriptMessage();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Enable all tracks to load cues
|
|
415
|
+
const tracksToLoad = [captionTrack, descriptionTrack, metadataTrack].filter(Boolean);
|
|
416
|
+
tracksToLoad.forEach(track => {
|
|
417
|
+
if (track.mode === 'disabled') {
|
|
418
|
+
track.mode = 'hidden';
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Check if any tracks are still loading
|
|
423
|
+
const needsLoading = tracksToLoad.some(track => !track.cues || track.cues.length === 0);
|
|
424
|
+
|
|
425
|
+
if (needsLoading) {
|
|
426
|
+
// Wait for cues to load
|
|
427
|
+
const loadingMessage = DOMUtils.createElement('div', {
|
|
428
|
+
className: `${this.player.options.classPrefix}-transcript-loading`,
|
|
429
|
+
textContent: i18n.t('transcript.loading')
|
|
430
|
+
});
|
|
431
|
+
this.transcriptContent.appendChild(loadingMessage);
|
|
432
|
+
|
|
433
|
+
let loaded = 0;
|
|
434
|
+
const onLoad = () => {
|
|
435
|
+
loaded++;
|
|
436
|
+
if (loaded >= tracksToLoad.length) {
|
|
437
|
+
this.loadTranscriptData();
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
tracksToLoad.forEach(track => {
|
|
442
|
+
track.addEventListener('load', onLoad, { once: true });
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Fallback timeout
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
this.loadTranscriptData();
|
|
448
|
+
}, 500);
|
|
449
|
+
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Collect all cues from all tracks with their type
|
|
454
|
+
const allCues = [];
|
|
455
|
+
|
|
456
|
+
if (captionTrack && captionTrack.cues) {
|
|
457
|
+
Array.from(captionTrack.cues).forEach(cue => {
|
|
458
|
+
allCues.push({ cue, type: 'caption' });
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (descriptionTrack && descriptionTrack.cues) {
|
|
463
|
+
Array.from(descriptionTrack.cues).forEach(cue => {
|
|
464
|
+
allCues.push({ cue, type: 'description' });
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Store metadata separately for programmatic use (don't display in transcript)
|
|
469
|
+
if (metadataTrack && metadataTrack.cues) {
|
|
470
|
+
this.metadataCues = Array.from(metadataTrack.cues);
|
|
471
|
+
this.setupMetadataHandling();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Sort all cues by start time
|
|
475
|
+
allCues.sort((a, b) => a.cue.startTime - b.cue.startTime);
|
|
476
|
+
|
|
477
|
+
// Build transcript from captions and descriptions only
|
|
478
|
+
allCues.forEach((item, index) => {
|
|
479
|
+
const entry = this.createTranscriptEntry(item.cue, index, item.type);
|
|
480
|
+
this.transcriptEntries.push({
|
|
481
|
+
element: entry,
|
|
482
|
+
cue: item.cue,
|
|
483
|
+
type: item.type,
|
|
484
|
+
startTime: item.cue.startTime,
|
|
485
|
+
endTime: item.cue.endTime
|
|
486
|
+
});
|
|
487
|
+
this.transcriptContent.appendChild(entry);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Apply current styles to newly loaded entries
|
|
491
|
+
this.applyTranscriptStyles();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Setup metadata handling
|
|
496
|
+
* Metadata cues are not displayed but can be used programmatically
|
|
497
|
+
*/
|
|
498
|
+
setupMetadataHandling() {
|
|
499
|
+
if (!this.metadataCues || this.metadataCues.length === 0) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Listen for cuechange events on the metadata track to trigger custom actions
|
|
504
|
+
const textTracks = Array.from(this.player.element.textTracks);
|
|
505
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
506
|
+
|
|
507
|
+
if (metadataTrack) {
|
|
508
|
+
metadataTrack.addEventListener('cuechange', () => {
|
|
509
|
+
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
510
|
+
activeCues.forEach(cue => {
|
|
511
|
+
this.handleMetadataCue(cue);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Handle individual metadata cues
|
|
519
|
+
* Parses metadata text and emits events or triggers actions
|
|
520
|
+
*/
|
|
521
|
+
handleMetadataCue(cue) {
|
|
522
|
+
const text = cue.text.trim();
|
|
523
|
+
|
|
524
|
+
// Emit a generic metadata event that developers can listen to
|
|
525
|
+
this.player.emit('metadata', {
|
|
526
|
+
time: cue.startTime,
|
|
527
|
+
endTime: cue.endTime,
|
|
528
|
+
text: text,
|
|
529
|
+
cue: cue
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
533
|
+
if (text.includes('PAUSE')) {
|
|
534
|
+
// Emit pause suggestion event (don't auto-pause, let developer decide)
|
|
535
|
+
this.player.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Parse for focus directives
|
|
539
|
+
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
540
|
+
if (focusMatch) {
|
|
541
|
+
this.player.emit('metadata:focus', {
|
|
542
|
+
time: cue.startTime,
|
|
543
|
+
target: focusMatch[1],
|
|
544
|
+
text: text
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Parse for hashtag references
|
|
549
|
+
const hashtags = text.match(/#[\w-]+/g);
|
|
550
|
+
if (hashtags) {
|
|
551
|
+
this.player.emit('metadata:hashtags', {
|
|
552
|
+
time: cue.startTime,
|
|
553
|
+
hashtags: hashtags,
|
|
554
|
+
text: text
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Create a single transcript entry element
|
|
561
|
+
*/
|
|
562
|
+
createTranscriptEntry(cue, index, type = 'caption') {
|
|
563
|
+
const entry = DOMUtils.createElement('div', {
|
|
564
|
+
className: `${this.player.options.classPrefix}-transcript-entry ${this.player.options.classPrefix}-transcript-${type}`,
|
|
565
|
+
attributes: {
|
|
566
|
+
'data-start': String(cue.startTime),
|
|
567
|
+
'data-end': String(cue.endTime),
|
|
568
|
+
'data-type': type,
|
|
569
|
+
'role': 'button',
|
|
570
|
+
'tabindex': '0'
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const timestamp = DOMUtils.createElement('span', {
|
|
575
|
+
className: `${this.player.options.classPrefix}-transcript-time`,
|
|
576
|
+
textContent: TimeUtils.formatTime(cue.startTime)
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const text = DOMUtils.createElement('span', {
|
|
580
|
+
className: `${this.player.options.classPrefix}-transcript-text`,
|
|
581
|
+
textContent: this.stripVTTFormatting(cue.text)
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
entry.appendChild(timestamp);
|
|
585
|
+
entry.appendChild(text);
|
|
586
|
+
|
|
587
|
+
// Click to seek
|
|
588
|
+
const seekToTime = () => {
|
|
589
|
+
this.player.seek(cue.startTime);
|
|
590
|
+
if (this.player.state.paused) {
|
|
591
|
+
this.player.play();
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
entry.addEventListener('click', seekToTime);
|
|
596
|
+
entry.addEventListener('keydown', (e) => {
|
|
597
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
598
|
+
e.preventDefault();
|
|
599
|
+
seekToTime();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return entry;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Strip VTT formatting tags from text
|
|
608
|
+
*/
|
|
609
|
+
stripVTTFormatting(text) {
|
|
610
|
+
// Remove VTT tags like <v Speaker>, <c>, etc.
|
|
611
|
+
return text
|
|
612
|
+
.replace(/<[^>]+>/g, '')
|
|
613
|
+
.replace(/\n/g, ' ')
|
|
614
|
+
.trim();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Show message when no transcript is available
|
|
619
|
+
*/
|
|
620
|
+
showNoTranscriptMessage() {
|
|
621
|
+
const message = DOMUtils.createElement('div', {
|
|
622
|
+
className: `${this.player.options.classPrefix}-transcript-empty`,
|
|
623
|
+
textContent: i18n.t('transcript.noTranscript')
|
|
624
|
+
});
|
|
625
|
+
this.transcriptContent.appendChild(message);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Update active transcript entry based on current time
|
|
630
|
+
*/
|
|
631
|
+
updateActiveEntry() {
|
|
632
|
+
if (!this.isVisible || this.transcriptEntries.length === 0) return;
|
|
633
|
+
|
|
634
|
+
const currentTime = this.player.state.currentTime;
|
|
635
|
+
|
|
636
|
+
// Find the entry that matches current time
|
|
637
|
+
const activeEntry = this.transcriptEntries.find(
|
|
638
|
+
entry => currentTime >= entry.startTime && currentTime < entry.endTime
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
if (activeEntry && activeEntry !== this.currentActiveEntry) {
|
|
642
|
+
// Remove previous active class
|
|
643
|
+
if (this.currentActiveEntry) {
|
|
644
|
+
this.currentActiveEntry.element.classList.remove(
|
|
645
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Add active class to current entry
|
|
650
|
+
activeEntry.element.classList.add(
|
|
651
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Scroll to active entry
|
|
655
|
+
this.scrollToEntry(activeEntry.element);
|
|
656
|
+
|
|
657
|
+
this.currentActiveEntry = activeEntry;
|
|
658
|
+
} else if (!activeEntry && this.currentActiveEntry) {
|
|
659
|
+
// No active entry, remove active class
|
|
660
|
+
this.currentActiveEntry.element.classList.remove(
|
|
661
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
662
|
+
);
|
|
663
|
+
this.currentActiveEntry = null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Scroll transcript window to show active entry
|
|
669
|
+
*/
|
|
670
|
+
scrollToEntry(entryElement) {
|
|
671
|
+
if (!this.transcriptContent) return;
|
|
672
|
+
|
|
673
|
+
const contentRect = this.transcriptContent.getBoundingClientRect();
|
|
674
|
+
const entryRect = entryElement.getBoundingClientRect();
|
|
675
|
+
|
|
676
|
+
// Check if entry is out of view
|
|
677
|
+
if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
|
|
678
|
+
// Scroll to center the entry
|
|
679
|
+
const scrollTop = entryElement.offsetTop - (this.transcriptContent.clientHeight / 2) + (entryElement.clientHeight / 2);
|
|
680
|
+
this.transcriptContent.scrollTo({
|
|
681
|
+
top: scrollTop,
|
|
682
|
+
behavior: 'smooth'
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Setup drag and drop functionality
|
|
689
|
+
*/
|
|
690
|
+
setupDragAndDrop() {
|
|
691
|
+
if (!this.transcriptHeader || !this.transcriptWindow) return;
|
|
692
|
+
|
|
693
|
+
// Create and store handler functions
|
|
694
|
+
this.handlers.mousedown = (e) => {
|
|
695
|
+
// Don't drag if clicking on close button
|
|
696
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Don't drag if clicking on settings button
|
|
701
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Don't drag if clicking on settings menu
|
|
706
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Don't drag if clicking on style dialog
|
|
711
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
this.startDragging(e.clientX, e.clientY);
|
|
716
|
+
e.preventDefault();
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
this.handlers.mousemove = (e) => {
|
|
720
|
+
if (this.isDragging) {
|
|
721
|
+
this.drag(e.clientX, e.clientY);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
this.handlers.mouseup = () => {
|
|
726
|
+
if (this.isDragging) {
|
|
727
|
+
this.stopDragging();
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
this.handlers.touchstart = (e) => {
|
|
732
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Don't drag if touching settings button
|
|
737
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Don't drag if touching settings menu
|
|
742
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Don't drag if touching style dialog
|
|
747
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const isMobile = window.innerWidth < 640;
|
|
752
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
753
|
+
const touch = e.touches[0];
|
|
754
|
+
|
|
755
|
+
if (isMobile && !isFullscreen) {
|
|
756
|
+
// Mobile (not fullscreen): No dragging/swiping, transcript is part of layout
|
|
757
|
+
return;
|
|
758
|
+
} else {
|
|
759
|
+
// Desktop or fullscreen: Normal dragging
|
|
760
|
+
this.startDragging(touch.clientX, touch.clientY);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
this.handlers.touchmove = (e) => {
|
|
765
|
+
const isMobile = window.innerWidth < 640;
|
|
766
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
767
|
+
|
|
768
|
+
if (isMobile && !isFullscreen) {
|
|
769
|
+
// Mobile (not fullscreen): No dragging/swiping
|
|
770
|
+
return;
|
|
771
|
+
} else if (this.isDragging) {
|
|
772
|
+
// Desktop or fullscreen: Normal drag
|
|
773
|
+
const touch = e.touches[0];
|
|
774
|
+
this.drag(touch.clientX, touch.clientY);
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
this.handlers.touchend = () => {
|
|
780
|
+
if (this.isDragging) {
|
|
781
|
+
// Stop dragging
|
|
782
|
+
this.stopDragging();
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
this.handlers.keydown = (e) => {
|
|
787
|
+
// Handle arrow keys only in keyboard drag mode
|
|
788
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
|
789
|
+
if (!this.keyboardDragMode) {
|
|
790
|
+
// Not in drag mode, let other handlers deal with it
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// In drag mode - move the window
|
|
795
|
+
e.preventDefault();
|
|
796
|
+
e.stopPropagation();
|
|
797
|
+
|
|
798
|
+
const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
|
|
799
|
+
|
|
800
|
+
// Get current position
|
|
801
|
+
let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
|
|
802
|
+
let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
|
|
803
|
+
|
|
804
|
+
// If window is still centered with transform, convert to absolute position first
|
|
805
|
+
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
806
|
+
if (computedStyle.transform !== 'none') {
|
|
807
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
808
|
+
currentLeft = rect.left;
|
|
809
|
+
currentTop = rect.top;
|
|
810
|
+
this.transcriptWindow.style.transform = 'none';
|
|
811
|
+
this.transcriptWindow.style.left = `${currentLeft}px`;
|
|
812
|
+
this.transcriptWindow.style.top = `${currentTop}px`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Calculate new position based on arrow key
|
|
816
|
+
let newX = currentLeft;
|
|
817
|
+
let newY = currentTop;
|
|
818
|
+
|
|
819
|
+
switch(e.key) {
|
|
820
|
+
case 'ArrowLeft':
|
|
821
|
+
newX -= step;
|
|
822
|
+
break;
|
|
823
|
+
case 'ArrowRight':
|
|
824
|
+
newX += step;
|
|
825
|
+
break;
|
|
826
|
+
case 'ArrowUp':
|
|
827
|
+
newY -= step;
|
|
828
|
+
break;
|
|
829
|
+
case 'ArrowDown':
|
|
830
|
+
newY += step;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Set new position directly
|
|
835
|
+
this.transcriptWindow.style.left = `${newX}px`;
|
|
836
|
+
this.transcriptWindow.style.top = `${newY}px`;
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Handle other special keys
|
|
841
|
+
if (e.key === 'Home') {
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
e.stopPropagation();
|
|
844
|
+
this.resetPosition();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (e.key === 'Escape') {
|
|
849
|
+
e.preventDefault();
|
|
850
|
+
e.stopPropagation();
|
|
851
|
+
if (this.styleDialogVisible) {
|
|
852
|
+
// Close style dialog first
|
|
853
|
+
this.hideStyleDialog();
|
|
854
|
+
} else if (this.keyboardDragMode) {
|
|
855
|
+
// Exit drag mode
|
|
856
|
+
this.disableKeyboardDragMode();
|
|
857
|
+
} else if (this.settingsMenuVisible) {
|
|
858
|
+
// Close settings menu
|
|
859
|
+
this.hideSettingsMenu();
|
|
860
|
+
} else {
|
|
861
|
+
// Close transcript
|
|
862
|
+
this.hideTranscript();
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// Add event listeners using stored handlers
|
|
869
|
+
this.transcriptHeader.addEventListener('mousedown', this.handlers.mousedown);
|
|
870
|
+
document.addEventListener('mousemove', this.handlers.mousemove);
|
|
871
|
+
document.addEventListener('mouseup', this.handlers.mouseup);
|
|
872
|
+
|
|
873
|
+
this.transcriptHeader.addEventListener('touchstart', this.handlers.touchstart);
|
|
874
|
+
document.addEventListener('touchmove', this.handlers.touchmove);
|
|
875
|
+
document.addEventListener('touchend', this.handlers.touchend);
|
|
876
|
+
|
|
877
|
+
this.transcriptHeader.addEventListener('keydown', this.handlers.keydown);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Start dragging
|
|
882
|
+
*/
|
|
883
|
+
startDragging(clientX, clientY) {
|
|
884
|
+
// Get current rendered position (this is where it actually appears on screen)
|
|
885
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
886
|
+
|
|
887
|
+
// Get the parent container position (player container)
|
|
888
|
+
const containerRect = this.player.container.getBoundingClientRect();
|
|
889
|
+
|
|
890
|
+
// Calculate position RELATIVE to container (not viewport)
|
|
891
|
+
const relativeLeft = rect.left - containerRect.left;
|
|
892
|
+
const relativeTop = rect.top - containerRect.top;
|
|
893
|
+
|
|
894
|
+
// If window is centered with transform, convert to absolute position
|
|
895
|
+
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
896
|
+
if (computedStyle.transform !== 'none') {
|
|
897
|
+
// Remove transform and set position relative to container
|
|
898
|
+
this.transcriptWindow.style.transform = 'none';
|
|
899
|
+
this.transcriptWindow.style.left = `${relativeLeft}px`;
|
|
900
|
+
this.transcriptWindow.style.top = `${relativeTop}px`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Calculate offset based on viewport coordinates (where user clicked)
|
|
904
|
+
this.dragOffsetX = clientX - rect.left;
|
|
905
|
+
this.dragOffsetY = clientY - rect.top;
|
|
906
|
+
|
|
907
|
+
this.isDragging = true;
|
|
908
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
909
|
+
document.body.style.cursor = 'grabbing';
|
|
910
|
+
document.body.style.userSelect = 'none';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Perform drag
|
|
915
|
+
*/
|
|
916
|
+
drag(clientX, clientY) {
|
|
917
|
+
if (!this.isDragging) return;
|
|
918
|
+
|
|
919
|
+
// Calculate new viewport position based on mouse position minus the offset
|
|
920
|
+
const newViewportX = clientX - this.dragOffsetX;
|
|
921
|
+
const newViewportY = clientY - this.dragOffsetY;
|
|
922
|
+
|
|
923
|
+
// Convert to position relative to container
|
|
924
|
+
const containerRect = this.player.container.getBoundingClientRect();
|
|
925
|
+
const newX = newViewportX - containerRect.left;
|
|
926
|
+
const newY = newViewportY - containerRect.top;
|
|
927
|
+
|
|
928
|
+
// During drag, set position relative to container
|
|
929
|
+
this.transcriptWindow.style.left = `${newX}px`;
|
|
930
|
+
this.transcriptWindow.style.top = `${newY}px`;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Stop dragging
|
|
935
|
+
*/
|
|
936
|
+
stopDragging() {
|
|
937
|
+
this.isDragging = false;
|
|
938
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
939
|
+
document.body.style.cursor = '';
|
|
940
|
+
document.body.style.userSelect = '';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Set window position with boundary constraints
|
|
945
|
+
*/
|
|
946
|
+
setPosition(x, y) {
|
|
947
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
948
|
+
|
|
949
|
+
// Use document dimensions for fixed positioning
|
|
950
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
951
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
952
|
+
|
|
953
|
+
// Very relaxed boundaries - allow window to go mostly off-screen
|
|
954
|
+
// Just keep a small part visible so user can always drag it back
|
|
955
|
+
const minVisible = 100; // Keep at least 100px visible
|
|
956
|
+
const minX = -(rect.width - minVisible); // Can go way off-screen to the left
|
|
957
|
+
const minY = -(rect.height - minVisible); // Can go way off-screen to the top
|
|
958
|
+
const maxX = viewportWidth - minVisible; // Can go way off-screen to the right
|
|
959
|
+
const maxY = viewportHeight - minVisible; // Can go way off-screen to the bottom
|
|
960
|
+
|
|
961
|
+
// Clamp position to boundaries (very loose)
|
|
962
|
+
x = Math.max(minX, Math.min(x, maxX));
|
|
963
|
+
y = Math.max(minY, Math.min(y, maxY));
|
|
964
|
+
|
|
965
|
+
this.transcriptWindow.style.left = `${x}px`;
|
|
966
|
+
this.transcriptWindow.style.top = `${y}px`;
|
|
967
|
+
this.transcriptWindow.style.transform = 'none';
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Reset position to center
|
|
972
|
+
*/
|
|
973
|
+
resetPosition() {
|
|
974
|
+
this.transcriptWindow.style.left = '50%';
|
|
975
|
+
this.transcriptWindow.style.top = '50%';
|
|
976
|
+
this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Toggle keyboard drag mode
|
|
981
|
+
*/
|
|
982
|
+
toggleKeyboardDragMode() {
|
|
983
|
+
if (this.keyboardDragMode) {
|
|
984
|
+
this.disableKeyboardDragMode();
|
|
985
|
+
} else {
|
|
986
|
+
this.enableKeyboardDragMode();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Enable keyboard drag mode
|
|
992
|
+
*/
|
|
993
|
+
enableKeyboardDragMode() {
|
|
994
|
+
this.keyboardDragMode = true;
|
|
995
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
|
|
996
|
+
|
|
997
|
+
// Update settings button aria label
|
|
998
|
+
if (this.settingsButton) {
|
|
999
|
+
this.settingsButton.setAttribute('aria-label', 'Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Add visual indicator
|
|
1003
|
+
const indicator = DOMUtils.createElement('div', {
|
|
1004
|
+
className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
|
|
1005
|
+
textContent: i18n.t('transcript.keyboardDragActive')
|
|
1006
|
+
});
|
|
1007
|
+
this.transcriptHeader.appendChild(indicator);
|
|
1008
|
+
|
|
1009
|
+
// Hide settings menu if open
|
|
1010
|
+
if (this.settingsMenuVisible) {
|
|
1011
|
+
this.hideSettingsMenu();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Focus the header for keyboard navigation
|
|
1015
|
+
this.transcriptHeader.focus();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Disable keyboard drag mode
|
|
1020
|
+
*/
|
|
1021
|
+
disableKeyboardDragMode() {
|
|
1022
|
+
this.keyboardDragMode = false;
|
|
1023
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
|
|
1024
|
+
|
|
1025
|
+
// Update settings button aria label
|
|
1026
|
+
if (this.settingsButton) {
|
|
1027
|
+
this.settingsButton.setAttribute('aria-label', 'Transcript settings. Press Enter to open menu, or D to enable drag mode');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Remove visual indicator
|
|
1031
|
+
const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
|
|
1032
|
+
if (indicator) {
|
|
1033
|
+
indicator.remove();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Focus back to settings button
|
|
1037
|
+
if (this.settingsButton) {
|
|
1038
|
+
this.settingsButton.focus();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Toggle settings menu visibility
|
|
1044
|
+
*/
|
|
1045
|
+
toggleSettingsMenu() {
|
|
1046
|
+
if (this.settingsMenuVisible) {
|
|
1047
|
+
this.hideSettingsMenu();
|
|
1048
|
+
} else {
|
|
1049
|
+
this.showSettingsMenu();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Show settings menu
|
|
1055
|
+
*/
|
|
1056
|
+
showSettingsMenu() {
|
|
1057
|
+
// Set flag to prevent immediate closing
|
|
1058
|
+
this.settingsMenuJustOpened = true;
|
|
1059
|
+
setTimeout(() => {
|
|
1060
|
+
this.settingsMenuJustOpened = false;
|
|
1061
|
+
}, 350);
|
|
1062
|
+
|
|
1063
|
+
// Add document click handler on FIRST menu open (not at window creation)
|
|
1064
|
+
if (!this.documentClickHandlerAdded) {
|
|
1065
|
+
setTimeout(() => {
|
|
1066
|
+
document.addEventListener('click', this.handlers.documentClick);
|
|
1067
|
+
this.documentClickHandlerAdded = true;
|
|
1068
|
+
}, 300);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (this.settingsMenu) {
|
|
1072
|
+
this.settingsMenu.style.display = 'block';
|
|
1073
|
+
this.settingsMenuVisible = true;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
// Create settings menu
|
|
1077
|
+
this.settingsMenu = DOMUtils.createElement('div', {
|
|
1078
|
+
className: `${this.player.options.classPrefix}-transcript-settings-menu`
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Keyboard drag option
|
|
1082
|
+
const keyboardDragOption = DOMUtils.createElement('button', {
|
|
1083
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1084
|
+
attributes: {
|
|
1085
|
+
'type': 'button',
|
|
1086
|
+
'aria-label': i18n.t('transcript.keyboardDragMode')
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
const keyboardIcon = createIconElement('move');
|
|
1090
|
+
const keyboardText = DOMUtils.createElement('span', {
|
|
1091
|
+
textContent: i18n.t('transcript.keyboardDragMode')
|
|
1092
|
+
});
|
|
1093
|
+
keyboardDragOption.appendChild(keyboardIcon);
|
|
1094
|
+
keyboardDragOption.appendChild(keyboardText);
|
|
1095
|
+
keyboardDragOption.addEventListener('click', () => {
|
|
1096
|
+
this.toggleKeyboardDragMode();
|
|
1097
|
+
this.hideSettingsMenu();
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Style option
|
|
1101
|
+
const styleOption = DOMUtils.createElement('button', {
|
|
1102
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1103
|
+
attributes: {
|
|
1104
|
+
'type': 'button',
|
|
1105
|
+
'aria-label': i18n.t('transcript.styleTranscript')
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
const styleIcon = createIconElement('settings');
|
|
1109
|
+
const styleText = DOMUtils.createElement('span', {
|
|
1110
|
+
textContent: i18n.t('transcript.styleTranscript')
|
|
1111
|
+
});
|
|
1112
|
+
styleOption.appendChild(styleIcon);
|
|
1113
|
+
styleOption.appendChild(styleText);
|
|
1114
|
+
styleOption.addEventListener('click', (e) => {
|
|
1115
|
+
e.preventDefault();
|
|
1116
|
+
e.stopPropagation();
|
|
1117
|
+
this.hideSettingsMenu();
|
|
1118
|
+
// Delay to ensure menu is fully closed before opening dialog
|
|
1119
|
+
setTimeout(() => {
|
|
1120
|
+
this.showStyleDialog();
|
|
1121
|
+
}, 50);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Resize option
|
|
1125
|
+
const resizeOption = DOMUtils.createElement('button', {
|
|
1126
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1127
|
+
attributes: {
|
|
1128
|
+
'type': 'button',
|
|
1129
|
+
'aria-label': i18n.t('transcript.resizeWindow')
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
const resizeIcon = createIconElement('resize');
|
|
1133
|
+
const resizeText = DOMUtils.createElement('span', {
|
|
1134
|
+
textContent: i18n.t('transcript.resizeWindow')
|
|
1135
|
+
});
|
|
1136
|
+
resizeOption.appendChild(resizeIcon);
|
|
1137
|
+
resizeOption.appendChild(resizeText);
|
|
1138
|
+
resizeOption.addEventListener('click', () => {
|
|
1139
|
+
this.toggleResizeMode();
|
|
1140
|
+
this.hideSettingsMenu();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// Close option
|
|
1144
|
+
const closeOption = DOMUtils.createElement('button', {
|
|
1145
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1146
|
+
attributes: {
|
|
1147
|
+
'type': 'button',
|
|
1148
|
+
'aria-label': i18n.t('transcript.closeMenu')
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
const closeIcon = createIconElement('close');
|
|
1152
|
+
const closeText = DOMUtils.createElement('span', {
|
|
1153
|
+
textContent: i18n.t('transcript.closeMenu')
|
|
1154
|
+
});
|
|
1155
|
+
closeOption.appendChild(closeIcon);
|
|
1156
|
+
closeOption.appendChild(closeText);
|
|
1157
|
+
closeOption.addEventListener('click', () => {
|
|
1158
|
+
this.hideSettingsMenu();
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
this.settingsMenu.appendChild(keyboardDragOption);
|
|
1162
|
+
this.settingsMenu.appendChild(resizeOption);
|
|
1163
|
+
this.settingsMenu.appendChild(styleOption);
|
|
1164
|
+
this.settingsMenu.appendChild(closeOption);
|
|
1165
|
+
|
|
1166
|
+
// Append menu to header left container for proper positioning
|
|
1167
|
+
if (this.headerLeft) {
|
|
1168
|
+
this.headerLeft.appendChild(this.settingsMenu);
|
|
1169
|
+
} else {
|
|
1170
|
+
this.transcriptHeader.appendChild(this.settingsMenu);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Set the menu as visible and display it
|
|
1174
|
+
this.settingsMenuVisible = true;
|
|
1175
|
+
this.settingsMenu.style.display = 'block';
|
|
1176
|
+
|
|
1177
|
+
// Update aria-expanded
|
|
1178
|
+
if (this.settingsButton) {
|
|
1179
|
+
this.settingsButton.setAttribute('aria-expanded', 'true');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Focus first menu item
|
|
1183
|
+
setTimeout(() => {
|
|
1184
|
+
const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
|
|
1185
|
+
if (firstItem) {
|
|
1186
|
+
firstItem.focus();
|
|
1187
|
+
}
|
|
1188
|
+
}, 0);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Hide settings menu
|
|
1193
|
+
*/
|
|
1194
|
+
hideSettingsMenu() {
|
|
1195
|
+
if (this.settingsMenu) {
|
|
1196
|
+
this.settingsMenu.style.display = 'none';
|
|
1197
|
+
this.settingsMenuVisible = false;
|
|
1198
|
+
this.settingsMenuJustOpened = false;
|
|
1199
|
+
|
|
1200
|
+
// Update aria-expanded
|
|
1201
|
+
if (this.settingsButton) {
|
|
1202
|
+
this.settingsButton.setAttribute('aria-expanded', 'false');
|
|
1203
|
+
// Return focus to settings button
|
|
1204
|
+
this.settingsButton.focus();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Enable move mode (gives visual feedback)
|
|
1211
|
+
*/
|
|
1212
|
+
enableMoveMode() {
|
|
1213
|
+
// Add visual feedback for move mode
|
|
1214
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
|
|
1215
|
+
|
|
1216
|
+
// Show tooltip about keyboard drag option
|
|
1217
|
+
const tooltip = DOMUtils.createElement('div', {
|
|
1218
|
+
className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
|
|
1219
|
+
textContent: 'Drag with mouse or press D for keyboard drag mode'
|
|
1220
|
+
});
|
|
1221
|
+
this.transcriptHeader.appendChild(tooltip);
|
|
1222
|
+
|
|
1223
|
+
// Remove after 2 seconds
|
|
1224
|
+
setTimeout(() => {
|
|
1225
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-move-mode`);
|
|
1226
|
+
if (tooltip.parentNode) {
|
|
1227
|
+
tooltip.remove();
|
|
1228
|
+
}
|
|
1229
|
+
}, 2000);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Toggle resize mode
|
|
1234
|
+
*/
|
|
1235
|
+
toggleResizeMode() {
|
|
1236
|
+
this.resizeEnabled = !this.resizeEnabled;
|
|
1237
|
+
|
|
1238
|
+
if (this.resizeEnabled) {
|
|
1239
|
+
this.enableResizeHandles();
|
|
1240
|
+
} else {
|
|
1241
|
+
this.disableResizeHandles();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Enable resize handles
|
|
1247
|
+
*/
|
|
1248
|
+
enableResizeHandles() {
|
|
1249
|
+
if (!this.transcriptWindow) return;
|
|
1250
|
+
|
|
1251
|
+
// Add resize handles if they don't exist
|
|
1252
|
+
const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
|
|
1253
|
+
|
|
1254
|
+
directions.forEach(direction => {
|
|
1255
|
+
const handle = DOMUtils.createElement('div', {
|
|
1256
|
+
className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
|
|
1257
|
+
attributes: {
|
|
1258
|
+
'data-direction': direction
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
handle.addEventListener('mousedown', (e) => this.startResize(e, direction));
|
|
1263
|
+
handle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], direction));
|
|
1264
|
+
|
|
1265
|
+
this.transcriptWindow.appendChild(handle);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
|
|
1269
|
+
|
|
1270
|
+
// Setup resize event handlers
|
|
1271
|
+
this.handlers.resizeMove = (e) => {
|
|
1272
|
+
if (this.isResizing) {
|
|
1273
|
+
this.performResize(e.clientX, e.clientY);
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
this.handlers.resizeEnd = () => {
|
|
1278
|
+
if (this.isResizing) {
|
|
1279
|
+
this.stopResize();
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
this.handlers.resizeTouchMove = (e) => {
|
|
1284
|
+
if (this.isResizing) {
|
|
1285
|
+
this.performResize(e.touches[0].clientX, e.touches[0].clientY);
|
|
1286
|
+
e.preventDefault();
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
document.addEventListener('mousemove', this.handlers.resizeMove);
|
|
1291
|
+
document.addEventListener('mouseup', this.handlers.resizeEnd);
|
|
1292
|
+
document.addEventListener('touchmove', this.handlers.resizeTouchMove);
|
|
1293
|
+
document.addEventListener('touchend', this.handlers.resizeEnd);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Disable resize handles
|
|
1298
|
+
*/
|
|
1299
|
+
disableResizeHandles() {
|
|
1300
|
+
if (!this.transcriptWindow) return;
|
|
1301
|
+
|
|
1302
|
+
// Remove all resize handles
|
|
1303
|
+
const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
|
|
1304
|
+
handles.forEach(handle => handle.remove());
|
|
1305
|
+
|
|
1306
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
|
|
1307
|
+
|
|
1308
|
+
// Remove resize event handlers
|
|
1309
|
+
if (this.handlers.resizeMove) {
|
|
1310
|
+
document.removeEventListener('mousemove', this.handlers.resizeMove);
|
|
1311
|
+
}
|
|
1312
|
+
if (this.handlers.resizeEnd) {
|
|
1313
|
+
document.removeEventListener('mouseup', this.handlers.resizeEnd);
|
|
1314
|
+
}
|
|
1315
|
+
if (this.handlers.resizeTouchMove) {
|
|
1316
|
+
document.removeEventListener('touchmove', this.handlers.resizeTouchMove);
|
|
1317
|
+
}
|
|
1318
|
+
document.removeEventListener('touchend', this.handlers.resizeEnd);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Start resizing
|
|
1323
|
+
*/
|
|
1324
|
+
startResize(e, direction) {
|
|
1325
|
+
e.stopPropagation();
|
|
1326
|
+
e.preventDefault();
|
|
1327
|
+
|
|
1328
|
+
this.isResizing = true;
|
|
1329
|
+
this.resizeDirection = direction;
|
|
1330
|
+
this.resizeStartX = e.clientX;
|
|
1331
|
+
this.resizeStartY = e.clientY;
|
|
1332
|
+
|
|
1333
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
1334
|
+
this.resizeStartWidth = rect.width;
|
|
1335
|
+
this.resizeStartHeight = rect.height;
|
|
1336
|
+
|
|
1337
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
|
|
1338
|
+
document.body.style.cursor = this.getResizeCursor(direction);
|
|
1339
|
+
document.body.style.userSelect = 'none';
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Perform resize
|
|
1344
|
+
*/
|
|
1345
|
+
performResize(clientX, clientY) {
|
|
1346
|
+
if (!this.isResizing) return;
|
|
1347
|
+
|
|
1348
|
+
const deltaX = clientX - this.resizeStartX;
|
|
1349
|
+
const deltaY = clientY - this.resizeStartY;
|
|
1350
|
+
|
|
1351
|
+
let newWidth = this.resizeStartWidth;
|
|
1352
|
+
let newHeight = this.resizeStartHeight;
|
|
1353
|
+
|
|
1354
|
+
const direction = this.resizeDirection;
|
|
1355
|
+
|
|
1356
|
+
// Calculate new dimensions based on direction
|
|
1357
|
+
if (direction.includes('e')) {
|
|
1358
|
+
newWidth = this.resizeStartWidth + deltaX;
|
|
1359
|
+
}
|
|
1360
|
+
if (direction.includes('w')) {
|
|
1361
|
+
newWidth = this.resizeStartWidth - deltaX;
|
|
1362
|
+
}
|
|
1363
|
+
if (direction.includes('s')) {
|
|
1364
|
+
newHeight = this.resizeStartHeight + deltaY;
|
|
1365
|
+
}
|
|
1366
|
+
if (direction.includes('n')) {
|
|
1367
|
+
newHeight = this.resizeStartHeight - deltaY;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Apply minimum and maximum constraints
|
|
1371
|
+
const minWidth = 300;
|
|
1372
|
+
const minHeight = 200;
|
|
1373
|
+
const maxWidth = window.innerWidth - 40;
|
|
1374
|
+
const maxHeight = window.innerHeight - 40;
|
|
1375
|
+
|
|
1376
|
+
newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
|
|
1377
|
+
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
|
|
1378
|
+
|
|
1379
|
+
// Apply new dimensions
|
|
1380
|
+
this.transcriptWindow.style.width = `${newWidth}px`;
|
|
1381
|
+
this.transcriptWindow.style.height = `${newHeight}px`;
|
|
1382
|
+
this.transcriptWindow.style.maxWidth = `${newWidth}px`;
|
|
1383
|
+
this.transcriptWindow.style.maxHeight = `${newHeight}px`;
|
|
1384
|
+
|
|
1385
|
+
// Adjust position if resizing from top or left
|
|
1386
|
+
if (direction.includes('w')) {
|
|
1387
|
+
const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
|
|
1388
|
+
this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
|
|
1389
|
+
}
|
|
1390
|
+
if (direction.includes('n')) {
|
|
1391
|
+
const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
|
|
1392
|
+
this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Stop resizing
|
|
1398
|
+
*/
|
|
1399
|
+
stopResize() {
|
|
1400
|
+
this.isResizing = false;
|
|
1401
|
+
this.resizeDirection = null;
|
|
1402
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
|
|
1403
|
+
document.body.style.cursor = '';
|
|
1404
|
+
document.body.style.userSelect = '';
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Get cursor style for resize direction
|
|
1409
|
+
*/
|
|
1410
|
+
getResizeCursor(direction) {
|
|
1411
|
+
const cursors = {
|
|
1412
|
+
'n': 'ns-resize',
|
|
1413
|
+
's': 'ns-resize',
|
|
1414
|
+
'e': 'ew-resize',
|
|
1415
|
+
'w': 'ew-resize',
|
|
1416
|
+
'ne': 'nesw-resize',
|
|
1417
|
+
'nw': 'nwse-resize',
|
|
1418
|
+
'se': 'nwse-resize',
|
|
1419
|
+
'sw': 'nesw-resize'
|
|
1420
|
+
};
|
|
1421
|
+
return cursors[direction] || 'default';
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Show style dialog
|
|
1426
|
+
*/
|
|
1427
|
+
showStyleDialog() {
|
|
1428
|
+
// If dialog already exists, just show it
|
|
1429
|
+
if (this.styleDialog) {
|
|
1430
|
+
this.styleDialog.style.display = 'block';
|
|
1431
|
+
this.styleDialogVisible = true;
|
|
1432
|
+
|
|
1433
|
+
// Set flag to prevent immediate closing from document click
|
|
1434
|
+
this.styleDialogJustOpened = true;
|
|
1435
|
+
setTimeout(() => {
|
|
1436
|
+
this.styleDialogJustOpened = false;
|
|
1437
|
+
}, 350);
|
|
1438
|
+
|
|
1439
|
+
// Focus first control
|
|
1440
|
+
setTimeout(() => {
|
|
1441
|
+
const firstSelect = this.styleDialog.querySelector('select, input');
|
|
1442
|
+
if (firstSelect) {
|
|
1443
|
+
firstSelect.focus();
|
|
1444
|
+
}
|
|
1445
|
+
}, 0);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Create style dialog
|
|
1450
|
+
this.styleDialog = DOMUtils.createElement('div', {
|
|
1451
|
+
className: `${this.player.options.classPrefix}-transcript-style-dialog`
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// Dialog title
|
|
1455
|
+
const title = DOMUtils.createElement('h4', {
|
|
1456
|
+
textContent: i18n.t('transcript.styleTitle'),
|
|
1457
|
+
className: `${this.player.options.classPrefix}-transcript-style-title`
|
|
1458
|
+
});
|
|
1459
|
+
this.styleDialog.appendChild(title);
|
|
1460
|
+
|
|
1461
|
+
// Font Size
|
|
1462
|
+
const fontSizeControl = this.createStyleSelectControl(
|
|
1463
|
+
i18n.t('captions.fontSize'),
|
|
1464
|
+
'fontSize',
|
|
1465
|
+
[
|
|
1466
|
+
{ label: i18n.t('fontSizes.small'), value: '87.5%' },
|
|
1467
|
+
{ label: i18n.t('fontSizes.normal'), value: '100%' },
|
|
1468
|
+
{ label: i18n.t('fontSizes.large'), value: '125%' },
|
|
1469
|
+
{ label: i18n.t('fontSizes.xlarge'), value: '150%' }
|
|
1470
|
+
]
|
|
1471
|
+
);
|
|
1472
|
+
this.styleDialog.appendChild(fontSizeControl);
|
|
1473
|
+
|
|
1474
|
+
// Font Family
|
|
1475
|
+
const fontFamilyControl = this.createStyleSelectControl(
|
|
1476
|
+
i18n.t('captions.fontFamily'),
|
|
1477
|
+
'fontFamily',
|
|
1478
|
+
[
|
|
1479
|
+
{ label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
|
|
1480
|
+
{ label: i18n.t('fontFamilies.serif'), value: 'serif' },
|
|
1481
|
+
{ label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
|
|
1482
|
+
]
|
|
1483
|
+
);
|
|
1484
|
+
this.styleDialog.appendChild(fontFamilyControl);
|
|
1485
|
+
|
|
1486
|
+
// Text Color
|
|
1487
|
+
const colorControl = this.createStyleColorControl(i18n.t('captions.color'), 'color');
|
|
1488
|
+
this.styleDialog.appendChild(colorControl);
|
|
1489
|
+
|
|
1490
|
+
// Background Color
|
|
1491
|
+
const bgColorControl = this.createStyleColorControl(i18n.t('captions.backgroundColor'), 'backgroundColor');
|
|
1492
|
+
this.styleDialog.appendChild(bgColorControl);
|
|
1493
|
+
|
|
1494
|
+
// Opacity
|
|
1495
|
+
const opacityControl = this.createStyleOpacityControl(i18n.t('captions.opacity'), 'opacity');
|
|
1496
|
+
this.styleDialog.appendChild(opacityControl);
|
|
1497
|
+
|
|
1498
|
+
// Close button
|
|
1499
|
+
const closeBtn = DOMUtils.createElement('button', {
|
|
1500
|
+
className: `${this.player.options.classPrefix}-transcript-style-close`,
|
|
1501
|
+
textContent: i18n.t('settings.close'),
|
|
1502
|
+
attributes: {
|
|
1503
|
+
'type': 'button'
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
closeBtn.addEventListener('click', () => this.hideStyleDialog());
|
|
1507
|
+
this.styleDialog.appendChild(closeBtn);
|
|
1508
|
+
|
|
1509
|
+
// ESC key handler for style dialog
|
|
1510
|
+
this.handlers.styleDialogKeydown = (e) => {
|
|
1511
|
+
if (e.key === 'Escape') {
|
|
1512
|
+
e.preventDefault();
|
|
1513
|
+
e.stopPropagation();
|
|
1514
|
+
this.hideStyleDialog();
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
this.styleDialog.addEventListener('keydown', this.handlers.styleDialogKeydown);
|
|
1518
|
+
|
|
1519
|
+
// Append to header left container (same as settings menu) for correct positioning
|
|
1520
|
+
if (this.headerLeft) {
|
|
1521
|
+
this.headerLeft.appendChild(this.styleDialog);
|
|
1522
|
+
} else {
|
|
1523
|
+
this.transcriptHeader.appendChild(this.styleDialog);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Apply current styles
|
|
1527
|
+
this.applyTranscriptStyles();
|
|
1528
|
+
|
|
1529
|
+
// Important: Set visible state and display before focusing
|
|
1530
|
+
this.styleDialogVisible = true;
|
|
1531
|
+
this.styleDialog.style.display = 'block';
|
|
1532
|
+
|
|
1533
|
+
// Set flag to prevent immediate closing from document click
|
|
1534
|
+
this.styleDialogJustOpened = true;
|
|
1535
|
+
setTimeout(() => {
|
|
1536
|
+
this.styleDialogJustOpened = false;
|
|
1537
|
+
}, 350);
|
|
1538
|
+
|
|
1539
|
+
// Focus first control for keyboard accessibility
|
|
1540
|
+
setTimeout(() => {
|
|
1541
|
+
const firstSelect = this.styleDialog.querySelector('select, input');
|
|
1542
|
+
if (firstSelect) {
|
|
1543
|
+
firstSelect.focus();
|
|
1544
|
+
}
|
|
1545
|
+
}, 0);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Hide style dialog
|
|
1550
|
+
*/
|
|
1551
|
+
hideStyleDialog() {
|
|
1552
|
+
if (this.styleDialog) {
|
|
1553
|
+
this.styleDialog.style.display = 'none';
|
|
1554
|
+
this.styleDialogVisible = false;
|
|
1555
|
+
|
|
1556
|
+
// Return focus to settings button
|
|
1557
|
+
if (this.settingsButton) {
|
|
1558
|
+
this.settingsButton.focus();
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Create style select control
|
|
1565
|
+
*/
|
|
1566
|
+
createStyleSelectControl(label, property, options) {
|
|
1567
|
+
const group = DOMUtils.createElement('div', {
|
|
1568
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1572
|
+
textContent: label
|
|
1573
|
+
});
|
|
1574
|
+
group.appendChild(labelEl);
|
|
1575
|
+
|
|
1576
|
+
const select = DOMUtils.createElement('select', {
|
|
1577
|
+
className: `${this.player.options.classPrefix}-transcript-style-select`
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
options.forEach(opt => {
|
|
1581
|
+
const option = DOMUtils.createElement('option', {
|
|
1582
|
+
textContent: opt.label,
|
|
1583
|
+
attributes: {
|
|
1584
|
+
'value': opt.value
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
if (this.transcriptStyle[property] === opt.value) {
|
|
1588
|
+
option.selected = true;
|
|
1589
|
+
}
|
|
1590
|
+
select.appendChild(option);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
select.addEventListener('change', (e) => {
|
|
1594
|
+
this.transcriptStyle[property] = e.target.value;
|
|
1595
|
+
this.applyTranscriptStyles();
|
|
1596
|
+
this.savePreferences();
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
group.appendChild(select);
|
|
1600
|
+
return group;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Create style color control
|
|
1605
|
+
*/
|
|
1606
|
+
createStyleColorControl(label, property) {
|
|
1607
|
+
const group = DOMUtils.createElement('div', {
|
|
1608
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1612
|
+
textContent: label
|
|
1613
|
+
});
|
|
1614
|
+
group.appendChild(labelEl);
|
|
1615
|
+
|
|
1616
|
+
const input = DOMUtils.createElement('input', {
|
|
1617
|
+
attributes: {
|
|
1618
|
+
'type': 'color',
|
|
1619
|
+
'value': this.transcriptStyle[property]
|
|
1620
|
+
},
|
|
1621
|
+
className: `${this.player.options.classPrefix}-transcript-style-color`
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
input.addEventListener('input', (e) => {
|
|
1625
|
+
this.transcriptStyle[property] = e.target.value;
|
|
1626
|
+
this.applyTranscriptStyles();
|
|
1627
|
+
this.savePreferences();
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
group.appendChild(input);
|
|
1631
|
+
return group;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Create style opacity control
|
|
1636
|
+
*/
|
|
1637
|
+
createStyleOpacityControl(label, property) {
|
|
1638
|
+
const group = DOMUtils.createElement('div', {
|
|
1639
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1643
|
+
textContent: label
|
|
1644
|
+
});
|
|
1645
|
+
group.appendChild(labelEl);
|
|
1646
|
+
|
|
1647
|
+
const valueDisplay = DOMUtils.createElement('span', {
|
|
1648
|
+
textContent: Math.round(this.transcriptStyle[property] * 100) + '%',
|
|
1649
|
+
className: `${this.player.options.classPrefix}-transcript-style-value`
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
const input = DOMUtils.createElement('input', {
|
|
1653
|
+
attributes: {
|
|
1654
|
+
'type': 'range',
|
|
1655
|
+
'min': '0',
|
|
1656
|
+
'max': '1',
|
|
1657
|
+
'step': '0.1',
|
|
1658
|
+
'value': String(this.transcriptStyle[property])
|
|
1659
|
+
},
|
|
1660
|
+
className: `${this.player.options.classPrefix}-transcript-style-range`
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
input.addEventListener('input', (e) => {
|
|
1664
|
+
const value = parseFloat(e.target.value);
|
|
1665
|
+
this.transcriptStyle[property] = value;
|
|
1666
|
+
valueDisplay.textContent = Math.round(value * 100) + '%';
|
|
1667
|
+
this.applyTranscriptStyles();
|
|
1668
|
+
this.savePreferences();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
const inputContainer = DOMUtils.createElement('div', {
|
|
1672
|
+
className: `${this.player.options.classPrefix}-transcript-style-range-container`
|
|
1673
|
+
});
|
|
1674
|
+
inputContainer.appendChild(input);
|
|
1675
|
+
inputContainer.appendChild(valueDisplay);
|
|
1676
|
+
|
|
1677
|
+
group.appendChild(labelEl);
|
|
1678
|
+
group.appendChild(inputContainer);
|
|
1679
|
+
return group;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Save transcript preferences to localStorage
|
|
1684
|
+
*/
|
|
1685
|
+
savePreferences() {
|
|
1686
|
+
this.storage.saveTranscriptPreferences(this.transcriptStyle);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Apply transcript styles
|
|
1691
|
+
*/
|
|
1692
|
+
applyTranscriptStyles() {
|
|
1693
|
+
if (!this.transcriptWindow) return;
|
|
1694
|
+
|
|
1695
|
+
// Apply to transcript window background
|
|
1696
|
+
this.transcriptWindow.style.backgroundColor = this.transcriptStyle.backgroundColor;
|
|
1697
|
+
this.transcriptWindow.style.opacity = String(this.transcriptStyle.opacity);
|
|
1698
|
+
|
|
1699
|
+
// Apply to content area
|
|
1700
|
+
if (this.transcriptContent) {
|
|
1701
|
+
this.transcriptContent.style.fontSize = this.transcriptStyle.fontSize;
|
|
1702
|
+
this.transcriptContent.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1703
|
+
this.transcriptContent.style.color = this.transcriptStyle.color;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Apply to all text entries (important: override CSS defaults)
|
|
1707
|
+
const textEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-text`);
|
|
1708
|
+
textEntries.forEach(entry => {
|
|
1709
|
+
entry.style.fontSize = this.transcriptStyle.fontSize;
|
|
1710
|
+
entry.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1711
|
+
entry.style.color = this.transcriptStyle.color;
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
// Apply to timestamp entries as well
|
|
1715
|
+
const timeEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-time`);
|
|
1716
|
+
timeEntries.forEach(entry => {
|
|
1717
|
+
entry.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Cleanup
|
|
1723
|
+
*/
|
|
1724
|
+
destroy() {
|
|
1725
|
+
// Disable modes if active
|
|
1726
|
+
if (this.resizeEnabled) {
|
|
1727
|
+
this.disableResizeHandles();
|
|
1728
|
+
}
|
|
1729
|
+
if (this.keyboardDragMode) {
|
|
1730
|
+
this.disableKeyboardDragMode();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Remove timeupdate listener from player
|
|
1734
|
+
if (this.handlers.timeupdate) {
|
|
1735
|
+
this.player.off('timeupdate', this.handlers.timeupdate);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Remove drag event listeners
|
|
1739
|
+
if (this.transcriptHeader) {
|
|
1740
|
+
if (this.handlers.mousedown) {
|
|
1741
|
+
this.transcriptHeader.removeEventListener('mousedown', this.handlers.mousedown);
|
|
1742
|
+
}
|
|
1743
|
+
if (this.handlers.touchstart) {
|
|
1744
|
+
this.transcriptHeader.removeEventListener('touchstart', this.handlers.touchstart);
|
|
1745
|
+
}
|
|
1746
|
+
if (this.handlers.keydown) {
|
|
1747
|
+
this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Remove settings button event listeners
|
|
1752
|
+
if (this.settingsButton) {
|
|
1753
|
+
if (this.handlers.settingsClick) {
|
|
1754
|
+
this.settingsButton.removeEventListener('click', this.handlers.settingsClick);
|
|
1755
|
+
}
|
|
1756
|
+
if (this.handlers.settingsKeydown) {
|
|
1757
|
+
this.settingsButton.removeEventListener('keydown', this.handlers.settingsKeydown);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Remove style dialog event listeners
|
|
1762
|
+
if (this.styleDialog && this.handlers.styleDialogKeydown) {
|
|
1763
|
+
this.styleDialog.removeEventListener('keydown', this.handlers.styleDialogKeydown);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Remove document-level listeners
|
|
1767
|
+
if (this.handlers.mousemove) {
|
|
1768
|
+
document.removeEventListener('mousemove', this.handlers.mousemove);
|
|
1769
|
+
}
|
|
1770
|
+
if (this.handlers.mouseup) {
|
|
1771
|
+
document.removeEventListener('mouseup', this.handlers.mouseup);
|
|
1772
|
+
}
|
|
1773
|
+
if (this.handlers.touchmove) {
|
|
1774
|
+
document.removeEventListener('touchmove', this.handlers.touchmove);
|
|
1775
|
+
}
|
|
1776
|
+
if (this.handlers.touchend) {
|
|
1777
|
+
document.removeEventListener('touchend', this.handlers.touchend);
|
|
1778
|
+
}
|
|
1779
|
+
if (this.handlers.documentClick) {
|
|
1780
|
+
document.removeEventListener('click', this.handlers.documentClick);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Remove window-level listeners
|
|
1784
|
+
if (this.handlers.resize) {
|
|
1785
|
+
window.removeEventListener('resize', this.handlers.resize);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// Clear handlers
|
|
1789
|
+
this.handlers = null;
|
|
1790
|
+
|
|
1791
|
+
// Remove DOM element
|
|
1792
|
+
if (this.transcriptWindow && this.transcriptWindow.parentNode) {
|
|
1793
|
+
this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
this.transcriptWindow = null;
|
|
1797
|
+
this.transcriptHeader = null;
|
|
1798
|
+
this.transcriptContent = null;
|
|
1799
|
+
this.transcriptEntries = [];
|
|
1800
|
+
this.settingsMenu = null;
|
|
1801
|
+
this.styleDialog = null;
|
|
1802
|
+
}
|
|
1803
|
+
}
|