B站播放器速度控制(最大16倍速)

支持折叠宽度变化、主题切换和速度预设的播放控制等

// ==UserScript==
// @name         B站播放器速度控制(最大16倍速)
// @namespace    http://tampermonkey.net/
// @version      3.6
// @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';

	// ================
	// 全局CSS变量
	// ================
	document.documentElement.style.setProperty('--greyLightText', '#9baacf');
	document.documentElement.style.setProperty('--greyLightBg', '#e4ebf5');
	document.documentElement.style.setProperty('--greyLightShadow1', '#c8d0e7');
	document.documentElement.style.setProperty('--greyLightShadow2', '#fff');

	document.documentElement.style.setProperty('--greyDarkText', 'white');
	document.documentElement.style.setProperty('--greyDarkBg', '#696969');
	document.documentElement.style.setProperty('--greyDarkShadow1', '#595959');
	document.documentElement.style.setProperty('--greyDarkShadow2', '#797979');

	// ================
	// 配置和常量
	// ================
	const CONFIG = {
		pos: GM_getValue('controlPos', { x: 20, y: 20 }),
		isCollapsed: GM_getValue('isCollapsed', false),
		theme: GM_getValue('theme', 'light'),
	};

	const THEMES = {
		dark: {
			bg: 'var(--greyDarkBg)',
			text: 'var(--greyDarkText)',
			border: '#666',
			buttonBg: '#555',
			buttonText: 'var(--greyDarkText)',
			inputBg: '#333',
			boxShadow: '3px 3px 6px var(--greyDarkShadow1), -2px -2px 5px var(--greyDarkShadow2)',
			clickBoxShadow: 'inset 2px 2px 5px var(--greyDarkShadow1), inset -2px -2px 5px var(--greyDarkShadow2) !important',
			rangeSlider1: 'white',
			rangeSlider2: '#b1b1b1',
		},
		light: {
			bg: 'var(--greyLightBg)',
			text: 'var(--greyLightText)',
			border: '#ddd',
			buttonBg: '#eee',
			buttonText: 'var(--greyLightText)',
			inputBg: '#fff',
			boxShadow: '3px 3px 6px var(--greyLightShadow1), -2px -2px 5px var(--greyLightShadow2)',
			clickBoxShadow: 'inset 2px 2px 5px var(--greyLightShadow1), inset -2px -2px 5px var(--greyLightShadow2) !important',
			rangeSlider1: 'white',
			rangeSlider2: 'var(--greyLightText)',
		},
	};

	// ================
	// 全局变量
	// ================
	let video = null;
	let isDragging = false;
	let startX, startY, initLeft, initTop;

	// ================
	// DOM 元素
	// ================
	const controls = createControls();
	const header = createHeader();

    const speedDisplay = document.createElement('span');
	const speedSlider = document.createElement('input');
	const numInput = document.createElement('input');
	const content = createContent();

	// ================
	// 动态创建 CSS 类
	// ================
	const style123 = document.createElement('style');
	style123.textContent = '#bili-speed-control .lightBtn:active{box-shadow:' + THEMES[CONFIG.theme].clickBoxShadow + '}#bili-speed-control .darkBtn:active{box-shadow:' + THEMES[CONFIG.theme].clickBoxShadow + '}';
	document.head.appendChild(style123);

	const styleRange = document.createElement('style');
	styleRange.textContent = `
#bili-speed-control .slider {
--slider-width: 100%;
--slider-height: 6px;
--slider-border-radius: 999px;
--level-transition-duration: .1s;
}
#bili-speed-control .slider {
cursor: pointer;
}
#bili-speed-control .slider .level {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: var(--slider-width);
height: var(--slider-height);
background: var(--slider-bg);
overflow: hidden;
border-radius: var(--slider-border-radius);
-webkit-transition: height var(--level-transition-duration);
-o-transition: height var(--level-transition-duration);
transition: height var(--level-transition-duration);
cursor: inherit;
}
#bili-speed-control .level::-webkit-slider-thumb {
-webkit-appearance: none;
width: 0;
height: 0;
-webkit-box-shadow: -200px 0 0 200px var(--level-color);
box-shadow: -200px 0 0 200px var(--level-color);
}
#bili-speed-control .slider:hover .level {
height: calc(var(--slider-height) * 2);
}
  `;
	document.head.appendChild(styleRange);

	// ================
	// 主初始化流程
	// ================
	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',
				boxShadow: THEMES[CONFIG.theme].boxShadow,
			},
			() => toggleCollapse()
		);

		const themeBtn = createButton(CONFIG.theme === 'dark' ? '🌞' : '🌙', { boxShadow: THEMES[CONFIG.theme].boxShadow }, () => 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);
		});

		// 速度控制组件
		speedDisplay.style.marginRight = '10px';
		speedDisplay.textContent = '当前速度:1x';

		speedSlider.type = 'range';
		Object.assign(speedSlider, {
			min: '0.10',
			max: '16',
			step: '0.05',
			value: '1',
		});
		Object.assign(speedSlider.style, {
			width: '100%',
			verticalAlign: 'middle',
			cursor: 'pointer',
		});

		numInput.type = 'number';
		Object.assign(numInput, {
			min: '0.10',
			max: '16',
			step: '0.05',
			value: '1',
		});
		Object.assign(numInput.style, {
			width: '50px',
			marginLeft: '10px',
			padding: '3px 6px',
			borderRadius: '4px',
		});
		speedSlider.classList.add('level');
		content.append(presetContainer, speedDisplay);
		content2.append(speedSlider, numInput);
		content2.style.display = 'flex';
		content2.style.alignItems = 'center';
		content2.classList.add('slider');
		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.classList.add(CONFIG.theme + 'Btn');
		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];

		document.documentElement.style.setProperty('--level-color', theme.rangeSlider1);
		document.documentElement.style.setProperty('--slider-bg', theme.rangeSlider2);

		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: 'transparent',
				color: theme.buttonText,
				border: 'none',
				boxShadow: THEMES[CONFIG.theme].boxShadow,
			});
		});

		Object.assign(numInput.style, {
			border: 'none',
			background: 'transparent',
			boxShadow: THEMES[CONFIG.theme].clickBoxShadow,
		});
	}

	function setupEventListeners() {
		// 拖拽
		header.addEventListener('mousedown', startDrag);
		document.addEventListener('mousemove', handleDrag);
		document.addEventListener('mouseup', endDrag);

		// 速度控制
		speedSlider.addEventListener('input', (e) => syncInputs(e.target.value));
		numInput.addEventListener('change', handleNumberInput);

		// 快捷键
		document.addEventListener('keydown', handleKeyboardShortcuts);

		// 视频检测
		setTimeout(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(16, Math.max(0.1, e.target.value));
		syncInputs(val);
	}

	function handleKeyboardShortcuts(e) {
		if (e.altKey) {
			const current = parseFloat(speedSlider.value);
			if (current - 0.05 < 0.1 || current + 0.05 > 16) return;
			if (e.key === 'ArrowUp') syncInputs(current + 0.05);
			if (e.key === 'ArrowDown') syncInputs(current - 0.05);
			if (e.key === 'r') syncInputs(1.0);
		}
	}

	function updateVideoElement() {
		video = document.querySelector('video');
		if (video) {
			video.playbackRate = speedSlider.value;
			syncInputs(video.playbackRate);
		}
	}

	function syncInputs(value) {
		const speed = parseFloat(value).toFixed(2);
		speedSlider.value = speed;
		numInput.value = speed;
		speedDisplay.textContent = `当前速度:${speed}x`;
		if (video) video.playbackRate = speed;
	}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址