Greasy Fork is available in English.
修复长按快进快退问题,优化手势体验
当前为
// ==UserScript==
// @name Global Video Swipe + AV + Desktop Gestures Fixed
// @namespace http://tampermonkey.net/
// @version 1.7
// @description 修复长按快进快退问题,优化手势体验
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
SWIPE_PIXELS_PER_SECOND: 8,
SWIPE_MAX_SECONDS: 60,
HORIZONTAL_TRIGGER: 15, // 降低触发阈值
VERTICAL_TRIGGER: 15, // 降低触发阈值
VERTICAL_PIXELS_PER_UNIT: 100,
BRIGHTNESS_MIN: 0.2,
BRIGHTNESS_MAX: 2.0,
BRIGHTNESS_STEP: 0.1,
VOLUME_MIN: 0.0,
VOLUME_MAX: 1.0,
VOLUME_STEP: 0.05,
DOUBLE_TAP_INTERVAL: 300,
DOUBLE_TAP_DISTANCE: 140,
DOUBLE_TAP_STEP: 10,
HOLD_SEEK_INITIAL_DELAY: 400, // 缩短初始延迟
HOLD_SEEK_REPEAT_INTERVAL: 80, // 加快重复间隔
HOLD_SEEK_ACCELERATION: 1.3,
HOLD_SEEK_MAX_SPEED: 15,
HINT_FADE_DELAY: 1200,
GESTURE_CLASS: 'tm-mobile-gesture-overlay',
LOCK_ICON_SIZE: 32,
LOCK_ICON_POSITION: { top: '16px', right: '16px' },
SCREEN_LOCK_OVERLAY_COLOR: 'rgba(0, 0, 0, 0.3)',
SCREEN_LOCK_BORDER_COLOR: 'rgba(255, 255, 255, 0.5)'
};
class SettingsManager {
constructor() {
this.settings = {
enableGestures: GM_getValue('enableGestures', true),
enableScreenLock: GM_getValue('enableScreenLock', true),
enableShortcuts: GM_getValue('enableShortcuts', true),
enableHints: GM_getValue('enableHints', true),
enableHoldSeek: GM_getValue('enableHoldSeek', true),
defaultScreenLock: GM_getValue('defaultScreenLock', false),
swipeSensitivity: GM_getValue('swipeSensitivity', 1.0)
};
}
updateSetting(key, value) {
this.settings[key] = value;
GM_setValue(key, value);
}
}
class HoldSeekManager {
constructor(settingsManager) {
this.settingsManager = settingsManager;
this.repeatTimers = new WeakMap();
this.holdStates = new WeakMap();
}
startHoldSeek(video, direction, onTick) {
if (!this.settingsManager.settings.enableHoldSeek) return;
this.stopHoldSeek(video);
let speed = 1;
let iteration = 0;
const performSeek = () => {
const step = direction * speed;
Utils.seekVideo(video, step);
iteration += 1;
// 每3次加速一次,让加速更平滑
if (iteration % 3 === 0) {
speed = Math.min(speed * CONFIG.HOLD_SEEK_ACCELERATION, CONFIG.HOLD_SEEK_MAX_SPEED);
}
onTick(step, speed);
};
const intervalId = setInterval(performSeek, CONFIG.HOLD_SEEK_REPEAT_INTERVAL);
this.repeatTimers.set(video, intervalId);
this.holdStates.set(video, { direction, speed, iteration });
// 立即执行第一次
performSeek();
}
stopHoldSeek(video) {
const timer = this.repeatTimers.get(video);
if (timer) {
clearInterval(timer);
this.repeatTimers.delete(video);
this.holdStates.delete(video);
}
}
isHolding(video) {
return this.repeatTimers.has(video);
}
}
// ScreenLockManager 类保持不变...
class ScreenLockManager {
constructor() {
this.lockedVideos = new WeakSet();
}
isLocked(video) {
return this.lockedVideos.has(video);
}
toggleLock(video) {
if (this.isLocked(video)) {
this.unlock(video);
} else {
this.lock(video);
}
return this.isLocked(video);
}
lock(video) {
this.lockedVideos.add(video);
video.dataset.tmScreenLocked = 'true';
this.applyLockStyle(video);
}
unlock(video) {
this.lockedVideos.delete(video);
delete video.dataset.tmScreenLocked;
this.removeLockStyle(video);
}
applyLockStyle(video) {
const overlay = video.parentElement?.querySelector('.tm-screen-lock-overlay');
if (overlay) overlay.style.display = 'block';
video.style.boxShadow = 'inset 0 0 0 3px rgba(255, 100, 100, 0.8)';
video.style.borderRadius = '4px';
}
removeLockStyle(video) {
const overlay = video.parentElement?.querySelector('.tm-screen-lock-overlay');
if (overlay) overlay.style.display = 'none';
video.style.boxShadow = '';
video.style.borderRadius = '';
}
createLockIcon(video, gestureManager) {
const lockIcon = document.createElement('div');
lockIcon.className = 'tm-screen-lock-icon';
Object.assign(lockIcon.style, {
position: 'absolute',
top: CONFIG.LOCK_ICON_POSITION.top,
right: CONFIG.LOCK_ICON_POSITION.right,
width: `${CONFIG.LOCK_ICON_SIZE}px`,
height: `${CONFIG.LOCK_ICON_SIZE}px`,
borderRadius: '8px',
background: 'rgba(30, 30, 30, 0.8)',
backdropFilter: 'blur(10px)',
border: '2px solid rgba(255, 255, 255, 0.6)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
color: 'rgba(255, 255, 255, 0.9)',
transition: 'all 0.3s ease',
zIndex: '2147483647',
pointerEvents: 'auto',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)'
});
this.updateLockIconAppearance(lockIcon, this.isLocked(video));
lockIcon.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const locked = this.toggleLock(video);
this.updateLockIconAppearance(lockIcon, locked);
const hint = video.parentElement?.querySelector('.tm-gesture-hint');
if (hint) {
const show = gestureManager.makeHintHandler(hint);
show(locked ? '🔒 屏幕已锁定' : '🔓 屏幕已解锁');
}
});
return lockIcon;
}
createScreenLockOverlay() {
const overlay = document.createElement('div');
overlay.className = 'tm-screen-lock-overlay';
Object.assign(overlay.style, {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: CONFIG.SCREEN_LOCK_OVERLAY_COLOR,
border: `3px solid ${CONFIG.SCREEN_LOCK_BORDER_COLOR}`,
borderRadius: '4px',
pointerEvents: 'none',
zIndex: '2147483646',
display: 'none',
boxSizing: 'border-box'
});
return overlay;
}
updateLockIconAppearance(lockIcon, locked) {
if (locked) {
lockIcon.innerHTML = '🔒';
lockIcon.style.background = 'rgba(220, 80, 80, 0.9)';
lockIcon.style.borderColor = 'rgba(255, 150, 150, 0.8)';
} else {
lockIcon.innerHTML = '🔓';
lockIcon.style.background = 'rgba(30, 30, 30, 0.8)';
lockIcon.style.borderColor = 'rgba(255, 255, 255, 0.6)';
}
}
}
class GestureManager {
constructor(lockManager, holdManager, settingsManager) {
this.lockManager = lockManager;
this.holdManager = holdManager;
this.settingsManager = settingsManager;
this.bound = new WeakSet();
}
ensure(video) {
if (this.bound.has(video)) return;
this.bound.add(video);
if (!this.settingsManager.settings.enableGestures) return;
const overlay = this.buildOverlay();
const hint = overlay.querySelector('.tm-gesture-hint');
if (this.settingsManager.settings.enableScreenLock) {
overlay.appendChild(this.lockManager.createLockIcon(video, this));
overlay.appendChild(this.lockManager.createScreenLockOverlay());
if (this.settingsManager.settings.defaultScreenLock) {
this.lockManager.lock(video);
}
}
this.attachOverlay(video, overlay);
this.initBrightness(video);
this.bindPointer(video, hint);
this.bindDoubleClick(video, hint);
console.log('🎬 视频手势已绑定:', video);
}
buildOverlay() {
const overlay = document.createElement('div');
overlay.className = CONFIG.GESTURE_CLASS;
Object.assign(overlay.style, {
position: 'absolute',
inset: '0',
pointerEvents: 'none',
zIndex: '2147483647'
});
const hint = document.createElement('div');
hint.className = 'tm-gesture-hint';
Object.assign(hint.style, {
position: 'absolute',
top: '45%',
left: '50%',
transform: 'translate(-50%, -50%)',
padding: '12px 18px',
borderRadius: '18px',
background: 'rgba(30,30,30,0.65)',
color: '#fff',
fontSize: '16px',
fontWeight: '500',
boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
backdropFilter: 'blur(10px)',
opacity: '0',
transition: 'opacity 0.15s ease',
pointerEvents: 'none',
textAlign: 'center',
minWidth: '140px',
whiteSpace: 'pre-line'
});
overlay.appendChild(hint);
return overlay;
}
attachOverlay(video, overlay) {
const parent = video.parentElement;
if (!parent) return;
const style = getComputedStyle(parent);
if (style.position === 'static') parent.style.position = 'relative';
parent.appendChild(overlay);
}
initBrightness(video) {
if (!video.dataset.tmOriginalFilterCaptured) {
video.dataset.tmOriginalFilterCaptured = '1';
video.dataset.tmOriginalFilter = video.style.filter || '';
if (!video.dataset.tmBrightness) {
video.dataset.tmBrightness = '1';
}
Utils.applyBrightness(video, Number(video.dataset.tmBrightness));
}
}
makeHintHandler(hint) {
return (text) => {
if (!this.settingsManager.settings.enableHints) return;
hint.textContent = text;
hint.style.opacity = '1';
clearTimeout(hint.hideTimer);
hint.hideTimer = setTimeout(() => {
hint.style.opacity = '0';
}, CONFIG.HINT_FADE_DELAY);
};
}
bindPointer(video, hint) {
const showHint = this.makeHintHandler(hint);
const state = {
pointerId: null,
pointerType: '',
startX: 0,
startY: 0,
deltaX: 0,
deltaY: 0,
mode: null,
isLeftHalf: true,
startBrightness: 1,
startVolume: video.volume,
holdTimer: null,
holdActive: false,
holdDirection: 0
};
const clearHoldTimer = () => {
if (state.holdTimer) {
clearTimeout(state.holdTimer);
state.holdTimer = null;
}
};
const stopHold = (withMessage = true) => {
if (!state.holdActive) return;
this.holdManager.stopHoldSeek(video);
if (withMessage) {
const txt = state.holdDirection > 0 ? '快进结束' : '快退结束';
showHint(`✅ ${txt}`);
}
state.holdActive = false;
state.holdDirection = 0;
};
const startHoldIfPossible = () => {
if (!this.settingsManager.settings.enableHoldSeek) return;
if (this.lockManager.isLocked(video)) return;
const direction = state.isLeftHalf ? -1 : 1;
state.holdDirection = direction;
state.holdActive = true;
state.mode = 'hold';
const txt = direction > 0 ? '长按快进中...' : '长按快退中...';
showHint(`⏩ ${txt}`);
this.holdManager.startHoldSeek(video, direction, (_step, speed) => {
const dirTxt = direction > 0 ? '快进' : '快退';
const ct = this.formatTime(video.currentTime);
showHint(`⏩ ${dirTxt} ${speed.toFixed(1)}x\n${ct}`);
});
};
const onPointerDown = (e) => {
if (!this.settingsManager.settings.enableGestures) return;
if (state.pointerId !== null) return;
if (e.pointerType === 'mouse' && e.button !== 0) return;
if (e.target.closest('.tm-screen-lock-icon')) return;
state.pointerId = e.pointerId;
state.pointerType = e.pointerType;
state.startX = e.clientX;
state.startY = e.clientY;
state.deltaX = 0;
state.deltaY = 0;
state.mode = null;
state.startBrightness = Utils.getBrightness(video);
state.startVolume = video.volume;
state.holdActive = false;
state.holdDirection = 0;
const rect = video.getBoundingClientRect();
state.isLeftHalf = e.clientX < rect.left + rect.width / 2;
// 立即设置长按定时器
if (this.settingsManager.settings.enableHoldSeek && !this.lockManager.isLocked(video)) {
state.holdTimer = setTimeout(() => {
state.holdTimer = null;
startHoldIfPossible();
}, CONFIG.HOLD_SEEK_INITIAL_DELAY);
}
try {
video.setPointerCapture(e.pointerId);
} catch (_) {}
};
const onPointerMove = (e) => {
if (state.pointerId === null || e.pointerId !== state.pointerId) return;
state.deltaX = (e.clientX - state.startX) * this.settingsManager.settings.swipeSensitivity;
state.deltaY = (e.clientY - state.startY) * this.settingsManager.settings.swipeSensitivity;
// 如果已经在长按状态,直接返回
if (state.holdActive) {
if (state.pointerType === 'touch') e.preventDefault();
return;
}
// 检查是否移动过大,如果是则取消长按
if (state.holdTimer) {
const moveDist = Math.hypot(state.deltaX, state.deltaY);
if (moveDist > 8) { // 降低取消长按的移动阈值
clearHoldTimer();
}
}
// 检测手势模式
if (!state.mode) {
const absX = Math.abs(state.deltaX);
const absY = Math.abs(state.deltaY);
if (absX >= CONFIG.HORIZONTAL_TRIGGER && absX >= absY) {
state.mode = 'seek';
clearHoldTimer();
} else if (absY >= CONFIG.VERTICAL_TRIGGER) {
state.mode = state.isLeftHalf ? 'brightness' : 'volume';
clearHoldTimer();
}
}
// 处理各种手势模式
if (state.mode) {
switch (state.mode) {
case 'seek': {
const seconds = Utils.clamp(
Math.round(state.deltaX / CONFIG.SWIPE_PIXELS_PER_SECOND),
-CONFIG.SWIPE_MAX_SECONDS,
CONFIG.SWIPE_MAX_SECONDS
);
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}⏩ ${seconds >= 0 ? '+' : ''}${seconds}s`);
break;
}
case 'brightness': {
const change = -state.deltaY / CONFIG.VERTICAL_PIXELS_PER_UNIT;
const next = Utils.clamp(
state.startBrightness + change,
CONFIG.BRIGHTNESS_MIN,
CONFIG.BRIGHTNESS_MAX
);
Utils.setBrightness(video, next);
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}🔆 亮度 ${Math.round(next * 100)}%`);
break;
}
case 'volume': {
const change = -state.deltaY / CONFIG.VERTICAL_PIXELS_PER_UNIT;
const next = Utils.clamp(
state.startVolume + change,
CONFIG.VOLUME_MIN,
CONFIG.VOLUME_MAX
);
Utils.setVolume(video, next);
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}🔊 音量 ${Math.round(next * 100)}%`);
break;
}
}
}
if (state.pointerType === 'touch') e.preventDefault();
};
const finishGesture = (currentMode) => {
switch (currentMode) {
case 'hold':
stopHold();
break;
case 'seek': {
const seconds = Utils.clamp(
Math.round(state.deltaX / CONFIG.SWIPE_PIXELS_PER_SECOND),
-CONFIG.SWIPE_MAX_SECONDS,
CONFIG.SWIPE_MAX_SECONDS
);
if (seconds !== 0) {
Utils.seekVideo(video, seconds);
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}✅ ${seconds >= 0 ? '快进' : '后退'} ${Math.abs(seconds)}s`);
}
break;
}
case 'brightness': {
const brightness = Utils.getBrightness(video);
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}🔆 亮度 ${Math.round(brightness * 100)}%`);
break;
}
case 'volume': {
const volume = video.volume;
const lockTag = this.lockManager.isLocked(video) ? '🔒 ' : '';
showHint(`${lockTag}🔊 音量 ${Math.round(volume * 100)}%`);
break;
}
default:
// 无手势模式,可能是点击
break;
}
};
const onPointerUp = (e) => {
if (state.pointerId === null || e.pointerId !== state.pointerId) return;
clearHoldTimer();
finishGesture(state.mode || (state.holdActive ? 'hold' : null));
stopHold(false);
try {
video.releasePointerCapture(e.pointerId);
} catch (_) {}
state.pointerId = null;
state.mode = null;
};
const onPointerCancel = (e) => {
if (state.pointerId !== null && e.pointerId === state.pointerId) {
clearHoldTimer();
stopHold(false);
state.pointerId = null;
state.mode = null;
}
};
video.addEventListener('pointerdown', onPointerDown, { passive: false });
video.addEventListener('pointermove', onPointerMove, { passive: false });
video.addEventListener('pointerup', onPointerUp);
video.addEventListener('pointercancel', onPointerCancel);
}
bindDoubleClick(video, hint) {
const showHint = this.makeHintHandler(hint);
video.addEventListener('dblclick', (e) => {
if (!this.settingsManager.settings.enableGestures) return;
e.preventDefault();
e.stopPropagation();
const rect = video.getBoundingClientRect();
const isRight = e.clientX > rect.left + rect.width / 2;
const step = isRight ? CONFIG.DOUBLE_TAP_STEP : -CONFIG.DOUBLE_TAP_STEP;
Utils.seekVideo(video, step);
showHint(`${step > 0 ? '▶▶ 快进' : '◀◀ 后退'} ${Math.abs(step)}s`);
}, true);
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
scan(root = document) {
root.querySelectorAll('video').forEach(v => this.ensure(v));
}
}
const Utils = {
clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
},
seekVideo(video, delta) {
const target = Math.min(
Math.max(video.currentTime + delta, 0),
Number.isFinite(video.duration) ? video.duration : Number.MAX_SAFE_INTEGER
);
video.currentTime = target;
},
getBrightness(video) {
return Number(video.dataset.tmBrightness || '1');
},
setBrightness(video, value) {
video.dataset.tmBrightness = value.toString();
this.applyBrightness(video, value);
},
applyBrightness(video, value) {
const base = (video.dataset.tmOriginalFilter || '').trim();
const parts = [];
if (base) parts.push(base);
parts.push(`brightness(${value.toFixed(2)})`);
video.style.filter = parts.join(' ');
},
setVolume(video, value) {
video.volume = Utils.clamp(value, CONFIG.VOLUME_MIN, CONFIG.VOLUME_MAX);
if (video.volume > 0 && video.muted) video.muted = false;
}
};
// 初始化
const settingsManager = new SettingsManager();
const lockManager = new ScreenLockManager();
const holdManager = new HoldSeekManager(settingsManager);
const gestureManager = new GestureManager(lockManager, holdManager, settingsManager);
// 监听DOM变化
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.tagName === 'VIDEO') {
gestureManager.ensure(node);
} else {
gestureManager.scan(node);
}
});
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
gestureManager.scan();
console.log('🎬 视频手势控制脚本已加载 - 长按快进快退功能已优化');
})();