// ==UserScript== // @name 全网视频倍速控制 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 支持所有网站的视频倍速播放控制,可锁定速度防止网站重置 // @author GQLJ // @license MIT // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/559237/%E5%85%A8%E7%BD%91%E8%A7%86%E9%A2%91%E5%80%8D%E9%80%9F%E6%8E%A7%E5%88%B6.user.js // @updateURL https://update.greasyfork.icu/scripts/559237/%E5%85%A8%E7%BD%91%E8%A7%86%E9%A2%91%E5%80%8D%E9%80%9F%E6%8E%A7%E5%88%B6.meta.js // ==/UserScript== (function() { 'use strict'; // ==================== 配置 ==================== const CONFIG = { MIN_SPEED: 0.25, MAX_SPEED: 5, STEP: 0.25, DEFAULT_SPEED: 1, QUICK_BUTTONS: [0.5, 1, 1.5, 2, 3], STORAGE_KEY: 'videoSpeedControl_', CHECK_INTERVAL: 1000, DEBOUNCE_DELAY: 100 }; // ==================== 状态 ==================== const hostname = location.hostname; let currentSpeed = GM_getValue(CONFIG.STORAGE_KEY + hostname + '_speed', CONFIG.DEFAULT_SPEED); let isLocked = GM_getValue(CONFIG.STORAGE_KEY + hostname + '_locked', false); let isCollapsed = GM_getValue(CONFIG.STORAGE_KEY + 'collapsed', false); let panelPosition = GM_getValue(CONFIG.STORAGE_KEY + 'position', { right: 20, top: 100 }); let checkInterval = null; let videos = []; // ==================== 工具函数 ==================== function fixPrecision(num) { return Math.round(num * 100) / 100; } function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } function showToast(message) { const existingToast = document.getElementById('speed-toast'); if (existingToast) existingToast.remove(); const toast = document.createElement('div'); toast.id = 'speed-toast'; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 18px; font-weight: bold; z-index: 2147483647; pointer-events: none; animation: toastFade 1.5s ease-in-out forwards; font-family: Arial, sans-serif; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 1500); } // ==================== 核心功能 ==================== function applySpeedToAll(showTip = false) { videos = document.querySelectorAll('video'); videos.forEach(video => { try { if (video.playbackRate !== currentSpeed) { video.playbackRate = currentSpeed; } } catch (e) { console.warn('[倍速控制] 设置速度失败:', e); } }); if (showTip && videos.length > 0) { showToast(`${currentSpeed}x`); } } function setSpeed(speed, showTip = true, saveToStorage = true) { currentSpeed = fixPrecision(Math.max(CONFIG.MIN_SPEED, Math.min(CONFIG.MAX_SPEED, speed))); if (saveToStorage) { GM_setValue(CONFIG.STORAGE_KEY + hostname + '_speed', currentSpeed); } applySpeedToAll(showTip); updateUI(); updateCheckInterval(); } // 同步网站速度到面板(不保存、不提示) function syncSpeedFromVideo(speed) { if (isLocked) return; // 锁定模式不同步 const fixedSpeed = fixPrecision(speed); if (fixedSpeed >= CONFIG.MIN_SPEED && fixedSpeed <= CONFIG.MAX_SPEED && fixedSpeed !== currentSpeed) { currentSpeed = fixedSpeed; updateUI(); } } function toggleLock(locked) { isLocked = locked; GM_setValue(CONFIG.STORAGE_KEY + hostname + '_locked', isLocked); updateCheckInterval(); if (isLocked) { applySpeedToAll(); showToast(`🔒 锁定 ${currentSpeed}x`); } else { showToast(`🔓 跟随网站`); } } function updateCheckInterval() { if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } if (isLocked && document.querySelectorAll('video').length > 0) { checkInterval = setInterval(() => { const currentVideos = document.querySelectorAll('video'); if (currentVideos.length === 0) { clearInterval(checkInterval); checkInterval = null; return; } applySpeedToAll(); }, CONFIG.CHECK_INTERVAL); } } function setupVideoListeners(video) { if (video._speedControlSetup) return; video._speedControlSetup = true; // 监听速度变化 video.addEventListener('ratechange', () => { if (isLocked) { // 锁定模式:强制恢复 if (video.playbackRate !== currentSpeed) { try { video.playbackRate = currentSpeed; } catch (e) {} } } else { // 跟随模式:同步到面板 syncSpeedFromVideo(video.playbackRate); } }); // 初始应用速度 if (isLocked || currentSpeed !== CONFIG.DEFAULT_SPEED) { try { video.playbackRate = currentSpeed; } catch (e) {} } } // ==================== UI 相关 ==================== let panel, miniBtn, slider, speedDisplay, lockBtn, followBtn; function createUI() { const style = document.createElement('style'); style.textContent = ` @keyframes toastFade { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } 15% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 85% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } } #speed-control-panel *, #speed-mini-btn * { box-sizing: border-box; font-family: Arial, sans-serif; } #speed-control-panel button:hover, #speed-mini-btn:hover { filter: brightness(1.1); } #speed-control-panel button:active { transform: scale(0.95); } `; (document.head || document.documentElement).appendChild(style); // 迷你按钮(折叠时显示) miniBtn = document.createElement('div'); miniBtn.id = 'speed-mini-btn'; miniBtn.style.cssText = ` position: fixed; right: ${panelPosition.right}px; top: ${panelPosition.top}px; width: 40px; height: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: ${isCollapsed ? 'flex' : 'none'}; justify-content: center; align-items: center; cursor: pointer; z-index: 2147483646; box-shadow: 0 2px 10px rgba(0,0,0,0.3); font-size: 16px; transition: transform 0.2s; `; miniBtn.innerHTML = '⚡'; miniBtn.title = `当前: ${currentSpeed}x | 点击展开`; // 完整面板 panel = document.createElement('div'); panel.id = 'speed-control-panel'; panel.style.cssText = ` position: fixed; right: ${panelPosition.right}px; top: ${panelPosition.top}px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 12px; z-index: 2147483646; box-shadow: 0 4px 20px rgba(0,0,0,0.3); user-select: none; min-width: 200px; color: #fff; font-size: 14px; display: ${isCollapsed ? 'none' : 'block'}; `; // 标题栏 const header = document.createElement('div'); header.id = 'speed-header'; header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; cursor: move; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.2); margin-bottom: 10px; `; header.innerHTML = ` 🎬 倍速控制 收起 `; panel.appendChild(header); // 速度显示 speedDisplay = document.createElement('div'); speedDisplay.style.cssText = ` text-align: center; font-size: 24px; font-weight: bold; margin: 10px 0; `; speedDisplay.textContent = `${currentSpeed}x`; panel.appendChild(speedDisplay); // 滑块 slider = document.createElement('input'); slider.type = 'range'; slider.min = CONFIG.MIN_SPEED; slider.max = CONFIG.MAX_SPEED; slider.step = CONFIG.STEP; slider.value = currentSpeed; slider.style.cssText = ` width: 100%; margin: 10px 0; cursor: pointer; `; panel.appendChild(slider); // 快捷按钮 const quickBtns = document.createElement('div'); quickBtns.style.cssText = ` display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; margin: 10px 0; `; CONFIG.QUICK_BUTTONS.forEach(speed => { const btn = document.createElement('button'); btn.textContent = `${speed}x`; btn.style.cssText = ` padding: 5px 10px; border: none; border-radius: 5px; background: rgba(255,255,255,0.2); color: #fff; cursor: pointer; transition: all 0.2s; `; btn.onclick = () => setSpeed(speed); quickBtns.appendChild(btn); }); panel.appendChild(quickBtns); // 模式切换 const modeDiv = document.createElement('div'); modeDiv.style.cssText = ` display: flex; gap: 8px; margin-top: 10px; `; followBtn = document.createElement('button'); followBtn.textContent = '🔓 跟随网站'; followBtn.style.cssText = ` flex: 1; padding: 8px; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 12px; `; lockBtn = document.createElement('button'); lockBtn.textContent = '🔒 锁定倍速'; lockBtn.style.cssText = ` flex: 1; padding: 8px; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 12px; `; modeDiv.appendChild(followBtn); modeDiv.appendChild(lockBtn); panel.appendChild(modeDiv); // 快捷键提示 const shortcutTip = document.createElement('div'); shortcutTip.style.cssText = ` text-align: center; font-size: 11px; color: rgba(255,255,255,0.7); margin-top: 10px; `; shortcutTip.textContent = '快捷键: [ 减速 | ] 加速'; panel.appendChild(shortcutTip); const appendPanel = () => { if (document.body) { document.body.appendChild(miniBtn); document.body.appendChild(panel); bindEvents(); updateUI(); } else { requestAnimationFrame(appendPanel); } }; appendPanel(); } function updateUI() { if (!speedDisplay) return; speedDisplay.textContent = `${currentSpeed}x`; slider.value = currentSpeed; miniBtn.title = `当前: ${currentSpeed}x${isLocked ? ' 🔒' : ''} | 点击展开`; if (isLocked) { lockBtn.style.background = '#4CAF50'; lockBtn.style.color = '#fff'; followBtn.style.background = 'rgba(255,255,255,0.2)'; followBtn.style.color = '#fff'; } else { followBtn.style.background = '#2196F3'; followBtn.style.color = '#fff'; lockBtn.style.background = 'rgba(255,255,255,0.2)'; lockBtn.style.color = '#fff'; } } function toggleCollapse(collapsed) { isCollapsed = collapsed; panel.style.display = isCollapsed ? 'none' : 'block'; miniBtn.style.display = isCollapsed ? 'flex' : 'none'; GM_setValue(CONFIG.STORAGE_KEY + 'collapsed', isCollapsed); } function bindEvents() { const header = document.getElementById('speed-header'); const collapseBtn = document.getElementById('collapse-btn'); // 折叠 collapseBtn.onclick = (e) => { e.stopPropagation(); toggleCollapse(true); }; // 迷你按钮点击展开(带拖拽判断) let miniBtnDragged = false; miniBtn.addEventListener('click', () => { if (!miniBtnDragged) { toggleCollapse(false); } miniBtnDragged = false; }); // 滑块事件 const debouncedSetSpeed = debounce((value) => { setSpeed(parseFloat(value)); }, CONFIG.DEBOUNCE_DELAY); slider.addEventListener('input', (e) => { speedDisplay.textContent = `${fixPrecision(parseFloat(e.target.value))}x`; }); slider.addEventListener('change', (e) => { debouncedSetSpeed(e.target.value); }); // 模式按钮 followBtn.onclick = () => toggleLock(false); lockBtn.onclick = () => toggleLock(true); // 拖拽功能 function makeDraggable(element, onDragEnd) { let isDragging = false; let hasMoved = false; let startX, startY, startRight, startTop; const onMouseMove = (e) => { if (!isDragging) return; const deltaX = startX - e.clientX; const deltaY = e.clientY - startY; // 判断是否真的移动了 if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) { hasMoved = true; } const newRight = Math.max(0, Math.min(window.innerWidth - element.offsetWidth, startRight + deltaX)); const newTop = Math.max(0, Math.min(window.innerHeight - element.offsetHeight, startTop + deltaY)); element.style.right = newRight + 'px'; element.style.top = newTop + 'px'; // 同步另一个元素的位置 if (element === panel) { miniBtn.style.right = newRight + 'px'; miniBtn.style.top = newTop + 'px'; } else { panel.style.right = newRight + 'px'; panel.style.top = newTop + 'px'; } }; const onMouseUp = () => { if (isDragging) { isDragging = false; if (hasMoved) { panelPosition = { right: parseInt(element.style.right), top: parseInt(element.style.top) }; GM_setValue(CONFIG.STORAGE_KEY + 'position', panelPosition); if (onDragEnd) onDragEnd(true); } else { if (onDragEnd) onDragEnd(false); } } document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; return (e) => { if (e.target.id === 'collapse-btn') return; isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; startRight = parseInt(element.style.right); startTop = parseInt(element.style.top); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; } header.addEventListener('mousedown', makeDraggable(panel)); miniBtn.addEventListener('mousedown', makeDraggable(miniBtn, (dragged) => { miniBtnDragged = dragged; })); // 键盘快捷键 document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { return; } if (e.key === '[') { e.preventDefault(); setSpeed(currentSpeed - CONFIG.STEP); } else if (e.key === ']') { e.preventDefault(); setSpeed(currentSpeed + CONFIG.STEP); } }); } // ==================== 视频监听 ==================== function observeVideos() { // 处理已存在的视频 document.querySelectorAll('video').forEach(video => { setupVideoListeners(video); }); // 监听新增视频 const observer = new MutationObserver((mutations) => { let hasNewVideo = false; mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeName === 'VIDEO') { hasNewVideo = true; setupVideoListeners(node); } if (node.querySelectorAll) { node.querySelectorAll('video').forEach(video => { hasNewVideo = true; setupVideoListeners(video); }); } }); }); if (hasNewVideo) { updateCheckInterval(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } // ==================== 初始化 ==================== function init() { createUI(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { observeVideos(); updateCheckInterval(); }); } else { observeVideos(); updateCheckInterval(); } } init(); })();