Bilibili 播放器操作增強

功能映射:5=播放暫停,7/9=[上一集]下一集,小鍵盤Enter=全螢幕切換,滑鼠滾輪=音量(支持飛梭/傳統模式)

// ==UserScript==
// @name         Bilibili 播放器操作增強
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  功能映射:5=播放暫停,7/9=[上一集]下一集,小鍵盤Enter=全螢幕切換,滑鼠滾輪=音量(支持飛梭/傳統模式)
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /* 音量控制系統參數 (與YouTube版本相同) */
    let volumeAccumulator = 0; // 飛梭滾輪累積量
    let lastVolumeChangeTime = 0; // 最後一次音量改變時間戳
    const DEBOUNCE_INTERVAL = 300; // 防抖間隔(毫秒)
    const FLYWHEEL_THRESHOLD = 10; // 飛梭滾輪觸發閾值
    const TRADITIONAL_THRESHOLD = 40; // 傳統滾輪閾值
    const VOLUME_STEP = 10; // 音量變化步長(百分比)

    /* 緩存DOM元素提升性能 */
    let cachedVideoElement = null;
    let cachedPlayerWrap = null;

    function getVideoElement() {
        if (cachedVideoElement) return cachedVideoElement;
        cachedVideoElement = document.querySelector('video');
        return cachedVideoElement;
    }

    function getPlayerWrap() {
        if (cachedPlayerWrap) return cachedPlayerWrap;
        cachedPlayerWrap = document.querySelector('.bpx-player-video-wrap');
        return cachedPlayerWrap;
    }

    /* 音量調整函數 */
    function adjustVolume(deltaY) {
        const video = getVideoElement();
        if (!video) return;

        const now = Date.now();
        // 防抖檢查
        if (now - lastVolumeChangeTime < DEBOUNCE_INTERVAL) return;

        let volumeChange = 0;
        const absDelta = Math.abs(deltaY);

        /* 飛梭滾輪處理邏輯 */
        if (absDelta < TRADITIONAL_THRESHOLD) {
            volumeAccumulator += absDelta;

            if (volumeAccumulator >= FLYWHEEL_THRESHOLD) {
                volumeChange = VOLUME_STEP;
                volumeAccumulator = 0;
            }
        }
        /* 傳統滾輪處理邏輯 */
        else {
            volumeChange = VOLUME_STEP;
            volumeAccumulator = 0;
        }

        if (volumeChange > 0) {
            // B站音量控制需通過模擬按鍵事件
            const key = deltaY > 0 ? 'Numpad2' : 'Numpad8';
            document.dispatchEvent(new KeyboardEvent('keydown', {
                code: key,
                key: key.replace('Numpad', ''),
                keyCode: key === 'Numpad8' ? 104 : 98,
                bubbles: true,
                cancelable: true
            }));

            lastVolumeChangeTime = now;
        }
    }

    /* 滾輪事件監聽器 (修改為支持飛梭/傳統模式) */
    document.addEventListener('wheel', function(e) {
        const player = getPlayerWrap();
        if (player && player.contains(e.target)) {
            adjustVolume(e.deltaY);
            e.preventDefault();
        }
    }, { passive: false });

    /* 以下保留原有功能不變 */

    // 空白鍵播放控制 (高優先級)
    const handleSpaceKey = (e) => {
        const video = getVideoElement();
        if (!video || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

        if (e.code === 'Space') {
            e.stopImmediatePropagation();
            e.preventDefault();
            video.paused ? video.play() : video.pause();
        }
    };
    document.addEventListener('keydown', handleSpaceKey, true);

    // 小鍵盤全功能映射
    document.addEventListener('keydown', function(e) {
        const video = getVideoElement();
        if (!video) return;

        const keyMap = {
            'Numpad5': { code: 'Space', key: ' ', keyCode: 32 }, // 播放/暫停
            'Numpad7': { code: 'BracketLeft', key: '[', keyCode: 219 }, // 上一集
            'Numpad9': { code: 'BracketRight', key: ']', keyCode: 221 }, // 下一集
            'NumpadEnter': { code: 'KeyF', key: 'f', keyCode: 70 } // 全螢幕
        };

        if (keyMap[e.code]) {
            e.stopImmediatePropagation();
            e.preventDefault();
            document.dispatchEvent(new KeyboardEvent('keydown', {
                ...keyMap[e.code],
                bubbles: true,
                cancelable: true
            }));
        }
    }, { capture: true, passive: false });

    // 網址清理
    const cleanURL = () => {
        const url = window.location.href;
        const cleanRegex = /^(https:\/\/www\.bilibili\.com\/video\/BV\w+)\//;
        const match = url.match(cleanRegex);
        if (match) window.history.replaceState({}, '', match[0]);
    };
    setTimeout(cleanURL, 4000);
})();