您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a button to load local SRT or VTT subtitle files on YouTube. Supports both hh:mm:ss,ms and mm:ss,ms time formats.
// ==UserScript== // @name YouTube Local Subtitle Loader // @namespace http://tampermonkey.net/ // @version 1.3 // @description Adds a button to load local SRT or VTT subtitle files on YouTube. Supports both hh:mm:ss,ms and mm:ss,ms time formats. // @match https://www.youtube.com/watch?* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 在这里自定义字幕样式 --- const subtitleStyle = ` position: absolute; bottom: 8%; /* 字幕距离底部的距离 */ width: 90%; /* 字幕区域宽度 */ left: 5%; /* 居中 */ text-align: center; pointer-events: none; z-index: 99999; /* 确保在最顶层,避免被遮挡 */ font-size: 2.2em; /* 字体大小 */ color: white; /* 字体颜色 */ font-weight: bold; text-shadow: 2px 2px 2px #000000, -2px -2px 2px #000000, 2px -2px 2px #000000, -2px 2px 2px #000000; /* 黑色描边,增强可读性 */ font-family: Arial, "Heiti SC", "Microsoft Yahei", sans-serif; line-height: 1.4; `; let subtitles = []; let videoElement = null; let subtitleContainer = null; let animationFrameId = null; // 将时间字符串 (HH:MM:SS,ms 或 MM:SS,ms) 转换为秒 function timeToSeconds(timeString) { const parts = timeString.replace('.', ',').split(/[:,]/).reverse(); // 反转数组,从毫秒开始处理 if (parts.length < 3) return 0; // 容错处理,至少有 M:S,ms const milliseconds = parseInt(parts[0], 10) || 0; const seconds = parseInt(parts[1], 10) || 0; const minutes = parseInt(parts[2], 10) || 0; const hours = parseInt(parts[3], 10) || 0; // 如果不存在(mm:ss,ms),则为 undefined,parseInt结果为NaN,最终取0 return (hours * 3600) + (minutes * 60) + seconds + (milliseconds / 1000); } // 解析 SRT/VTT 字幕内容 (兼容两种格式) function parseSubtitles(subtitleContent) { const lines = subtitleContent.replace(/\r/g, '').split('\n\n'); const subs = []; for (const line of lines) { const parts = line.split('\n'); if (parts.length < 2) continue; const timeMatch = parts.find(p => p.includes('-->')); if (!timeMatch) continue; const timeParts = timeMatch.split(' --> '); if (timeParts.length !== 2) continue; const startTime = timeToSeconds(timeParts[0]); const endTime = timeToSeconds(timeParts[1]); const text = parts.slice(parts.indexOf(timeMatch) + 1).join('\n').trim(); if (text) { subs.push({ start: startTime, end: endTime, text: text }); } } return subs; } // 更新字幕显示 function updateSubtitles() { if (!videoElement || !subtitleContainer) { animationFrameId = requestAnimationFrame(updateSubtitles); return; } const currentTime = videoElement.currentTime; const currentSub = subtitles.find(sub => currentTime >= sub.start && currentTime <= sub.end); if (currentSub && subtitleContainer.innerHTML !== currentSub.text) { subtitleContainer.innerHTML = currentSub.text.replace(/\n/g, '<br>'); subtitleContainer.style.visibility = 'visible'; } else if (!currentSub && subtitleContainer.innerHTML !== '') { subtitleContainer.innerHTML = ''; subtitleContainer.style.visibility = 'hidden'; } animationFrameId = requestAnimationFrame(updateSubtitles); } // 处理文件选择 function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { const content = e.target.result; subtitles = parseSubtitles(content); if(subtitles.length > 0){ alert(`字幕加载成功!共 ${subtitles.length} 条。`); if (animationFrameId) { cancelAnimationFrame(animationFrameId); } updateSubtitles(); } else { alert('无法解析字幕文件,请检查文件格式是否正确 (支持 SRT 和 VTT) 或文件编码是否为 UTF-8。'); } }; reader.readAsText(file, 'UTF-8'); } // 创建并注入UI元素 function injectUI() { const player = document.querySelector('#movie_player'); // 新的按钮位置,在点赞按钮的容器里 const actionsContainer = document.querySelector('#below #actions'); if (!player || !actionsContainer) { return; // 如果元素未找到,则不执行任何操作 } // --- 创建字幕显示容器 (如果不存在) --- if (!document.getElementById('custom-subtitle-display')) { subtitleContainer = document.createElement('div'); subtitleContainer.id = 'custom-subtitle-display'; subtitleContainer.style.cssText = subtitleStyle; player.appendChild(subtitleContainer); console.log('Subtitle container injected.'); } // --- 创建文件选择器 (如果不存在) --- if (!document.getElementById('local-subtitle-input-container')) { const container = document.createElement('div'); container.id = 'local-subtitle-input-container'; container.style.cssText = ` display: flex; align-items: center; margin: 0 8px; `; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.srt,.vtt'; fileInput.style.display = 'none'; fileInput.id = 'local-subtitle-input'; fileInput.addEventListener('change', handleFileSelect); const fileInputLabel = document.createElement('label'); fileInputLabel.htmlFor = 'local-subtitle-input'; fileInputLabel.textContent = '加载字幕'; fileInputLabel.style.cssText = ` cursor: pointer; background-color: #eee; color: #333; padding: 8px 12px; border-radius: 18px; font-size: 14px; font-weight: 500; transition: background-color 0.3s; white-space: nowrap; /* 关键改动:强制不换行 */ `; // 适配暗色模式 if(document.querySelector('html[dark=true]')) { fileInputLabel.style.backgroundColor = '#3f3f3f'; fileInputLabel.style.color = '#fff'; } fileInputLabel.onmouseover = () => { fileInputLabel.style.backgroundColor = '#ccc'; }; fileInputLabel.onmouseout = () => { fileInputLabel.style.backgroundColor = document.querySelector('html[dark=true]') ? '#3f3f3f' : '#eee'; }; container.appendChild(fileInput); container.appendChild(fileInputLabel); // 插入到 "分享" 按钮后面 const shareButton = actionsContainer.querySelector('ytd-button-renderer:nth-child(2)'); if (shareButton) { shareButton.parentNode.insertBefore(container, shareButton.nextSibling); } else { actionsContainer.appendChild(container); // 备用方案 } console.log('UI button injected.'); } // 获取 video 元素 videoElement = document.querySelector('#movie_player video'); if (!videoElement) { console.error("Could not find video element."); return; } // 确保字幕循环只在需要时启动 if (!animationFrameId) { updateSubtitles(); } } // 使用 MutationObserver 来确保在页面动态加载完成后执行脚本 const observer = new MutationObserver((mutations, obs) => { if (document.querySelector('#below #actions') && document.querySelector('#movie_player video')) { injectUI(); obs.disconnect(); // 找到元素后停止观察,避免重复执行 } }); observer.observe(document.body, { childList: true, subtree: true }); })();