NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name SpeedMaster — Universal Media Playback Controller // @namespace https://openuserjs.org/users/barretlee // @version 2.2 // @description 视频/音频控制面板:支持倍速调节、广告快进、关闭清理、域名排除、ARIA 无障碍。优化关闭按钮浮层与“本域名下不再显示”交互体验。 // @author Barret Lee // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; const SPEED_OPTIONS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 3.5, 4, 4.5, 5, 16]; let observer = null; let intervalId = null; const getExcludedDomains = () => GM_getValue('excludedDomains', []); const saveExcludedDomains = (list) => GM_setValue('excludedDomains', list); GM_registerMenuCommand('⚙️ 配置排除域名', () => { const currentList = getExcludedDomains(); const input = prompt('请输入不显示控制器的域名(用逗号分隔,例如:youtube.com,bilibili.com)', currentList.join(',')); if (input !== null) { const newList = input.split(',').map(d => d.trim()).filter(Boolean); saveExcludedDomains(newList); alert('✅ 域名排除列表已更新:\n' + newList.join(', ')); } }); const isExcludedDomain = () => getExcludedDomains().some(domain => location.hostname.endsWith(domain)); if (isExcludedDomain()) return console.log('[SpeedMaster] 当前域名在排除列表中,跳过加载。'); const htmlPolicy = window.trustedTypes?.createPolicy('speed-controller-policy', { createHTML: s => s }); const createController = (media) => { if (document.querySelector('#speed-controller')) return; const controller = document.createElement('div'); controller.id = 'speed-controller'; const html = ` <style> #speed-controller { position: fixed; right: 24px; bottom: 24px; background: rgba(25, 25, 25, 0.78); color: #fff; padding: 6px 18px 14px; border-radius: 16px; font-size: 14px; z-index: 999999; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); backdrop-filter: blur(10px); transition: all 0.25s ease; min-width: 200px; } #speed-controller:hover { background: rgba(35,35,35,0.9); transform: translateY(-2px); } #speed-controller:focus { outline: 2px solid #00b4ff; } #speed-controller button, #speed-controller select { background: rgba(255,255,255,0.08); color: #fff; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; padding: 6px 10px; margin: 3px; cursor: pointer; } #speed-controller button:hover, #speed-controller select:hover { background: rgba(255,255,255,0.18); transform: translateY(-1px); } #close-wrapper { position: absolute; top: 10px; right: 8px; display: flex; align-items: center; flex-direction: column; } #close-btn { font-size: 16px; color: #ccc; background: transparent; border: none; cursor: pointer; transition: color 0.2s; padding: 2px 4px; } #close-btn:hover { color: #fff; } #exclude-tip { display: none; position: absolute; top: -32px; right: -4px; background: rgba(40,40,40,0.92); border: 1px solid rgba(255,255,255,0.15); color: #eee; font-size: 12px; padding: 6px 10px; border-radius: 8px; white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.3); backdrop-filter: blur(6px); } #close-wrapper:hover #exclude-tip { display: block; } #exclude-tip label { display: flex; align-items: center; gap: 6px; cursor: pointer; } #exclude-checkbox { accent-color: #00b4ff; transform: scale(1.1); } #control-row { display: flex; align-items: center; margin-top: 18px; margin-bottom: 8px; } #speed-display { margin: 0 8px; font-weight: bold; font-size: 15px; } </style> <div id="close-wrapper"> <button id="close-btn" tabindex="0" aria-label="关闭控制面板">×</button> <div id="exclude-tip"> <label for="exclude-checkbox" title="关闭后将不再在此域名显示"> <input type="checkbox" id="exclude-checkbox" aria-label="不再在此域名下显示控制面板"> 本域名下不再显示 </label> </div> </div> <div id="control-row"> <span>速度:</span> <button id="speed-down" tabindex="0" aria-label="减慢播放速度">-</button> <span id="speed-display" role="status" aria-live="polite">${media.playbackRate.toFixed(2)}x</span> <button id="speed-up" tabindex="0" aria-label="加快播放速度">+</button> </div> <div> <select id="speed-select" tabindex="0" aria-label="选择播放速度"></select> <button id="speed-reset" tabindex="0" aria-label="重置播放速度为一倍">重置</button> <button id="skip-ads" tabindex="0" aria-label="跳过广告或恢复正常播放">跳过广告 🚀</button> </div> `; controller.innerHTML = htmlPolicy ? htmlPolicy.createHTML(html) : html; document.body.appendChild(controller); const speedSelect = controller.querySelector('#speed-select'); const btnDown = controller.querySelector('#speed-down'); const btnUp = controller.querySelector('#speed-up'); const btnReset = controller.querySelector('#speed-reset'); const btnSkip = controller.querySelector('#skip-ads'); const btnClose = controller.querySelector('#close-btn'); const display = controller.querySelector('#speed-display'); const excludeCheckbox = controller.querySelector('#exclude-checkbox'); SPEED_OPTIONS.forEach(speed => { const opt = document.createElement('option'); opt.value = speed; opt.textContent = `${speed}x`; if (speed === 1) opt.selected = true; speedSelect.appendChild(opt); }); const updateDisplay = () => { display.textContent = `${media.playbackRate.toFixed(2)}x`; speedSelect.value = SPEED_OPTIONS.includes(media.playbackRate) ? media.playbackRate : 1; }; btnDown.onclick = () => { media.playbackRate = Math.max(0.5, media.playbackRate - 0.25); updateDisplay(); }; btnUp.onclick = () => { media.playbackRate = Math.min(5, media.playbackRate + 0.25); updateDisplay(); }; btnReset.onclick = () => { media.playbackRate = 1; updateDisplay(); }; speedSelect.onchange = () => { media.playbackRate = parseFloat(speedSelect.value); updateDisplay(); }; let skipping = false; btnSkip.onclick = () => { media.playbackRate = skipping ? 1 : 16; skipping = !skipping; btnSkip.textContent = skipping ? '恢复正常' : '跳过广告 🚀'; updateDisplay(); }; btnClose.onclick = () => { observer?.disconnect(); clearInterval(intervalId); controller.remove(); console.log('[SpeedMaster] 控制面板已关闭。'); if (excludeCheckbox.checked) { const currentList = getExcludedDomains(); const hostname = location.hostname.split('.').slice(-2).join('.'); if (!currentList.includes(hostname)) { currentList.push(hostname); saveExcludedDomains(currentList); alert(`✅ 已将 ${hostname} 加入排除列表,下次将不再显示。`); } } }; }; const detectAndCreate = () => { const media = document.querySelector('video, audio'); if (media) createController(media); }; document.addEventListener('DOMContentLoaded', detectAndCreate); observer = new MutationObserver(detectAndCreate); observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); intervalId = setInterval(detectAndCreate, 2000); })();