Greasy Fork

Greasy Fork is available in English.

Bilibili Surface

单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量

当前为 2026-04-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bilibili Surface
// @namespace    http://tampermonkey.net/
// @version      1.5.16
// @description  单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量
// @author       You
// @match        *://*.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================
    // #region 参数配置
    // ============================================================
    const PRESS_DELAY = 300;
    const TARGET_SPEED = 3.0;
    const SEEK_SENSITIVITY = 1.0;
    const CLICK_TIMEOUT = 200;

    let playerArea = null;
    let video = null;

    let pressTimer = null;
    let clickTimer = null;
    let isDown = false;

    let originalSpeed = 1.0;
    let gestureType = "";
    let wasPlaying = false;

    let startX = 0;
    let startY = 0;
    let deltaX = 0;
    let deltaY = 0;
    let absX = 0;
    let absY = 0;

    let startVal = 0;
    let prevBrightnessY = null;   // 追踪亮度手势的前一个 Y,避免定位跳跃
    let prevVolumeY = null;       // 追踪音量手势的前一个 Y
    let moveDelta = 0;
    let sensitivity = 0;

    const speedIcon = `
        <svg viewBox="0 0 111 66" width="34" height="20" style="overflow:visible">
            <g transform="matrix(0,3,-3,0,94.5,32.5)">
                <path d="M6.138,3.546 C6.468,4.106 6.278,4.826 5.718,5.156 C5.538,5.266 5.338,5.326 5.118,5.326 C5.118,5.326 -5.122,5.326 -5.122,5.326 C-5.772,5.326 -6.302,4.796 -6.302,4.146 C-6.302,3.936 -6.242,3.726 -6.142,3.546 C-6.142,3.546 -1.352,-4.554 -1.352,-4.554 C-0.912,-5.294 0.048,-5.544 0.798,-5.104 C1.028,-4.974 1.218,-4.784 1.348,-4.554 C1.348,-4.554 6.138,3.546 6.138,3.546z" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0s"/>
            </g>
            <g transform="matrix(0,3,-3,0,55.5,32.5)">
                <path d="M6.138,3.546 C6.468,4.106 6.278,4.826 5.718,5.156 C5.538,5.266 5.338,5.326 5.118,5.326 C5.118,5.326 -5.122,5.326 -5.122,5.326 C-5.772,5.326 -6.302,4.796 -6.302,4.146 C-6.302,3.936 -6.242,3.726 -6.142,3.546 C-6.142,3.546 -1.352,-4.554 -1.352,-4.554 C-0.912,-5.294 0.048,-5.544 0.798,-5.104 C1.028,-4.974 1.218,-4.784 1.348,-4.554 C1.348,-4.554 6.138,3.546 6.138,3.546z" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0.18s"/>
            </g>
            <g transform="matrix(0,3,-3,0,16.5,32.5)">
                <path d="M6.138,3.546 C6.468,4.106 6.278,4.826 5.718,5.156 C5.538,5.266 5.338,5.326 5.118,5.326 C5.118,5.326 -5.122,5.326 -5.122,5.326 C-5.772,5.326 -6.302,4.796 -6.302,4.146 C-6.302,3.936 -6.242,3.726 -6.142,3.546 C-6.142,3.546 -1.352,-4.554 -1.352,-4.554 C-0.912,-5.294 0.048,-5.544 0.798,-5.104 C1.028,-4.974 1.218,-4.784 1.348,-4.554 C1.348,-4.554 6.138,3.546 6.138,3.546z" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0.35s"/>
            </g>
        </svg>`;
    const brightnessIcon = `
        <svg viewBox="0 0 24 24" style="width:100%;height:100%">
            <path d="M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" fill="currentColor" />
        </svg>`;
    const volumeIcon = `
        <svg viewBox="0 0 24 24" style="width:100%;height:100%">
            <path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06Z" fill="currentColor" />
            <path d="M15.9 8.2 A4.5 4.5 0 0 1 15.9 15.8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
            <path d="M19.1 5.7 A8.25 8.25 0 0 1 19.1 18.3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
        </svg>`;

    // #endregion



    // --- 1. 注入核心 CSS ---
    const css = `
        @keyframes geminiFadeToWhite {
            0%   { opacity: 1; filter: brightness(0.3); }
            25%  { opacity: 1; filter: brightness(0.6); }
            50%  { opacity: 1; filter: brightness(1); }
            75%  { opacity: 1; filter: brightness(0.6); }
            100% { opacity: 1; filter: brightness(0.3); }
        }
    `;
    const style = document.createElement('style');
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);




    // ============================================================
    // #region 提示框函数
    // ============================================================

    function formatTime(seconds) {
        const m = Math.floor(seconds / 60);
        const s = Math.floor(seconds % 60);
        return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
    }


    function getToast(playerArea) {
        let div = playerArea.querySelector('#gemini-clean-toast');
        if (!div) {
            div = document.createElement('div');
            div.id = 'gemini-clean-toast';
            div.style.cssText = `
                position: absolute;
                top: 15%; left: 50%; transform: translateX(-50%);
                padding: 12px 24px;
                background: rgba(0,0,0,0.75);
                color: #fff;
                border-radius: 8px;
                font-size: 20px;
                font-family: "Segoe UI", sans-serif; font-weight: 500;
                z-index: 100001;
                pointer-events: none;
                display: none;
                backdrop-filter: blur(4px);
                text-align: center;
                white-space: nowrap;
                box-shadow: 0 4px 10px rgba(0,0,0,0.3);
            `;
            playerArea.appendChild(div);
        }
        return div;
    }


    function showToast(playerArea, text) {
        const div = getToast(playerArea);
        div.innerHTML = '';
        div.style.display = 'flex';
        div.style.alignItems = 'center';
        div.style.gap = '8px';
        div.appendChild(document.createTextNode(text));
    }


    function hideToast(playerArea) {
        const div = playerArea.querySelector('#gemini-clean-toast');
        if (div) div.style.display = 'none';
    }


    function showIconToast(playerArea, svg, text, iconStyle) {
        const div = getToast(playerArea);
        div.innerHTML = '';
        div.style.display = 'flex';
        div.style.alignItems = 'center';
        div.style.gap = '8px';

        const iconWrap = document.createElement('span');
        iconWrap.innerHTML = svg;
        iconWrap.style.cssText = iconStyle || `
            width: 20px;
            height: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            vertical-align: middle;
        `;

        div.appendChild(iconWrap);
        if (text) {
            div.appendChild(document.createTextNode(text));
        }
    }

    // #endregion




    // ============================================================
    // #region 单指单击:显示/隐藏控制栏
    // ============================================================

    function getPlayerContainer(playerArea) {
        return playerArea.closest('.bpx-player-container') ||
               playerArea.closest('#bilibili-player') ||
               playerArea;
    }

    function handleCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        const isHidden = container.getAttribute('data-ctrl-hidden');

        if (isHidden !== 'true') {
            hideCtrl(playerArea);
        } else {
            showCtrl(playerArea);
        }
    }

    function showCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        container.classList.remove('bpx-state-no-cursor');
        container.setAttribute('data-ctrl-hidden', 'false');
        const shadow = container.querySelector('.bilibili-player-progress-shadow');
        if (shadow) shadow.setAttribute('data-shadow-show', 'false');
        const title = container.querySelector('.bpx-player-video-info');
        if (title) {
            title.classList.remove('bpx-state-hide');
            title.classList.add('bpx-state-show');
        }
    }

    function hideCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        container.classList.add('bpx-state-no-cursor');
        container.setAttribute('data-ctrl-hidden', 'true');
        const shadow = container.querySelector('.bilibili-player-progress-shadow');
        if (shadow) shadow.setAttribute('data-shadow-show', 'true');
        const title = container.querySelector('.bpx-player-video-info');
        if (title) {
            title.classList.remove('bpx-state-show');
            title.classList.add('bpx-state-hide');
        }
    }

    // #endregion



    // ============================================================
    // #region 单指双击:播放/暂停
    // ============================================================

    function onDoubleTap(video, playerArea) {
        if (video.paused) {
            video.play();
        } else {
            video.pause();
        }
    }

    // #endregion



    // ============================================================
    // #region 单指长按:倍速播放
    // ============================================================

    // 长按手势(按下时触发):切换到目标倍速
    function onLongPressStart(video, playerArea) {
        originalSpeed = video.playbackRate;
        video.playbackRate = TARGET_SPEED;
        const speedIconStyle = `
            display: flex;
            align-items: center;
            gap: 6px;
        `;
        showIconToast(playerArea, speedIcon, TARGET_SPEED.toFixed(1) + "x", speedIconStyle);
    }


    // 长按手势(松手时触发):恢复原速
    function onLongPressEnd(video, playerArea) {
        video.playbackRate = originalSpeed;
        hideToast(playerArea);
    }

    // #endregion



    // ============================================================
    // #region 横向滑动:调节进度
    // ============================================================

    // 横向滑动手势(开始时):暂停视频并显示控制栏
    function onSeekStart(video, playerArea) {
        startVal = video.currentTime;
        wasPlaying = !video.paused;
        video.pause();
    }


    //横向滑动手势(进行中):拖动进度
    function onSeek(video, playerArea, deltaX) {
        const seekPercent = deltaX / window.innerWidth;
        let targetTime = startVal + video.duration * seekPercent * SEEK_SENSITIVITY;
        if (targetTime < 0) targetTime = 0;
        if (targetTime > video.duration) targetTime = video.duration;

        video.currentTime = targetTime;
        showToast(playerArea, `${formatTime(targetTime)} / ${formatTime(video.duration)}`);
    }


    // 横向滑动手势(结束时):恢复播放
    function onSeekEnd(video, playerArea) {
        if (wasPlaying) video.play();
    }

    // #endregion



    // ============================================================
    // #region 左纵向滑动:调节亮度
    // ============================================================

    function getCurrentBrightness(video) {
        const filter = video.style.filter;
        if (!filter || !filter.includes('brightness')) return 100;
        const match = filter.match(/brightness\((\d+)%\)/);
        return match ? parseInt(match[1], 10) : 100;
    }


    function onBrightnessStart(video) {
        startVal = getCurrentBrightness(video);
        prevBrightnessY = null;
    }


    function onBrightness(video, playerArea, clientY) {
        // 第一次 move:初始化 prevY
        if (prevBrightnessY === null) {
            prevBrightnessY = clientY;
            return;
        }

        // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正)
        moveDelta = prevBrightnessY - clientY;
        sensitivity = window.innerHeight * 0.4;
        startVal = startVal + (moveDelta / sensitivity * 100);
        if (startVal > 100) startVal = 100;
        else if (startVal < 0) startVal = 0;

        // 每次 move 后都更新 prevY,保持 delta 始终是相邻两次 move 的差
        prevBrightnessY = clientY;

        video.style.filter = `brightness(${Math.round(startVal)}%)`;
        showIconToast(playerArea, brightnessIcon, `${Math.round(startVal)}%`);
    }

    // #endregion



    // ============================================================
    // #region 右纵向滑动:调节音量
    // ============================================================

    function onVolumeStart(video) {
        startVal = video.volume;
        prevVolumeY = null;
    }


    function onVolume(video, playerArea, clientY) {
        // 第一次 move:初始化 prevY
        if (prevVolumeY === null) {
            prevVolumeY = clientY;
            return;
        }

        // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正)
        moveDelta = prevVolumeY - clientY;
        sensitivity = window.innerHeight * 0.4;
        startVal = startVal + (moveDelta / sensitivity);
        if (startVal > 1) startVal = 1;
        else if (startVal < 0) startVal = 0;

        // 每次 move 后都更新 prevY,保持 delta 始终是相邻两次 move 的差
        prevVolumeY = clientY;

        video.volume = startVal;
        showIconToast(playerArea, volumeIcon, `${Math.round(startVal * 100)}%`);
    }

    // #endregion



    // ============================================================
    // #region 手势识别与分发
    // ============================================================

    // 手指按下时 → 长按
    function handleDown(e, playerArea) {
        if (!e.isPrimary || e.button === 2) return;
        video = playerArea.querySelector('video');
        if (!video) return;

        // 参考脚本关键:阻止 B站 原生 touch/click 事件
        e.preventDefault();
        e.stopPropagation();

        isDown = true;
        gestureType = "";
        startX = e.clientX;
        startY = e.clientY;

        // 启动长按计时器
        pressTimer = setTimeout(() => {
            if (gestureType == "") {
                gestureType = "speed";
                onLongPressStart(video, playerArea);
            }
        }, PRESS_DELAY);
    }


    // 手指移动时 → 横向滑动/纵向滑动
    function handleMove(e, playerArea) {
        if (!isDown) return;
        video = playerArea.querySelector('video');
        if (!video) return;

        deltaX = e.clientX - startX;
        deltaY = startY - e.clientY;
        absX = Math.abs(deltaX);
        absY = Math.abs(deltaY);

        // 手势未确定,判断滑动方向
        if (gestureType == "" && (absX > 15 || absY > 15)) {
            if (pressTimer) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }

            if (absX > absY) {
                // 横向滑动 → 调节进度
                gestureType = "seek";
                onSeekStart(video, playerArea);
            } else {
                // 纵向滑动 → 调节亮度/音量
                if (startX < window.innerWidth / 2) {
                    gestureType = "brightness";
                    onBrightnessStart(video);
                } else {
                    gestureType = "volume";
                    onVolumeStart(video);
                }
            }
        }

        // 手势已确定,持续更新
        if (gestureType != "") {
            if (gestureType == "seek") {
                onSeek(video, playerArea, deltaX);
            } else if (gestureType == "volume") {
                onVolume(video, playerArea, e.clientY);
            } else if (gestureType == "brightness") {
                onBrightness(video, playerArea, e.clientY);
            }
        }
    }

    // 手指抬起时 → 单击/双击/长按结束/横向滑动结束
    function handleUp(e, playerArea) {
        if (pressTimer) {
            clearTimeout(pressTimer);
            pressTimer = null;
        }

        video = playerArea.querySelector('video');
        if (!video) {
            startX = 0;
            startY = 0;
            return;
        }

        deltaX = e.clientX - startX;
        deltaY = startY - e.clientY;
        absX = Math.abs(deltaX);
        absY = Math.abs(deltaY);

        // 手势结束收尾
        if (gestureType != "") {
            
            if (gestureType == "speed") {
                onLongPressEnd(video, playerArea);
            } else if (gestureType == "seek") {
                onSeekEnd(video, playerArea);
            }

            gestureType = "";
            setTimeout(() => hideToast(playerArea), 500);
        } 

        // 无滑动、无长按 → 单击或双击
        if (gestureType == "") {
            if (absX < 10 && absY < 10) {
                if (clickTimer) {
                    // 再次点击 → 双击
                    clearTimeout(clickTimer);
                    clickTimer = null;
                    onDoubleTap(video, playerArea);
                } else {
                    // 首次点击 → 等待是否有第二次
                    clickTimer = setTimeout(() => {
                        clickTimer = null;
                        handleCtrl(playerArea);
                    }, CLICK_TIMEOUT);
                }
            }
        }

        startX = 0;
        startY = 0;
        prevBrightnessY = null;
        prevVolumeY = null;
        isDown = false;
    }

    // #endregion



    // ============================================================
    // #region 初始化
    // ============================================================

    function createSafeShield() {
        playerArea = document.querySelector('.bpx-player-video-area') || document.querySelector('.bilibili-player-video-wrap');

        if (!playerArea) return;
        if (playerArea.querySelector('#gemini-mobile-shield')) return;

        console.log('双击播放暂停 / 长按倍速 / 滑动进度亮度音量 版已部署');

        const shield = document.createElement('div');
        shield.id = 'gemini-mobile-shield';
        shield.style.cssText = `
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 85%;
            z-index: 20;
            background: transparent;
            touch-action: none !important;
            user-select: none;
        `;

        playerArea.appendChild(shield);

        shield.addEventListener('pointerdown', (e) => handleDown(e, playerArea));
        shield.addEventListener('pointermove', (e) => handleMove(e, playerArea));
        shield.addEventListener('pointerup', (e) => handleUp(e, playerArea));
        shield.addEventListener('pointercancel', (e) => handleUp(e, playerArea));
        shield.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); });
    }


    const observer = new MutationObserver(() => createSafeShield());
    observer.observe(document.body, { childList: true, subtree: true });
    window.addEventListener('load', createSafeShield);

    // #endregion

})();