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.6.4
// @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 CLICK_TIMEOUT = 200;
    const HORIZONTAL_SENSITIVITY = 0.7;
    const VERTICAL_SENSITIVITY = 0.5;

    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 prevX = 0;
    let prevY = 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, clientX) {
        startVal = video.currentTime;
        prevX = clientX;
        wasPlaying = !video.paused;
        video.pause();
    }


    function onSeek(video, playerArea, clientX) {
        startVal = startVal + (clientX - prevX) / (playerArea.clientWidth * HORIZONTAL_SENSITIVITY) * video.duration;
        if (startVal < 0) startVal = 0;
        if (startVal > video.duration) startVal = video.duration;
        
        prevX = clientX;
        video.currentTime = startVal;
        showToast(playerArea, `${formatTime(startVal)} / ${formatTime(video.duration)}`);
    }


    function onSeekEnd(video, playerArea) {
        if (wasPlaying) video.play();
        hideToast(playerArea);
    }

    // #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, clientY) {
        startVal = getCurrentBrightness(video);
        prevY = clientY;
    }


    function onBrightness(video, playerArea, clientY) {
        startVal = startVal + (prevY - clientY) / (playerArea.clientHeight * VERTICAL_SENSITIVITY) * 100;
        if (startVal > 100) startVal = 100;
        if (startVal < 0) startVal = 0;

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


    function onBrightnessEnd() {
    }

    // #endregion



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

    function onVolumeStart(video, clientY) {
        startVal = video.volume;
        prevY = clientY;
    }


    function onVolume(video, playerArea, clientY) {
        startVal = startVal + (prevY - clientY) / (playerArea.clientHeight * VERTICAL_SENSITIVITY);
        if (startVal > 1) startVal = 1;
        if (startVal < 0) startVal = 0;

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


    function onVolumeEnd() {
    }

    // #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, e.clientX);
            } else {
                // 纵向滑动 → 调节亮度/音量
                if (startX < playerArea.clientWidth / 2) {
                    gestureType = "brightness";
                    onBrightnessStart(video, e.clientY);
                } else {
                    gestureType = "volume";
                    onVolumeStart(video, e.clientY);
                }
            }
        }

        // 手势已确定,持续更新
        if (gestureType != "") {
            if (gestureType == "seek") {
                onSeek(video, playerArea, e.clientX);
            } 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;
            isDown = false;
            return;
        }

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

        // 无滑动、无长按 → 单击或双击
        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);
                }
            }
        }

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

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

        startX = 0;
        startY = 0;
        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));
        document.addEventListener('pointermove', (e) => handleMove(e, playerArea));
        document.addEventListener('pointerup', (e) => handleUp(e, playerArea));
        document.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

})();