Greasy Fork

Greasy Fork is available in English.

Bilibili Surface

单击切换进度条显示/隐藏(显示时7秒后自动隐藏),双击仅播放/暂停,保留长按倍速、左右滑动进度、左右半屏上下滑亮度/音量

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bilibili Surface
// @namespace    http://tampermonkey.net/
// @version      1.2.3
// @description  单击切换进度条显示/隐藏(显示时7秒后自动隐藏),双击仅播放/暂停,保留长按倍速、左右滑动进度、左右半屏上下滑亮度/音量
// @author       You
// @match        *://*.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @run-at       document-end
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 参数配置 ---
    const PRESS_DELAY = 250;
    const TARGET_SPEED = 3.0;
    const DOUBLE_TAP_DELAY = 250;
    const SEEK_SENSITIVITY = 0.2;
    const CTRL_SHOW_DURATION = 7000;

    let pressTimer = null;
    let clickTimer = null;
    let clickCount = 0;
    let ctrlTimeoutID = null;

    let originalSpeed = 1.0;
    let gestureType = '';
    let isInteracting = false;
    let wasPlaying = false;

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

    // --- 1. 注入核心 CSS ---
    const css = ``;
    const style = document.createElement('style');
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);

    // --- 提示框函数 ---
    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: 20%; left: 50%; transform: translateX(-50%);
                padding: 10px 22px;
                background: rgba(0,0,0,0.75);
                color: #fff;
                border-radius: 8px;
                font-size: 18px;
                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.innerText = text;
        div.style.display = 'block';
    }

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

    // --- 控制栏显示/隐藏辅助函数 ---

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

    function showCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        const jindutiao = container.querySelector('.bpx-player-control-entity');

        container.classList.remove('bpx-state-no-cursor');
        container.setAttribute('data-ctrl-hidden', 'false');
        if (jindutiao) {
            jindutiao.setAttribute('data-shadow-show', 'false');
        }
    }

    function hideCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        const jindutiao = container.querySelector('.bpx-player-control-entity');

        container.classList.add('bpx-state-no-cursor');
        container.setAttribute('data-ctrl-hidden', 'true');
        if (jindutiao) {
            jindutiao.setAttribute('data-shadow-show', 'true');
        }
    }

    function hideCtrlMenus(playerArea) {
        const container = getPlayerContainer(playerArea);
        const rightMenus = container.querySelector('.bpx-player-control-bottom-right');
        if (!rightMenus) return;

        rightMenus.childNodes.forEach(menu => {
            if (menu.classList) {
                menu.classList.remove('bpx-state-show');
            }
        });
    }

    /**
     * 控制栏切换核心逻辑,对齐参考脚本 handleCtrl()。
     * @param {Element} playerArea
     */
    function handleCtrl(playerArea) {
        const container = getPlayerContainer(playerArea);
        const isCtrlHidden = container.getAttribute('data-ctrl-hidden');

        if (isCtrlHidden === 'false' || ctrlTimeoutID) {
            // 控制栏显示中或有计时器运行中 → 隐藏并清除计时器
            if (ctrlTimeoutID) {
                clearTimeout(ctrlTimeoutID);
                ctrlTimeoutID = null;
            }
            hideCtrl(playerArea);
        } else {
            // 控制栏隐藏中 → 显示并启动计时器
            showCtrl(playerArea);
            ctrlTimeoutID = setTimeout(() => {
                hideCtrl(playerArea);
                ctrlTimeoutID = null;
            }, CTRL_SHOW_DURATION);
        }
    }

    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;
    }


    // ============================================================
    // --- 手势处理函数(每种手势独立封装)---
    // ============================================================

    /**
     * 单击手势:切换控制栏显示/隐藏
     * @param {Element} playerArea
     */
    function onSingleTap(playerArea) {
        handleCtrl(playerArea);
    }

    /**
     * 双击手势:仅切换播放/暂停
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     */
    function onDoubleTap(video, playerArea) {
        if (video.paused) {
            video.play();
            showToast(playerArea, '播放');
        } else {
            video.pause();
            showToast(playerArea, '暂停');
        }
        setTimeout(() => hideToast(playerArea), 500);
    }

    /**
     * 长按手势(按下时触发):切换到目标倍速
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     */
    function onLongPressStart(video, playerArea) {
        originalSpeed = video.playbackRate;
        video.playbackRate = TARGET_SPEED;
        showToast(playerArea, `倍速 ${TARGET_SPEED}x`);
    }

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

    /**
     * 横向滑动手势(进行中):拖动进度
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     * @param {number} deltaX - 当前 X 轴位移(px)
     */
    function onSeek(video, playerArea, deltaX) {
        const seekDelta = deltaX * SEEK_SENSITIVITY;
        let targetTime = startVal + seekDelta;
        if (targetTime < 0) targetTime = 0;
        if (targetTime > video.duration) targetTime = video.duration;

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

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

    /**
     * 横向滑动手势(结束时):恢复播放并隐藏控制栏
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     */
    function onSeekEnd(video, playerArea) {
        if (wasPlaying) video.play();
        hideCtrl(playerArea);
    }

    /**
     * 左半屏纵向滑动手势:调节亮度
     * 每次 move 都更新 prevY,亮度直接累加,对齐参考脚本的简洁逻辑
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     * @param {number} clientY - 当前手指 Y 坐标
     */
    function onBrightness(video, playerArea, clientY) {
        // 第一次 move:初始化 prevY
        if (prevBrightnessY === null) {
            prevBrightnessY = clientY;
            return;
        }

        // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正)
        const moveDelta = prevBrightnessY - clientY;

        const 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)}%)`;
        showToast(playerArea, `亮度 ${Math.round(startVal)}%`);
    }

    /**
     * 左半屏纵向滑动手势(开始时):记录初始亮度
     * @param {HTMLVideoElement} video
     */
    function onBrightnessStart(video) {
        startVal = getCurrentBrightness(video);
        prevBrightnessY = null;
    }

    /**
     * 右半屏纵向滑动手势:调节音量
     * 每次 move 都更新 prevY,音量直接累加
     * @param {HTMLVideoElement} video
     * @param {Element} playerArea
     * @param {number} clientY - 当前手指 Y 坐标
     */
    function onVolume(video, playerArea, clientY) {
        // 第一次 move:初始化 prevY
        if (prevVolumeY === null) {
            prevVolumeY = clientY;
            return;
        }

        // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正)
        const moveDelta = prevVolumeY - clientY;

        const 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;
        showToast(playerArea, `音量 ${Math.round(startVal * 100)}%`);
    }

    /**
     * 右半屏纵向滑动手势(开始时):记录初始音量
     * @param {HTMLVideoElement} video
     */
    function onVolumeStart(video) {
        startVal = video.volume;
        prevVolumeY = null;
    }


    // ============================================================
    // --- 事件调度层:识别手势类型并调用对应手势函数 ---
    // ============================================================

    function handleDown(e, playerArea) {
        if (!e.isPrimary || e.button === 2) return;
        const video = playerArea.querySelector('video');
        if (!video) return;

        startX = e.clientX;
        startY = e.clientY;
        gestureType = '';
        isInteracting = false;

        originalSpeed = video.playbackRate;

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

    function handleMove(e, playerArea) {
        if (!startX) return;
        const video = playerArea.querySelector('video');
        if (!video) return;

        const deltaX = e.clientX - startX;
        const deltaY = startY - e.clientY;   // 向上为正
        const absX = Math.abs(deltaX);
        const absY = Math.abs(deltaY);

        // 尚未确定手势类型时,判断滑动方向
        if (!isInteracting && (absX > 15 || absY > 15)) {
            if (pressTimer) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }
            isInteracting = true;

            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 (isInteracting) {
            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 (isInteracting) {
            // 手势结束收尾
            if (gestureType === 'speed') {
                onLongPressEnd(video);
            } else if (gestureType === 'seek') {
                onSeekEnd(video, playerArea);
            }

            isInteracting = false;
            gestureType = '';
            setTimeout(() => hideToast(playerArea), 500);
        } else {
            // 无滑动 → 判断单击 / 双击
            if (Math.abs(e.clientX - startX) < 10 && Math.abs(e.clientY - startY) < 10) {
                clickCount++;
                if (clickCount === 1) {
                    clickTimer = setTimeout(() => {
                        // 单击
                        onSingleTap(playerArea);
                        clickCount = 0;
                        clickTimer = null;
                    }, DOUBLE_TAP_DELAY);
                } else if (clickCount === 2) {
                    if (clickTimer) {
                        clearTimeout(clickTimer);
                        clickTimer = null;
                    }
                    // 双击
                    onDoubleTap(video, playerArea);
                    clickCount = 0;
                }
            }
        }

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


    // ============================================================
    // --- 初始化 ---
    // ============================================================

    function createSafeShield(playerArea) {
        if (playerArea.querySelector('#gemini-mobile-shield')) return;

        console.log('Gemini: 单击切换显示/隐藏进度条 / 双击播放暂停 版已部署');
        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 container = getPlayerContainer(playerArea);
        const ctrlWrap = container.querySelector('.bpx-player-control-bottom');
        if (ctrlWrap) {
            ctrlWrap.addEventListener('pointerdown', () => {
                if (ctrlTimeoutID) {
                    clearTimeout(ctrlTimeoutID);
                    ctrlTimeoutID = null;
                }
            });
        }
    }

    function init() {
        const targetArea = document.querySelector('.bpx-player-video-area') ||
                           document.querySelector('.bilibili-player-video-wrap');

        if (targetArea) {
            createSafeShield(targetArea);
        }
    }

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

})();