myetv-player 1.1.2 → 1.1.4
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/build.js +9 -3
- package/css/myetv-player.css +44 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +296 -120
- package/dist/myetv-player.min.js +5405 -5719
- package/package.json +3 -1
- package/plugins/youtube/myetv-player-youtube-plugin.js +378 -130
- package/scss/_menus.scss +49 -0
- package/src/controls.js +122 -51
- package/src/subtitles.js +174 -69
package/scss/_menus.scss
CHANGED
|
@@ -381,3 +381,52 @@
|
|
|
381
381
|
display: none !important;
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
// Expandable settings menu styles
|
|
385
|
+
.settings-expandable-wrapper {
|
|
386
|
+
position: relative;
|
|
387
|
+
display: block;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.settings-option.expandable-trigger {
|
|
391
|
+
display: flex;
|
|
392
|
+
justify-content: space-between;
|
|
393
|
+
align-items: center;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
font-size: 10px !important;
|
|
396
|
+
|
|
397
|
+
.settings-option-label {
|
|
398
|
+
font-size: 10px !important;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.expand-arrow {
|
|
402
|
+
font-size: 8px;
|
|
403
|
+
transition: transform 0.2s ease;
|
|
404
|
+
margin-left: 8px;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.settings-expandable-content {
|
|
409
|
+
padding-left: 15px;
|
|
410
|
+
margin-top: 4px;
|
|
411
|
+
|
|
412
|
+
.settings-suboption {
|
|
413
|
+
padding: 6px 12px;
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
color: white;
|
|
416
|
+
font-size: 10px;
|
|
417
|
+
white-space: normal;
|
|
418
|
+
word-wrap: break-word;
|
|
419
|
+
opacity: 0.8;
|
|
420
|
+
transition: opacity 0.2s;
|
|
421
|
+
|
|
422
|
+
&.active {
|
|
423
|
+
opacity: 1;
|
|
424
|
+
font-weight: bold;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
&:hover {
|
|
428
|
+
opacity: 1;
|
|
429
|
+
background: rgba(255, 255, 255, 0.1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
package/src/controls.js
CHANGED
|
@@ -600,7 +600,9 @@ updateSettingsMenuVisibility() {
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Populate settings menu with controls
|
|
605
|
+
*/
|
|
604
606
|
populateSettingsMenu() {
|
|
605
607
|
const settingsMenu = this.controls?.querySelector('.settings-menu');
|
|
606
608
|
if (!settingsMenu) return;
|
|
@@ -615,54 +617,104 @@ populateSettingsMenu() {
|
|
|
615
617
|
</div>`;
|
|
616
618
|
}
|
|
617
619
|
|
|
618
|
-
// Speed Control
|
|
620
|
+
// Speed Control - expandable
|
|
619
621
|
if (this.options.showSpeedControl) {
|
|
620
622
|
const speedLabel = this.t('playback_speed') || 'Playback Speed';
|
|
621
623
|
const currentSpeed = this.video ? this.video.playbackRate : 1;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
<
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
<div class="settings-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
624
|
+
|
|
625
|
+
menuHTML += `
|
|
626
|
+
<div class="settings-expandable-wrapper">
|
|
627
|
+
<div class="settings-option expandable-trigger" data-action="speed-expand">
|
|
628
|
+
<span class="settings-option-label">${speedLabel}: ${currentSpeed}x</span>
|
|
629
|
+
<span class="expand-arrow">▼</span>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="settings-expandable-content" style="display: none;">`;
|
|
632
|
+
|
|
633
|
+
const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
634
|
+
speeds.forEach(speed => {
|
|
635
|
+
const isActive = Math.abs(speed - currentSpeed) < 0.01;
|
|
636
|
+
menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-speed="${speed}">${speed}x</div>`;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
menuHTML += `</div></div>`;
|
|
634
640
|
}
|
|
635
641
|
|
|
636
|
-
// Subtitles
|
|
642
|
+
// Subtitles - expandable
|
|
637
643
|
if (this.options.showSubtitles && this.textTracks && this.textTracks.length > 0) {
|
|
638
644
|
const subtitlesLabel = this.t('subtitles') || 'Subtitles';
|
|
639
645
|
const currentTrack = this.currentSubtitleTrack;
|
|
640
|
-
const currentLabel = this.subtitlesEnabled && currentTrack
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
<div class="settings-
|
|
649
|
-
${this.t('subtitlesoff') || 'Off'}
|
|
650
|
-
</div>`;
|
|
646
|
+
const currentLabel = this.subtitlesEnabled && currentTrack ? currentTrack.label : (this.t('subtitlesoff') || 'Off');
|
|
647
|
+
|
|
648
|
+
menuHTML += `
|
|
649
|
+
<div class="settings-expandable-wrapper">
|
|
650
|
+
<div class="settings-option expandable-trigger" data-action="subtitles-expand">
|
|
651
|
+
<span class="settings-option-label">${subtitlesLabel}: ${currentLabel}</span>
|
|
652
|
+
<span class="expand-arrow">▼</span>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="settings-expandable-content" style="display: none;">`;
|
|
651
655
|
|
|
656
|
+
// Off option
|
|
657
|
+
menuHTML += `<div class="settings-suboption ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
|
|
658
|
+
|
|
659
|
+
// Subtitle tracks
|
|
652
660
|
this.textTracks.forEach((trackData, index) => {
|
|
653
661
|
const isActive = this.currentSubtitleTrack === trackData.track;
|
|
654
|
-
menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}"
|
|
655
|
-
${trackData.label}
|
|
656
|
-
</div>`;
|
|
662
|
+
menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
|
|
657
663
|
});
|
|
658
664
|
|
|
659
|
-
menuHTML +=
|
|
665
|
+
menuHTML += `</div></div>`;
|
|
660
666
|
}
|
|
661
667
|
|
|
662
668
|
settingsMenu.innerHTML = menuHTML;
|
|
669
|
+
|
|
670
|
+
// Add scrollbar if needed
|
|
671
|
+
this.addSettingsMenuScrollbar();
|
|
663
672
|
}
|
|
664
673
|
|
|
665
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Add scrollbar to settings menu on mobile
|
|
676
|
+
*/
|
|
677
|
+
addSettingsMenuScrollbar() {
|
|
678
|
+
const settingsMenu = this.controls?.querySelector('.settings-menu');
|
|
679
|
+
if (!settingsMenu) return;
|
|
680
|
+
|
|
681
|
+
const playerHeight = this.container.offsetHeight;
|
|
682
|
+
const maxMenuHeight = playerHeight - 100;
|
|
683
|
+
|
|
684
|
+
settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
|
|
685
|
+
settingsMenu.style.overflowY = 'auto';
|
|
686
|
+
settingsMenu.style.overflowX = 'hidden';
|
|
687
|
+
|
|
688
|
+
// Add scrollbar styling
|
|
689
|
+
if (!document.getElementById('player-settings-scrollbar-style')) {
|
|
690
|
+
const scrollbarStyle = document.createElement('style');
|
|
691
|
+
scrollbarStyle.id = 'player-settings-scrollbar-style';
|
|
692
|
+
scrollbarStyle.textContent = `
|
|
693
|
+
.settings-menu::-webkit-scrollbar {
|
|
694
|
+
width: 6px;
|
|
695
|
+
}
|
|
696
|
+
.settings-menu::-webkit-scrollbar-track {
|
|
697
|
+
background: rgba(255,255,255,0.05);
|
|
698
|
+
border-radius: 3px;
|
|
699
|
+
}
|
|
700
|
+
.settings-menu::-webkit-scrollbar-thumb {
|
|
701
|
+
background: rgba(255,255,255,0.3);
|
|
702
|
+
border-radius: 3px;
|
|
703
|
+
}
|
|
704
|
+
.settings-menu::-webkit-scrollbar-thumb:hover {
|
|
705
|
+
background: rgba(255,255,255,0.5);
|
|
706
|
+
}
|
|
707
|
+
`;
|
|
708
|
+
document.head.appendChild(scrollbarStyle);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
settingsMenu.style.scrollbarWidth = 'thin';
|
|
712
|
+
settingsMenu.style.scrollbarColor = 'rgba(255,255,255,0.3) transparent';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Bind settings menu events
|
|
717
|
+
*/
|
|
666
718
|
bindSettingsMenuEvents() {
|
|
667
719
|
const settingsMenu = this.controls?.querySelector('.settings-menu');
|
|
668
720
|
if (!settingsMenu) return;
|
|
@@ -670,7 +722,26 @@ bindSettingsMenuEvents() {
|
|
|
670
722
|
settingsMenu.addEventListener('click', (e) => {
|
|
671
723
|
e.stopPropagation();
|
|
672
724
|
|
|
673
|
-
// Handle
|
|
725
|
+
// Handle expandable triggers
|
|
726
|
+
if (e.target.classList.contains('expandable-trigger') || e.target.closest('.expandable-trigger')) {
|
|
727
|
+
const trigger = e.target.classList.contains('expandable-trigger') ? e.target : e.target.closest('.expandable-trigger');
|
|
728
|
+
const wrapper = trigger.closest('.settings-expandable-wrapper');
|
|
729
|
+
const content = wrapper.querySelector('.settings-expandable-content');
|
|
730
|
+
const arrow = trigger.querySelector('.expand-arrow');
|
|
731
|
+
|
|
732
|
+
const isExpanded = content.style.display !== 'none';
|
|
733
|
+
|
|
734
|
+
if (isExpanded) {
|
|
735
|
+
content.style.display = 'none';
|
|
736
|
+
arrow.style.transform = 'rotate(0deg)';
|
|
737
|
+
} else {
|
|
738
|
+
content.style.display = 'block';
|
|
739
|
+
arrow.style.transform = 'rotate(180deg)';
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Handle direct actions (like PiP)
|
|
674
745
|
if (e.target.classList.contains('settings-option') || e.target.closest('.settings-option')) {
|
|
675
746
|
const option = e.target.classList.contains('settings-option') ? e.target : e.target.closest('.settings-option');
|
|
676
747
|
const action = option.getAttribute('data-action');
|
|
@@ -683,32 +754,31 @@ bindSettingsMenuEvents() {
|
|
|
683
754
|
|
|
684
755
|
// Handle submenu actions
|
|
685
756
|
if (e.target.classList.contains('settings-suboption')) {
|
|
686
|
-
const
|
|
687
|
-
const
|
|
757
|
+
const wrapper = e.target.closest('.settings-expandable-wrapper');
|
|
758
|
+
const trigger = wrapper.querySelector('.expandable-trigger');
|
|
759
|
+
const action = trigger.getAttribute('data-action');
|
|
688
760
|
|
|
689
|
-
if (action === 'speed') {
|
|
761
|
+
if (action === 'speed-expand') {
|
|
690
762
|
const speed = parseFloat(e.target.getAttribute('data-speed'));
|
|
691
763
|
if (speed && speed > 0 && this.video && !this.isChangingQuality) {
|
|
692
764
|
this.video.playbackRate = speed;
|
|
693
765
|
|
|
694
766
|
// Update active states
|
|
695
|
-
|
|
696
|
-
opt.classList.remove('active');
|
|
697
|
-
});
|
|
767
|
+
wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
|
|
698
768
|
e.target.classList.add('active');
|
|
699
769
|
|
|
700
|
-
// Update
|
|
701
|
-
const
|
|
702
|
-
if (
|
|
770
|
+
// Update trigger text
|
|
771
|
+
const label = trigger.querySelector('.settings-option-label');
|
|
772
|
+
if (label) {
|
|
773
|
+
const speedLabel = this.t('playback_speed') || 'Playback Speed';
|
|
774
|
+
label.textContent = `${speedLabel}: ${speed}x`;
|
|
775
|
+
}
|
|
703
776
|
|
|
704
777
|
// Trigger event
|
|
705
778
|
this.triggerEvent('speedchange', { speed, previousSpeed: this.video.playbackRate });
|
|
706
779
|
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
else if (action === 'subtitles') {
|
|
780
|
+
} else if (action === 'subtitles-expand') {
|
|
710
781
|
const trackData = e.target.getAttribute('data-track');
|
|
711
|
-
|
|
712
782
|
if (trackData === 'off') {
|
|
713
783
|
this.disableSubtitles();
|
|
714
784
|
} else {
|
|
@@ -717,14 +787,15 @@ bindSettingsMenuEvents() {
|
|
|
717
787
|
}
|
|
718
788
|
|
|
719
789
|
// Update active states
|
|
720
|
-
|
|
721
|
-
opt.classList.remove('active');
|
|
722
|
-
});
|
|
790
|
+
wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
|
|
723
791
|
e.target.classList.add('active');
|
|
724
792
|
|
|
725
|
-
// Update
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
793
|
+
// Update trigger text
|
|
794
|
+
const label = trigger.querySelector('.settings-option-label');
|
|
795
|
+
if (label) {
|
|
796
|
+
const subtitlesLabel = this.t('subtitles') || 'Subtitles';
|
|
797
|
+
label.textContent = `${subtitlesLabel}: ${e.target.textContent}`;
|
|
798
|
+
}
|
|
728
799
|
}
|
|
729
800
|
}
|
|
730
801
|
});
|
package/src/subtitles.js
CHANGED
|
@@ -41,10 +41,10 @@ createCustomSubtitleOverlay() {
|
|
|
41
41
|
'bottom: 80px;' +
|
|
42
42
|
'left: 50%;' +
|
|
43
43
|
'transform: translateX(-50%);' +
|
|
44
|
-
'z-index:
|
|
44
|
+
'z-index: 999;' +
|
|
45
45
|
'color: white;' +
|
|
46
46
|
'font-family: Arial, sans-serif;' +
|
|
47
|
-
'font-size: clamp(12px, 4vw, 18px);' +
|
|
47
|
+
'font-size: clamp(12px, 4vw, 18px);' +
|
|
48
48
|
'font-weight: bold;' +
|
|
49
49
|
'text-align: center;' +
|
|
50
50
|
'text-shadow: 2px 2px 4px rgba(0, 0, 0, 1);' +
|
|
@@ -71,52 +71,29 @@ createCustomSubtitleOverlay() {
|
|
|
71
71
|
if (this.options.debug) console.log('✅ Custom subtitle overlay created with responsive settings');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
var trackElements = document.querySelectorAll('track[kind="subtitles"], track[kind="captions"]');
|
|
77
|
-
var loadPromises = [];
|
|
78
|
-
|
|
79
|
-
if (this.options.debug) console.log('📥 Loading ' + trackElements.length + ' subtitle files...');
|
|
80
|
-
|
|
81
|
-
for (var i = 0; i < trackElements.length; i++) {
|
|
82
|
-
var track = trackElements[i];
|
|
83
|
-
|
|
84
|
-
(function (trackElement, index) {
|
|
85
|
-
var promise = fetch(trackElement.src)
|
|
86
|
-
.then(function (response) {
|
|
87
|
-
return response.text();
|
|
88
|
-
})
|
|
89
|
-
.then(function (srtText) {
|
|
90
|
-
var subtitles = self.parseCustomSRT(srtText);
|
|
91
|
-
self.customSubtitles.push({
|
|
92
|
-
label: trackElement.label || 'Track ' + (index + 1),
|
|
93
|
-
language: trackElement.srclang || 'unknown',
|
|
94
|
-
subtitles: subtitles
|
|
95
|
-
});
|
|
74
|
+
customTimeToSeconds(timeString) {
|
|
75
|
+
if (!timeString) return 0;
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
.catch(function (error) {
|
|
102
|
-
if (self.options.debug) {
|
|
103
|
-
console.error('❌ Error loading ' + trackElement.src + ':', error);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
77
|
+
var parts = timeString.split(',');
|
|
78
|
+
if (parts.length !== 2) return 0;
|
|
106
79
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
80
|
+
var time = parts[0];
|
|
81
|
+
var millis = parts[1];
|
|
110
82
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.log('✅ All custom subtitle tracks loaded');
|
|
114
|
-
}
|
|
83
|
+
var timeParts = time.split(':');
|
|
84
|
+
if (timeParts.length !== 3) return 0;
|
|
115
85
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
86
|
+
var hours = parseInt(timeParts[0], 10);
|
|
87
|
+
var minutes = parseInt(timeParts[1], 10);
|
|
88
|
+
var seconds = parseInt(timeParts[2], 10);
|
|
89
|
+
var milliseconds = parseInt(millis, 10);
|
|
90
|
+
|
|
91
|
+
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || isNaN(milliseconds)) {
|
|
92
|
+
console.error('❌ customTimeToSeconds failed for:', timeString);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
|
120
97
|
}
|
|
121
98
|
|
|
122
99
|
parseCustomSRT(srtText) {
|
|
@@ -135,9 +112,9 @@ parseCustomSRT(srtText) {
|
|
|
135
112
|
if (timeMatch) {
|
|
136
113
|
var startTime = this.customTimeToSeconds(timeMatch[1]);
|
|
137
114
|
var endTime = this.customTimeToSeconds(timeMatch[2]);
|
|
138
|
-
var text =
|
|
115
|
+
var text = lines.slice(2).join('\n').trim().replace(/<[^>]*>/g, '');
|
|
139
116
|
|
|
140
|
-
if (text.length > 0 && startTime < endTime) {
|
|
117
|
+
if (text && text.length > 0 && startTime < endTime) {
|
|
141
118
|
subtitles.push({
|
|
142
119
|
start: startTime,
|
|
143
120
|
end: endTime,
|
|
@@ -148,20 +125,97 @@ parseCustomSRT(srtText) {
|
|
|
148
125
|
}
|
|
149
126
|
}
|
|
150
127
|
|
|
128
|
+
if (this.options.debug) console.log('✅ Parsed ' + subtitles.length + ' subtitles');
|
|
151
129
|
return subtitles;
|
|
152
130
|
}
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
var
|
|
156
|
-
var
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
132
|
+
loadCustomSubtitleTracks() {
|
|
133
|
+
var self = this;
|
|
134
|
+
var tracks = this.video.querySelectorAll('track[kind="subtitles"]');
|
|
135
|
+
if (tracks.length === 0) return;
|
|
136
|
+
|
|
137
|
+
tracks.forEach(function (track, index) {
|
|
138
|
+
var src = track.getAttribute('src');
|
|
139
|
+
var label = track.getAttribute('label') || 'Unknown';
|
|
140
|
+
var srclang = track.getAttribute('srclang') || '';
|
|
141
|
+
|
|
142
|
+
// CREA L'OGGETTO PRIMA E AGGIUNGILO SUBITO
|
|
143
|
+
var trackObj = {
|
|
144
|
+
label: label,
|
|
145
|
+
language: srclang,
|
|
146
|
+
subtitles: [],
|
|
147
|
+
trackIndex: index
|
|
148
|
+
};
|
|
149
|
+
self.customSubtitles.push(trackObj);
|
|
150
|
+
|
|
151
|
+
fetch(src)
|
|
152
|
+
.then(function (response) {
|
|
153
|
+
return response.text();
|
|
154
|
+
})
|
|
155
|
+
.then(function (srtText) {
|
|
156
|
+
var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
157
|
+
var blocks = normalizedText.trim().split('\n\n');
|
|
158
|
+
|
|
159
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
160
|
+
var block = blocks[i].trim();
|
|
161
|
+
if (!block) continue;
|
|
162
|
+
var lines = block.split('\n');
|
|
163
|
+
|
|
164
|
+
if (lines.length >= 3) {
|
|
165
|
+
var timeLine = lines[1].trim();
|
|
166
|
+
var timeMatch = timeLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/);
|
|
167
|
+
|
|
168
|
+
if (timeMatch) {
|
|
169
|
+
var startParts = timeMatch[1].split(',');
|
|
170
|
+
var startTimeParts = startParts[0].split(':');
|
|
171
|
+
var startTime = parseInt(startTimeParts[0], 10) * 3600 + parseInt(startTimeParts[1], 10) * 60 + parseInt(startTimeParts[2], 10) + parseInt(startParts[1], 10) / 1000;
|
|
172
|
+
|
|
173
|
+
var endParts = timeMatch[2].split(',');
|
|
174
|
+
var endTimeParts = endParts[0].split(':');
|
|
175
|
+
var endTime = parseInt(endTimeParts[0], 10) * 3600 + parseInt(endTimeParts[1], 10) * 60 + parseInt(endTimeParts[2], 10) + parseInt(endParts[1], 10) / 1000;
|
|
176
|
+
|
|
177
|
+
var text = lines.slice(2).join('\n').trim().replace(/<[^>]*>/g, '');
|
|
178
|
+
|
|
179
|
+
if (text && text.length > 0 && !isNaN(startTime) && !isNaN(endTime) && startTime < endTime) {
|
|
180
|
+
trackObj.subtitles.push({
|
|
181
|
+
start: startTime,
|
|
182
|
+
end: endTime,
|
|
183
|
+
text: text
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
163
189
|
|
|
164
|
-
|
|
190
|
+
if (self.options.debug) {
|
|
191
|
+
console.log('✅ Loaded ' + trackObj.subtitles.length + ' subtitles for ' + label);
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
.catch(function (error) {
|
|
195
|
+
console.error('❌ Error loading ' + label + ':', error);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
sanitizeSubtitleText(text) {
|
|
201
|
+
if (!text) return '';
|
|
202
|
+
|
|
203
|
+
// Remove HTML tags
|
|
204
|
+
var sanitized = text.replace(/<[^>]*>/g, '');
|
|
205
|
+
|
|
206
|
+
// Remove styling tags common in SRT files
|
|
207
|
+
sanitized = sanitized.replace(/{\\.*?}/g, '');
|
|
208
|
+
sanitized = sanitized.replace(/\\N/g, '\n');
|
|
209
|
+
|
|
210
|
+
// Clean up multiple spaces
|
|
211
|
+
sanitized = sanitized.replace(/\s+/g, ' ').trim();
|
|
212
|
+
|
|
213
|
+
// Decode HTML entities if present
|
|
214
|
+
var tempDiv = document.createElement('div');
|
|
215
|
+
tempDiv.innerHTML = sanitized;
|
|
216
|
+
sanitized = tempDiv.textContent || tempDiv.innerText || sanitized;
|
|
217
|
+
|
|
218
|
+
return sanitized;
|
|
165
219
|
}
|
|
166
220
|
|
|
167
221
|
enableCustomSubtitleTrack(trackIndex) {
|
|
@@ -254,28 +308,39 @@ detectTextTracks() {
|
|
|
254
308
|
enableSubtitleTrack(trackIndex) {
|
|
255
309
|
if (trackIndex < 0 || trackIndex >= this.textTracks.length) return;
|
|
256
310
|
|
|
311
|
+
// Disable all tracks first
|
|
257
312
|
this.disableAllTracks();
|
|
258
313
|
|
|
314
|
+
// Enable ONLY the custom subtitle system (not native browser)
|
|
259
315
|
var success = this.enableCustomSubtitleTrack(trackIndex);
|
|
260
316
|
|
|
261
317
|
if (success) {
|
|
262
318
|
this.currentSubtitleTrack = this.textTracks[trackIndex].track;
|
|
263
319
|
this.subtitlesEnabled = true;
|
|
264
320
|
|
|
321
|
+
// Make sure native tracks stay DISABLED
|
|
322
|
+
if (this.video.textTracks && this.video.textTracks[trackIndex]) {
|
|
323
|
+
this.video.textTracks[trackIndex].mode = 'disabled'; // Keep native disabled
|
|
324
|
+
}
|
|
325
|
+
|
|
265
326
|
this.updateSubtitlesButton();
|
|
266
327
|
this.populateSubtitlesMenu();
|
|
267
328
|
|
|
268
329
|
if (this.options.debug) {
|
|
269
|
-
console.log('✅
|
|
330
|
+
console.log('✅ Custom subtitles enabled:', this.textTracks[trackIndex].label);
|
|
270
331
|
}
|
|
271
332
|
|
|
272
|
-
// Trigger
|
|
333
|
+
// Trigger subtitle change event
|
|
273
334
|
this.triggerEvent('subtitlechange', {
|
|
274
335
|
enabled: true,
|
|
275
336
|
trackIndex: trackIndex,
|
|
276
337
|
trackLabel: this.textTracks[trackIndex].label,
|
|
277
338
|
trackLanguage: this.textTracks[trackIndex].language
|
|
278
339
|
});
|
|
340
|
+
} else {
|
|
341
|
+
if (this.options.debug) {
|
|
342
|
+
console.error('❌ Failed to enable custom subtitles for track', trackIndex);
|
|
343
|
+
}
|
|
279
344
|
}
|
|
280
345
|
}
|
|
281
346
|
|
|
@@ -298,11 +363,15 @@ disableSubtitles() {
|
|
|
298
363
|
}
|
|
299
364
|
|
|
300
365
|
disableAllTracks() {
|
|
301
|
-
if (this.video.textTracks)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
366
|
+
if (!this.video || !this.video.textTracks) return;
|
|
367
|
+
|
|
368
|
+
// Disable all native tracks
|
|
369
|
+
for (var i = 0; i < this.video.textTracks.length; i++) {
|
|
370
|
+
this.video.textTracks[i].mode = 'hidden';
|
|
305
371
|
}
|
|
372
|
+
|
|
373
|
+
// Also disable custom subtitles
|
|
374
|
+
this.disableCustomSubtitles();
|
|
306
375
|
}
|
|
307
376
|
|
|
308
377
|
getAvailableSubtitles() {
|
|
@@ -383,25 +452,61 @@ updateSubtitlesUI() {
|
|
|
383
452
|
bindSubtitleEvents() {
|
|
384
453
|
var self = this;
|
|
385
454
|
|
|
455
|
+
if (this.video.textTracks) {
|
|
456
|
+
this.isChangingSubtitles = false; // flag to prevent loops
|
|
457
|
+
|
|
458
|
+
this.video.textTracks.addEventListener('change', function () {
|
|
459
|
+
// ignore changes initiated by the player itself
|
|
460
|
+
if (self.isChangingSubtitles) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// only update ui
|
|
465
|
+
self.updateSubtitlesUI();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Add timeupdate listener for custom subtitle display
|
|
470
|
+
this.video.addEventListener('timeupdate', () => {
|
|
471
|
+
if (this.customSubtitlesEnabled) {
|
|
472
|
+
this.updateCustomSubtitleDisplay();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Menu click events
|
|
386
477
|
var subtitlesMenu = this.controls && this.controls.querySelector('.subtitles-menu');
|
|
387
478
|
if (subtitlesMenu) {
|
|
388
479
|
subtitlesMenu.addEventListener('click', function (e) {
|
|
389
|
-
|
|
480
|
+
var option = e.target.closest('.subtitles-option');
|
|
481
|
+
if (!option) return;
|
|
482
|
+
|
|
483
|
+
self.isChangingSubtitles = true; // active flag
|
|
484
|
+
|
|
485
|
+
var trackIndex = option.getAttribute('data-track');
|
|
486
|
+
if (trackIndex === 'off') {
|
|
487
|
+
self.disableSubtitles();
|
|
488
|
+
} else {
|
|
489
|
+
self.enableSubtitleTrack(parseInt(trackIndex));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
setTimeout(function () {
|
|
493
|
+
self.isChangingSubtitles = false; // disable flag
|
|
494
|
+
}, 100);
|
|
390
495
|
});
|
|
391
496
|
}
|
|
392
497
|
}
|
|
393
498
|
|
|
394
|
-
|
|
395
499
|
handleSubtitlesMenuClick(e) {
|
|
396
|
-
|
|
500
|
+
var option = e.target.closest('.subtitles-option');
|
|
501
|
+
if (!option) return; // This prevents button clicks from toggling
|
|
397
502
|
|
|
398
|
-
var
|
|
503
|
+
var trackIndex = option.getAttribute('data-track');
|
|
399
504
|
|
|
400
|
-
if (
|
|
505
|
+
if (trackIndex === 'off') {
|
|
401
506
|
this.disableSubtitles();
|
|
402
507
|
} else {
|
|
403
|
-
|
|
404
|
-
this.enableSubtitleTrack(trackIndex);
|
|
508
|
+
// Don't check for 'toggle' - just enable the track
|
|
509
|
+
this.enableSubtitleTrack(parseInt(trackIndex));
|
|
405
510
|
}
|
|
406
511
|
|
|
407
512
|
this.updateSubtitlesButton();
|