Greasy Fork is available in English.
支持通过菜单配置中文字体大小
当前为
// ==UserScript==
// @name YouTube 中英双语字幕
// @version 3.0
// @author Gemini/ChatGPT
// @description 支持通过菜单配置中文字体大小
// @match *://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @run-at document-start
// @namespace MyYouTubeSubtitle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const TARGET_LANG = 'zh-CN';
const translationCache = new Map();
const timers = new Map();
// -------------------- 0. Trusted Types 兼容处理 --------------------
let ttPolicy;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
ttPolicy = window.trustedTypes.createPolicy('youtube-dual-subtitles-policy', {
createHTML: (string) => string
});
}
function setSafeContent(el, content) {
if (!el) return;
el.innerText = content; // 优先使用 innerText 规避 CSP
}
// -------------------- 1. 配置管理 & 菜单 --------------------
let fontSize = GM_getValue('subtitle_font_size', 2);
function registerMenu() {
// 设置字体大小菜单
GM_registerMenuCommand(`⚙️ 设置中文字体大小 (当前: ${fontSize}em)`, () => {
let newSize = prompt("请输入字体大小 (单位为 em,例如 1.1 或 1.5):", fontSize);
if (newSize !== null) {
newSize = parseFloat(newSize);
if (!isNaN(newSize) && newSize > 0) {
fontSize = newSize;
GM_setValue('subtitle_font_size', newSize);
updateStyle();
alert(`设置成功!当前字号为: ${newSize}em`);
location.reload();
} else {
alert("请输入有效的数字!");
}
}
});
// 反馈菜单
GM_registerMenuCommand('💬 反馈 & 建议', () => {
GM_openInTab('http://greasyfork.icu/zh-CN/scripts/567512/feedback', { active: true });
});
}
// -------------------- 2. 注入 CSS --------------------
function updateStyle() {
const oldStyle = document.getElementById('my-subtitle-style');
if (oldStyle) oldStyle.remove();
const cssText = `
.ytp-caption-window-container .caption-visual-line {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-end !important;
}
.ytp-caption-segment {
display: inline !important;
white-space: pre-wrap !important;
}
.my-trans-line {
display: block !important;
color: #FFCC00 !important;
font-size: ${fontSize}em !important;
font-weight: bold !important;
line-height: 1.3 !important;
margin-top: 6px !important;
padding: 3px 10px !important;
background: rgba(0, 0, 0, 0.65) !important;
border-radius: 5px !important;
text-shadow: 1px 1px 2px rgba(0,0,0,1) !important;
white-space: pre-wrap !important;
z-index: 10 !important;
text-align: center !important;
}
`;
const style = document.createElement('style');
style.id = 'my-subtitle-style';
// 同样对 CSS 进行 Trusted Types 处理
if (ttPolicy && style.hasOwnProperty('innerHTML')) {
style.innerHTML = ttPolicy.createHTML(cssText);
} else {
style.textContent = cssText;
}
document.head.appendChild(style);
}
// -------------------- 3. 翻译引擎 --------------------
async function translate(text) {
if (!text || text.length < 2) return null;
if (translationCache.has(text)) return translationCache.get(text);
try {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${TARGET_LANG}&dt=t&q=${encodeURIComponent(text)}`;
const res = await fetch(url);
const data = await res.json();
if (data && data[0]) {
const result = data[0].map(x => x[0]).join('');
translationCache.set(text, result);
return result;
}
} catch (e) {
console.error('Translation Error:', e);
}
return null;
}
// -------------------- 4. 处理逻辑 --------------------
function processLine(line) {
const segments = line.querySelectorAll('.ytp-caption-segment');
if (!segments.length) return;
const fullText = Array.from(segments)
.map(s => s.innerText)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
if (!fullText) return;
let transDiv = line.querySelector('.my-trans-line');
if (translationCache.has(fullText)) {
const cachedText = translationCache.get(fullText);
if (!transDiv) {
transDiv = document.createElement('div');
transDiv.className = 'my-trans-line';
line.appendChild(transDiv);
}
if (transDiv.innerText !== cachedText) setSafeContent(transDiv, cachedText);
line.dataset.lastSentText = fullText;
return;
}
if (line.dataset.lastSentText === fullText) {
if (!transDiv && line.dataset.lastTranslatedText) {
transDiv = document.createElement('div');
transDiv.className = 'my-trans-line';
setSafeContent(transDiv, line.dataset.lastTranslatedText);
line.appendChild(transDiv);
}
return;
}
line.dataset.lastSentText = fullText;
clearTimeout(timers.get(line));
const timer = setTimeout(async () => {
if (!line.isConnected) return;
const result = await translate(fullText);
if (result && line.isConnected) {
line.dataset.lastTranslatedText = result;
let currentTransDiv = line.querySelector('.my-trans-line');
if (!currentTransDiv) {
currentTransDiv = document.createElement('div');
currentTransDiv.className = 'my-trans-line';
line.appendChild(currentTransDiv);
}
setSafeContent(currentTransDiv, result);
}
}, 250);
timers.set(line, timer);
}
// -------------------- 5. 启动与保底 --------------------
const observer = new MutationObserver(() => {
const lines = document.querySelectorAll('.caption-visual-line');
if (lines.length > 0) lines.forEach(processLine);
});
function init() {
const container = document.querySelector('.ytp-caption-window-container');
if (container) {
observer.observe(container, { childList: true, subtree: true, characterData: true });
} else {
setTimeout(init, 1000);
}
}
registerMenu();
updateStyle();
init();
window.addEventListener('yt-navigate-finish', () => {
timers.clear();
init();
});
setInterval(() => {
const lines = document.querySelectorAll('.caption-visual-line');
if (lines.length > 0) lines.forEach(processLine);
}, 400);
})();