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

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

目前为 2025-04-17 提交的版本。查看 最新版本

// ==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
// ==/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;
    }
})();

QingJ © 2025

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