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.5
// @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, intervalId = null, currentMedia = null, skipping = false;
let controllerPos = { left: null, top: 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 resetControls = (media) => {
if (!media) return;
media.playbackRate = 1;
skipping = false;
const btnSkip = document.querySelector('#skip-ads');
if (btnSkip) btnSkip.textContent = '跳过广告 🚀';
const display = document.querySelector('#speed-display');
if (display) display.textContent = `${media.playbackRate.toFixed(2)}x`;
const select = document.querySelector('#speed-select');
if (select) select.value = 1;
};
const clampPosition = (left, top, el) => {
const maxLeft = window.innerWidth - el.offsetWidth - 10;
const maxTop = window.innerHeight - el.offsetHeight - 10;
return {
left: Math.min(Math.max(10, left), maxLeft),
top: Math.min(Math.max(10, top), maxTop)
};
};
const createController = (media) => {
if (document.querySelector('#speed-controller')) return;
const controller = document.createElement('div');
controller.id = 'speed-controller';
controller.style.left = controllerPos.left ? `${controllerPos.left}px` : 'auto';
controller.style.top = controllerPos.top ? `${controllerPos.top}px` : 'auto';
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;
cursor: default;
}
#speed-controller.dragging { opacity: 0.9; cursor: grabbing; }
#speed-controller:hover { background: rgba(35,35,35,0.9); transform: translateY(-2px); }
#speed-controller:focus { outline: 2px solid #00b4ff; }
#drag-handle { position: absolute; top: 4px; left: 10px; font-size: 16px; color: #ccc; cursor: grab; user-select: none; }
#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: 8px; right: 8px; display: flex; flex-direction: row; gap: 6px; }
#close-btn, #minimize-btn {
width: 22px; height: 22px; border-radius: 50%; font-size: 14px; color: #ccc;
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15);
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.2s ease; padding: 0;
}
#close-btn:hover, #minimize-btn:hover { background: rgba(255,255,255,0.2); color: #fff; transform: scale(1.08); }
#exclude-tip { display: none; position: absolute; top: -28px; 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; }
#minimized-bar {
position: fixed; right: -4px; bottom: 24px; width: 18px; height: 48px;
background: rgba(30, 30, 30, 0.8); border-radius: 12px 0 0 12px; display: none;
align-items: center; justify-content: center; color: #fff; cursor: pointer;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); transition: transform 0.25s ease;
z-index: 999999; font-size: 12px; text-align: center; padding-left: 6px;
}
#minimized-bar:hover { transform: translateX(-4px); }
</style>
<div id="drag-handle" title="拖动面板位置">☰</div>
<div id="close-wrapper">
<button id="minimize-btn" title="最小化面板">–</button>
<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 minimizedBar = document.createElement('div');
minimizedBar.id = 'minimized-bar';
minimizedBar.textContent = '▶';
document.body.appendChild(minimizedBar);
// === 拖拽逻辑(鼠标 + 触摸 + 边界检测)===
const dragHandle = controller.querySelector('#drag-handle');
let isDragging = false, offsetX = 0, offsetY = 0;
const startDrag = (x, y) => {
isDragging = true;
controller.classList.add('dragging');
offsetX = x - controller.getBoundingClientRect().left;
offsetY = y - controller.getBoundingClientRect().top;
};
const doDrag = (x, y) => {
if (!isDragging) return;
const { left, top } = clampPosition(x - offsetX, y - offsetY, controller);
controller.style.left = `${left}px`;
controller.style.top = `${top}px`;
controller.style.right = 'auto';
controller.style.bottom = 'auto';
controllerPos = { left, top }; // ✅ 实时保存
};
const endDrag = () => {
isDragging = false;
controller.classList.remove('dragging');
};
dragHandle.addEventListener('mousedown', (e) => startDrag(e.clientX, e.clientY));
document.addEventListener('mousemove', (e) => doDrag(e.clientX, e.clientY));
document.addEventListener('mouseup', endDrag);
dragHandle.addEventListener('touchstart', (e) => {
const t = e.touches[0];
startDrag(t.clientX, t.clientY);
}, { passive: true });
document.addEventListener('touchmove', (e) => {
const t = e.touches[0];
doDrag(t.clientX, t.clientY);
}, { passive: true });
document.addEventListener('touchend', endDrag);
// ✅ 窗口尺寸变化时自动校正位置
window.addEventListener('resize', () => {
if (controllerPos.left !== null && controllerPos.top !== null) {
const { left, top } = clampPosition(controllerPos.left, controllerPos.top, controller);
controller.style.left = `${left}px`;
controller.style.top = `${top}px`;
controllerPos = { left, top };
}
});
// === 最小化逻辑 ===
const minimizeBtn = controller.querySelector('#minimize-btn');
minimizeBtn.onclick = () => { controller.style.display = 'none'; minimizedBar.style.display = 'flex'; };
minimizedBar.onclick = () => { controller.style.display = 'block'; minimizedBar.style.display = 'none'; };
// === 功能绑定 ===
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 = () => { resetControls(media); };
speedSelect.onchange = () => { media.playbackRate = parseFloat(speedSelect.value); updateDisplay(); };
btnSkip.onclick = () => {
media.playbackRate = skipping ? 1 : 16;
skipping = !skipping;
btnSkip.textContent = skipping ? '恢复正常' : '跳过广告 🚀';
updateDisplay();
};
btnClose.onclick = () => {
observer?.disconnect();
clearInterval(intervalId);
controller.remove();
minimizedBar.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 && media !== currentMedia) {
currentMedia = media;
resetControls(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);
})();