vidply 1.0.8 → 1.0.9
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/vidply.css +113 -44
- package/dist/vidply.esm.js +806 -70
- package/dist/vidply.esm.js.map +2 -2
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +7 -7
- package/dist/vidply.js +806 -70
- package/dist/vidply.js.map +2 -2
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +7 -7
- package/package.json +1 -1
- package/src/controls/TranscriptManager.js +328 -27
- package/src/core/Player.js +682 -56
- package/src/i18n/translations.js +10 -5
- package/src/icons/Icons.js +2 -2
- package/src/styles/vidply.css +113 -44
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"format": "esm"
|
|
12
12
|
},
|
|
13
13
|
"src/i18n/translations.js": {
|
|
14
|
-
"bytes":
|
|
14
|
+
"bytes": 22623,
|
|
15
15
|
"imports": [],
|
|
16
16
|
"format": "esm"
|
|
17
17
|
},
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"format": "esm"
|
|
101
101
|
},
|
|
102
102
|
"src/controls/TranscriptManager.js": {
|
|
103
|
-
"bytes":
|
|
103
|
+
"bytes": 69891,
|
|
104
104
|
"imports": [
|
|
105
105
|
{
|
|
106
106
|
"path": "src/utils/DOMUtils.js",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"format": "esm"
|
|
158
158
|
},
|
|
159
159
|
"src/core/Player.js": {
|
|
160
|
-
"bytes":
|
|
160
|
+
"bytes": 139352,
|
|
161
161
|
"imports": [
|
|
162
162
|
{
|
|
163
163
|
"path": "src/utils/EventEmitter.js",
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
"bytesInOutput": 1581
|
|
280
280
|
},
|
|
281
281
|
"src/i18n/translations.js": {
|
|
282
|
-
"bytesInOutput":
|
|
282
|
+
"bytesInOutput": 19801
|
|
283
283
|
},
|
|
284
284
|
"src/i18n/i18n.js": {
|
|
285
285
|
"bytesInOutput": 720
|
|
@@ -303,10 +303,10 @@
|
|
|
303
303
|
"bytesInOutput": 3738
|
|
304
304
|
},
|
|
305
305
|
"src/controls/TranscriptManager.js": {
|
|
306
|
-
"bytesInOutput":
|
|
306
|
+
"bytesInOutput": 36165
|
|
307
307
|
},
|
|
308
308
|
"src/core/Player.js": {
|
|
309
|
-
"bytesInOutput":
|
|
309
|
+
"bytesInOutput": 47003
|
|
310
310
|
},
|
|
311
311
|
"src/renderers/YouTubeRenderer.js": {
|
|
312
312
|
"bytesInOutput": 4140
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
"bytesInOutput": 8100
|
|
322
322
|
}
|
|
323
323
|
},
|
|
324
|
-
"bytes":
|
|
324
|
+
"bytes": 191533
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
}
|
package/package.json
CHANGED
|
@@ -49,9 +49,18 @@ export class TranscriptManager {
|
|
|
49
49
|
this.styleDialogVisible = false;
|
|
50
50
|
this.styleDialogJustOpened = false;
|
|
51
51
|
|
|
52
|
+
// Language selector state
|
|
53
|
+
this.languageSelector = null;
|
|
54
|
+
this.currentTranscriptLanguage = null;
|
|
55
|
+
this.availableTranscriptLanguages = [];
|
|
56
|
+
this.languageSelectorHandler = null;
|
|
57
|
+
|
|
52
58
|
// Load saved preferences from localStorage
|
|
53
59
|
const savedPreferences = this.storage.getTranscriptPreferences();
|
|
54
60
|
|
|
61
|
+
// Autoscroll state (default: true)
|
|
62
|
+
this.autoscrollEnabled = savedPreferences?.autoscroll !== undefined ? savedPreferences.autoscroll : true;
|
|
63
|
+
|
|
55
64
|
// Transcript styling options (with defaults, then player options, then saved preferences)
|
|
56
65
|
this.transcriptStyle = {
|
|
57
66
|
fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || '100%',
|
|
@@ -78,10 +87,16 @@ export class TranscriptManager {
|
|
|
78
87
|
styleDialogKeydown: null
|
|
79
88
|
};
|
|
80
89
|
|
|
90
|
+
// Timeout management (for cleanup)
|
|
91
|
+
this.timeouts = new Set();
|
|
92
|
+
|
|
81
93
|
this.init();
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
init() {
|
|
97
|
+
// Set up metadata handling immediately (independent of transcript display)
|
|
98
|
+
this.setupMetadataHandlingOnLoad();
|
|
99
|
+
|
|
85
100
|
// Listen for time updates to highlight active transcript entry
|
|
86
101
|
this.player.on('timeupdate', this.handlers.timeupdate);
|
|
87
102
|
|
|
@@ -89,7 +104,7 @@ export class TranscriptManager {
|
|
|
89
104
|
this.player.on('fullscreenchange', () => {
|
|
90
105
|
if (this.isVisible) {
|
|
91
106
|
// Add a small delay to ensure DOM has updated after fullscreen transition
|
|
92
|
-
|
|
107
|
+
this.setManagedTimeout(() => this.positionTranscript(), 100);
|
|
93
108
|
}
|
|
94
109
|
});
|
|
95
110
|
}
|
|
@@ -114,7 +129,7 @@ export class TranscriptManager {
|
|
|
114
129
|
this.isVisible = true;
|
|
115
130
|
|
|
116
131
|
// Focus the settings button for keyboard accessibility
|
|
117
|
-
|
|
132
|
+
this.setManagedTimeout(() => {
|
|
118
133
|
if (this.settingsButton) {
|
|
119
134
|
this.settingsButton.focus();
|
|
120
135
|
}
|
|
@@ -130,10 +145,10 @@ export class TranscriptManager {
|
|
|
130
145
|
if (this.transcriptWindow) {
|
|
131
146
|
this.transcriptWindow.style.display = 'flex';
|
|
132
147
|
// Re-position after showing (in case window was resized while hidden)
|
|
133
|
-
|
|
148
|
+
this.setManagedTimeout(() => this.positionTranscript(), 0);
|
|
134
149
|
|
|
135
150
|
// Focus the settings button for keyboard accessibility
|
|
136
|
-
|
|
151
|
+
this.setManagedTimeout(() => {
|
|
137
152
|
if (this.settingsButton) {
|
|
138
153
|
this.settingsButton.focus();
|
|
139
154
|
}
|
|
@@ -227,8 +242,49 @@ export class TranscriptManager {
|
|
|
227
242
|
textContent: i18n.t('transcript.title')
|
|
228
243
|
});
|
|
229
244
|
|
|
245
|
+
// Autoscroll checkbox
|
|
246
|
+
const autoscrollLabel = DOMUtils.createElement('label', {
|
|
247
|
+
className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
|
|
248
|
+
attributes: {
|
|
249
|
+
'title': i18n.t('transcript.autoscroll')
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this.autoscrollCheckbox = DOMUtils.createElement('input', {
|
|
254
|
+
attributes: {
|
|
255
|
+
'type': 'checkbox',
|
|
256
|
+
'checked': this.autoscrollEnabled,
|
|
257
|
+
'aria-label': i18n.t('transcript.autoscroll')
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const autoscrollText = DOMUtils.createElement('span', {
|
|
262
|
+
textContent: i18n.t('transcript.autoscroll'),
|
|
263
|
+
className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
autoscrollLabel.appendChild(this.autoscrollCheckbox);
|
|
267
|
+
autoscrollLabel.appendChild(autoscrollText);
|
|
268
|
+
|
|
269
|
+
// Handle autoscroll checkbox change
|
|
270
|
+
this.autoscrollCheckbox.addEventListener('change', (e) => {
|
|
271
|
+
this.autoscrollEnabled = e.target.checked;
|
|
272
|
+
this.saveAutoscrollPreference();
|
|
273
|
+
});
|
|
274
|
+
|
|
230
275
|
this.headerLeft.appendChild(this.settingsButton);
|
|
231
276
|
this.headerLeft.appendChild(title);
|
|
277
|
+
this.headerLeft.appendChild(autoscrollLabel);
|
|
278
|
+
|
|
279
|
+
// Language selector (will be populated after tracks are loaded)
|
|
280
|
+
this.languageSelector = DOMUtils.createElement('select', {
|
|
281
|
+
className: `${this.player.options.classPrefix}-transcript-language-select`,
|
|
282
|
+
attributes: {
|
|
283
|
+
'aria-label': i18n.t('settings.language') || 'Language',
|
|
284
|
+
'style': 'display: none;' // Hidden until we detect multiple languages
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
this.headerLeft.appendChild(this.languageSelector);
|
|
232
288
|
|
|
233
289
|
const closeButton = DOMUtils.createElement('button', {
|
|
234
290
|
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
@@ -300,8 +356,10 @@ export class TranscriptManager {
|
|
|
300
356
|
// Re-position on window resize (debounced)
|
|
301
357
|
let resizeTimeout;
|
|
302
358
|
this.handlers.resize = () => {
|
|
303
|
-
|
|
304
|
-
|
|
359
|
+
if (resizeTimeout) {
|
|
360
|
+
this.clearManagedTimeout(resizeTimeout);
|
|
361
|
+
}
|
|
362
|
+
resizeTimeout = this.setManagedTimeout(() => this.positionTranscript(), 100);
|
|
305
363
|
};
|
|
306
364
|
window.addEventListener('resize', this.handlers.resize);
|
|
307
365
|
}
|
|
@@ -388,6 +446,84 @@ export class TranscriptManager {
|
|
|
388
446
|
}
|
|
389
447
|
}
|
|
390
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Get available transcript languages from tracks
|
|
451
|
+
*/
|
|
452
|
+
getAvailableTranscriptLanguages() {
|
|
453
|
+
const textTracks = this.player.textTracks;
|
|
454
|
+
const languages = new Map();
|
|
455
|
+
|
|
456
|
+
// Collect all caption/subtitle tracks with their languages
|
|
457
|
+
textTracks.forEach(track => {
|
|
458
|
+
if ((track.kind === 'captions' || track.kind === 'subtitles') && track.language) {
|
|
459
|
+
if (!languages.has(track.language)) {
|
|
460
|
+
languages.set(track.language, {
|
|
461
|
+
language: track.language,
|
|
462
|
+
label: track.label || track.language,
|
|
463
|
+
track: track
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return Array.from(languages.values());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Update language selector dropdown
|
|
474
|
+
*/
|
|
475
|
+
updateLanguageSelector() {
|
|
476
|
+
if (!this.languageSelector) return;
|
|
477
|
+
|
|
478
|
+
this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
|
|
479
|
+
|
|
480
|
+
// Clear existing options
|
|
481
|
+
this.languageSelector.innerHTML = '';
|
|
482
|
+
|
|
483
|
+
// Only show selector if there are 2+ languages
|
|
484
|
+
if (this.availableTranscriptLanguages.length < 2) {
|
|
485
|
+
this.languageSelector.style.display = 'none';
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Show selector and populate options
|
|
490
|
+
this.languageSelector.style.display = 'block';
|
|
491
|
+
|
|
492
|
+
this.availableTranscriptLanguages.forEach((langInfo, index) => {
|
|
493
|
+
const option = DOMUtils.createElement('option', {
|
|
494
|
+
textContent: langInfo.label,
|
|
495
|
+
attributes: {
|
|
496
|
+
'value': langInfo.language
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.languageSelector.appendChild(option);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Set current selection
|
|
503
|
+
if (this.currentTranscriptLanguage) {
|
|
504
|
+
this.languageSelector.value = this.currentTranscriptLanguage;
|
|
505
|
+
} else if (this.availableTranscriptLanguages.length > 0) {
|
|
506
|
+
// Default to first language or active track
|
|
507
|
+
const activeTrack = this.player.textTracks.find(
|
|
508
|
+
track => (track.kind === 'captions' || track.kind === 'subtitles') && track.mode === 'showing'
|
|
509
|
+
);
|
|
510
|
+
this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
|
|
511
|
+
this.languageSelector.value = this.currentTranscriptLanguage;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Remove existing change listener if any
|
|
515
|
+
if (this.languageSelectorHandler) {
|
|
516
|
+
this.languageSelector.removeEventListener('change', this.languageSelectorHandler);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Handle language change
|
|
520
|
+
this.languageSelectorHandler = (e) => {
|
|
521
|
+
this.currentTranscriptLanguage = e.target.value;
|
|
522
|
+
this.loadTranscriptData();
|
|
523
|
+
};
|
|
524
|
+
this.languageSelector.addEventListener('change', this.languageSelectorHandler);
|
|
525
|
+
}
|
|
526
|
+
|
|
391
527
|
/**
|
|
392
528
|
* Load transcript data from caption/subtitle tracks
|
|
393
529
|
*/
|
|
@@ -396,13 +532,39 @@ export class TranscriptManager {
|
|
|
396
532
|
this.transcriptContent.innerHTML = '';
|
|
397
533
|
|
|
398
534
|
// Get all text tracks
|
|
399
|
-
const textTracks =
|
|
535
|
+
const textTracks = this.player.textTracks;
|
|
536
|
+
|
|
537
|
+
// Find track for selected language, or default to first available
|
|
538
|
+
let captionTrack = null;
|
|
539
|
+
if (this.currentTranscriptLanguage) {
|
|
540
|
+
captionTrack = textTracks.find(
|
|
541
|
+
track => (track.kind === 'captions' || track.kind === 'subtitles') &&
|
|
542
|
+
track.language === this.currentTranscriptLanguage
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Fallback to first available caption/subtitle track
|
|
547
|
+
if (!captionTrack) {
|
|
548
|
+
captionTrack = textTracks.find(
|
|
549
|
+
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
550
|
+
);
|
|
551
|
+
if (captionTrack) {
|
|
552
|
+
this.currentTranscriptLanguage = captionTrack.language;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Find description track matching the selected language
|
|
557
|
+
let descriptionTrack = null;
|
|
558
|
+
if (this.currentTranscriptLanguage) {
|
|
559
|
+
descriptionTrack = textTracks.find(
|
|
560
|
+
track => track.kind === 'descriptions' && track.language === this.currentTranscriptLanguage
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
// Fallback to first available description track if no match found
|
|
564
|
+
if (!descriptionTrack) {
|
|
565
|
+
descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
566
|
+
}
|
|
400
567
|
|
|
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
568
|
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
407
569
|
|
|
408
570
|
// We need at least one track type
|
|
@@ -443,7 +605,7 @@ export class TranscriptManager {
|
|
|
443
605
|
});
|
|
444
606
|
|
|
445
607
|
// Fallback timeout
|
|
446
|
-
|
|
608
|
+
this.setManagedTimeout(() => {
|
|
447
609
|
this.loadTranscriptData();
|
|
448
610
|
}, 500);
|
|
449
611
|
|
|
@@ -489,28 +651,84 @@ export class TranscriptManager {
|
|
|
489
651
|
|
|
490
652
|
// Apply current styles to newly loaded entries
|
|
491
653
|
this.applyTranscriptStyles();
|
|
654
|
+
|
|
655
|
+
// Update language selector after loading
|
|
656
|
+
this.updateLanguageSelector();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Setup metadata handling on player load
|
|
661
|
+
* This runs independently of transcript loading
|
|
662
|
+
*/
|
|
663
|
+
setupMetadataHandlingOnLoad() {
|
|
664
|
+
// Wait for metadata to be loaded
|
|
665
|
+
const setupMetadata = () => {
|
|
666
|
+
const textTracks = this.player.textTracks;
|
|
667
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
668
|
+
|
|
669
|
+
if (metadataTrack) {
|
|
670
|
+
// Enable the metadata track so cuechange events fire
|
|
671
|
+
// Use 'hidden' mode so it doesn't display anything, but events still work
|
|
672
|
+
if (metadataTrack.mode === 'disabled') {
|
|
673
|
+
metadataTrack.mode = 'hidden';
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Check if we already added the listener
|
|
677
|
+
if (this.metadataCueChangeHandler) {
|
|
678
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Add event listener for cue changes
|
|
682
|
+
this.metadataCueChangeHandler = () => {
|
|
683
|
+
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
684
|
+
if (activeCues.length > 0) {
|
|
685
|
+
// Debug logging (can be removed in production)
|
|
686
|
+
if (this.player.options.debug) {
|
|
687
|
+
console.log('[VidPly Metadata] Active cues:', activeCues.map(c => ({
|
|
688
|
+
start: c.startTime,
|
|
689
|
+
end: c.endTime,
|
|
690
|
+
text: c.text
|
|
691
|
+
})));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
activeCues.forEach(cue => {
|
|
695
|
+
this.handleMetadataCue(cue);
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
metadataTrack.addEventListener('cuechange', this.metadataCueChangeHandler);
|
|
700
|
+
|
|
701
|
+
// Debug: Log metadata track setup
|
|
702
|
+
if (this.player.options.debug) {
|
|
703
|
+
const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
|
|
704
|
+
console.log('[VidPly Metadata] Track enabled,', cueCount, 'cues available');
|
|
705
|
+
}
|
|
706
|
+
} else if (this.player.options.debug) {
|
|
707
|
+
console.warn('[VidPly Metadata] No metadata track found');
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Try immediately
|
|
712
|
+
setupMetadata();
|
|
713
|
+
|
|
714
|
+
// Also try after loadedmetadata event
|
|
715
|
+
this.player.on('loadedmetadata', setupMetadata);
|
|
492
716
|
}
|
|
493
717
|
|
|
494
718
|
/**
|
|
495
719
|
* Setup metadata handling
|
|
496
720
|
* Metadata cues are not displayed but can be used programmatically
|
|
721
|
+
* This is called when transcript data is loaded (for storing cues)
|
|
497
722
|
*/
|
|
498
723
|
setupMetadataHandling() {
|
|
499
724
|
if (!this.metadataCues || this.metadataCues.length === 0) {
|
|
500
725
|
return;
|
|
501
726
|
}
|
|
502
727
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
});
|
|
728
|
+
// The actual event handling is set up in setupMetadataHandlingOnLoad()
|
|
729
|
+
// This method just stores the cues for reference
|
|
730
|
+
if (this.player.options.debug) {
|
|
731
|
+
console.log('[VidPly Metadata]', this.metadataCues.length, 'cues stored from transcript load');
|
|
514
732
|
}
|
|
515
733
|
}
|
|
516
734
|
|
|
@@ -521,6 +739,14 @@ export class TranscriptManager {
|
|
|
521
739
|
handleMetadataCue(cue) {
|
|
522
740
|
const text = cue.text.trim();
|
|
523
741
|
|
|
742
|
+
// Debug logging
|
|
743
|
+
if (this.player.options.debug) {
|
|
744
|
+
console.log('[VidPly Metadata] Processing cue:', {
|
|
745
|
+
time: cue.startTime,
|
|
746
|
+
text: text
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
524
750
|
// Emit a generic metadata event that developers can listen to
|
|
525
751
|
this.player.emit('metadata', {
|
|
526
752
|
time: cue.startTime,
|
|
@@ -531,16 +757,39 @@ export class TranscriptManager {
|
|
|
531
757
|
|
|
532
758
|
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
533
759
|
if (text.includes('PAUSE')) {
|
|
534
|
-
//
|
|
760
|
+
// Automatically pause the video
|
|
761
|
+
if (!this.player.state.paused) {
|
|
762
|
+
if (this.player.options.debug) {
|
|
763
|
+
console.log('[VidPly Metadata] Pausing video at', cue.startTime);
|
|
764
|
+
}
|
|
765
|
+
this.player.pause();
|
|
766
|
+
}
|
|
767
|
+
// Also emit event for developers who want to listen
|
|
535
768
|
this.player.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
536
769
|
}
|
|
537
770
|
|
|
538
771
|
// Parse for focus directives
|
|
539
772
|
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
540
773
|
if (focusMatch) {
|
|
774
|
+
const targetSelector = focusMatch[1];
|
|
775
|
+
// Automatically focus the target element
|
|
776
|
+
const targetElement = document.querySelector(targetSelector);
|
|
777
|
+
if (targetElement) {
|
|
778
|
+
if (this.player.options.debug) {
|
|
779
|
+
console.log('[VidPly Metadata] Focusing element:', targetSelector);
|
|
780
|
+
}
|
|
781
|
+
// Use setTimeout to ensure DOM is ready
|
|
782
|
+
this.setManagedTimeout(() => {
|
|
783
|
+
targetElement.focus();
|
|
784
|
+
}, 10);
|
|
785
|
+
} else if (this.player.options.debug) {
|
|
786
|
+
console.warn('[VidPly Metadata] Element not found:', targetSelector);
|
|
787
|
+
}
|
|
788
|
+
// Also emit event for developers who want to listen
|
|
541
789
|
this.player.emit('metadata:focus', {
|
|
542
790
|
time: cue.startTime,
|
|
543
|
-
target:
|
|
791
|
+
target: targetSelector,
|
|
792
|
+
element: targetElement,
|
|
544
793
|
text: text
|
|
545
794
|
});
|
|
546
795
|
}
|
|
@@ -548,6 +797,9 @@ export class TranscriptManager {
|
|
|
548
797
|
// Parse for hashtag references
|
|
549
798
|
const hashtags = text.match(/#[\w-]+/g);
|
|
550
799
|
if (hashtags) {
|
|
800
|
+
if (this.player.options.debug) {
|
|
801
|
+
console.log('[VidPly Metadata] Hashtags found:', hashtags);
|
|
802
|
+
}
|
|
551
803
|
this.player.emit('metadata:hashtags', {
|
|
552
804
|
time: cue.startTime,
|
|
553
805
|
hashtags: hashtags,
|
|
@@ -668,7 +920,7 @@ export class TranscriptManager {
|
|
|
668
920
|
* Scroll transcript window to show active entry
|
|
669
921
|
*/
|
|
670
922
|
scrollToEntry(entryElement) {
|
|
671
|
-
if (!this.transcriptContent) return;
|
|
923
|
+
if (!this.transcriptContent || !this.autoscrollEnabled) return;
|
|
672
924
|
|
|
673
925
|
const contentRect = this.transcriptContent.getBoundingClientRect();
|
|
674
926
|
const entryRect = entryElement.getBoundingClientRect();
|
|
@@ -683,6 +935,15 @@ export class TranscriptManager {
|
|
|
683
935
|
});
|
|
684
936
|
}
|
|
685
937
|
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Save autoscroll preference to localStorage
|
|
941
|
+
*/
|
|
942
|
+
saveAutoscrollPreference() {
|
|
943
|
+
const savedPreferences = this.storage.getTranscriptPreferences() || {};
|
|
944
|
+
savedPreferences.autoscroll = this.autoscrollEnabled;
|
|
945
|
+
this.storage.saveTranscriptPreferences(savedPreferences);
|
|
946
|
+
}
|
|
686
947
|
|
|
687
948
|
/**
|
|
688
949
|
* Setup drag and drop functionality
|
|
@@ -702,6 +963,11 @@ export class TranscriptManager {
|
|
|
702
963
|
return;
|
|
703
964
|
}
|
|
704
965
|
|
|
966
|
+
// Don't drag if clicking on language selector
|
|
967
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
705
971
|
// Don't drag if clicking on settings menu
|
|
706
972
|
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
707
973
|
return;
|
|
@@ -738,6 +1004,11 @@ export class TranscriptManager {
|
|
|
738
1004
|
return;
|
|
739
1005
|
}
|
|
740
1006
|
|
|
1007
|
+
// Don't drag if touching language selector
|
|
1008
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
741
1012
|
// Don't drag if touching settings menu
|
|
742
1013
|
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
743
1014
|
return;
|
|
@@ -1718,6 +1989,32 @@ export class TranscriptManager {
|
|
|
1718
1989
|
});
|
|
1719
1990
|
}
|
|
1720
1991
|
|
|
1992
|
+
/**
|
|
1993
|
+
* Set a managed timeout that will be cleaned up on destroy
|
|
1994
|
+
* @param {Function} callback - Callback function
|
|
1995
|
+
* @param {number} delay - Delay in milliseconds
|
|
1996
|
+
* @returns {number} Timeout ID
|
|
1997
|
+
*/
|
|
1998
|
+
setManagedTimeout(callback, delay) {
|
|
1999
|
+
const timeoutId = setTimeout(() => {
|
|
2000
|
+
this.timeouts.delete(timeoutId);
|
|
2001
|
+
callback();
|
|
2002
|
+
}, delay);
|
|
2003
|
+
this.timeouts.add(timeoutId);
|
|
2004
|
+
return timeoutId;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Clear a managed timeout
|
|
2009
|
+
* @param {number} timeoutId - Timeout ID to clear
|
|
2010
|
+
*/
|
|
2011
|
+
clearManagedTimeout(timeoutId) {
|
|
2012
|
+
if (timeoutId) {
|
|
2013
|
+
clearTimeout(timeoutId);
|
|
2014
|
+
this.timeouts.delete(timeoutId);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1721
2018
|
/**
|
|
1722
2019
|
* Cleanup
|
|
1723
2020
|
*/
|
|
@@ -1785,6 +2082,10 @@ export class TranscriptManager {
|
|
|
1785
2082
|
window.removeEventListener('resize', this.handlers.resize);
|
|
1786
2083
|
}
|
|
1787
2084
|
|
|
2085
|
+
// Cleanup all managed timeouts
|
|
2086
|
+
this.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
2087
|
+
this.timeouts.clear();
|
|
2088
|
+
|
|
1788
2089
|
// Clear handlers
|
|
1789
2090
|
this.handlers = null;
|
|
1790
2091
|
|