您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
모바일 브라우저에서 좌우 스와이프 제스처로 동영상 탐색 및 길게 눌러 2배속 재생 (Shadow DOM 포함)
// ==UserScript== // @name Mobile Video Seek Gesture // @namespace http://tampermonkey.net/ // @version 4.0 // @description 모바일 브라우저에서 좌우 스와이프 제스처로 동영상 탐색 및 길게 눌러 2배속 재생 (Shadow DOM 포함) // @author 사용자 // @license MIT // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; let startX = 0; let initialTime = 0; let seeking = false; let timeChange = 0; let longPressTimeout = null; // 길게 누름 감지 let isSpeedingUp = false; // 현재 배속 상태 확인 let movedEnoughForSeek = false; // 스와이프 감지 여부 let userPlaybackRates = new Map(); // 사용자 설정 배속 저장 // 비디오별 오버레이 생성 function createOverlay(video) { // 이미 오버레이가 있다면 제거 if (video.overlay) video.overlay.remove(); let overlay = document.createElement('div'); overlay.style.position = 'absolute'; overlay.style.top = '50%'; overlay.style.left = '50%'; overlay.style.transform = 'translate(-50%, -50%)'; overlay.style.padding = '10px 20px'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; overlay.style.color = '#fff'; overlay.style.fontSize = '18px'; overlay.style.textAlign = 'center'; overlay.style.borderRadius = '8px'; overlay.style.zIndex = '9999'; overlay.style.display = 'none'; overlay.style.lineHeight = '1.5'; // 줄 간격 설정 video.parentElement.appendChild(overlay); video.overlay = overlay; // 비디오에 오버레이 속성 추가 } // 터치 시작 이벤트 function onTouchStart(e, video) { if (!video) return; startX = e.touches[0].clientX; initialTime = video.currentTime; seeking = true; movedEnoughForSeek = false; // 초기화 video.overlay.style.display = 'block'; // 길게 누르면 배속 시작 longPressTimeout = setTimeout(() => { if (!movedEnoughForSeek) { // 탐색 중이 아닐 때만 배속 적용 userPlaybackRates.set(video, video.playbackRate); // 기존 배속 저장 video.playbackRate = 2.0; // 2배속 video.overlay.innerHTML = `<div>2x Speed</div>`; isSpeedingUp = true; } }, 500); // 0.5초 이상 누르면 배속 } // 터치 이동 이벤트 function onTouchMove(e, video) { if (!seeking || !video || isSpeedingUp) return; let deltaX = e.touches[0].clientX - startX; if (Math.abs(deltaX) > 10) { // 일정 거리 이상 움직이면 탐색 모드로 간주 movedEnoughForSeek = true; clearTimeout(longPressTimeout); // 길게 누름 취소 } timeChange = deltaX * 0.05; // 민감도 조정 let newTime = initialTime + timeChange; // 비디오 길이를 넘지 않도록 시간 범위 제한 newTime = Math.max(0, Math.min(newTime, video.duration)); let timeChangeFormatted = formatTimeChange(timeChange); video.overlay.innerHTML = ` <div>${formatCurrentTime(newTime)}</div> <div>(${timeChange >= 0 ? '+' : ''}${timeChangeFormatted})</div> `; } // 터치 종료 이벤트 function onTouchEnd(video) { seeking = false; clearTimeout(longPressTimeout); // 길게 누름 감지 중단 longPressTimeout = null; // longPressTimeout 초기화 if (isSpeedingUp) { video.playbackRate = userPlaybackRates.get(video) || 1.0; // 원래 속도로 복귀 isSpeedingUp = false; } else if (movedEnoughForSeek) { let newTime = initialTime + timeChange; // 비디오 길이를 넘지 않도록 시간 범위 제한 newTime = Math.max(0, Math.min(newTime, video.duration)); video.currentTime = newTime; } // 오버레이 숨기기 - 바로 숨겨짐 video.overlay.style.display = 'none'; video.overlay.innerHTML = ''; // 이전에 표시된 내용도 비움 } // 시간을 시:분:초 형식으로 변환 function formatCurrentTime(seconds) { let absSeconds = Math.abs(seconds); let hours = Math.floor(absSeconds / 3600); let minutes = Math.floor((absSeconds % 3600) / 60); let secs = Math.floor(absSeconds % 60); if (hours > 0) { return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`; } else { return `${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`; } } // 시간 변화량을 형식화 function formatTimeChange(seconds) { let sign = seconds < 0 ? '-' : ''; // 음수 표시 let absSeconds = Math.abs(seconds); let hours = Math.floor(absSeconds / 3600); let minutes = Math.floor((absSeconds % 3600) / 60); let secs = Math.floor(absSeconds % 60); let fraction = Math.round((absSeconds % 1) * 100); if (absSeconds >= 3600) { return `${sign}${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`; } else if (absSeconds >= 60) { return `${sign}${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`; } else { return `${sign}${secs < 10 ? '0' : ''}${secs}.${fraction < 10 ? '0' : ''}${fraction}`; } } // 비디오에 제스처 기능 추가 (내부 플래그 사용) function addGestureControls(video) { if (!video || video._gestureAdded) return; // 내부 플래그를 이용해 중복 추가 방지 video._gestureAdded = true; createOverlay(video); // 배속을 초기화하고 사용자 설정 값으로 복원 let userRate = userPlaybackRates.get(video) || 1.0; video.playbackRate = userRate; // 사용자 설정에 맞춰 배속 초기화 // 사용자가 배속을 직접 변경했을 때 저장 video.addEventListener('ratechange', () => { if (!isSpeedingUp) { userPlaybackRates.set(video, video.playbackRate); } }); video.addEventListener('touchstart', (e) => onTouchStart(e, video)); video.addEventListener('touchmove', (e) => onTouchMove(e, video)); video.addEventListener('touchend', () => onTouchEnd(video)); } // Shadow DOM 내 비디오 탐색 function findVideosInShadow(root) { if (!root) return; let videos = root.querySelectorAll('video'); videos.forEach(addGestureControls); root.querySelectorAll('*').forEach(el => { if (el.shadowRoot) findVideosInShadow(el.shadowRoot); }); } // 모든 비디오에 제스처 추가 function scanForVideos() { document.querySelectorAll('video').forEach(addGestureControls); document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) findVideosInShadow(el.shadowRoot); }); } // DOM 변경 감지 및 비디오 발견 시 제스처 추가 const observer = new MutationObserver(scanForVideos); observer.observe(document.body, { childList: true, subtree: true }); // 페이지 로딩 시 비디오 탐색 window.addEventListener('load', scanForVideos); })();