// ==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 // @downloadURL none // ==/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); })();