// ==UserScript== // @name B站播放器速度控制(最大16倍速) // @namespace http://tampermonkey.net/ // @version 3.1 // @description 支持折叠宽度变化、主题切换和速度预设的播放控制等 // @author YourName // @match https://www.bilibili.com/video/* // @match https://v.qq.com/x/cover/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/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 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); }); // 速度控制组件 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: '16', 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: '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, }); }); const numInput = content.querySelector('input[type="number"]'); 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); // 速度控制 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(16, 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; } })();