添加语音播放
Browse files- youtube_sub.js +172 -12
youtube_sub.js
CHANGED
|
@@ -23,6 +23,11 @@ GM_addStyle(`
|
|
| 23 |
(function () {
|
| 24 |
'use strict';
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
// 增强的样式定义,添加过渡效果
|
| 27 |
const styles = `
|
| 28 |
.subtitle-manager {
|
|
@@ -322,7 +327,7 @@ GM_addStyle(`
|
|
| 322 |
opacity: 1;
|
| 323 |
}
|
| 324 |
|
| 325 |
-
|
| 326 |
position: fixed;
|
| 327 |
right: -400px;
|
| 328 |
top: 50%;
|
|
@@ -336,6 +341,8 @@ GM_addStyle(`
|
|
| 336 |
z-index: 10000;
|
| 337 |
max-height: 80vh;
|
| 338 |
overflow-y: auto;
|
|
|
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
.word-definition-popup.show {
|
|
@@ -343,9 +350,33 @@ GM_addStyle(`
|
|
| 343 |
}
|
| 344 |
|
| 345 |
.word-definition-popup .markdown {
|
| 346 |
-
|
|
|
|
| 347 |
line-height: 1.6;
|
| 348 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
.mixed-text-container {
|
| 351 |
display: flex;
|
|
@@ -375,6 +406,69 @@ GM_addStyle(`
|
|
| 375 |
border-top: 1px solid rgba(255,255,255,0.1);
|
| 376 |
padding-top: 8px;
|
| 377 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
`;
|
| 379 |
|
| 380 |
// Add popup HTML
|
|
@@ -422,8 +516,44 @@ GM_addStyle(`
|
|
| 422 |
this.container = null;
|
| 423 |
this.currentTime = 0;
|
| 424 |
this.isCollapsed = false;
|
|
|
|
| 425 |
this.setupUI();
|
| 426 |
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
}
|
| 428 |
|
| 429 |
checkCaptionSources(player) {
|
|
@@ -806,13 +936,15 @@ GM_addStyle(`
|
|
| 806 |
wordBtn.onclick = async (e) => {
|
| 807 |
e.stopPropagation();
|
| 808 |
console.log(`You click "${word.text.trim()}" word`);
|
| 809 |
-
|
| 810 |
let popup = document.querySelector('.word-definition-popup');
|
| 811 |
if (!popup) {
|
| 812 |
// Create popup container
|
| 813 |
popup = document.createElement('div');
|
| 814 |
popup.className = 'word-definition-popup';
|
| 815 |
|
|
|
|
|
|
|
| 816 |
// Create markdown container
|
| 817 |
const markdownDiv = document.createElement('div');
|
| 818 |
markdownDiv.className = 'markdown';
|
|
@@ -826,6 +958,30 @@ GM_addStyle(`
|
|
| 826 |
popup.classList.remove('show');
|
| 827 |
});
|
| 828 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
|
| 830 |
// Ensure markdown container exists
|
| 831 |
let markdownContainer = popup.querySelector('.markdown');
|
|
@@ -968,15 +1124,15 @@ GM_addStyle(`
|
|
| 968 |
this.subtitles.english?.length || 0,
|
| 969 |
this.subtitles.chinese?.length || 0
|
| 970 |
);
|
| 971 |
-
|
| 972 |
for (let i = 0; i < maxLength; i++) {
|
| 973 |
const line = document.createElement('div');
|
| 974 |
line.className = 'subtitle-line mixed-line';
|
| 975 |
-
|
| 976 |
// Text container
|
| 977 |
const textContainer = document.createElement('div');
|
| 978 |
textContainer.className = 'mixed-text-container';
|
| 979 |
-
|
| 980 |
// Chinese text with word buttons
|
| 981 |
const zhText = document.createElement('div');
|
| 982 |
zhText.className = 'zh-text';
|
|
@@ -995,7 +1151,7 @@ GM_addStyle(`
|
|
| 995 |
} else {
|
| 996 |
zhText.textContent = '翻译中...';
|
| 997 |
}
|
| 998 |
-
|
| 999 |
// English text with word buttons
|
| 1000 |
const enText = document.createElement('div');
|
| 1001 |
enText.className = 'en-text';
|
|
@@ -1012,23 +1168,23 @@ GM_addStyle(`
|
|
| 1012 |
}
|
| 1013 |
});
|
| 1014 |
}
|
| 1015 |
-
|
| 1016 |
textContainer.appendChild(zhText);
|
| 1017 |
textContainer.appendChild(enText);
|
| 1018 |
line.appendChild(textContainer);
|
| 1019 |
-
|
| 1020 |
// Add timestamp
|
| 1021 |
const timestamp = document.createElement('span');
|
| 1022 |
timestamp.className = 'timestamp';
|
| 1023 |
-
timestamp.textContent = this.subtitles.english[i] ?
|
| 1024 |
this.formatTime(this.subtitles.english[i].startTime) : '';
|
| 1025 |
line.appendChild(timestamp);
|
| 1026 |
-
|
| 1027 |
if (this.subtitles.english[i]) {
|
| 1028 |
line.dataset.time = this.subtitles.english[i].startTime;
|
| 1029 |
line.dataset.index = i;
|
| 1030 |
}
|
| 1031 |
-
|
| 1032 |
container.appendChild(line);
|
| 1033 |
}
|
| 1034 |
}
|
|
@@ -1591,6 +1747,10 @@ GM_addStyle(`
|
|
| 1591 |
debug.log('Current URL:', window.location.href);
|
| 1592 |
debug.log('Document ready state:', document.readyState);
|
| 1593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1594 |
if (!window.location.pathname.includes('/watch')) {
|
| 1595 |
debug.log('Not a video page, skipping');
|
| 1596 |
return;
|
|
|
|
| 23 |
(function () {
|
| 24 |
'use strict';
|
| 25 |
|
| 26 |
+
// Singleton check
|
| 27 |
+
if (window.subtitleManagerInstance) {
|
| 28 |
+
window.subtitleManagerInstance.cleanup();
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
// 增强的样式定义,添加过渡效果
|
| 32 |
const styles = `
|
| 33 |
.subtitle-manager {
|
|
|
|
| 327 |
opacity: 1;
|
| 328 |
}
|
| 329 |
|
| 330 |
+
.word-definition-popup {
|
| 331 |
position: fixed;
|
| 332 |
right: -400px;
|
| 333 |
top: 50%;
|
|
|
|
| 341 |
z-index: 10000;
|
| 342 |
max-height: 80vh;
|
| 343 |
overflow-y: auto;
|
| 344 |
+
font-size: 15px;
|
| 345 |
+
line-height: 1.6;
|
| 346 |
}
|
| 347 |
|
| 348 |
.word-definition-popup.show {
|
|
|
|
| 350 |
}
|
| 351 |
|
| 352 |
.word-definition-popup .markdown {
|
| 353 |
+
padding-top: 50px;
|
| 354 |
+
font-size: 15px;
|
| 355 |
line-height: 1.6;
|
| 356 |
}
|
| 357 |
+
.word-definition-popup .markdown p {
|
| 358 |
+
margin: 12px 0;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.word-definition-popup .markdown strong {
|
| 362 |
+
color: #4CAF50;
|
| 363 |
+
font-weight: 600;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.word-definition-popup .markdown em {
|
| 367 |
+
color: #FFC107;
|
| 368 |
+
font-style: normal;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.word-definition-popup[data-theme="light"] {
|
| 372 |
+
background: rgba(255, 255, 255, 0.95);
|
| 373 |
+
color: #333;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.word-definition-popup[data-theme="light"] .speaker-btn {
|
| 377 |
+
background: rgba(0, 0, 0, 0.1);
|
| 378 |
+
color: #333;
|
| 379 |
+
}
|
| 380 |
|
| 381 |
.mixed-text-container {
|
| 382 |
display: flex;
|
|
|
|
| 406 |
border-top: 1px solid rgba(255,255,255,0.1);
|
| 407 |
padding-top: 8px;
|
| 408 |
}
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@media (max-width: 768px), (orientation: portrait) {
|
| 412 |
+
.subtitle-manager {
|
| 413 |
+
position: relative;
|
| 414 |
+
right: auto;
|
| 415 |
+
top: auto;
|
| 416 |
+
width: 100%;
|
| 417 |
+
height: 50vh;
|
| 418 |
+
margin-top: 10px;
|
| 419 |
+
margin-top: 400px; /* Move down below video */
|
| 420 |
+
background: rgba(33, 33, 33, 0.98); /* Slightly more opaque for mobile */
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.subtitle-manager.collapsed {
|
| 424 |
+
transform: translateY(calc(100% - 40px));
|
| 425 |
+
margin-top: 400px; /* Keep margin when collapsed */
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.subtitle-toggle {
|
| 429 |
+
top: -30px;
|
| 430 |
+
left: 10px;
|
| 431 |
+
transform: rotate(90deg);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.theme-toggle {
|
| 435 |
+
top: -30px;
|
| 436 |
+
right: 10px;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.subtitle-line {
|
| 440 |
+
width: calc(100% - 30px);
|
| 441 |
+
max-width: none;
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
.word-definition-popup .speaker-btn {
|
| 447 |
+
position: absolute;
|
| 448 |
+
top: 15px;
|
| 449 |
+
right: 15px;
|
| 450 |
+
width: 36px;
|
| 451 |
+
height: 36px;
|
| 452 |
+
background: rgba(255, 255, 255, 0.1);
|
| 453 |
+
border: none;
|
| 454 |
+
border-radius: 50%;
|
| 455 |
+
color: white;
|
| 456 |
+
cursor: pointer;
|
| 457 |
+
font-size: 20px;
|
| 458 |
+
display: flex;
|
| 459 |
+
align-items: center;
|
| 460 |
+
justify-content: center;
|
| 461 |
+
transition: all 0.2s ease;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.word-definition-popup .speaker-btn:hover {
|
| 465 |
+
background: rgba(255, 255, 255, 0.1);
|
| 466 |
+
transform: scale(1.1);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.word-definition-popup[data-theme="light"] .speaker-btn {
|
| 470 |
+
color: #333;
|
| 471 |
+
}
|
| 472 |
`;
|
| 473 |
|
| 474 |
// Add popup HTML
|
|
|
|
| 516 |
this.container = null;
|
| 517 |
this.currentTime = 0;
|
| 518 |
this.isCollapsed = false;
|
| 519 |
+
this.isMobile = window.innerWidth <= 768 || window.matchMedia('(orientation: portrait)').matches;
|
| 520 |
this.setupUI();
|
| 521 |
this.setupEventListeners();
|
| 522 |
+
window.subtitleManagerInstance = this;
|
| 523 |
+
}
|
| 524 |
+
cleanup() {
|
| 525 |
+
// Remove existing instance
|
| 526 |
+
if (this.container) {
|
| 527 |
+
this.container.remove();
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// Clear any event listeners
|
| 531 |
+
window.removeEventListener('resize', this.resizeHandler);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
setupResizeHandler() {
|
| 535 |
+
this.resizeHandler = () => {
|
| 536 |
+
const isMobile = window.innerWidth <= 768 || window.matchMedia('(orientation: portrait)').matches;
|
| 537 |
+
if (this.isMobile !== isMobile) {
|
| 538 |
+
this.isMobile = isMobile;
|
| 539 |
+
this.updateLayout();
|
| 540 |
+
}
|
| 541 |
+
};
|
| 542 |
+
|
| 543 |
+
window.addEventListener('resize', this.resizeHandler);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
updateLayout() {
|
| 547 |
+
if (this.isMobile) {
|
| 548 |
+
// Insert after video player
|
| 549 |
+
const player = document.querySelector('.html5-video-player');
|
| 550 |
+
if (player?.parentNode) {
|
| 551 |
+
player.parentNode.insertBefore(this.container, player.nextSibling);
|
| 552 |
+
}
|
| 553 |
+
} else {
|
| 554 |
+
// Move back to body for desktop view
|
| 555 |
+
document.body.appendChild(this.container);
|
| 556 |
+
}
|
| 557 |
}
|
| 558 |
|
| 559 |
checkCaptionSources(player) {
|
|
|
|
| 936 |
wordBtn.onclick = async (e) => {
|
| 937 |
e.stopPropagation();
|
| 938 |
console.log(`You click "${word.text.trim()}" word`);
|
| 939 |
+
const wordText = word.text.trim();
|
| 940 |
let popup = document.querySelector('.word-definition-popup');
|
| 941 |
if (!popup) {
|
| 942 |
// Create popup container
|
| 943 |
popup = document.createElement('div');
|
| 944 |
popup.className = 'word-definition-popup';
|
| 945 |
|
| 946 |
+
|
| 947 |
+
|
| 948 |
// Create markdown container
|
| 949 |
const markdownDiv = document.createElement('div');
|
| 950 |
markdownDiv.className = 'markdown';
|
|
|
|
| 958 |
popup.classList.remove('show');
|
| 959 |
});
|
| 960 |
}
|
| 961 |
+
// Update speaker button for current word
|
| 962 |
+
let speakerBtn = popup.querySelector('.speaker-btn');
|
| 963 |
+
if (speakerBtn) {
|
| 964 |
+
speakerBtn.remove();
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
// Create new speaker button with current word
|
| 968 |
+
speakerBtn = document.createElement('button');
|
| 969 |
+
speakerBtn.className = 'speaker-btn';
|
| 970 |
+
speakerBtn.textContent = '🔊';
|
| 971 |
+
speakerBtn.onclick = (e) => {
|
| 972 |
+
e.stopPropagation();
|
| 973 |
+
try {
|
| 974 |
+
const utterance = new SpeechSynthesisUtterance(wordText); // Use current wordText
|
| 975 |
+
utterance.lang = 'en-US';
|
| 976 |
+
utterance.rate = 0.9;
|
| 977 |
+
speechSynthesis.speak(utterance);
|
| 978 |
+
} catch (error) {
|
| 979 |
+
console.error('TTS failed:', error);
|
| 980 |
+
}
|
| 981 |
+
};
|
| 982 |
+
|
| 983 |
+
// Insert new speaker button at the start
|
| 984 |
+
popup.insertBefore(speakerBtn, popup.firstChild);
|
| 985 |
|
| 986 |
// Ensure markdown container exists
|
| 987 |
let markdownContainer = popup.querySelector('.markdown');
|
|
|
|
| 1124 |
this.subtitles.english?.length || 0,
|
| 1125 |
this.subtitles.chinese?.length || 0
|
| 1126 |
);
|
| 1127 |
+
|
| 1128 |
for (let i = 0; i < maxLength; i++) {
|
| 1129 |
const line = document.createElement('div');
|
| 1130 |
line.className = 'subtitle-line mixed-line';
|
| 1131 |
+
|
| 1132 |
// Text container
|
| 1133 |
const textContainer = document.createElement('div');
|
| 1134 |
textContainer.className = 'mixed-text-container';
|
| 1135 |
+
|
| 1136 |
// Chinese text with word buttons
|
| 1137 |
const zhText = document.createElement('div');
|
| 1138 |
zhText.className = 'zh-text';
|
|
|
|
| 1151 |
} else {
|
| 1152 |
zhText.textContent = '翻译中...';
|
| 1153 |
}
|
| 1154 |
+
|
| 1155 |
// English text with word buttons
|
| 1156 |
const enText = document.createElement('div');
|
| 1157 |
enText.className = 'en-text';
|
|
|
|
| 1168 |
}
|
| 1169 |
});
|
| 1170 |
}
|
| 1171 |
+
|
| 1172 |
textContainer.appendChild(zhText);
|
| 1173 |
textContainer.appendChild(enText);
|
| 1174 |
line.appendChild(textContainer);
|
| 1175 |
+
|
| 1176 |
// Add timestamp
|
| 1177 |
const timestamp = document.createElement('span');
|
| 1178 |
timestamp.className = 'timestamp';
|
| 1179 |
+
timestamp.textContent = this.subtitles.english[i] ?
|
| 1180 |
this.formatTime(this.subtitles.english[i].startTime) : '';
|
| 1181 |
line.appendChild(timestamp);
|
| 1182 |
+
|
| 1183 |
if (this.subtitles.english[i]) {
|
| 1184 |
line.dataset.time = this.subtitles.english[i].startTime;
|
| 1185 |
line.dataset.index = i;
|
| 1186 |
}
|
| 1187 |
+
|
| 1188 |
container.appendChild(line);
|
| 1189 |
}
|
| 1190 |
}
|
|
|
|
| 1747 |
debug.log('Current URL:', window.location.href);
|
| 1748 |
debug.log('Document ready state:', document.readyState);
|
| 1749 |
|
| 1750 |
+
// Cleanup existing instance
|
| 1751 |
+
if (window.subtitleManagerInstance) {
|
| 1752 |
+
window.subtitleManagerInstance.cleanup();
|
| 1753 |
+
}
|
| 1754 |
if (!window.location.pathname.includes('/watch')) {
|
| 1755 |
debug.log('Not a video page, skipping');
|
| 1756 |
return;
|