myetv-player 1.1.6 → 1.3.0
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/css/myetv-player.css +153 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +654 -129
- package/dist/myetv-player.min.js +579 -115
- package/package.json +35 -16
- package/plugins/twitch/myetv-player-twitch-plugin.js +125 -11
- package/plugins/vimeo/myetv-player-vimeo.js +80 -49
- package/plugins/youtube/README.md +5 -2
- package/plugins/youtube/myetv-player-youtube-plugin.js +891 -14
- package/.github/workflows/codeql.yml +0 -100
- package/.github/workflows/npm-publish.yml +0 -30
- package/SECURITY.md +0 -50
- package/build.js +0 -195
- package/scss/README.md +0 -161
- package/scss/_audio-player.scss +0 -21
- package/scss/_base.scss +0 -116
- package/scss/_controls.scss +0 -188
- package/scss/_loading.scss +0 -111
- package/scss/_menus.scss +0 -432
- package/scss/_mixins.scss +0 -112
- package/scss/_poster.scss +0 -8
- package/scss/_progress-bar.scss +0 -319
- package/scss/_resolution.scss +0 -68
- package/scss/_responsive.scss +0 -1360
- package/scss/_themes.scss +0 -30
- package/scss/_title-overlay.scss +0 -60
- package/scss/_tooltips.scss +0 -7
- package/scss/_variables.scss +0 -49
- package/scss/_video.scss +0 -221
- package/scss/_volume.scss +0 -122
- package/scss/_watermark.scss +0 -128
- package/scss/myetv-player.scss +0 -51
- package/scss/package.json +0 -16
- package/src/README.md +0 -560
- package/src/chapters.js +0 -521
- package/src/controls.js +0 -1243
- package/src/core.js +0 -1922
- package/src/events.js +0 -456
- package/src/fullscreen.js +0 -82
- package/src/i18n.js +0 -374
- package/src/playlist.js +0 -177
- package/src/plugins.js +0 -384
- package/src/quality.js +0 -963
- package/src/streaming.js +0 -346
- package/src/subtitles.js +0 -524
- package/src/utils.js +0 -65
- package/src/watermark.js +0 -246
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
// Enable or disable click over youtube player
|
|
36
36
|
mouseClick: options.mouseClick !== undefined ? options.mouseClick : false,
|
|
37
37
|
|
|
38
|
+
// Show YouTube chapters in the timeline
|
|
39
|
+
showYoutubeChapters: options.showYoutubeChapters !== undefined ? options.showYoutubeChapters : true,
|
|
40
|
+
|
|
38
41
|
debug: true,
|
|
39
42
|
...options
|
|
40
43
|
};
|
|
@@ -67,6 +70,9 @@
|
|
|
67
70
|
this.resizeListenerAdded = false;
|
|
68
71
|
// Channel data cache
|
|
69
72
|
this.channelData = null;
|
|
73
|
+
// Chapters cache in localStorage
|
|
74
|
+
this.cacheExpiration = 21600000; // 6 hour in milliseconds
|
|
75
|
+
this.cachePrefix = 'myetv_yt_chapters_'; // prefix for localStorage keys
|
|
70
76
|
//live streaming
|
|
71
77
|
this.isLiveStream = false;
|
|
72
78
|
this.liveCheckInterval = null;
|
|
@@ -1388,6 +1394,18 @@
|
|
|
1388
1394
|
this.updatePlayerWatermark();
|
|
1389
1395
|
}
|
|
1390
1396
|
|
|
1397
|
+
// Load and display chapters if API key is available
|
|
1398
|
+
if (this.options.apiKey) {
|
|
1399
|
+
this.fetchVideoChapters(this.videoId).then(chapters => {
|
|
1400
|
+
if (chapters) {
|
|
1401
|
+
this.displayChaptersOnProgressBar(chapters);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Clear expired cache entries on player ready
|
|
1407
|
+
this.clearExpiredCache();
|
|
1408
|
+
|
|
1391
1409
|
// Set auto caption language
|
|
1392
1410
|
if (this.options.autoCaptionLanguage) {
|
|
1393
1411
|
setTimeout(() => this.setAutoCaptionLanguage(), 1500);
|
|
@@ -2077,15 +2095,58 @@
|
|
|
2077
2095
|
document.head.appendChild(style);
|
|
2078
2096
|
this.api.container.classList.add('youtube-active');
|
|
2079
2097
|
|
|
2098
|
+
// Add chapter styles
|
|
2099
|
+
const chapterStyles = `
|
|
2100
|
+
/* Chapter segments styling */
|
|
2101
|
+
.chapter-segment {
|
|
2102
|
+
box-sizing: border-box;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/* Make progress fill respect chapter segments */
|
|
2106
|
+
.progress-filled {
|
|
2107
|
+
z-index: 5 !important;
|
|
2108
|
+
pointer-events: none;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/* Ensure chapter markers are visible */
|
|
2112
|
+
.chapter-marker {
|
|
2113
|
+
z-index: 6 !important;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/* Enhanced tooltip */
|
|
2117
|
+
.yt-seek-tooltip {
|
|
2118
|
+
animation: fadeIn 0.15s ease-in-out;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
@keyframes fadeIn {
|
|
2122
|
+
from {
|
|
2123
|
+
opacity: 0;
|
|
2124
|
+
transform: translateX(-50%) translateY(-5px);
|
|
2125
|
+
}
|
|
2126
|
+
to {
|
|
2127
|
+
opacity: 1;
|
|
2128
|
+
transform: translateX(-50%) translateY(0);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/* Hover effect on progress container */
|
|
2133
|
+
.progress-container:hover .chapter-segment {
|
|
2134
|
+
background: rgba(255, 255, 255, 0.4) !important;
|
|
2135
|
+
}
|
|
2136
|
+
`;
|
|
2137
|
+
|
|
2138
|
+
style.textContent = chapterStyles;
|
|
2139
|
+
document.head.appendChild(style);
|
|
2140
|
+
|
|
2080
2141
|
if (this.api.player.options.debug) {
|
|
2081
2142
|
console.log('YT Plugin: CSS override injected');
|
|
2082
2143
|
}
|
|
2083
2144
|
}
|
|
2084
2145
|
|
|
2085
2146
|
/**
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2147
|
+
* Inject CSS styles for controlbar and title overlay gradients
|
|
2148
|
+
* Uses YouTube-specific selectors to avoid conflicts with other plugins
|
|
2149
|
+
*/
|
|
2089
2150
|
injectControlbarGradientStyles() {
|
|
2090
2151
|
// Check if styles are already injected
|
|
2091
2152
|
if (document.getElementById('yt-controlbar-gradient-styles')) {
|
|
@@ -3979,13 +4040,16 @@
|
|
|
3979
4040
|
this.api.container.style.setProperty('--player-volume-fill', `${initialVolume}%`);
|
|
3980
4041
|
}
|
|
3981
4042
|
|
|
4043
|
+
// Handle volume changes with proper unmute logic
|
|
4044
|
+
// VOLUME SLIDER DRAG SUPPORT
|
|
4045
|
+
let isDraggingVolume = false;
|
|
4046
|
+
|
|
3982
4047
|
// Handle volume changes with proper unmute logic
|
|
3983
4048
|
newVolumeSlider.addEventListener('input', (e) => {
|
|
3984
4049
|
const volume = parseFloat(e.target.value);
|
|
3985
|
-
|
|
3986
4050
|
if (this.ytPlayer && this.ytPlayer.setVolume) {
|
|
3987
4051
|
this.ytPlayer.setVolume(volume);
|
|
3988
|
-
this.api.container.style.setProperty('--player-volume-fill',
|
|
4052
|
+
this.api.container.style.setProperty('--player-volume-fill', volume + '%');
|
|
3989
4053
|
|
|
3990
4054
|
// Always update mute button state correctly
|
|
3991
4055
|
if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
|
|
@@ -4004,20 +4068,134 @@
|
|
|
4004
4068
|
this.api.player.updateVolumeTooltipPosition(volume / 100);
|
|
4005
4069
|
}
|
|
4006
4070
|
|
|
4007
|
-
// Update tooltip
|
|
4008
|
-
if (this.api.player.updateVolumeTooltipPosition) {
|
|
4009
|
-
this.api.player.updateVolumeTooltipPosition(volume / 100);
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
// Update tooltip text manually instead of using updateVolumeTooltip
|
|
4071
|
+
// Update tooltip text manually
|
|
4013
4072
|
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4014
4073
|
if (volumeTooltip) {
|
|
4015
4074
|
volumeTooltip.textContent = Math.round(volume) + '%';
|
|
4016
4075
|
}
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
4017
4078
|
|
|
4079
|
+
// MOUSE DRAG - Start
|
|
4080
|
+
newVolumeSlider.addEventListener('mousedown', (e) => {
|
|
4081
|
+
isDraggingVolume = true;
|
|
4082
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4083
|
+
if (volumeTooltip) {
|
|
4084
|
+
volumeTooltip.classList.add('visible');
|
|
4018
4085
|
}
|
|
4019
4086
|
});
|
|
4020
4087
|
|
|
4088
|
+
// MOUSE DRAG - Move
|
|
4089
|
+
document.addEventListener('mousemove', (e) => {
|
|
4090
|
+
if (isDraggingVolume && newVolumeSlider) {
|
|
4091
|
+
const rect = newVolumeSlider.getBoundingClientRect();
|
|
4092
|
+
const clickX = e.clientX - rect.left;
|
|
4093
|
+
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
4094
|
+
const volume = Math.round(percentage * 100);
|
|
4095
|
+
|
|
4096
|
+
newVolumeSlider.value = volume;
|
|
4097
|
+
|
|
4098
|
+
if (this.ytPlayer && this.ytPlayer.setVolume) {
|
|
4099
|
+
this.ytPlayer.setVolume(volume);
|
|
4100
|
+
this.api.container.style.setProperty('--player-volume-fill', volume + '%');
|
|
4101
|
+
|
|
4102
|
+
// Update mute state
|
|
4103
|
+
if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
|
|
4104
|
+
this.ytPlayer.unMute();
|
|
4105
|
+
this.updateMuteButtonState(false);
|
|
4106
|
+
} else if (volume === 0) {
|
|
4107
|
+
if (this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
|
|
4108
|
+
this.ytPlayer.mute();
|
|
4109
|
+
}
|
|
4110
|
+
this.updateMuteButtonState(true);
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// Update tooltip
|
|
4114
|
+
if (this.api.player.updateVolumeTooltipPosition) {
|
|
4115
|
+
this.api.player.updateVolumeTooltipPosition(volume / 100);
|
|
4116
|
+
}
|
|
4117
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4118
|
+
if (volumeTooltip) {
|
|
4119
|
+
volumeTooltip.textContent = volume + '%';
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4124
|
+
|
|
4125
|
+
// MOUSE DRAG - End
|
|
4126
|
+
document.addEventListener('mouseup', () => {
|
|
4127
|
+
if (isDraggingVolume) {
|
|
4128
|
+
isDraggingVolume = false;
|
|
4129
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4130
|
+
if (volumeTooltip) {
|
|
4131
|
+
setTimeout(() => {
|
|
4132
|
+
volumeTooltip.classList.remove('visible');
|
|
4133
|
+
}, 300);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
|
|
4138
|
+
// TOUCH DRAG - Start
|
|
4139
|
+
newVolumeSlider.addEventListener('touchstart', (e) => {
|
|
4140
|
+
isDraggingVolume = true;
|
|
4141
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4142
|
+
if (volumeTooltip) {
|
|
4143
|
+
volumeTooltip.classList.add('visible');
|
|
4144
|
+
}
|
|
4145
|
+
}, { passive: true });
|
|
4146
|
+
|
|
4147
|
+
// TOUCH DRAG - Move
|
|
4148
|
+
newVolumeSlider.addEventListener('touchmove', (e) => {
|
|
4149
|
+
if (isDraggingVolume) {
|
|
4150
|
+
const touch = e.touches[0];
|
|
4151
|
+
const rect = newVolumeSlider.getBoundingClientRect();
|
|
4152
|
+
const touchX = touch.clientX - rect.left;
|
|
4153
|
+
const percentage = Math.max(0, Math.min(1, touchX / rect.width));
|
|
4154
|
+
const volume = Math.round(percentage * 100);
|
|
4155
|
+
|
|
4156
|
+
newVolumeSlider.value = volume;
|
|
4157
|
+
|
|
4158
|
+
if (this.ytPlayer && this.ytPlayer.setVolume) {
|
|
4159
|
+
this.ytPlayer.setVolume(volume);
|
|
4160
|
+
this.api.container.style.setProperty('--player-volume-fill', volume + '%');
|
|
4161
|
+
|
|
4162
|
+
// Update mute state
|
|
4163
|
+
if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
|
|
4164
|
+
this.ytPlayer.unMute();
|
|
4165
|
+
this.updateMuteButtonState(false);
|
|
4166
|
+
} else if (volume === 0) {
|
|
4167
|
+
if (this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
|
|
4168
|
+
this.ytPlayer.mute();
|
|
4169
|
+
}
|
|
4170
|
+
this.updateMuteButtonState(true);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
// Update tooltip
|
|
4174
|
+
if (this.api.player.updateVolumeTooltipPosition) {
|
|
4175
|
+
this.api.player.updateVolumeTooltipPosition(volume / 100);
|
|
4176
|
+
}
|
|
4177
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4178
|
+
if (volumeTooltip) {
|
|
4179
|
+
volumeTooltip.textContent = volume + '%';
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}, { passive: true });
|
|
4184
|
+
|
|
4185
|
+
// TOUCH DRAG - End
|
|
4186
|
+
newVolumeSlider.addEventListener('touchend', () => {
|
|
4187
|
+
if (isDraggingVolume) {
|
|
4188
|
+
isDraggingVolume = false;
|
|
4189
|
+
const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
|
|
4190
|
+
if (volumeTooltip) {
|
|
4191
|
+
setTimeout(() => {
|
|
4192
|
+
volumeTooltip.classList.remove('visible');
|
|
4193
|
+
}, 300);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
}, { passive: true });
|
|
4197
|
+
|
|
4198
|
+
|
|
4021
4199
|
// Update tooltip position on mousemove
|
|
4022
4200
|
newVolumeSlider.addEventListener('mousemove', (e) => {
|
|
4023
4201
|
const rect = newVolumeSlider.getBoundingClientRect();
|
|
@@ -4088,6 +4266,706 @@
|
|
|
4088
4266
|
}
|
|
4089
4267
|
}
|
|
4090
4268
|
|
|
4269
|
+
/**
|
|
4270
|
+
* Fetch video chapters from description using YouTube Data API v3
|
|
4271
|
+
* Uses localStorage for persistent caching
|
|
4272
|
+
*/
|
|
4273
|
+
async fetchVideoChapters(videoId) {
|
|
4274
|
+
|
|
4275
|
+
// Check if chapters display is enabled
|
|
4276
|
+
if (!this.options.showYoutubeChapters) {
|
|
4277
|
+
if (this.api.player.options.debug) {
|
|
4278
|
+
console.log('YT Plugin: YouTube chapters display disabled by option');
|
|
4279
|
+
}
|
|
4280
|
+
return null;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
// Check if API key is available
|
|
4284
|
+
if (!this.options.apiKey) {
|
|
4285
|
+
if (this.api.player.options.debug) {
|
|
4286
|
+
console.warn('YT Plugin: API Key required to fetch chapters');
|
|
4287
|
+
}
|
|
4288
|
+
return null;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
// Check cache first (localStorage)
|
|
4292
|
+
const cached = this.getCachedChapters(videoId);
|
|
4293
|
+
if (cached) {
|
|
4294
|
+
if (this.api.player.options.debug) {
|
|
4295
|
+
console.log('YT Plugin: Using cached chapters for', videoId);
|
|
4296
|
+
}
|
|
4297
|
+
return cached;
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
try {
|
|
4301
|
+
const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.options.apiKey}`;
|
|
4302
|
+
const response = await fetch(url);
|
|
4303
|
+
const data = await response.json();
|
|
4304
|
+
|
|
4305
|
+
if (!data.items || data.items.length === 0) {
|
|
4306
|
+
// Cache null result too to avoid repeated calls
|
|
4307
|
+
this.setCachedChapters(videoId, null);
|
|
4308
|
+
return null;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
const description = data.items[0].snippet.description;
|
|
4312
|
+
const chapters = this.parseChaptersFromDescription(description);
|
|
4313
|
+
|
|
4314
|
+
// Cache the result (even if null)
|
|
4315
|
+
this.setCachedChapters(videoId, chapters);
|
|
4316
|
+
|
|
4317
|
+
if (this.api.player.options.debug) {
|
|
4318
|
+
console.log('YT Plugin: Chapters fetched and cached:', chapters);
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
return chapters;
|
|
4322
|
+
} catch (error) {
|
|
4323
|
+
if (this.api.player.options.debug) {
|
|
4324
|
+
console.error('YT Plugin: Error fetching chapters', error);
|
|
4325
|
+
}
|
|
4326
|
+
return null;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
/**
|
|
4331
|
+
* Get cached chapters from localStorage
|
|
4332
|
+
*/
|
|
4333
|
+
getCachedChapters(videoId) {
|
|
4334
|
+
try {
|
|
4335
|
+
const key = this.cachePrefix + videoId;
|
|
4336
|
+
const cached = localStorage.getItem(key);
|
|
4337
|
+
|
|
4338
|
+
if (!cached) return null;
|
|
4339
|
+
|
|
4340
|
+
const data = JSON.parse(cached);
|
|
4341
|
+
const now = Date.now();
|
|
4342
|
+
|
|
4343
|
+
// Check if cache is expired
|
|
4344
|
+
if (now - data.timestamp >= this.cacheExpiration) {
|
|
4345
|
+
localStorage.removeItem(key);
|
|
4346
|
+
if (this.api.player.options.debug) {
|
|
4347
|
+
console.log('YT Plugin: Cache expired for', videoId);
|
|
4348
|
+
}
|
|
4349
|
+
return null;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
return data.chapters;
|
|
4353
|
+
} catch (error) {
|
|
4354
|
+
if (this.api.player.options.debug) {
|
|
4355
|
+
console.error('YT Plugin: Error reading cache', error);
|
|
4356
|
+
}
|
|
4357
|
+
return null;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
/**
|
|
4362
|
+
* Set cached chapters in localStorage
|
|
4363
|
+
*/
|
|
4364
|
+
setCachedChapters(videoId, chapters) {
|
|
4365
|
+
try {
|
|
4366
|
+
const key = this.cachePrefix + videoId;
|
|
4367
|
+
const data = {
|
|
4368
|
+
chapters: chapters,
|
|
4369
|
+
timestamp: Date.now()
|
|
4370
|
+
};
|
|
4371
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
4372
|
+
|
|
4373
|
+
if (this.api.player.options.debug) {
|
|
4374
|
+
console.log('YT Plugin: Chapters cached for', videoId);
|
|
4375
|
+
}
|
|
4376
|
+
} catch (error) {
|
|
4377
|
+
if (this.api.player.options.debug) {
|
|
4378
|
+
console.error('YT Plugin: Error writing cache', error);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
/**
|
|
4384
|
+
* Parse chapters from video description
|
|
4385
|
+
* Validates YouTube chapter requirements: 3+ chapters, starts at 0:00, 10+ seconds each
|
|
4386
|
+
*/
|
|
4387
|
+
parseChaptersFromDescription(description) {
|
|
4388
|
+
if (!description) return null;
|
|
4389
|
+
|
|
4390
|
+
const chapters = [];
|
|
4391
|
+
// Regex for timestamps: 0:00, 00:00, 0:00:00
|
|
4392
|
+
// Matches both "0:00 Title" and "0:00 - Title"
|
|
4393
|
+
const timestampRegex = /(?:^|\n)(\d{1,2}:?\d{0,2}:?\d{2})\s*[-–—]?\s*(.+?)(?=\n|$)/gm;
|
|
4394
|
+
let match;
|
|
4395
|
+
|
|
4396
|
+
while ((match = timestampRegex.exec(description)) !== null) {
|
|
4397
|
+
const timeString = match[1].trim();
|
|
4398
|
+
const title = match[2].trim();
|
|
4399
|
+
|
|
4400
|
+
// Skip if title is too short or empty
|
|
4401
|
+
if (!title || title.length < 2) continue;
|
|
4402
|
+
|
|
4403
|
+
const seconds = this.parseTimeToSeconds(timeString);
|
|
4404
|
+
|
|
4405
|
+
if (seconds !== null) {
|
|
4406
|
+
chapters.push({
|
|
4407
|
+
time: seconds,
|
|
4408
|
+
title: title,
|
|
4409
|
+
timeString: timeString
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
// Sort by time
|
|
4415
|
+
chapters.sort((a, b) => a.time - b.time);
|
|
4416
|
+
|
|
4417
|
+
// Validate: at least 3 chapters and first starts at 0:00
|
|
4418
|
+
if (chapters.length >= 3 && chapters[0].time === 0) {
|
|
4419
|
+
// Validate minimum duration (10 seconds)
|
|
4420
|
+
let valid = true;
|
|
4421
|
+
for (let i = 0; i < chapters.length - 1; i++) {
|
|
4422
|
+
if (chapters[i + 1].time - chapters[i].time < 10) {
|
|
4423
|
+
valid = false;
|
|
4424
|
+
break;
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
if (valid) {
|
|
4429
|
+
if (this.api.player.options.debug) {
|
|
4430
|
+
console.log(`YT Plugin: Found ${chapters.length} valid chapters`);
|
|
4431
|
+
}
|
|
4432
|
+
return chapters;
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
if (this.api.player.options.debug) {
|
|
4437
|
+
console.log('YT Plugin: No valid chapters found (requires 3+ chapters, starting at 0:00, each 10+ seconds)');
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
return null;
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
/**
|
|
4444
|
+
* Parse time string to seconds
|
|
4445
|
+
*/
|
|
4446
|
+
parseTimeToSeconds(timeString) {
|
|
4447
|
+
const parts = timeString.split(':').map(p => parseInt(p, 10));
|
|
4448
|
+
|
|
4449
|
+
if (parts.some(p => isNaN(p))) return null;
|
|
4450
|
+
|
|
4451
|
+
if (parts.length === 2) {
|
|
4452
|
+
// mm:ss
|
|
4453
|
+
return parts[0] * 60 + parts[1];
|
|
4454
|
+
} else if (parts.length === 3) {
|
|
4455
|
+
// hh:mm:ss
|
|
4456
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
return null;
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
/**
|
|
4463
|
+
* Display chapter markers on progress bar with YouTube-style segments
|
|
4464
|
+
*/
|
|
4465
|
+
displayChaptersOnProgressBar(chapters) {
|
|
4466
|
+
|
|
4467
|
+
// Check if chapters display is enabled
|
|
4468
|
+
if (!this.options.showYoutubeChapters) {
|
|
4469
|
+
if (this.api.player.options.debug) {
|
|
4470
|
+
console.log('YT Plugin: YouTube chapters display disabled');
|
|
4471
|
+
}
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
if (!chapters || chapters.length === 0) return;
|
|
4476
|
+
|
|
4477
|
+
const progressContainer = this.api.container.querySelector('.progress-container');
|
|
4478
|
+
if (!progressContainer) return;
|
|
4479
|
+
|
|
4480
|
+
const duration = this.ytPlayer.getDuration();
|
|
4481
|
+
if (!duration || duration === 0) return;
|
|
4482
|
+
|
|
4483
|
+
// Store chapters for tooltip display
|
|
4484
|
+
this.chapters = chapters;
|
|
4485
|
+
|
|
4486
|
+
// Remove existing chapter markers and segments
|
|
4487
|
+
progressContainer.querySelectorAll('.chapter-marker, .chapter-segment').forEach(m => m.remove());
|
|
4488
|
+
|
|
4489
|
+
// Create segments for each chapter (physically divided)
|
|
4490
|
+
chapters.forEach((chapter, index) => {
|
|
4491
|
+
const nextChapter = chapters[index + 1];
|
|
4492
|
+
const startPercent = (chapter.time / duration) * 100;
|
|
4493
|
+
const endPercent = nextChapter ? (nextChapter.time / duration) * 100 : 100;
|
|
4494
|
+
|
|
4495
|
+
// Calculate segment width minus the gap
|
|
4496
|
+
const gapSize = nextChapter ? 6 : 0; // 6px gap between segments
|
|
4497
|
+
const widthPercent = endPercent - startPercent;
|
|
4498
|
+
|
|
4499
|
+
// Create segment container
|
|
4500
|
+
const segment = document.createElement('div');
|
|
4501
|
+
segment.className = 'chapter-segment';
|
|
4502
|
+
segment.style.cssText = `
|
|
4503
|
+
position: absolute;
|
|
4504
|
+
left: ${startPercent}%;
|
|
4505
|
+
top: 0;
|
|
4506
|
+
width: calc(${widthPercent}% - ${gapSize}px);
|
|
4507
|
+
height: 100%;
|
|
4508
|
+
background: rgba(255, 255, 255, 0.3);
|
|
4509
|
+
cursor: pointer;
|
|
4510
|
+
z-index: 3;
|
|
4511
|
+
transition: background 0.2s;
|
|
4512
|
+
pointer-events: none;
|
|
4513
|
+
`;
|
|
4514
|
+
|
|
4515
|
+
segment.dataset.chapterIndex = index;
|
|
4516
|
+
segment.dataset.time = chapter.time;
|
|
4517
|
+
segment.dataset.title = chapter.title;
|
|
4518
|
+
|
|
4519
|
+
progressContainer.appendChild(segment);
|
|
4520
|
+
|
|
4521
|
+
// Add marker at the START of next segment (not between)
|
|
4522
|
+
if (nextChapter) {
|
|
4523
|
+
const marker = document.createElement('div');
|
|
4524
|
+
marker.className = 'chapter-marker';
|
|
4525
|
+
marker.style.cssText = `
|
|
4526
|
+
position: absolute !important;
|
|
4527
|
+
left: ${endPercent}% !important;
|
|
4528
|
+
top: 0 !important;
|
|
4529
|
+
width: 6px !important;
|
|
4530
|
+
height: 100% !important;
|
|
4531
|
+
background: transparent !important;
|
|
4532
|
+
border: none !important;
|
|
4533
|
+
box-shadow: none !important;
|
|
4534
|
+
margin-left: -3px !important;
|
|
4535
|
+
cursor: pointer !important;
|
|
4536
|
+
z-index: 10 !important;
|
|
4537
|
+
`;
|
|
4538
|
+
|
|
4539
|
+
marker.dataset.chapterTime = nextChapter.time;
|
|
4540
|
+
marker.dataset.chapterTitle = nextChapter.title;
|
|
4541
|
+
|
|
4542
|
+
// Click on marker to jump to chapter start
|
|
4543
|
+
marker.addEventListener('click', (e) => {
|
|
4544
|
+
e.stopPropagation();
|
|
4545
|
+
this.ytPlayer.seekTo(nextChapter.time, true);
|
|
4546
|
+
|
|
4547
|
+
if (this.api.player.options.debug) {
|
|
4548
|
+
console.log(`YT Plugin: Jumped to chapter "${nextChapter.title}" at ${nextChapter.timeString}`);
|
|
4549
|
+
}
|
|
4550
|
+
});
|
|
4551
|
+
|
|
4552
|
+
progressContainer.appendChild(marker);
|
|
4553
|
+
}
|
|
4554
|
+
});
|
|
4555
|
+
|
|
4556
|
+
if (this.api.player.options.debug) {
|
|
4557
|
+
console.log(`YT Plugin: ${chapters.length} chapter segments displayed with gaps`);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
// Initialize chapter display in title overlay
|
|
4561
|
+
this.initChapterDisplayInOverlay();
|
|
4562
|
+
|
|
4563
|
+
// Add chapter title tooltip
|
|
4564
|
+
this.addChapterTooltip();
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
/**
|
|
4568
|
+
* Add chapter title tooltip with edge detection
|
|
4569
|
+
*/
|
|
4570
|
+
addChapterTooltip() {
|
|
4571
|
+
|
|
4572
|
+
// Check if chapters display is enabled
|
|
4573
|
+
if (!this.options.showYoutubeChapters) {
|
|
4574
|
+
if (this.api.player.options.debug) {
|
|
4575
|
+
console.log('YT Plugin: Chapter tooltip display disabled');
|
|
4576
|
+
}
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
if (!this.chapters || this.chapters.length === 0) return;
|
|
4581
|
+
|
|
4582
|
+
const progressContainer = this.api.container.querySelector('.progress-container');
|
|
4583
|
+
if (!progressContainer) return;
|
|
4584
|
+
|
|
4585
|
+
// Remove existing chapter tooltip
|
|
4586
|
+
let chapterTooltip = progressContainer.querySelector('.yt-chapter-tooltip');
|
|
4587
|
+
if (chapterTooltip) {
|
|
4588
|
+
chapterTooltip.remove();
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
// Create chapter tooltip
|
|
4592
|
+
chapterTooltip = document.createElement('div');
|
|
4593
|
+
chapterTooltip.className = 'yt-chapter-tooltip';
|
|
4594
|
+
chapterTooltip.style.cssText = `
|
|
4595
|
+
position: absolute;
|
|
4596
|
+
bottom: calc(100% + 35px);
|
|
4597
|
+
left: 0;
|
|
4598
|
+
background: rgba(28, 28, 28, 0.95);
|
|
4599
|
+
color: #fff;
|
|
4600
|
+
padding: 6px 12px;
|
|
4601
|
+
border-radius: 4px;
|
|
4602
|
+
font-size: 12px;
|
|
4603
|
+
font-weight: 600;
|
|
4604
|
+
white-space: nowrap;
|
|
4605
|
+
pointer-events: none;
|
|
4606
|
+
visibility: hidden;
|
|
4607
|
+
opacity: 0;
|
|
4608
|
+
z-index: 100001;
|
|
4609
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
4610
|
+
max-width: 250px;
|
|
4611
|
+
overflow: hidden;
|
|
4612
|
+
text-overflow: ellipsis;
|
|
4613
|
+
transform: translateX(-50%);
|
|
4614
|
+
transition: opacity 0.15s, visibility 0.15s;
|
|
4615
|
+
`;
|
|
4616
|
+
progressContainer.appendChild(chapterTooltip);
|
|
4617
|
+
|
|
4618
|
+
// Get player container bounds for edge detection
|
|
4619
|
+
const getPlayerBounds = () => {
|
|
4620
|
+
return this.api.container.getBoundingClientRect();
|
|
4621
|
+
};
|
|
4622
|
+
|
|
4623
|
+
// Show/hide chapter tooltip with edge detection
|
|
4624
|
+
progressContainer.addEventListener('mousemove', (e) => {
|
|
4625
|
+
if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
|
|
4626
|
+
|
|
4627
|
+
const rect = progressContainer.getBoundingClientRect();
|
|
4628
|
+
const playerRect = getPlayerBounds();
|
|
4629
|
+
const mouseX = e.clientX - rect.left;
|
|
4630
|
+
const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
|
|
4631
|
+
const duration = this.ytPlayer.getDuration();
|
|
4632
|
+
const time = percentage * duration;
|
|
4633
|
+
|
|
4634
|
+
// Find current chapter
|
|
4635
|
+
let currentChapter = null;
|
|
4636
|
+
for (let i = this.chapters.length - 1; i >= 0; i--) {
|
|
4637
|
+
if (time >= this.chapters[i].time) {
|
|
4638
|
+
currentChapter = this.chapters[i];
|
|
4639
|
+
break;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
if (currentChapter) {
|
|
4644
|
+
chapterTooltip.textContent = currentChapter.title;
|
|
4645
|
+
chapterTooltip.style.visibility = 'visible';
|
|
4646
|
+
chapterTooltip.style.opacity = '1';
|
|
4647
|
+
|
|
4648
|
+
// Wait for tooltip to render to get its width
|
|
4649
|
+
setTimeout(() => {
|
|
4650
|
+
const tooltipWidth = chapterTooltip.offsetWidth;
|
|
4651
|
+
const tooltipHalfWidth = tooltipWidth / 2;
|
|
4652
|
+
|
|
4653
|
+
// Calculate absolute position
|
|
4654
|
+
const absoluteX = e.clientX;
|
|
4655
|
+
|
|
4656
|
+
// Check left edge
|
|
4657
|
+
if (absoluteX - tooltipHalfWidth < playerRect.left) {
|
|
4658
|
+
// Stick to left edge
|
|
4659
|
+
const leftOffset = playerRect.left - rect.left;
|
|
4660
|
+
chapterTooltip.style.left = `${leftOffset + tooltipHalfWidth}px`;
|
|
4661
|
+
chapterTooltip.style.transform = 'translateX(-50%)';
|
|
4662
|
+
}
|
|
4663
|
+
// Check right edge
|
|
4664
|
+
else if (absoluteX + tooltipHalfWidth > playerRect.right) {
|
|
4665
|
+
// Stick to right edge
|
|
4666
|
+
const rightOffset = playerRect.right - rect.left;
|
|
4667
|
+
chapterTooltip.style.left = `${rightOffset - tooltipHalfWidth}px`;
|
|
4668
|
+
chapterTooltip.style.transform = 'translateX(-50%)';
|
|
4669
|
+
}
|
|
4670
|
+
// Normal centering
|
|
4671
|
+
else {
|
|
4672
|
+
chapterTooltip.style.left = `${mouseX}px`;
|
|
4673
|
+
chapterTooltip.style.transform = 'translateX(-50%)';
|
|
4674
|
+
}
|
|
4675
|
+
}, 0);
|
|
4676
|
+
} else {
|
|
4677
|
+
chapterTooltip.style.visibility = 'hidden';
|
|
4678
|
+
chapterTooltip.style.opacity = '0';
|
|
4679
|
+
}
|
|
4680
|
+
});
|
|
4681
|
+
|
|
4682
|
+
progressContainer.addEventListener('mouseleave', () => {
|
|
4683
|
+
chapterTooltip.style.visibility = 'hidden';
|
|
4684
|
+
chapterTooltip.style.opacity = '0';
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4687
|
+
if (this.api.player.options.debug) {
|
|
4688
|
+
console.log('YT Plugin: Chapter tooltip added with edge detection');
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
/**
|
|
4693
|
+
* Initialize chapter display in title overlay
|
|
4694
|
+
*/
|
|
4695
|
+
initChapterDisplayInOverlay() {
|
|
4696
|
+
if (!this.chapters || this.chapters.length === 0) return;
|
|
4697
|
+
|
|
4698
|
+
const titleOverlay = this.api.container.querySelector('.title-overlay');
|
|
4699
|
+
if (!titleOverlay) return;
|
|
4700
|
+
|
|
4701
|
+
// Remove existing chapter element if present
|
|
4702
|
+
let chapterElement = titleOverlay.querySelector('.chapter-name');
|
|
4703
|
+
if (chapterElement) {
|
|
4704
|
+
chapterElement.remove();
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// Create chapter name element
|
|
4708
|
+
chapterElement = document.createElement('div');
|
|
4709
|
+
chapterElement.className = 'chapter-name';
|
|
4710
|
+
chapterElement.style.cssText = `
|
|
4711
|
+
font-size: 13px;
|
|
4712
|
+
font-weight: 500;
|
|
4713
|
+
color: rgba(255, 255, 255, 0.9);
|
|
4714
|
+
margin-top: 6px;
|
|
4715
|
+
max-width: 400px;
|
|
4716
|
+
overflow: hidden;
|
|
4717
|
+
text-overflow: ellipsis;
|
|
4718
|
+
white-space: nowrap;
|
|
4719
|
+
`;
|
|
4720
|
+
|
|
4721
|
+
// Append to title overlay
|
|
4722
|
+
titleOverlay.appendChild(chapterElement);
|
|
4723
|
+
|
|
4724
|
+
// Start monitoring playback to update chapter name
|
|
4725
|
+
this.startChapterNameMonitoring();
|
|
4726
|
+
|
|
4727
|
+
if (this.api.player.options.debug) {
|
|
4728
|
+
console.log('YT Plugin: Chapter name display initialized in title overlay');
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
/**
|
|
4733
|
+
* Monitor playback and update chapter name dynamically
|
|
4734
|
+
*/
|
|
4735
|
+
startChapterNameMonitoring() {
|
|
4736
|
+
if (this.chapterMonitorInterval) {
|
|
4737
|
+
clearInterval(this.chapterMonitorInterval);
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
// Update every 500ms to catch chapter changes
|
|
4741
|
+
this.chapterMonitorInterval = setInterval(() => {
|
|
4742
|
+
if (!this.ytPlayer || !this.chapters || this.chapters.length === 0) {
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
try {
|
|
4747
|
+
const currentTime = this.ytPlayer.getCurrentTime();
|
|
4748
|
+
this.updateChapterNameDisplay(currentTime);
|
|
4749
|
+
} catch (error) {
|
|
4750
|
+
if (this.api.player.options.debug) {
|
|
4751
|
+
console.error('YT Plugin: Error updating chapter name', error);
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
}, 500);
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
/**
|
|
4758
|
+
* Update chapter name in overlay based on current time
|
|
4759
|
+
*/
|
|
4760
|
+
updateChapterNameDisplay(currentTime) {
|
|
4761
|
+
const chapterElement = this.api.container.querySelector('.title-overlay .chapter-name');
|
|
4762
|
+
if (!chapterElement) return;
|
|
4763
|
+
|
|
4764
|
+
// Find current chapter
|
|
4765
|
+
let currentChapter = null;
|
|
4766
|
+
for (let i = this.chapters.length - 1; i >= 0; i--) {
|
|
4767
|
+
if (currentTime >= this.chapters[i].time) {
|
|
4768
|
+
currentChapter = this.chapters[i];
|
|
4769
|
+
break;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// Update or clear chapter name
|
|
4774
|
+
if (currentChapter) {
|
|
4775
|
+
chapterElement.textContent = currentChapter.title;
|
|
4776
|
+
chapterElement.style.display = 'block';
|
|
4777
|
+
} else {
|
|
4778
|
+
chapterElement.style.display = 'none';
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
/**
|
|
4783
|
+
* Stop chapter name monitoring
|
|
4784
|
+
*/
|
|
4785
|
+
stopChapterNameMonitoring() {
|
|
4786
|
+
if (this.chapterMonitorInterval) {
|
|
4787
|
+
clearInterval(this.chapterMonitorInterval);
|
|
4788
|
+
this.chapterMonitorInterval = null;
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
/**
|
|
4793
|
+
* Enhance progress bar tooltip to show chapter title
|
|
4794
|
+
*/
|
|
4795
|
+
enhanceProgressTooltipWithChapters() {
|
|
4796
|
+
if (!this.chapters || this.chapters.length === 0) return;
|
|
4797
|
+
|
|
4798
|
+
const progressContainer = this.api.container.querySelector('.progress-container');
|
|
4799
|
+
if (!progressContainer) return;
|
|
4800
|
+
|
|
4801
|
+
// Find existing tooltip
|
|
4802
|
+
let tooltip = progressContainer.querySelector('.yt-seek-tooltip');
|
|
4803
|
+
|
|
4804
|
+
if (!tooltip) {
|
|
4805
|
+
// Create tooltip if it doesn't exist
|
|
4806
|
+
tooltip = document.createElement('div');
|
|
4807
|
+
tooltip.className = 'yt-seek-tooltip';
|
|
4808
|
+
tooltip.style.cssText = `
|
|
4809
|
+
position: absolute;
|
|
4810
|
+
bottom: calc(100% + 10px);
|
|
4811
|
+
left: 0;
|
|
4812
|
+
background: rgba(28, 28, 28, 0.95);
|
|
4813
|
+
color: #fff;
|
|
4814
|
+
padding: 8px 12px;
|
|
4815
|
+
border-radius: 4px;
|
|
4816
|
+
font-size: 13px;
|
|
4817
|
+
font-weight: 500;
|
|
4818
|
+
white-space: nowrap;
|
|
4819
|
+
pointer-events: none;
|
|
4820
|
+
visibility: hidden;
|
|
4821
|
+
z-index: 99999;
|
|
4822
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
4823
|
+
display: flex;
|
|
4824
|
+
flex-direction: column;
|
|
4825
|
+
gap: 4px;
|
|
4826
|
+
min-width: 80px;
|
|
4827
|
+
`;
|
|
4828
|
+
progressContainer.appendChild(tooltip);
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
// Add chapter title element to tooltip
|
|
4832
|
+
let chapterTitle = tooltip.querySelector('.chapter-title');
|
|
4833
|
+
if (!chapterTitle) {
|
|
4834
|
+
chapterTitle = document.createElement('div');
|
|
4835
|
+
chapterTitle.className = 'chapter-title';
|
|
4836
|
+
chapterTitle.style.cssText = `
|
|
4837
|
+
font-size: 12px;
|
|
4838
|
+
font-weight: 600;
|
|
4839
|
+
color: #fff;
|
|
4840
|
+
max-width: 200px;
|
|
4841
|
+
overflow: hidden;
|
|
4842
|
+
text-overflow: ellipsis;
|
|
4843
|
+
white-space: nowrap;
|
|
4844
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
4845
|
+
padding-bottom: 4px;
|
|
4846
|
+
margin-bottom: 2px;
|
|
4847
|
+
`;
|
|
4848
|
+
tooltip.insertBefore(chapterTitle, tooltip.firstChild);
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
// Time display element
|
|
4852
|
+
let timeDisplay = tooltip.querySelector('.time-display');
|
|
4853
|
+
if (!timeDisplay) {
|
|
4854
|
+
timeDisplay = document.createElement('div');
|
|
4855
|
+
timeDisplay.className = 'time-display';
|
|
4856
|
+
timeDisplay.style.cssText = `
|
|
4857
|
+
font-size: 13px;
|
|
4858
|
+
font-weight: 500;
|
|
4859
|
+
color: rgba(255, 255, 255, 0.9);
|
|
4860
|
+
`;
|
|
4861
|
+
tooltip.appendChild(timeDisplay);
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
// Update tooltip on mousemove
|
|
4865
|
+
progressContainer.addEventListener('mousemove', (e) => {
|
|
4866
|
+
if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
|
|
4867
|
+
|
|
4868
|
+
const rect = progressContainer.getBoundingClientRect();
|
|
4869
|
+
const mouseX = e.clientX - rect.left;
|
|
4870
|
+
const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
|
|
4871
|
+
const duration = this.ytPlayer.getDuration();
|
|
4872
|
+
const time = percentage * duration;
|
|
4873
|
+
|
|
4874
|
+
// Find current chapter at this time
|
|
4875
|
+
let currentChapter = null;
|
|
4876
|
+
for (let i = this.chapters.length - 1; i >= 0; i--) {
|
|
4877
|
+
if (time >= this.chapters[i].time) {
|
|
4878
|
+
currentChapter = this.chapters[i];
|
|
4879
|
+
break;
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// Update chapter title
|
|
4884
|
+
if (currentChapter) {
|
|
4885
|
+
chapterTitle.textContent = currentChapter.title;
|
|
4886
|
+
chapterTitle.style.display = 'block';
|
|
4887
|
+
} else {
|
|
4888
|
+
chapterTitle.style.display = 'none';
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
// Update time display
|
|
4892
|
+
timeDisplay.textContent = this.formatTime(time);
|
|
4893
|
+
|
|
4894
|
+
// Position tooltip
|
|
4895
|
+
tooltip.style.left = `${mouseX}px`;
|
|
4896
|
+
tooltip.style.visibility = 'visible';
|
|
4897
|
+
tooltip.style.transform = 'translateX(-50%)';
|
|
4898
|
+
});
|
|
4899
|
+
|
|
4900
|
+
progressContainer.addEventListener('mouseleave', () => {
|
|
4901
|
+
tooltip.style.visibility = 'hidden';
|
|
4902
|
+
});
|
|
4903
|
+
|
|
4904
|
+
if (this.api.player.options.debug) {
|
|
4905
|
+
console.log('YT Plugin: Progress tooltip enhanced with chapter info');
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
/**
|
|
4910
|
+
* Clear all cached chapters from localStorage
|
|
4911
|
+
*/
|
|
4912
|
+
clearChaptersCache() {
|
|
4913
|
+
try {
|
|
4914
|
+
const keys = [];
|
|
4915
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
4916
|
+
const key = localStorage.key(i);
|
|
4917
|
+
if (key && key.startsWith(this.cachePrefix)) {
|
|
4918
|
+
keys.push(key);
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
keys.forEach(key => localStorage.removeItem(key));
|
|
4923
|
+
|
|
4924
|
+
if (this.api.player.options.debug) {
|
|
4925
|
+
console.log(`YT Plugin: Cleared ${keys.length} cached chapters`);
|
|
4926
|
+
}
|
|
4927
|
+
} catch (error) {
|
|
4928
|
+
if (this.api.player.options.debug) {
|
|
4929
|
+
console.error('YT Plugin: Error clearing cache', error);
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
/**
|
|
4935
|
+
* Clear expired cache entries from localStorage
|
|
4936
|
+
*/
|
|
4937
|
+
clearExpiredCache() {
|
|
4938
|
+
try {
|
|
4939
|
+
const now = Date.now();
|
|
4940
|
+
const keys = [];
|
|
4941
|
+
|
|
4942
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
4943
|
+
const key = localStorage.key(i);
|
|
4944
|
+
if (key && key.startsWith(this.cachePrefix)) {
|
|
4945
|
+
try {
|
|
4946
|
+
const data = JSON.parse(localStorage.getItem(key));
|
|
4947
|
+
if (now - data.timestamp >= this.cacheExpiration) {
|
|
4948
|
+
keys.push(key);
|
|
4949
|
+
}
|
|
4950
|
+
} catch (e) {
|
|
4951
|
+
// Invalid data, remove it
|
|
4952
|
+
keys.push(key);
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
keys.forEach(key => localStorage.removeItem(key));
|
|
4958
|
+
|
|
4959
|
+
if (this.api.player.options.debug) {
|
|
4960
|
+
console.log(`YT Plugin: Cleared ${keys.length} expired cache entries`);
|
|
4961
|
+
}
|
|
4962
|
+
} catch (error) {
|
|
4963
|
+
if (this.api.player.options.debug) {
|
|
4964
|
+
console.error('YT Plugin: Error clearing expired cache', error);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4091
4969
|
// ===== CLEANUP =====
|
|
4092
4970
|
|
|
4093
4971
|
dispose() {
|
|
@@ -4152,9 +5030,8 @@
|
|
|
4152
5030
|
const styleEl = document.getElementById('youtube-controls-override');
|
|
4153
5031
|
if (styleEl) styleEl.remove();
|
|
4154
5032
|
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
}
|
|
5033
|
+
// Stop chapter monitoring when plugin is destroyed
|
|
5034
|
+
this.stopChapterNameMonitoring();
|
|
4158
5035
|
|
|
4159
5036
|
if (this.api.video) {
|
|
4160
5037
|
this.api.video.style.display = '';
|