BiliTouch

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         BiliTouch
// @namespace    https://github.com/RevenLiu
// @version      1.0.0
// @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/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 样式与 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; }
        .bpx-player-video-perch { 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: 1;
        }
    `;
    document.head.appendChild(style);

    // 状态变量
    const TARGET = '.bpx-player-video-perch';
    let clickTimer = null, touchStartX = 0, touchStartY = 0;
    let baseTime = 0, targetTime = 0, baseVolume = 0, startOpacity = 0, currentOpacity = 0;
    let gestureMode = null, isGestureMoving = false;

    // 工具函数
    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', '.bpx-player-video-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 toggleControls = () => {
        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;

        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;
        
        gestureMode = null;
        isGestureMoving = false;
    }, { passive: true });

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

        const deltaX = e.touches[0].clientX - touchStartX;
        const deltaY = touchStartY - e.touches[0].clientY;

        if (!gestureMode) {
            if (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 === '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') {
                const volDelta = deltaY / (perch.offsetHeight || 300);
                v.volume = Math.max(0, Math.min(1, baseVolume + volDelta));
                showHUD(`🔊 音量: ${Math.round(v.volume * 100)}%`);
            } 
            else if (gestureMode === 'brightness') {
                const sensitivity = 0.5; // 亮度调节灵敏度
                const brightDelta = (deltaY / (perch.offsetHeight || 300)) * sensitivity;
                currentOpacity = Math.max(0, Math.min(0.8, startOpacity - brightDelta));
                const overlay = getEl('brightness-overlay', '.bpx-player-video-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', () => {
        if (isGestureMoving && gestureMode === 'progress') {
            const v = document.querySelector('video');
            if (v) v.currentTime = targetTime;
        }
        const hud = document.getElementById('gesture-hud');
        if (hud) hud.style.display = 'none';
    }, { passive: true });

    // 点击与冲突拦截
    document.addEventListener('click', (e) => {
        const perch = e.target.closest(TARGET);
        if (!perch) return;

        if (isGestureMoving) { 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('dblclick', (e) => {
        if (e.target.closest(TARGET)) { e.stopImmediatePropagation(); e.preventDefault(); }
    }, true);

    // 自动化宽屏
    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 });

})();