// ==UserScript==
// @name B站播放器速度控制(最大10倍速)
// @namespace http://tampermonkey.net/
// @version 2.5
// @description 支持折叠宽度变化、主题切换和速度预设的播放控制
// @author YourName
// @match https://www.bilibili.com/video/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ================
// 配置和常量
// ================
const CONFIG = {
pos: GM_getValue('controlPos', {x: 20, y: 20}),
isCollapsed: GM_getValue('isCollapsed', false),
theme: GM_getValue('theme', 'dark')
};
const THEMES = {
dark: {
bg: 'rgba(0,0,0,0.7)',
text: 'white',
border: '#666',
buttonBg: '#555',
buttonText: '#fff',
inputBg: '#333'
},
light: {
bg: 'rgba(255,255,255,0.9)',
text: '#333',
border: '#ddd',
buttonBg: '#eee',
buttonText: '#333',
inputBg: '#fff'
}
};
// ================
// 全局变量
// ================
let video = null;
let isDragging = false;
let startX, startY, initLeft, initTop;
// ================
// DOM 元素
// ================
const controls = createControls();
const header = createHeader();
const content = createContent();
// ================
// 主初始化流程
// ================
initializeControls();
function createControls() {
const el = document.createElement('div');
el.id = 'bili-speed-control';
Object.assign(el.style, {
position: 'fixed',
zIndex: '9999',
padding: CONFIG.isCollapsed ? '8px' : '10px',
borderRadius: '5px',
cursor: 'move',
userSelect: 'none',
transition: 'all 0.3s ease',
width: CONFIG.isCollapsed ? '150px' : '200px'
});
return el;
}
function createHeader() {
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.marginBottom = CONFIG.isCollapsed ? '0' : '10px';
const title = document.createElement('span');
title.textContent = '🎚️ 播放控制';
const btnContainer = document.createElement('div');
const toggleBtn = createButton(
CONFIG.isCollapsed ? '▶' : '▼',
{ marginRight: '5px' },
() => toggleCollapse()
);
const themeBtn = createButton(
CONFIG.theme === 'dark' ? '🌞' : '🌙',
{},
() => toggleTheme()
);
btnContainer.append(toggleBtn, themeBtn);
header.append(title, btnContainer);
return header;
}
function createContent() {
const content = document.createElement('div');
const content2 = document.createElement('div');
Object.assign(content.style, {
overflow: 'hidden',
transition: 'all 0.3s ease',
opacity: CONFIG.isCollapsed ? '0' : '1',
maxHeight: CONFIG.isCollapsed ? '0px' : '200px',
marginTop: CONFIG.isCollapsed ? '0' : '10px'
});
// 预设按钮
const presetContainer = document.createElement('div');
presetContainer.style.marginBottom = '10px';
[0.5, 0.65, 0.85, 1.0, 1.15, 1.25].forEach(speed => {
const btn = createButton(
`${speed}x`,
{
margin: '3px',
width: CONFIG.isCollapsed ? '40px' : '60px',
transition: 'width 0.3s ease'
},
() => syncInputs(speed)
);
presetContainer.appendChild(btn);
});
// 速度控制组件
const speedDisplay = document.createElement('span');
speedDisplay.style.marginRight = '10px';
speedDisplay.textContent = '当前速度:1x';
const speedSlider = document.createElement('input');
speedSlider.type = 'range';
Object.assign(speedSlider, {
min: '0.05',
max: '10',
step: '0.05',
value: '1'
});
Object.assign(speedSlider.style, {
width: '100%',
verticalAlign: 'middle',
cursor: 'pointer'
});
const numInput = document.createElement('input');
numInput.type = 'number';
Object.assign(numInput, {
min: '0.05',
max: '10',
step: '0.05',
value: '1'
});
Object.assign(numInput.style, {
width: '60px',
marginLeft: '10px',
padding: '3px',
borderRadius: '3px',
border: '1px solid'
});
content.append(presetContainer, speedDisplay);
content2.append(speedSlider, numInput)
content2.style.display = 'flex';
content.append(content2)
return content;
}
function createButton(text, style, clickHandler) {
const btn = document.createElement('button');
btn.textContent = text;
Object.assign(btn.style, {
padding: '2px 8px',
borderRadius: '3px',
cursor: 'pointer',
...style
});
btn.addEventListener('click', clickHandler);
return btn;
}
// ================
// 核心功能
// ================
function initializeControls() {
controls.style.left = `${CONFIG.pos.x}px`;
controls.style.top = `${CONFIG.pos.y}px`;
controls.append(header, content);
document.body.appendChild(controls);
applyTheme();
setupEventListeners();
}
function applyTheme() {
const theme = THEMES[CONFIG.theme];
Object.assign(controls.style, {
background: theme.bg,
color: theme.text,
border: `1px solid ${theme.border}`
});
document.querySelectorAll('#bili-speed-control button').forEach(btn => {
Object.assign(btn.style, {
background: theme.buttonBg,
color: theme.buttonText,
borderColor: theme.border
});
});
const numInput = content.querySelector('input[type="number"]');
Object.assign(numInput.style, {
background: theme.inputBg,
color: theme.text,
borderColor: theme.border
});
}
function setupEventListeners() {
// 拖拽
header.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag);
// 速度控制
const speedSlider = content.querySelector('input[type="range"]');
const numInput = content.querySelector('input[type="number"]');
speedSlider.addEventListener('input', e => syncInputs(e.target.value));
numInput.addEventListener('change', handleNumberInput);
// 快捷键
document.addEventListener('keydown', handleKeyboardShortcuts);
// 视频检测
setInterval(updateVideoElement, 500);
}
// ================
// 功能实现
// ================
function toggleCollapse() {
CONFIG.isCollapsed = !CONFIG.isCollapsed;
// 宽度切换
controls.style.width = CONFIG.isCollapsed ? '150px' : '200px';
controls.style.padding = CONFIG.isCollapsed ? '8px' : '10px';
// 内容区域切换
content.style.maxHeight = CONFIG.isCollapsed ? '0px' : '200px';
content.style.opacity = CONFIG.isCollapsed ? '0' : '1';
content.style.marginTop = CONFIG.isCollapsed ? '0' : '10px';
// 按钮尺寸切换
content.querySelectorAll('button').forEach(btn => {
btn.style.width = CONFIG.isCollapsed ? '40px' : '60px';
});
// 标题栏间距调整
header.style.marginBottom = CONFIG.isCollapsed ? '0' : '10px';
// 更新按钮图标
header.querySelector('button').textContent = CONFIG.isCollapsed ? '▶' : '▼';
GM_setValue('isCollapsed', CONFIG.isCollapsed);
}
function toggleTheme() {
CONFIG.theme = CONFIG.theme === 'dark' ? 'light' : 'dark';
const themeBtn = header.querySelectorAll('button')[1];
themeBtn.textContent = CONFIG.theme === 'dark' ? '🌞' : '🌙';
applyTheme();
GM_setValue('theme', CONFIG.theme);
}
function startDrag(e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initLeft = parseFloat(controls.style.left);
initTop = parseFloat(controls.style.top);
controls.style.cursor = 'grabbing';
controls.style.transition = 'none';
}
function handleDrag(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
controls.style.left = `${initLeft + dx}px`;
controls.style.top = `${initTop + dy}px`;
}
function endDrag() {
if (!isDragging) return;
isDragging = false;
controls.style.cursor = 'move';
controls.style.transition = 'all 0.3s ease';
GM_setValue('controlPos', {
x: parseFloat(controls.style.left),
y: parseFloat(controls.style.top)
});
}
function handleNumberInput(e) {
const val = Math.min(10, Math.max(0.05, e.target.value));
syncInputs(val.toFixed(2));
}
function handleKeyboardShortcuts(e) {
if (e.altKey) {
const slider = content.querySelector('input[type="range"]');
const current = parseFloat(slider.value);
if (e.key === 'ArrowUp') syncInputs((current + 0.05).toFixed(2));
if (e.key === 'ArrowDown') syncInputs((current - 0.05).toFixed(2));
if (e.key === 'r') syncInputs(1.00);
}
}
function updateVideoElement() {
video = document.querySelector('video');
if (video) {
const slider = content.querySelector('input[type="range"]');
video.playbackRate = slider.value;
syncInputs(video.playbackRate);
}
}
function syncInputs(value) {
const speed = parseFloat(value).toFixed(2);
const speedDisplay = content.querySelector('span');
const slider = content.querySelector('input[type="range"]');
const numInput = content.querySelector('input[type="number"]');
slider.value = speed;
numInput.value = speed;
speedDisplay.textContent = `当前速度:${speed}x`;
if (video) video.playbackRate = speed;
}
})();