// ==UserScript== // @name 鼠标手势视频控制 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 快速拖动触发快进快退,原地长按切换三倍速,默认支持b站,抖音,油管 // @author necrodancer // @match https://www.bilibili.com/* // @match https://www.youtube.com/* // @match https://www.douyin.com/* // @icon https://www.bilibili.com/favicon.ico // @grant none // @run-at document-start // ==/UserScript== 'use strict'; (function() { const CONFIG = { quickDragThreshold: 200, longPressThreshold: 200, clickMoveThreshold: 10, seekSeconds: 5, showIndicator: true, speed: 3 }; let state = { isDown: false, startX: 0, startY: 0, startTime: 0, isLongPress: false, currentVideo: null, indicator: null, startVideoTime: 0, startPlaybackRate: 1, isDragging: false, wasDragging: false, wasLongPress: false, shouldBlockClick: false, hideTimer: null, longPressTimer: null }; function getPlayerContainer(video) { let el = video; let parent = el.parentNode; const videoWidth = video.clientWidth; const videoHeight = video.clientHeight; while (parent && parent !== document.body) { if (parent.clientWidth - videoWidth > 5 || parent.clientHeight - videoHeight > 5) { break; } el = parent; parent = el.parentNode; } return el; } function getOrCreateIndicator(video) { let container = getPlayerContainer(video); if (!container) container = video.parentElement; if (!container) return null; let indicator = container.querySelector('.gm-gesture-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.className = 'gm-gesture-indicator'; indicator.style.cssText = ` background: rgba(0, 0, 0, 0.7); color: white; padding: 15px 30px; border-radius: 5px; font-size: 20px; font-weight: bold; text-align: center; white-space: nowrap; position: absolute; top: 20%; left: 50%; transform: translate(-50%, -50%); z-index: 2147483647; pointer-events: none; display: none; `; const style = window.getComputedStyle(container); if (style.position === 'static') { container.style.position = 'relative'; } container.appendChild(indicator); } return indicator; } function showIndicator(text) { if (!state.currentVideo) return; const indicator = getOrCreateIndicator(state.currentVideo); if (indicator) { if (state.hideTimer) { clearTimeout(state.hideTimer); state.hideTimer = null; } indicator.textContent = text; indicator.style.display = 'block'; state.indicator = indicator; } } function hideIndicator() { if (state.indicator) { state.indicator.style.display = 'none'; } } function hideIndicatorDelayed(delay = 800) { if (state.hideTimer) { clearTimeout(state.hideTimer); } state.hideTimer = setTimeout(() => { hideIndicator(); state.hideTimer = null; }, delay); } function formatTime(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } function getVideoFromEvent(e) { let el = e.target; while (el) { if (el.tagName === 'VIDEO') return el; el = el.parentElement; } return null; } function handleMouseDown(e) { if (e.button !== 0) return; const video = getVideoFromEvent(e); if (!video) return; e.preventDefault(); e.stopPropagation(); state.isDown = true; state.startX = e.clientX; state.startY = e.clientY; state.startTime = Date.now(); state.isLongPress = false; state.currentVideo = video; state.startVideoTime = video.currentTime; state.startPlaybackRate = video.playbackRate; state.isDragging = false; state.shouldBlockClick = true; if (state.longPressTimer) { clearTimeout(state.longPressTimer); } state.longPressTimer = setTimeout(() => { if (state.isDown && !state.isDragging && state.currentVideo) { state.isLongPress = true; state.currentVideo.playbackRate = CONFIG.speed; showIndicator(`${CONFIG.speed}x`); } }, CONFIG.longPressThreshold); } function handleMouseMove(e) { if (!state.isDown || !state.currentVideo) return; const dx = e.clientX - state.startX; const dy = e.clientY - state.startY; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > CONFIG.clickMoveThreshold) { state.isDragging = true; e.preventDefault(); e.stopPropagation(); if (state.longPressTimer) { clearTimeout(state.longPressTimer); state.longPressTimer = null; } } } function handleMouseUp(e) { if (!state.isDown || !state.currentVideo) return; if (state.longPressTimer) { clearTimeout(state.longPressTimer); state.longPressTimer = null; } const dx = e.clientX - state.startX; const dy = e.clientY - state.startY; const dt = Date.now() - state.startTime; const distance = Math.sqrt(dx * dx + dy * dy); const video = state.currentVideo; if (state.isDragging) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (dt < CONFIG.quickDragThreshold) { const seekTime = dx > 0 ? CONFIG.seekSeconds : -CONFIG.seekSeconds; video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + seekTime)); showIndicator(dx > 0 ? `>>` : `<<`); hideIndicatorDelayed(); } else { hideIndicator(); } } else if (state.isLongPress) { if (video.playbackRate === 3 && state.startPlaybackRate !== 3) { video.playbackRate = state.startPlaybackRate; showIndicator(`${state.startPlaybackRate.toFixed(2).replace(/\.?0+$/, '')}x`); hideIndicatorDelayed(); } else { hideIndicator(); } } else { hideIndicator(); } state.isDown = false; state.startX = 0; state.startY = 0; state.startTime = 0; state.wasDragging = state.isDragging; state.wasLongPress = state.isLongPress; state.isLongPress = false; state.currentVideo = null; state.startVideoTime = 0; state.startPlaybackRate = 1; state.isDragging = false; } function handleClick(e) { if (!state.shouldBlockClick) return; const video = getVideoFromEvent(e); if (!video) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (!state.wasDragging && !state.wasLongPress) { video.paused ? video.play() : video.pause(); } state.shouldBlockClick = false; state.wasDragging = false; state.wasLongPress = false; } function init() { document.addEventListener('mousedown', handleMouseDown, true); document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('mouseup', handleMouseUp, true); document.addEventListener('click', handleClick, true); } init(); })();