Greasy Fork is available in English.

BiliTouch

一个为移动端打造的Web端B站网页播放器交互重构的篡改猴脚本。支持两侧滑动调节亮度与音量、横向滑动调节时间进度、单击显隐工具栏、双击播放/暂停、双指缩放位移、长按倍速。让网页版拥有原生 App 般的使用体验。

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         BiliTouch
// @namespace    https://github.com/RevenLiu
// @version      1.1.2
// @description  一个为移动端打造的Web端B站网页播放器交互重构的篡改猴脚本。支持两侧滑动调节亮度与音量、横向滑动调节时间进度、单击显隐工具栏、双击播放/暂停、双指缩放位移、长按倍速。让网页版拥有原生 App 般的使用体验。
// @author       RevenLiu
// @license      MIT
// @icon         https://raw.githubusercontent.com/RevenLiu/BiliTouch/main/Icon.png
// @homepage     https://github.com/RevenLiu/BiliTouch
// @supportURL   https://github.com/RevenLiu/BiliTouch/issues
// @match        *://www.bilibili.com/video/*
// @match        *://www.bilibili.com/bangumi/*
// @match        *://live.bilibili.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const IS_LIVE = location.hostname === 'live.bilibili.com';
    const TARGET = IS_LIVE ? '.live-player-mounter' : '.bpx-player-video-perch';
    const AREA = IS_LIVE ? '.live-player-mounter' : '.bpx-player-video-area';

    // 样式与 UI 注入
    const style = document.createElement('style');
    style.innerHTML = `
        .my-force-show .bpx-player-control-entity, .my-force-show .bpx-player-pbp {
            opacity: 1 !important; visibility: visible !important; display: block !important;
        }
        .bpx-player-pbp:not(.show) { pointer-events: none !important; }
        ${TARGET} { touch-action: none !important; -webkit-tap-highlight-color: transparent !important; }
        #gesture-hud {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 10px;
            z-index: 999999; display: none; pointer-events: none; font-size: 20px; font-weight: bold; text-align: center;
        }
        #brightness-overlay {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: black; opacity: 0; pointer-events: none; z-index: 1000000;
        }
        .bpx-player-contextmenu.bpx-player-active { display: none !important; visibility: hidden !important; opacity: 0 !important; }
    `;
    document.head.appendChild(style);

    // 状态变量
    let clickTimer = null, longPressTimer = null, touchStartX = 0, touchStartY = 0;
    let baseTime = 0, targetTime = 0, baseVolume = 0, startOpacity = 0;
    let gestureMode = null, isGestureMoving = false, isLongPressing = false;
    let lastGestureTime = 0; 

    // 缩放状态
    let scale = 1, lastScale = 1, translateX = 0, translateY = 0, lastPosX = 0, lastPosY = 0;
    let startDistance = 0, startMidX = 0, startMidY = 0;

    // 工具函数
    const getEl = (id, parent, creator) => {
        let el = document.getElementById(id);
        if (!el) { el = creator(); (document.querySelector(parent) || document.body).appendChild(el); }
        return el;
    };

    const showHUD = (text) => {
        const hud = getEl('gesture-hud', AREA, () => {
            const d = document.createElement('div'); d.id = 'gesture-hud'; return d;
        });
        hud.innerText = text; hud.style.display = 'block';
    };

    const formatTime = (s) => isNaN(s) ? "00:00" : `${Math.floor(s/60).toString().padStart(2,'0')}:${Math.floor(s%60).toString().padStart(2,'0')}`;

    const updateTransform = (v) => {
        if (!v) return;
        if (scale <= 1.01) { scale = 1; translateX = 0; translateY = 0; }
        v.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
    };

    const toggleControls = () => {
        if (IS_LIVE) return;
        const container = document.querySelector('.bpx-player-container');
        const entity = document.querySelector('.bpx-player-control-entity');
        const pbp = document.querySelector('.bpx-player-pbp');
        if (!container || !entity) return;
        const isLocked = container.classList.toggle('my-force-show');
        container.setAttribute('data-ctrl-hidden', !isLocked);
        entity.setAttribute('data-shadow-show', !isLocked);
        if (pbp) { pbp.classList.toggle('show', isLocked); window.dispatchEvent(new Event('resize')); }
    };

    // 手势逻辑
    document.addEventListener('touchstart', (e) => {
        const perch = e.target.closest(TARGET);
        const v = document.querySelector('video');
        if (!perch || !v) return;

        gestureMode = null;
        isGestureMoving = false;

        if (e.touches.length === 2) {
            gestureMode = 'zoom';
            startDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
            startMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
            startMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
            lastScale = scale; lastPosX = translateX; lastPosY = translateY;
            clearTimeout(longPressTimer);
        } else if (e.touches.length === 1) {
            touchStartX = e.touches[0].clientX;
            touchStartY = e.touches[0].clientY;
            baseTime = v.currentTime;
            baseVolume = v.volume;
            const overlay = document.getElementById('brightness-overlay');
            startOpacity = overlay ? parseFloat(overlay.style.opacity) || 0 : 0;

            if (!IS_LIVE) {
                longPressTimer = setTimeout(() => {
                    if (!isGestureMoving && !gestureMode && (Date.now() - lastGestureTime > 200)) {
                        isLongPressing = true;
                        v.playbackRate = 2.0;
                        showHUD(">> 2.0X 倍速播放中");
                    }
                }, 300);
            }
        }
    }, { passive: true });

    document.addEventListener('touchmove', (e) => {
        const perch = e.target.closest(TARGET);
        const v = document.querySelector('video');
        if (!perch || !v) return;

        if (e.touches.length === 1 && (Date.now() - lastGestureTime < 200)) return;

        if (gestureMode === 'zoom' && e.touches.length === 2) {
            isGestureMoving = true;
            const curDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
            const curMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
            const curMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
            const zoomFactor = curDist / startDistance;
            scale = Math.max(1, Math.min(4, lastScale * zoomFactor));
            translateX = lastPosX + (curMidX - startMidX);
            translateY = lastPosY + (curMidY - startMidY);
            updateTransform(v);
            showHUD(`缩放: ${Math.round(scale * 100)}%`);
            if (e.cancelable) e.preventDefault();
        } 
        else if (e.touches.length === 1) {
            const deltaX = e.touches[0].clientX - touchStartX;
            const deltaY = touchStartY - e.touches[0].clientY;

            if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) clearTimeout(longPressTimer);

            if (!gestureMode && !isLongPressing) {
                if (scale > 1.01) {
                    gestureMode = 'drag';
                    lastPosX = translateX; lastPosY = translateY;
                } else {
                    if (!IS_LIVE && Math.abs(deltaX) > 20) gestureMode = 'progress';
                    else if (Math.abs(deltaY) > 20) gestureMode = touchStartX < (perch.offsetWidth / 2) ? 'brightness' : 'volume';
                }
            }

            if (gestureMode) {
                isGestureMoving = true;
                if (e.cancelable) e.preventDefault();
                if (gestureMode === 'drag') {
                    translateX = lastPosX + (e.touches[0].clientX - touchStartX);
                    translateY = lastPosY + (e.touches[0].clientY - touchStartY);
                    updateTransform(v);
                } else if (gestureMode === 'progress') {
                    const speed = 120 / (perch.offsetWidth || 500);
                    targetTime = Math.max(0, Math.min(v.duration, baseTime + deltaX * speed));
                    showHUD(`${deltaX > 0 ? '▶▶' : '◀◀'} ${formatTime(targetTime)} / ${formatTime(v.duration)}`);
                } else if (gestureMode === 'volume') {
                    v.volume = Math.max(0, Math.min(1, baseVolume + (deltaY / (perch.offsetHeight || 300))));
                    showHUD(`🔊 音量: ${Math.round(v.volume * 100)}%`);
                } else if (gestureMode === 'brightness') {
                    const currentOpacity = Math.max(0, Math.min(0.8, startOpacity - (deltaY / (perch.offsetHeight || 300)) * 0.5));
                    const overlay = getEl('brightness-overlay', AREA, () => {
                        const d = document.createElement('div'); d.id = 'brightness-overlay'; return d;
                    });
                    overlay.style.opacity = currentOpacity;
                    showHUD(`🔆 亮度: ${Math.round((1 - currentOpacity) * 100)}%`);
                }
            }
        }
    }, { passive: false });

    document.addEventListener('touchend', (e) => {
        clearTimeout(longPressTimer);
        const v = document.querySelector('video');
        
        if (gestureMode === 'zoom' || gestureMode === 'drag') {
            lastGestureTime = Date.now();
        }

        if (isLongPressing) {
            isLongPressing = false;
            if (v) v.playbackRate = 1.0;
        }

        if (isGestureMoving && gestureMode === 'progress') {
            if (v) v.currentTime = targetTime;
        }
        
        const hud = document.getElementById('gesture-hud');
        if (hud) hud.style.display = 'none';
        
        if (e.touches.length === 0) {
            gestureMode = null;
        }
    }, { passive: true });

    // 点击与冲突拦截
    document.addEventListener('click', (e) => {
        const perch = e.target.closest(TARGET);
        if (!perch || IS_LIVE) return; // 直播页跳过单击拦截

        if (isGestureMoving || isLongPressing || (Date.now() - lastGestureTime < 200)) { 
            isGestureMoving = false; 
            e.stopImmediatePropagation(); e.preventDefault(); 
            return; 
        }
        e.stopImmediatePropagation(); e.preventDefault();
        if (clickTimer) {
            clearTimeout(clickTimer); clickTimer = null;
            const v = document.querySelector('video'); if (v) v.paused ? v.play() : v.pause();
        } else {
            clickTimer = setTimeout(() => { clickTimer = null; toggleControls(); }, 250);
        }
    }, true);

    // 屏蔽右键菜单
    document.addEventListener('contextmenu', (e) => {
        if (e.target.closest(TARGET)) { e.preventDefault(); e.stopImmediatePropagation(); }
    }, true);

    // 处理直播页双击 (暂停/播放)
    document.addEventListener('dblclick', (e) => {
        if (e.target.closest(TARGET)) { 
            e.stopImmediatePropagation(); 
            e.preventDefault(); 
            if (IS_LIVE) {
                const v = document.querySelector('video');
                if (v) v.paused ? v.play() : v.pause();
            }
        }
    }, true);

    // 自动化宽屏
    if (!IS_LIVE) {
        const observer = new MutationObserver((_, obs) => {
            const btn = document.querySelector('.bpx-player-ctrl-wide-enter');
            if (btn) { btn.click(); obs.disconnect(); }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
})();