// ==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 // @downloadURL none // ==/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('🎬 视频手势控制脚本已加载 - 长按快进快退功能已优化'); })();