myetv-player 1.2.0 → 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 +131 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +547 -102
- package/dist/myetv-player.min.js +486 -93
- package/package.json +35 -17
- 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 +766 -6
- 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 -204
- 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 -1368
- 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 -1242
- package/src/core.js +0 -1922
- package/src/events.js +0 -537
- 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')) {
|
|
@@ -4205,6 +4266,706 @@
|
|
|
4205
4266
|
}
|
|
4206
4267
|
}
|
|
4207
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
|
+
|
|
4208
4969
|
// ===== CLEANUP =====
|
|
4209
4970
|
|
|
4210
4971
|
dispose() {
|
|
@@ -4269,9 +5030,8 @@
|
|
|
4269
5030
|
const styleEl = document.getElementById('youtube-controls-override');
|
|
4270
5031
|
if (styleEl) styleEl.remove();
|
|
4271
5032
|
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
}
|
|
5033
|
+
// Stop chapter monitoring when plugin is destroyed
|
|
5034
|
+
this.stopChapterNameMonitoring();
|
|
4275
5035
|
|
|
4276
5036
|
if (this.api.video) {
|
|
4277
5037
|
this.api.video.style.display = '';
|