小叶的b站学习助手(视频时间查询器+视频倍速播放)

这一款专为B站用户打造的实用小工具,包含了B站视频的倍速播放(支持快捷键和控制面板设置),以及视频时间查询器(够便捷地计算视频的总时长,并根据不同的倍速计算实际的观看时间)。这款工具除了提供精确的时间统计,还具备窗口拖动、动态样式调整等功能,非常适合在B站学习课程的用户使用;2.x版本将“倍速功能”单独拆分到独立UI和单独热键监听中,并且原来的“时间计算”功能保持不变。

// ==UserScript==
// @name         小叶的b站学习助手(视频时间查询器+视频倍速播放)
// @namespace    http://tampermonkey.net/
// @version      2.1.2
// @description  这一款专为B站用户打造的实用小工具,包含了B站视频的倍速播放(支持快捷键和控制面板设置),以及视频时间查询器(够便捷地计算视频的总时长,并根据不同的倍速计算实际的观看时间)。这款工具除了提供精确的时间统计,还具备窗口拖动、动态样式调整等功能,非常适合在B站学习课程的用户使用;2.x版本将“倍速功能”单独拆分到独立UI和单独热键监听中,并且原来的“时间计算”功能保持不变。
// @author       小叶
// @license      AGPL License
// @match        *://*.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // ===============【 一、原有的“视频时长计算”逻辑 】================

    // 全局配置对象
    const CONFIG = {
        // UI 配置
        UI: {
            TRIGGER_ID: 'popup-trigger-container',
            CONTAINER_ID: 'time-calculator-container',
            RESULT_DIV_ID: 'resultDiv',
            DEFAULT_OPACITY: 0.8,
            Z_INDEX: 999999,
            ICON_URL: 'https://www.bilibili.com/favicon.ico'
        },

        // 倍速配置
        SPEED_UI: {
            SHOW_SPEED_UI: true // 设置是否显示倍速UI,默认为true
        },

        // 样式配置
        STYLE: {
            COLORS: {
                // 主色调(B站蓝)
                PRIMARY: '#00A1D6',
                // 次要色调
                SECONDARY: '#40E0D0',
                // 警告色
                WARNING: '#FF6347',
                // 悬停色
                HOVER: '#008BB5',
                TEXT: {
                    // 主要文本色
                    PRIMARY: '#333',
                    // 次要文本色
                    SECONDARY: '#888'
                }
            },
            BORDER_RADIUS: {
                SMALL: '4px',
                MEDIUM: '8px',
                LARGE: '16px'
            },
            TRANSITIONS: {
                DEFAULT: 'all 0.3s ease'
            }
        },

        // 功能配置
        FEATURES: {
            RESULT_DISPLAY_TIME: 15000, // 结果显示时间(毫秒)
            // 最小集数
            MIN_EPISODE: 1,
            // 最小倍速
            MIN_SPEED: 0.5,
            // 倍速调整步长(不用于本脚本功能了)
            SPEED_STEP: 0.1,
            // 默认倍速(不用于本脚本功能了)
            DEFAULT_SPEED: 1,
            TIME_FORMATS: ["时分秒", "仅小时", "仅分钟", "仅秒"]
        },

        // 布局配置
        LAYOUT: {
            SNAP_PADDING: 20,
            CONTAINER_WIDTH: '280px',
            TRIGGER_WIDTH: {
                DEFAULT: '40px',
                EXPANDED: '80px'
            }
        },

        // 文本配置
        TEXT: {
            TRIGGER_TEXT: "小叶计时器",
            CLOSE_TEXT: "关闭计时器",
            TITLE: "小叶的B站时间查询器",
            FOOTER: "小叶计时器",
            MESSAGES: {
                INVALID_INPUT: "请输入有效的数值。",
                MIN_EPISODE: "最小为第1集",
                INVALID_RANGE: "输入的集数范围不正确。",
                NO_DURATION: "无法获取视频时长,请确保已加载视频列表。",
                MAX_EPISODE: "最大为第{count}集"
            }
        },

        // 元素类名配置
        CLASSES: {
            DURATION: 'duration',
            STATS: 'stats'
        }
    };

    // 读取存储的透明度或使用默认值
    let containerOpacity = GM_getValue('containerOpacity', CONFIG.UI.DEFAULT_OPACITY);
    let isPopupVisible = false;
    // 计时器ID
    let resultTimeoutId = null;

    // 创建触发器
    const createPopupTrigger = () => {
        // 检查页面上是否存在指定的类名
        if (!document.querySelector(`.${CONFIG.CLASSES.STATS}`)) {
            console.log('没有找到视频元素,触发器不会显示。');
            return; // 如果没有找到,直接返回不创建触发器
        }
        // 删除现有的触发器(如果存在)
        const existingTrigger = document.getElementById(CONFIG.UI.TRIGGER_ID);
        if (existingTrigger) {
            existingTrigger.remove();
        }

        const body = document.body;
        const triggerContainer = document.createElement("div");
        triggerContainer.id = CONFIG.UI.TRIGGER_ID;

        // 修改了容器的样式
        triggerContainer.style.cssText = `
            position: fixed;
            right: 0;
            top: 12%;
            transform: translateY(-50%);
            z-index: ${CONFIG.UI.Z_INDEX};
            text-align: center;
            border: 1px solid ${CONFIG.STYLE.COLORS.PRIMARY};
            border-radius: ${CONFIG.STYLE.BORDER_RADIUS.MEDIUM};
            background-color: rgba(255, 255, 255, ${containerOpacity});
            padding: 8px;
            width: ${CONFIG.LAYOUT.TRIGGER_WIDTH.DEFAULT};
            transition: ${CONFIG.STYLE.TRANSITIONS.DEFAULT};
            cursor: pointer;
            margin-left: 5px;
        `;

        // 创建并设置图标
        const icon = document.createElement("img");
        icon.src = CONFIG.UI.ICON_URL;
        icon.alt = "B站图标";
        icon.style.cssText = `
            width: 24px;
            height: 24px;
            display: block;
            margin: 0 auto;
            transition: ${CONFIG.STYLE.TRANSITIONS.DEFAULT};
        `;

        // 创建文本容器
        const textContainer = document.createElement("div");
        textContainer.style.cssText = `
            font-size: 12px;
            color: ${CONFIG.STYLE.COLORS.PRIMARY};
            margin-top: 4px;
            white-space: nowrap;
            overflow: hidden;
            display: none;
        `;
        textContainer.innerText = CONFIG.TEXT.TRIGGER_TEXT;

        // 添加hover效果
        triggerContainer.onmouseenter = () => {
            triggerContainer.style.width = CONFIG.LAYOUT.TRIGGER_WIDTH.EXPANDED;
            textContainer.style.display = 'block';
        };

        triggerContainer.onmouseleave = () => {
            if (!isPopupVisible) {
                triggerContainer.style.width = CONFIG.LAYOUT.TRIGGER_WIDTH.DEFAULT;
                textContainer.style.display = 'none';
            }
        };

        // 添加点击事件
        triggerContainer.onclick = togglePopup;

        // 组装触发器
        triggerContainer.appendChild(icon);
        triggerContainer.appendChild(textContainer);
        body.appendChild(triggerContainer);

        return triggerContainer;
    };

    //切换弹出框
    const togglePopup = () => {
        isPopupVisible = !isPopupVisible;
        const triggerContainer = document.getElementById(CONFIG.UI.TRIGGER_ID);
        const textContainer = triggerContainer.querySelector('div'); // 获取文本容器

        if (isPopupVisible) {
            createTimeCalcUI();
            triggerContainer.style.width = '80px';
            textContainer.style.display = 'block';
            textContainer.style.color = '#FF0000';
            textContainer.innerText = '关闭计时器';
        } else {
            closeTimeCalcUI();
            triggerContainer.style.width = '40px';
            textContainer.style.color = '#00A1D6';
            textContainer.innerText = '小叶计时器';

            // 恢复hover效果
            triggerContainer.onmouseenter = () => {
                triggerContainer.style.width = '80px';
                textContainer.style.display = 'block';
            };
            triggerContainer.onmouseleave = () => {
                triggerContainer.style.width = '40px';
                textContainer.style.display = 'none';
            };
        }
    };

    //创建计时器UI
    const createTimeCalcUI = () => {
        const existingDiv = document.getElementById(CONFIG.UI.CONTAINER_ID);
        if (existingDiv) {
            existingDiv.remove();
        }

        const body = document.body;
        const container = document.createElement("div");
        container.id = CONFIG.UI.CONTAINER_ID;
        container.style.cssText = `
            padding: 20px;
            background-color: rgba(255, 255, 255, ${containerOpacity});
            position: fixed;
            right: 20px;
            top: 20%;
            width: 280px;
            max-width: 90%;
            border-radius: 16px;
            box-shadow: 0 8px 16px rgba(0,0,0,0.2);
            border: 1px solid #40E0D0;
            z-index: 999;
            text-align: center;
            font-size: 14px;
            color: #333;
        `;

        makeElementDraggable(container);

        const closeButton = document.createElement("button");
        closeButton.innerText = "关闭";
        closeButton.style.cssText = `
            position: absolute;
            top: 5px;
            right: 5px;
            border: none;
            background-color: #FF6347;
            color: #FFF;
            padding: 5px 10px;
            cursor: pointer;
            border-radius: 4px;
        `;
        closeButton.onclick = togglePopup;
        container.appendChild(closeButton);

        const title = document.createElement("h4");
        title.innerText = CONFIG.TEXT.TITLE;
        title.style.cssText = `
            margin-bottom: 20px;
            color: #00A1D6;
            font-weight: bold;
            text-align: center;
        `;
        container.appendChild(title);

        const inputDiv = document.createElement("div");
        inputDiv.style.cssText = "margin-bottom: 15px; display: flex; justify-content: center; align-items: center;";

        const label1 = document.createElement("label");
        label1.innerText = "从第";
        label1.style.cssText = "margin-right: 5px;";
        inputDiv.appendChild(label1);

        const input1 = document.createElement('input');
        input1.type = "number";
        input1.style.cssText = `
            border: 1px solid deepskyblue;
            width: 50px;
            text-align: center;
            margin-right: 5px;
            padding: 5px;
            border-radius: 4px;
        `;
        input1.min = 1;
        inputDiv.appendChild(input1);

        const label2 = document.createElement("label");
        label2.innerText = "集 到";
        label2.style.cssText = "margin-right: 5px;";
        inputDiv.appendChild(label2);

        const input2 = document.createElement('input');
        input2.type = "number";
        input2.style.cssText = `
            border: 1px solid deepskyblue;
            width: 50px;
            text-align: center;
            padding: 5px;
            border-radius: 4px;
        `;
        input2.min = 1;
        inputDiv.appendChild(input2);

        container.appendChild(inputDiv);

        const formatDiv = document.createElement("div");
        formatDiv.style.cssText = "margin-bottom: 20px; display: flex; justify-content: center; align-items: center;";

        const formatLabel = document.createElement("label");
        formatLabel.innerText = "显示格式:";
        formatLabel.style.cssText = "margin-right: 5px;";
        formatDiv.appendChild(formatLabel);

        const formatSelect = document.createElement('select');
        formatSelect.style.cssText = `
            padding: 5px;
            border-radius: 4px;
            border: 1px solid deepskyblue;
        `;
        const options = CONFIG.FEATURES.TIME_FORMATS;  // ["时分秒", "仅小时", "仅分钟", "仅秒"]
        options.forEach(optionText => {
            const option = document.createElement('option');
            option.value = optionText;
            option.innerText = optionText;
            formatSelect.appendChild(option);
        });
        formatDiv.appendChild(formatSelect);
        container.appendChild(formatDiv);

        const transparencyDiv = document.createElement("div");
        transparencyDiv.style.cssText = "margin-bottom: 20px; text-align: center;";

        const transparencyLabel = document.createElement("label");
        transparencyLabel.innerText = "调整透明度:";
        transparencyDiv.appendChild(transparencyLabel);

        const transparencySlider = document.createElement('input');
        transparencySlider.type = "range";
        transparencySlider.min = 0.1;
        transparencySlider.max = 1;
        transparencySlider.step = 0.1;
        transparencySlider.value = containerOpacity;
        transparencySlider.style.cssText = "margin-left: 10px;";
        transparencySlider.oninput = (e) => {
            containerOpacity = e.target.value;
            container.style.backgroundColor = `rgba(255, 255, 255, ${containerOpacity})`;
            const triggerContainer = document.getElementById(CONFIG.UI.TRIGGER_ID);
            if (triggerContainer) {
                triggerContainer.style.backgroundColor = `rgba(255, 255, 255, ${containerOpacity})`;
            }
            GM_setValue('containerOpacity', containerOpacity);
        };
        transparencyDiv.appendChild(transparencySlider);

        container.appendChild(transparencyDiv);

        const btn = document.createElement('button');
        btn.innerText = "计算时间";
        btn.style.cssText = `
            width: 100%;
            padding: 12px;
            border: none;
            background-color: #00A1D6;
            color: #FFFFFF;
            cursor: pointer;
            border-radius: 8px;
            font-size: 16px;
            margin-bottom: 20px;
        `;
        btn.onmouseover = () => { btn.style.backgroundColor = "#008BB5"; };
        btn.onmouseout = () => { btn.style.backgroundColor = "#00A1D6"; };
        btn.onclick = () => {
            calculateTime(formatSelect.value, input1.value, input2.value);
        };
        container.appendChild(btn);

        const resultDiv = document.createElement("div");
        resultDiv.id = CONFIG.UI.RESULT_DIV_ID;
        resultDiv.style.cssText = `
            margin-top: 15px;
            color: #333;
            font-weight: bold;
            text-align: center;
        `;
        container.appendChild(resultDiv);

        const footer = document.createElement("div");
        footer.innerText = CONFIG.TEXT.FOOTER;
        footer.style.cssText = `
            margin-top: 20px;
            color: #888;
            font-size: 12px;
            text-align: center;
        `;
        container.appendChild(footer);

        body.appendChild(container);
    };

    //关闭UI
    const closeTimeCalcUI = () => {
        const existingDiv = document.getElementById(CONFIG.UI.CONTAINER_ID);
        if (existingDiv) {
            existingDiv.remove();
        }
    };

    // 计算时间函数 (已经移除倍速相关逻辑)
    const calculateTime = (format, startStr, endStr) => {
        // 获取所有duration元素
        const allDurations = document.getElementsByClassName(CONFIG.CLASSES.DURATION);
        // 只筛选父元素className包含stats的元素
        const durations = Array.from(allDurations).filter(el =>
            el.parentElement.className.includes(CONFIG.CLASSES.STATS)
        );

        const input1Value = parseInt(startStr, 10);
        const input2Value = parseInt(endStr, 10);

        // 输入验证
        if (isNaN(input1Value) || isNaN(input2Value)) {
            updateResult(CONFIG.TEXT.MESSAGES.INVALID_INPUT);
            return;
        }

        // 验证最小集数
        if (input1Value < CONFIG.FEATURES.MIN_EPISODE) {
            updateResult(CONFIG.TEXT.MESSAGES.MIN_EPISODE);
            return;
        }

        // 验证集数范围
        if (input2Value < input1Value) {
            updateResult(CONFIG.TEXT.MESSAGES.INVALID_RANGE);
            return;
        }

        // 验证是否获取到视频时长
        if (durations.length === 0) {
            updateResult(CONFIG.TEXT.MESSAGES.NO_DURATION);
            return;
        }

        // 验证最大集数
        if (input2Value > durations.length) {
            const message = CONFIG.TEXT.MESSAGES.MAX_EPISODE.replace('{count}', durations.length);
            updateResult(message);
            return;
        }

        // 计算总时长
        let totalSeconds = 0;
        for (let i = input1Value - 1; i < input2Value; i++) {
            const duration = durations[i].innerText;
            const timeParts = duration.split(':').map(Number);
            let seconds = timeParts.pop();
            let minutes = timeParts.pop() || 0;
            let hours = timeParts.pop() || 0;
            totalSeconds += hours * 3600 + minutes * 60 + seconds;
        }

        // 转换时间格式
        const hours = Math.floor(totalSeconds / 3600);
        const minutes = Math.floor((totalSeconds % 3600) / 60);
        const seconds = Math.floor(totalSeconds % 60);

        // 根据选择的格式生成结果文本
        let resultText;
        switch (format) {
            case "时分秒":
                resultText = `总时长:${hours}时${minutes}分${seconds}秒`;
                break;
            case "仅小时":
                resultText = `总时长:${(totalSeconds / 3600).toFixed(2)} 小时`;
                break;
            case "仅分钟":
                resultText = `总时长:${(totalSeconds / 60).toFixed(2)} 分钟`;
                break;
            case "仅秒":
                resultText = `总时长:${Math.round(totalSeconds)} 秒`;
                break;
        }

        // 显示结果
        updateResult(resultText);
    };

    // 更新结果显示
    const updateResult = (text) => {
        const resultDiv = document.getElementById(CONFIG.UI.RESULT_DIV_ID);
        if (!resultDiv) return;
        resultDiv.innerText = text;
        // 如果已经存在计时器,先清除它
        if (resultTimeoutId) {
            clearTimeout(resultTimeoutId);
        }
        // 设置新的计时器并保存ID
        resultTimeoutId = setTimeout(() => {
            if (resultDiv) { // 检查元素是否还存在
                resultDiv.innerText = '';
            }
            resultTimeoutId = null;
        }, CONFIG.FEATURES.RESULT_DISPLAY_TIME);
    };

    // 拖动逻辑
    const makeElementDraggable = (element) => {
        let offsetX = 0, offsetY = 0, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') {
                return;
            }
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
            element.style.transition = "none";
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            element.style.left = `${e.clientX - offsetX}px`;
            element.style.top = `${e.clientY - offsetY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            element.style.transition = "all 0.3s ease";
        });
    };

    // 初始化时间查询触发器
    createPopupTrigger();

    // ===============【 二、全新“倍速调节”UI与按键监听 】================

    /**
     * 存储本脚本内部的倍速值
     * 如需记忆上次倍速,可以使用 GM_getValue/GM_setValue 来持久化
     */
    let currentSpeed = 1.0;
    let originalSpeed = 1.0; // 记录原始倍速

    /**
     * 将页面上所有 <video> 标签的播放速率设置为指定值
     * @param {number} speed 要设定的倍速
     */
    function setAllVideoPlaybackRate(speed) {
        if (!speed || isNaN(speed) || speed <= 0) return;
        currentSpeed = speed;
        const videos = document.querySelectorAll("video");
        videos.forEach(video => {
            try {
                video.playbackRate = speed;
            } catch (e) {
                console.warn("[小叶-倍速] 设置视频倍速失败:", e);
            }
        });
        console.log("[小叶-倍速] 已将所有视频调节为", speed, "倍速");

        // 显示倍速
        displaySpeedOnVideo(speed);

         // 更新UI中的当前倍速显示
        updateSpeedDisplay(speed);
    }


    function displaySpeedOnVideo(speed) {
        const videoElement = document.querySelector('video');
        if (videoElement) {
             // Check if a speed display already exists, if so, remove it
            let existingSpeedDisplay = videoElement.parentElement.querySelector('.speed-display');
            if (existingSpeedDisplay) {
                existingSpeedDisplay.remove();
            }
             // Create new speed display
            const speedDisplay = document.createElement('div');
            speedDisplay.innerText = `倍速:${speed}`;
            speedDisplay.style.position = 'absolute';
            speedDisplay.style.top = '10px';
            speedDisplay.style.left = '10px';
            speedDisplay.style.padding = '5px';
            speedDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
            speedDisplay.style.color = 'white';
            speedDisplay.style.fontSize = '16px';
            speedDisplay.style.fontWeight = 'bold';
            speedDisplay.style.borderRadius = '5px';
            speedDisplay.className = 'speed-display'; // Add class for easy removal

            videoElement.parentElement.appendChild(speedDisplay);

            // Set timeout to remove the speed display after 3 seconds
            setTimeout(() => {
                speedDisplay.remove();
            }, 3000);
        }
    }

    // 更新倍速UI中的当前倍速
    function updateSpeedDisplay(speed) {
        const currentSpeedLabel = document.getElementById("current-speed-label");
        if (currentSpeedLabel) {
             // 更新倍速显示
            currentSpeedLabel.innerText = `当前倍速:${speed}x`;
        }
    }
     // 新建一个倍速按钮Trigger
    const SPEED_TRIGGER_ID = "speed-trigger-container";
    let isSpeedPopupVisible = false;

    function createSpeedTrigger() {
        // 如果倍速UI被禁用,则不创建倍速触发器
        if (!CONFIG.SPEED_UI.SHOW_SPEED_UI) return;

        const existingSpeedTrigger = document.getElementById(SPEED_TRIGGER_ID);
        if (existingSpeedTrigger) existingSpeedTrigger.remove();

        const body = document.body;
        const trigger = document.createElement("div");
        trigger.id = SPEED_TRIGGER_ID;
        trigger.style.cssText = `
            position: fixed;
            right: 0;
            top: 25%;
            transform: translateY(-50%);
            z-index: 999999;
            text-align: center;
            border: 1px solid #FF8C00;
            border-radius: 8px;
            background-color: rgba(255, 255, 255, ${containerOpacity});
            padding: 8px;
            width: 40px;
            transition: all 0.3s ease;
            cursor: pointer;
        `;
        const icon = document.createElement("div");
        icon.innerText = "倍速";
        icon.style.cssText = `
            font-size: 14px;
            color: #FF8C00;
            text-align: center;
            user-select: none;
        `;
        trigger.appendChild(icon);

        trigger.addEventListener("click", toggleSpeedPopup);

        // hover时宽度变大
        trigger.onmouseenter = () => {
            trigger.style.width = "80px";
            icon.style.display = "block";
        };
        trigger.onmouseleave = () => {
            if (!isSpeedPopupVisible) {
                trigger.style.width = "40px";
            }
        };

        body.appendChild(trigger);
    }

    function toggleSpeedPopup() {
        isSpeedPopupVisible = !isSpeedPopupVisible;
        if (isSpeedPopupVisible) {
            createSpeedUI();
            const trig = document.getElementById(SPEED_TRIGGER_ID);
            if (trig) {
                trig.style.width = "80px";
            }
        } else {
            closeSpeedUI();
            const trig = document.getElementById(SPEED_TRIGGER_ID);
            if (trig) {
                trig.style.width = "40px";
            }
        }
    }

    // 创建倍速UI
    // 创建倍速UI
    function createSpeedUI() {
        const body = document.body;
        if (document.getElementById("speed-ui-container")) {
            return; // 已存在则不重复创建
        }

        const container = document.createElement("div");
        container.id = "speed-ui-container";
        container.style.cssText = `
            padding: 10px;
            background-color: rgba(255, 255, 255, ${containerOpacity});
            position: fixed;
            right: 80px;
            top: 25%;
            width: 200px;
            max-width: 60%;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            border: 1px solid #FF8C00;
            z-index: 999999;
            text-align: center;
            font-size: 14px;
            color: #444;
        `;

        makeElementDraggable(container);

        const closeBtn = document.createElement("button");
        closeBtn.innerText = "关闭";
        closeBtn.style.cssText = `
            position: absolute;
            top: 5px;
            right: 5px;
            border: none;
            background-color: #f90;
            color: #FFF;
            padding: 2px 6px;
            cursor: pointer;
            border-radius: 4px;
        `;
        closeBtn.onclick = toggleSpeedPopup;
        container.appendChild(closeBtn);

        const title = document.createElement("h4");
        title.innerText = "视频倍速设置";
        title.style.cssText = `
            margin-bottom: 8px;
            color: #FF8C00;
            font-weight: bold;
        `;
        container.appendChild(title);

        // 倍速输入框
        const label = document.createElement("label");
        label.innerText = "请输入倍速:";
        label.style.cssText = "margin-right: 5px;";
        container.appendChild(label);

        const speedInput = document.createElement("input");
        speedInput.type = "number";
        speedInput.value = currentSpeed;
        speedInput.min = "0.1";
        speedInput.step = "0.1";
        speedInput.style.cssText = `
            width: 60px;
            text-align: center;
            border-radius: 4px;
            border: 1px solid #ccc;
            margin-bottom: 10px;
        `;
        container.appendChild(speedInput);

        // Add a label to show the current speed dynamically
        const currentSpeedLabel = document.createElement("div");
        currentSpeedLabel.id = "current-speed-label";
        currentSpeedLabel.style.cssText = `
            margin-top: 8px;
            color: #555;
            font-size: 14px;
        `;
        currentSpeedLabel.innerText = `当前倍速:${currentSpeed}x`;
        container.appendChild(currentSpeedLabel);

        const setBtn = document.createElement("button");
        setBtn.innerText = "应用";
        setBtn.style.cssText = `
            margin-left: 8px;
            background-color: #00A1D6;
            color: #FFF;
            border: none;
            padding: 4px 8px;
            cursor: pointer;
            border-radius: 4px;
        `;
        setBtn.onclick = () => {
            const val = parseFloat(speedInput.value) || 1;
            if (val <= 0) {
                alert("非法倍速值!");
                return;
            }
            setAllVideoPlaybackRate(val);
            // Update the current speed label
            currentSpeedLabel.innerText = `当前倍速:${val}x`;
        };
        container.appendChild(setBtn);

        const tips = document.createElement("div");
        tips.style.cssText = `
            margin-top: 10px;
            color: #888;
            font-size: 12px;
        `;
        tips.innerText = "预设了数字1-4进行倍速播放\n按 C键 / X键  加减速\n按 Z 键切换当前倍速和原始倍速\n也可用 Shift+↑/Shift+↓加减速\n Shift+0复位1.0倍\n在配置文件中可以关闭视频加速UI";
        container.appendChild(tips);

        body.appendChild(container);
    }

    function closeSpeedUI() {
        const speedUI = document.getElementById("speed-ui-container");
        if (speedUI) speedUI.remove();
    }

    // 按键监听:Shift + ↑/↓ 加减速0.1; Shift+0 -> 1倍速
    window.addEventListener("keydown", (e) => {
        const activeTag = document.activeElement.tagName.toLowerCase();
        if (activeTag === 'input' || activeTag === 'textarea') {
            return;
        }

        if (e.shiftKey && !e.ctrlKey && !e.altKey) {
            if (e.key === "ArrowUp") {
                //e.preventDefault();
                currentSpeed = parseFloat((currentSpeed + 0.1).toFixed(2));
                setAllVideoPlaybackRate(currentSpeed);
            } else if (e.key === "ArrowDown") {
                //e.preventDefault();
                let newSpeed = parseFloat((currentSpeed - 0.1).toFixed(2));
                if (newSpeed < 0.1) newSpeed = 0.1;
                currentSpeed = newSpeed;
                setAllVideoPlaybackRate(currentSpeed);
            } else if (e.key === "0") {
                //e.preventDefault();
                currentSpeed = 1.0;
                setAllVideoPlaybackRate(1.0);
            }
        }

        // 数字键 1 到 4 设置倍速
        if (e.key >= '1' && e.key <= '4') {
            //e.preventDefault();
            const speedMap = { '1': 1.0, '2': 2.0, '3': 3.0, '4': 4.0 };
            setAllVideoPlaybackRate(speedMap[e.key]);
        }

        // 按 C 增加倍速
        if (e.key === 'c' || e.key === 'C') {
            //e.preventDefault();
            currentSpeed = parseFloat((currentSpeed + 0.1).toFixed(2));
            setAllVideoPlaybackRate(currentSpeed);
        }

        // 按 X 减少倍速
        if (e.key === 'x' || e.key === 'X') {
            //e.preventDefault();
            currentSpeed = parseFloat((currentSpeed - 0.1).toFixed(2));
            if (currentSpeed < 0.1) currentSpeed = 0.1;
            setAllVideoPlaybackRate(currentSpeed);
        }

        // 按 Z 切换倍速与原倍速
        if (e.key === 'z' || e.key === 'Z') {
            //e.preventDefault();
            if (currentSpeed === 1.0) {
                setAllVideoPlaybackRate(originalSpeed);
            } else {
                originalSpeed = currentSpeed;
                setAllVideoPlaybackRate(1.0);
            }
        }
    });

    createSpeedTrigger(); // 初始化倍速按钮触发器

})();

QingJ © 2025

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