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.8
// @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 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 startVal = 0;
    let prevBrightnessY = null;   // 追踪亮度手势的前一个 Y,避免定位跳跃
    let prevVolumeY = null;       // 追踪音量手势的前一个 Y
    let moveDelta = 0;
    let sensitivity = 0;

    let speedHintEl = null;       // 倍速提示元素(三箭头 + 文字)
    // #endregion


    
    // --- 1. 注入核心 CSS ---
    const css = ``;
    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) {
        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 = `
            width: 20px;
            height: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            vertical-align: middle;
        `;
        
        const txt = document.createTextNode(text);
        div.appendChild(iconWrap);
        div.appendChild(txt);
    }

    // #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 createSpeedHint(playerArea) {
        if (speedHintEl) return speedHintEl;

        speedHintEl = document.createElement('div');
        speedHintEl.id = 'gemini-speed-hint';
        speedHintEl.style.cssText = `
            position: absolute;
            top: 15%; left: 50%;
            transform: translateX(-50%);
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 12px 24px;
            height: 44px;
            box-sizing: border-box;
            background: rgba(0, 0, 0, 0.75);
            border-radius: 8px;
            z-index: 100002;
            pointer-events: none;
            white-space: nowrap;
            backdrop-filter: blur(4px);
            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
        `;

        // 三箭头 SVG(三个三角形依次闪烁)
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 111 66');
        svg.setAttribute('width', '34');
        svg.setAttribute('height', '20');
        svg.style.overflow = 'visible';

        const trianglePath = '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';

        const transforms = ['matrix(0,3,-3,0,94.5,32.5)', 'matrix(0,3,-3,0,55.5,32.5)', 'matrix(0,3,-3,0,16.5,32.5)'];
        const ids = ['triangle3', 'triangle2', 'triangle1'];

        transforms.forEach((tf, i) => {
            const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
            g.setAttribute('transform', tf);
            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path.setAttribute('d', trianglePath);
            path.setAttribute('fill', 'rgb(255,255,255)');
            path.setAttribute('class', 'triangle');
            path.setAttribute('id', ids[i]);
            g.appendChild(path);
            svg.appendChild(g);
        });

        speedHintEl.appendChild(svg);

        // 文字
        const label = document.createElement('span');
        label.textContent = '3.0\u00D7';
        label.style.cssText = `
            color: #fff;
            font-size: 20px;
            font-family: "Segoe UI", sans-serif;
            font-weight: 500;
            line-height: 1;
        `;
        speedHintEl.appendChild(label);

        // CSS 动画(注入到同一元素内)
        const animStyle = document.createElement('style');
        animStyle.textContent = `
            .triangle {
                animation: geminiFadeToWhite 1.2s infinite;
            }
            #triangle1 { animation-delay: 0s; }
            #triangle2 { animation-delay: 0.18s; }
            #triangle3 { animation-delay: 0.35s; }
            @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); }
            }
        `;
        speedHintEl.appendChild(animStyle);

        playerArea.appendChild(speedHintEl);
        return speedHintEl;
    }


    function showSpeedHint(playerArea) {
        createSpeedHint(playerArea).style.display = 'flex';
    }


    function hideSpeedHint() {
        if (speedHintEl) {
            speedHintEl.style.display = 'none';
        }
    }


    // 长按手势(按下时触发):切换到目标倍速
    function onLongPressStart(video, playerArea) {
        originalSpeed = video.playbackRate;
        video.playbackRate = TARGET_SPEED;
        showSpeedHint(playerArea);
    }


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

    // #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)}%)`;
        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>`;
        showIconToast(playerArea, brightnessIcon, `${Math.round(startVal)}%`);
    }


    // --- 右半屏上下滑动 → 调节音量 ---
    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;
        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>`;
        showIconToast(playerArea, volumeIcon, `${Math.round(startVal * 100)}%`);
    }

    // #endregion



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

    function handleDown(e, playerArea) {
        if (!e.isPrimary || e.button === 2) return;
        const 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;
        const video = playerArea.querySelector('video');
        if (!video) return;

        deltaX = e.clientX - startX;
        deltaY = startY - e.clientY;   // 向上为正
        const absX = Math.abs(deltaX);
        const 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 {
                // 纵向滑动 → 左半屏亮度 / 右半屏音量
                const screenW = window.innerWidth;
                if (startX < screenW / 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;
        }

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

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

            gestureType = "";
            setTimeout(() => hideToast(playerArea), 500);
        } else {
            // 无滑动、无长按 → 单击或双击
            if (Math.abs(e.clientX - startX) < 10 && Math.abs(e.clientY - startY) < 10) {
                if (clickTimer) {
                    // 200ms 内再次点击 → 双击
                    clearTimeout(clickTimer);
                    clickTimer = null;
                    onDoubleTap(video, playerArea);
                } else {
                    // 首次点击 → 等待 200ms 看是否有第二次
                    clickTimer = setTimeout(() => {
                        clickTimer = null;
                        handleCtrl(playerArea);
                    }, CLICK_TIMEOUT);
                }
            }
        }

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

    // #endregion



    // #region --- 初始化 ---

    function createSafeShield() {
        const 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

})();