Greasy Fork is available in English.
单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量
当前为
// ==UserScript==
// @name Bilibili Surface
// @namespace http://tampermonkey.net/
// @version 1.5.16
// @description 单指单击切换控制栏显示/隐藏,双击播放/暂停,长按倍速(带三箭头闪烁提示),滑动进度自适应视频时长,左右半屏上下滑亮度/音量
// @author You
// @match *://*.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @run-at document-end
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ============================================================
// #region 参数配置
// ============================================================
const PRESS_DELAY = 300;
const TARGET_SPEED = 3.0;
const SEEK_SENSITIVITY = 1.0;
const CLICK_TIMEOUT = 200;
let playerArea = null;
let video = null;
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 absX = 0;
let absY = 0;
let startVal = 0;
let prevBrightnessY = null; // 追踪亮度手势的前一个 Y,避免定位跳跃
let prevVolumeY = null; // 追踪音量手势的前一个 Y
let moveDelta = 0;
let sensitivity = 0;
const speedIcon = `
<svg viewBox="0 0 111 66" width="34" height="20" style="overflow:visible">
<g transform="matrix(0,3,-3,0,94.5,32.5)">
<path d="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" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0s"/>
</g>
<g transform="matrix(0,3,-3,0,55.5,32.5)">
<path d="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" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0.18s"/>
</g>
<g transform="matrix(0,3,-3,0,16.5,32.5)">
<path d="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" fill="rgb(255,255,255)" style="animation:geminiFadeToWhite 1.2s infinite;animation-delay:0.35s"/>
</g>
</svg>`;
const brightnessIcon = `
<svg viewBox="0 0 24 24" style="width:100%;height:100%">
<path d="M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" fill="currentColor" />
</svg>`;
const volumeIcon = `
<svg viewBox="0 0 24 24" style="width:100%;height:100%">
<path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06Z" fill="currentColor" />
<path d="M15.9 8.2 A4.5 4.5 0 0 1 15.9 15.8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M19.1 5.7 A8.25 8.25 0 0 1 19.1 18.3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>`;
// #endregion
// --- 1. 注入核心 CSS ---
const css = `
@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); }
}
`;
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, iconStyle) {
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 = iconStyle || `
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
vertical-align: middle;
`;
div.appendChild(iconWrap);
if (text) {
div.appendChild(document.createTextNode(text));
}
}
// #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 onLongPressStart(video, playerArea) {
originalSpeed = video.playbackRate;
video.playbackRate = TARGET_SPEED;
const speedIconStyle = `
display: flex;
align-items: center;
gap: 6px;
`;
showIconToast(playerArea, speedIcon, TARGET_SPEED.toFixed(1) + "x", speedIconStyle);
}
// 长按手势(松手时触发):恢复原速
function onLongPressEnd(video, playerArea) {
video.playbackRate = originalSpeed;
hideToast(playerArea);
}
// #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)}%)`;
showIconToast(playerArea, brightnessIcon, `${Math.round(startVal)}%`);
}
// #endregion
// ============================================================
// #region 右纵向滑动:调节音量
// ============================================================
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;
showIconToast(playerArea, volumeIcon, `${Math.round(startVal * 100)}%`);
}
// #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, playerArea);
} else {
// 纵向滑动 → 调节亮度/音量
if (startX < window.innerWidth / 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;
}
video = playerArea.querySelector('video');
if (!video) {
startX = 0;
startY = 0;
return;
}
deltaX = e.clientX - startX;
deltaY = startY - e.clientY;
absX = Math.abs(deltaX);
absY = Math.abs(deltaY);
// 手势结束收尾
if (gestureType != "") {
if (gestureType == "speed") {
onLongPressEnd(video, playerArea);
} else if (gestureType == "seek") {
onSeekEnd(video, playerArea);
}
gestureType = "";
setTimeout(() => hideToast(playerArea), 500);
}
// 无滑动、无长按 → 单击或双击
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);
}
}
}
startX = 0;
startY = 0;
prevBrightnessY = null;
prevVolumeY = null;
isDown = false;
}
// #endregion
// ============================================================
// #region 初始化
// ============================================================
function createSafeShield() {
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
})();