// ==UserScript== // @name Bilibili Surface // @namespace http://tampermonkey.net/ // @version 1.6.6 // @description 单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量 // @author You // @match *://*.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @run-at document-end // @grant unsafeWindow // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // ============================================================ // #region 参数配置 // ============================================================ const PRESS_DELAY = 300; const TARGET_SPEED = 3.0; const CLICK_TIMEOUT = 200; const HORIZONTAL_SENSITIVITY = 0.7; const VERTICAL_SENSITIVITY = 0.5; let playerArea = null; let shield = null; let video = null; let div = null; let iconWrap = null; let container = null; let isHidden = false; let pressTimer = null; let clickTimer = null; let isDown = false; let originalSpeed = 1.0; let gestureType = ""; let wasPlaying = false; let startVal = 0; let startX = 0; let startY = 0; let deltaX = 0; let deltaY = 0; let absX = 0; let absY = 0; let prevX = 0; let prevY = 0; // #endregion // ============================================================ // #region 图标svg // ============================================================ const style = document.createElement('style'); style.textContent = ` @keyframes geminiFadeToWhite { 0% { opacity: 1.0; filter: brightness(1.0); } 25% { opacity: 0.6; filter: brightness(0.6); } 50% { opacity: 0.3; filter: brightness(0.3); } 75% { opacity: 0.6; filter: brightness(0.6); } 100% { opacity: 1.0; filter: brightness(1.0); } } `; document.head.appendChild(style); const speedIcon = ` `; const brightnessIcon = ` `; const volumeIcon = ` `; // #endregion // ============================================================ // #region 提示框 // ============================================================ function createToast(playerArea) { div = playerArea.querySelector('#gemini-clean-toast'); if (!div) { div = document.createElement('div'); div.id = 'gemini-clean-toast'; div.style.cssText = ` display: none; align-items: center; gap: 8px; position: absolute; z-index: 100001; top: 15%; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; color: #fff; background: rgba(0,0,0,0.75); box-shadow: 0 4px 10px rgba(0,0,0,0.3); backdrop-filter: blur(4px); font-family: "Segoe UI", sans-serif; font-size: 20px; font-weight: 500; text-align: center; white-space: nowrap; pointer-events: none; `; playerArea.appendChild(div); } return div; } function showToast(playerArea, text) { div = createToast(playerArea); div.innerHTML = ''; div.style.display = 'flex'; div.appendChild(document.createTextNode(text)); } function showIconToast(playerArea, svg, text) { div = createToast(playerArea); div.innerHTML = ''; div.style.display = 'flex'; iconWrap = document.createElement('span'); iconWrap.innerHTML = svg; iconWrap.style.cssText = ` display: flex; align-items: center; justify-content: center; flex-shrink: 0; `; div.appendChild(iconWrap); div.appendChild(document.createTextNode(text)); } function hideToast(playerArea) { div = playerArea.querySelector('#gemini-clean-toast'); if (div) div.style.display = 'none'; } // #endregion // ============================================================ // #region 单指单击:显示/隐藏控制栏 // ============================================================ function sendMouseEvent(element, type, x = 0, y = 0) { if (!element) return false; element.dispatchEvent(new unsafeWindow.MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y, view: unsafeWindow })); } function handleCtrl(playerArea) { container = playerArea.closest('.bpx-player-container') isHidden = playerArea.getAttribute('data-ctrl-hidden'); if (isHidden == 'true') { showCtrl(playerArea); container.setAttribute('data-ctrl-hidden', 'false'); } else { hideCtrl(playerArea); container.setAttribute('data-ctrl-hidden', 'true'); } } function showCtrl(playerArea) { const rect = playerArea.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height * 0.1; return sendMouseEvent(playerArea, 'mousemove', x, y); } function hideCtrl() { const rect = playerArea.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height * 0.1; return sendMouseEvent(playerArea, 'mouseleave', x, y); } // #endregion // ============================================================ // #region 单指双击:播放/暂停 // ============================================================ function onDoubleTap(video, playerArea) { if (video.paused) { video.play(); } else { video.pause(); } } // #endregion // ============================================================ // #region 单指长按:倍速播放 // ============================================================ function onLongPressStart(video, playerArea) { originalSpeed = video.playbackRate; video.playbackRate = TARGET_SPEED; showIconToast(playerArea, speedIcon, TARGET_SPEED.toFixed(1) + "x"); } function onLongPressEnd(video, playerArea) { video.playbackRate = originalSpeed; hideToast(playerArea); } // #endregion // ============================================================ // #region 横向滑动:调节进度 // ============================================================ function formatTime(seconds) { const hr = Math.floor(seconds / 3600); const min = Math.floor((seconds % 3600) / 60); const sec = Math.floor(seconds % 60); if (hr > 0) return `${hr}:${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; } function onSeekStart(video, clientX) { startVal = video.currentTime; prevX = clientX; wasPlaying = !video.paused; video.pause(); } function onSeek(video, playerArea, clientX) { startVal = startVal + (clientX - prevX) / (playerArea.clientWidth * HORIZONTAL_SENSITIVITY) * video.duration; if (startVal < 0) startVal = 0; if (startVal > video.duration) startVal = video.duration; prevX = clientX; video.currentTime = startVal; showToast(playerArea, `${formatTime(startVal)} / ${formatTime(video.duration)}`); } function onSeekEnd(video, playerArea) { if (wasPlaying) video.play(); hideToast(playerArea); } // #endregion // ============================================================ // #region 左纵向滑动:调节亮度 // ============================================================ function getCurrentBrightness(video) { const filter = video.style.filter; if (!filter || !filter.includes('brightness')) return 1; const match = filter.match(/brightness\(([\d.]+)\)/); return match ? parseFloat(match[1]) : 1; } function onBrightnessStart(video, clientY) { startVal = getCurrentBrightness(video); prevY = clientY; } function onBrightness(video, playerArea, clientY) { startVal = startVal + (prevY - clientY) / (playerArea.clientHeight * VERTICAL_SENSITIVITY); if (startVal > 1) startVal = 1; if (startVal < 0) startVal = 0; prevY = clientY; video.style.filter = `brightness(${startVal})`; showIconToast(playerArea, brightnessIcon, `${Math.round(startVal * 100)}%`); } function onBrightnessEnd() { } // #endregion // ============================================================ // #region 右纵向滑动:调节音量 // ============================================================ function onVolumeStart(video, clientY) { startVal = video.volume; prevY = clientY; } function onVolume(video, playerArea, clientY) { startVal = startVal + (prevY - clientY) / (playerArea.clientHeight * VERTICAL_SENSITIVITY); if (startVal > 1) startVal = 1; if (startVal < 0) startVal = 0; prevY = clientY; video.volume = startVal; showIconToast(playerArea, volumeIcon, `${Math.round(startVal * 100)}%`); } function onVolumeEnd() { } // #endregion // ============================================================ // #region 手势识别与分发 // ============================================================ // 手指按下时 → 长按 function handleDown(e, playerArea) { if (!e.isPrimary || e.button === 2) return; 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; video = playerArea.querySelector('video'); if (!video) return; deltaX = e.clientX - startX; deltaY = startY - e.clientY; absX = Math.abs(deltaX); absY = Math.abs(deltaY); // 手势未确定,判断滑动方向 if (gestureType == "" && (absX > 15 || absY > 15)) { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } if (absX > absY) { // 横向滑动 → 调节进度 gestureType = "seek"; onSeekStart(video, e.clientX); } else { // 纵向滑动 → 调节亮度/音量 if (startX < playerArea.clientWidth / 2) { gestureType = "brightness"; onBrightnessStart(video, e.clientY); } else { gestureType = "volume"; onVolumeStart(video, e.clientY); } } } // 手势已确定,持续更新 if (gestureType != "") { if (gestureType == "seek") { onSeek(video, playerArea, e.clientX); } 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; } video = playerArea.querySelector('video'); if (!video) { startX = 0; startY = 0; isDown = false; return; } deltaX = e.clientX - startX; deltaY = startY - e.clientY; absX = Math.abs(deltaX); absY = Math.abs(deltaY); // 无滑动、无长按 → 单击或双击 if (gestureType == "") { if (absX < 10 && absY < 10) { if (clickTimer) { // 再次点击 → 双击 clearTimeout(clickTimer); clickTimer = null; onDoubleTap(video, playerArea); } else { // 首次点击 → 等待是否有第二次 clickTimer = setTimeout(() => { clickTimer = null; handleCtrl(playerArea); }, CLICK_TIMEOUT); } } } // 手势结束收尾 if (gestureType != "") { if (gestureType == "speed") { onLongPressEnd(video, playerArea); } else if (gestureType == "seek") { onSeekEnd(video, playerArea); } else if (gestureType == "brightness") { onBrightnessEnd(); } else if (gestureType == "volume") { onVolumeEnd(); } gestureType = ""; setTimeout(() => hideToast(playerArea), 500); } startX = 0; startY = 0; isDown = false; } // #endregion // ============================================================ // #region 初始化 // ============================================================ function createSafeShield() { playerArea = document.querySelector('.bpx-player-video-area'); if (!playerArea) return; if (playerArea.querySelector('#gemini-mobile-shield')) return; console.log('双击播放暂停 / 长按倍速 / 滑动进度亮度音量 版已部署'); shield = document.createElement('div'); shield.id = 'gemini-mobile-shield'; shield.style.cssText = ` position: absolute; z-index: 20; top: 0; left: 0; width: 100%; height: 85%; background: transparent; touch-action: none !important; user-select: none; `; playerArea.appendChild(shield); shield.addEventListener('pointerdown', (e) => handleDown(e, playerArea)); document.addEventListener('pointermove', (e) => handleMove(e, playerArea)); document.addEventListener('pointerup', (e) => handleUp(e, playerArea)); document.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 })();