// ==UserScript== // @name Bilibili Surface // @namespace http://tampermonkey.net/ // @version 1.5.8 // @description 单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量 // @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'; // #region --- 参数配置 --- const PRESS_DELAY = 300; const TARGET_SPEED = 3.0; const SEEK_SENSITIVITY = 1.0; const CLICK_TIMEOUT = 200; let pressTimer = null; let clickTimer = null; let isDown = false; let originalSpeed = 1.0; let gestureType = ""; let wasPlaying = false; let startX = 0; let startY = 0; let deltaX = 0; let deltaY = 0; let startVal = 0; let prevBrightnessY = null; // 追踪亮度手势的前一个 Y,避免定位跳跃 let prevVolumeY = null; // 追踪音量手势的前一个 Y let moveDelta = 0; let sensitivity = 0; let speedHintEl = null; // 倍速提示元素(三箭头 + 文字) // #endregion // --- 1. 注入核心 CSS --- const css = ``; const style = document.createElement('style'); style.textContent = css; (document.head || document.documentElement).appendChild(style); // #region --- 提示框辅助函数 --- 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: 15%; left: 50%; transform: translateX(-50%); padding: 12px 24px; background: rgba(0,0,0,0.75); color: #fff; border-radius: 8px; font-size: 20px; 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.innerHTML = ''; div.style.display = 'flex'; div.style.alignItems = 'center'; div.style.gap = '8px'; div.appendChild(document.createTextNode(text)); } function hideToast(playerArea) { const div = playerArea.querySelector('#gemini-clean-toast'); if (div) div.style.display = 'none'; } function showIconToast(playerArea, svg, text) { const div = getToast(playerArea); div.innerHTML = ''; div.style.display = 'flex'; div.style.alignItems = 'center'; div.style.gap = '8px'; const iconWrap = document.createElement('span'); iconWrap.innerHTML = svg; iconWrap.style.cssText = ` width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; vertical-align: middle; `; const txt = document.createTextNode(text); div.appendChild(iconWrap); div.appendChild(txt); } // #endregion // #region --- 单指单击 → 显示/隐藏控制栏 --- function getPlayerContainer(playerArea) { return playerArea.closest('.bpx-player-container') || playerArea.closest('#bilibili-player') || playerArea; } function handleCtrl(playerArea) { const container = getPlayerContainer(playerArea); const isHidden = container.getAttribute('data-ctrl-hidden'); if (isHidden !== 'true') { hideCtrl(playerArea); } else { showCtrl(playerArea); } } function showCtrl(playerArea) { const container = getPlayerContainer(playerArea); container.classList.remove('bpx-state-no-cursor'); container.setAttribute('data-ctrl-hidden', 'false'); const shadow = container.querySelector('.bilibili-player-progress-shadow'); if (shadow) shadow.setAttribute('data-shadow-show', 'false'); const title = container.querySelector('.bpx-player-video-info'); if (title) { title.classList.remove('bpx-state-hide'); title.classList.add('bpx-state-show'); } } function hideCtrl(playerArea) { const container = getPlayerContainer(playerArea); container.classList.add('bpx-state-no-cursor'); container.setAttribute('data-ctrl-hidden', 'true'); const shadow = container.querySelector('.bilibili-player-progress-shadow'); if (shadow) shadow.setAttribute('data-shadow-show', 'true'); const title = container.querySelector('.bpx-player-video-info'); if (title) { title.classList.remove('bpx-state-show'); title.classList.add('bpx-state-hide'); } } // #endregion // #region --- 单指双击 → 播放/暂停 --- function onDoubleTap(video, playerArea) { if (video.paused) { video.play(); } else { video.pause(); } } // #endregion // #region --- 单指长按 → 倍速播放 --- // --- 三箭头闪烁 + 倍速文字 --- function createSpeedHint(playerArea) { if (speedHintEl) return speedHintEl; speedHintEl = document.createElement('div'); speedHintEl.id = 'gemini-speed-hint'; speedHintEl.style.cssText = ` position: absolute; top: 15%; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; padding: 12px 24px; height: 44px; box-sizing: border-box; background: rgba(0, 0, 0, 0.75); border-radius: 8px; z-index: 100002; pointer-events: none; white-space: nowrap; backdrop-filter: blur(4px); box-shadow: 0 4px 10px rgba(0,0,0,0.3); `; // 三箭头 SVG(三个三角形依次闪烁) const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 111 66'); svg.setAttribute('width', '34'); svg.setAttribute('height', '20'); svg.style.overflow = 'visible'; const trianglePath = 'M6.138,3.546 C6.468,4.106 6.278,4.826 5.718,5.156 C5.538,5.266 5.338,5.326 5.118,5.326 C5.118,5.326 -5.122,5.326 -5.122,5.326 C-5.772,5.326 -6.302,4.796 -6.302,4.146 C-6.302,3.936 -6.242,3.726 -6.142,3.546 C-6.142,3.546 -1.352,-4.554 -1.352,-4.554 C-0.912,-5.294 0.048,-5.544 0.798,-5.104 C1.028,-4.974 1.218,-4.784 1.348,-4.554 C1.348,-4.554 6.138,3.546 6.138,3.546z'; const transforms = ['matrix(0,3,-3,0,94.5,32.5)', 'matrix(0,3,-3,0,55.5,32.5)', 'matrix(0,3,-3,0,16.5,32.5)']; const ids = ['triangle3', 'triangle2', 'triangle1']; transforms.forEach((tf, i) => { const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); g.setAttribute('transform', tf); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', trianglePath); path.setAttribute('fill', 'rgb(255,255,255)'); path.setAttribute('class', 'triangle'); path.setAttribute('id', ids[i]); g.appendChild(path); svg.appendChild(g); }); speedHintEl.appendChild(svg); // 文字 const label = document.createElement('span'); label.textContent = '3.0\u00D7'; label.style.cssText = ` color: #fff; font-size: 20px; font-family: "Segoe UI", sans-serif; font-weight: 500; line-height: 1; `; speedHintEl.appendChild(label); // CSS 动画(注入到同一元素内) const animStyle = document.createElement('style'); animStyle.textContent = ` .triangle { animation: geminiFadeToWhite 1.2s infinite; } #triangle1 { animation-delay: 0s; } #triangle2 { animation-delay: 0.18s; } #triangle3 { animation-delay: 0.35s; } @keyframes geminiFadeToWhite { 0% { opacity: 1; filter: brightness(0.3); } 25% { opacity: 1; filter: brightness(0.6); } 50% { opacity: 1; filter: brightness(1); } 75% { opacity: 1; filter: brightness(0.6); } 100% { opacity: 1; filter: brightness(0.3); } } `; speedHintEl.appendChild(animStyle); playerArea.appendChild(speedHintEl); return speedHintEl; } function showSpeedHint(playerArea) { createSpeedHint(playerArea).style.display = 'flex'; } function hideSpeedHint() { if (speedHintEl) { speedHintEl.style.display = 'none'; } } // 长按手势(按下时触发):切换到目标倍速 function onLongPressStart(video, playerArea) { originalSpeed = video.playbackRate; video.playbackRate = TARGET_SPEED; showSpeedHint(playerArea); } // 长按手势(松手时触发):恢复原速 function onLongPressEnd(video) { video.playbackRate = originalSpeed; hideSpeedHint(); } // #endregion // #region --- 左右滑动 → 拖动进度 --- // 横向滑动手势(开始时):暂停视频并显示控制栏 function onSeekStart(video, playerArea) { startVal = video.currentTime; wasPlaying = !video.paused; video.pause(); } //横向滑动手势(进行中):拖动进度 function onSeek(video, playerArea, deltaX) { const seekPercent = deltaX / window.innerWidth; let targetTime = startVal + video.duration * seekPercent * SEEK_SENSITIVITY; if (targetTime < 0) targetTime = 0; if (targetTime > video.duration) targetTime = video.duration; video.currentTime = targetTime; showToast(playerArea, `${formatTime(targetTime)} / ${formatTime(video.duration)}`); } // 横向滑动手势(结束时):恢复播放 function onSeekEnd(video, playerArea) { if (wasPlaying) video.play(); } // #endregion // #region --- 上下滑动 → 调节亮度/音量 --- 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; } // --- 左半屏上下滑动 → 调节音量 --- function onBrightnessStart(video) { startVal = getCurrentBrightness(video); prevBrightnessY = null; } function onBrightness(video, playerArea, clientY) { // 第一次 move:初始化 prevY if (prevBrightnessY === null) { prevBrightnessY = clientY; return; } // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正) moveDelta = prevBrightnessY - clientY; 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)}%)`; const brightnessIcon = ` `; showIconToast(playerArea, brightnessIcon, `${Math.round(startVal)}%`); } // --- 右半屏上下滑动 → 调节音量 --- function onVolumeStart(video) { startVal = video.volume; prevVolumeY = null; } function onVolume(video, playerArea, clientY) { // 第一次 move:初始化 prevY if (prevVolumeY === null) { prevVolumeY = clientY; return; } // 计算本次移动的 Y 差值(向上滑 clientY 减小,moveDelta 为正) moveDelta = prevVolumeY - clientY; 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; const volumeIcon = ` `; showIconToast(playerArea, volumeIcon, `${Math.round(startVal * 100)}%`); } // #endregion // #region --- 手势识别与分发 --- function handleDown(e, playerArea) { if (!e.isPrimary || e.button === 2) return; const 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; const video = playerArea.querySelector('video'); if (!video) return; deltaX = e.clientX - startX; deltaY = startY - e.clientY; // 向上为正 const absX = Math.abs(deltaX); const absY = Math.abs(deltaY); // 尚未确定手势类型时,判断滑动方向 if (gestureType == "" && (absX > 15 || absY > 15)) { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } 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 (gestureType != "") { 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 (gestureType != "") { // 手势结束收尾 if (gestureType == "speed") { onLongPressEnd(video); } else if (gestureType == "seek") { onSeekEnd(video, playerArea); } gestureType = ""; setTimeout(() => hideToast(playerArea), 500); } else { // 无滑动、无长按 → 单击或双击 if (Math.abs(e.clientX - startX) < 10 && Math.abs(e.clientY - startY) < 10) { if (clickTimer) { // 200ms 内再次点击 → 双击 clearTimeout(clickTimer); clickTimer = null; onDoubleTap(video, playerArea); } else { // 首次点击 → 等待 200ms 看是否有第二次 clickTimer = setTimeout(() => { clickTimer = null; handleCtrl(playerArea); }, CLICK_TIMEOUT); } } } startX = 0; startY = 0; prevBrightnessY = null; prevVolumeY = null; isDown = false; } // #endregion // #region --- 初始化 --- function createSafeShield() { const playerArea = document.querySelector('.bpx-player-video-area') || document.querySelector('.bilibili-player-video-wrap'); if (!playerArea) return; if (playerArea.querySelector('#gemini-mobile-shield')) return; console.log('双击播放暂停 / 长按倍速 / 滑动进度亮度音量 版已部署'); 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 observer = new MutationObserver(() => createSafeShield()); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('load', createSafeShield); // #endregion })();