原脚本为【俺的手机视频脚本】。方便自用新增了百分比滑动(根据视频总时长动态调节滑动距离与跳转时长的比例关系)。内置两种滑动模式:百分比滑动和固定灵敏度滑动,两者只能同时启用其中一种。
// ==UserScript== // @name 自用手机视频脚本(百分比滑动) // @description 原脚本为【俺的手机视频脚本】。方便自用新增了百分比滑动(根据视频总时长动态调节滑动距离与跳转时长的比例关系)。内置两种滑动模式:百分比滑动和固定灵敏度滑动,两者只能同时启用其中一种。 // @version 1.8.10-merged // @author tcch // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @namespace https://greasyfork.org/users/992160 // ==/UserScript== /*jshint esversion: 8*/ (function () { 'use strict'; let mutationTimer; // 获取 video 与 iframe 的实时集合 let videos = document.getElementsByTagName("video"); let iframes = document.getElementsByTagName("iframe"); let makeVideoAndIframeReady = function () { for (let video of videos) { if (video.controls) { video.controlsList = ["nofullscreen"]; console.log("俺的手机视频脚本:已去除未使用框架视频的全屏按钮。"); } } for (let iframe of iframes) { iframe.allowFullscreen = true; } }; let mutationHandler = function (mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName.toLowerCase() === 'video' || node.tagName.toLowerCase() === 'iframe') { let _window = `${top === window ? "top" : "iframe"}>${location.host}`; if (mutationTimer) { clearTimeout(mutationTimer); console.log(`俺的手机视频脚本:${_window}清除定时任务。`); } mutationTimer = setTimeout(() => { mutationTimer = 0; makeVideoAndIframeReady(); console.log(`俺的手机视频脚本:${_window}处理完成。`); }, 1000); console.log(`俺的手机视频脚本:${_window}页面新增${node.tagName.toLowerCase()},1秒后处理。`); return; } } } } } }; makeVideoAndIframeReady(); new MutationObserver(mutationHandler).observe(document.body, {childList: true, subtree: true}); // 默认监听目标为 document let listenTarget = document; if (window.location.host === "m.youtube.com") { let listenTargetArray = document.getElementsByClassName("player-controls-background"); let shortListenTargetArray = document.getElementsByClassName("reel-player-overlay-main-content"); let refresh = function () { console.log("俺的手机视频脚本:页面刷新..."); if (window.location.href.search("\/(watch|shorts)") >= 0) { let waitForVideo = function () { console.log("俺的手机视频脚本:正在获取视频..."); if (videos.length > 0 && (listenTargetArray.length > 0 || shortListenTargetArray.length > 0)) { let video = videos[0]; if (video.readyState > 1 && !video.paused && !video.muted) { listenTarget = window.location.href.includes("watch") ? listenTargetArray[0] : shortListenTargetArray[0]; if (listenTarget.getAttribute("me_video_js")) { console.log("俺的手机视频脚本:防止重复添加。"); return; } listenTarget.setAttribute("me_video_js", "me_video_js"); console.log("俺的手机视频脚本:开始监听手势。"); listen(); return; } } setTimeout(waitForVideo, 500); }; waitForVideo(); } }; refresh(); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (state) { originalPushState.apply(history, arguments); console.log("监听到地址变化,pushState()调用。"); setTimeout(refresh, 500); }; history.replaceState = function (state) { originalReplaceState.apply(history, arguments); console.log("监听到地址变化,replaceState()调用。"); setTimeout(refresh, 500); }; } listen(); // 设置项:固定模式与百分比模式参数 let settings = { voiced: true, speed: true, rate: 4, // 固定模式参数(原始): sensitivity1: 3, sensitivity2: 0.2, threshold: 300, // 百分比模式参数(长短视频分类): skipPercentLong: 60, skipPercentShort: 60, // 模式开关:true 为百分比模式,false 为固定模式 usePercentage: true }; for (let key in settings) { let value = GM_getValue(key); if (value === undefined) { GM_setValue(key, settings[key]); } else { settings[key] = value; } } if (window === top) { function registerBoolean(btnName, key) { GM_registerMenuCommand(btnName, () => { try { GM_setValue(key, !settings[key]); settings[key] = !settings[key]; alert(`成功切换为:${key==="usePercentage" ? (settings.usePercentage ? "百分比模式" : "固定模式") : (settings[key] ? "开启" : "关闭")}`); } catch (e) { alert("浏览器bug捕获,请刷新页面后重试。\n" + e.message); } }); } function registerInput(btnName, description, key, integer, minimum, maximum) { GM_registerMenuCommand(btnName, () => { let input = window.prompt(description, settings[key]); if (input === null) return; input = Number(input); if (input && input >= minimum && input <= maximum) { if (integer && !Number.isInteger(input)) { alert("要求整数!"); return; } try { GM_setValue(key, input); settings[key] = input; } catch (e) { alert("浏览器bug捕获,请刷新页面后重试。\n" + e.message); } } else { alert("输入错误!"); } }); } // 切换滑动模式命令 GM_registerMenuCommand("切换滑动模式 (百分比模式/固定灵敏度模式)", () => { GM_setValue("usePercentage", !settings.usePercentage); settings.usePercentage = !settings.usePercentage; alert("滑动模式已切换为:" + (settings.usePercentage ? "百分比模式" : "固定灵敏度模式")); }); registerBoolean("开关【触摸视频时取消静音】", "voiced"); registerBoolean("开关【显示播放速度调整按钮】", "speed"); registerInput("修改长按倍速数值", "请指定倍速,输入0-6的数字即可,可为小数。", "rate", false, 0, 6); // 固定模式参数 registerInput("修改长视频滑动灵敏度", "默认为3,要求0-3之间。", "sensitivity1", false, 0, 3); registerInput("修改短视频滑动灵敏度", "默认为0.2,要求0-3之间。", "sensitivity2", false, 0, 3); registerInput("修改长短视频阈值", "默认300秒,要求0-36000之间。", "threshold", true, 0, 36000); // 百分比模式参数(长短视频分类) registerInput("修改长视频滑动百分比", "默认为60,有效范围0-100。", "skipPercentLong", false, 0, 100); registerInput("修改短视频滑动百分比", "默认为60,有效范围0-100。", "skipPercentShort", false, 0, 100); } // 辅助函数:格式化时间,若时长>=3600秒则显示为 HH:MM:SS,否则 MM:SS function formatTime(t) { if (isNaN(t) || t < 0) return "00:00"; if (t >= 3600) { let hours = Math.floor(t / 3600); let minutes = Math.floor((t % 3600) / 60); let seconds = Math.floor(t % 60); return (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; } else { let minutes = Math.floor(t / 60); let seconds = Math.floor(t % 60); return (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; } } // 固定模式下的提示格式 function getClearTimeChange(timeChange) { timeChange = Math.abs(timeChange); let minute = Math.floor(timeChange / 60); let second = timeChange % 60; return (minute === 0 ? "" : (minute + "min")) + second + "s"; } function listen() { if (listenTarget.tagName) { listenTarget.setAttribute("listen_mark", true); } listenTarget.addEventListener("touchstart", (e) => { let startX, startY, endX, endY; if (e.touches.length === 1) { let screenX = e.touches[0].screenX; let screenY = e.touches[0].screenY; if (document.fullscreenElement) { if (screenX < screen.width * 0.05 || screenX > screen.width * 0.95 || screenY < screen.height * 0.05 || screenY > screen.height * 0.95) return; } startX = Math.ceil(e.touches[0].clientX); startY = Math.ceil(screenY); endX = startX; endY = startY; } else return; let videoElement; let target = e.target; let biggestContainer; let targetWidth = target.clientWidth; let targetHeight = target.clientHeight; let suitParents = []; let allParents = []; let temp = target; let findAllSuitParent = false; let maybeTiktok = false; let scrollHeightOut = false; while (true) { temp = temp.parentElement; if (!temp) return; allParents.push(temp); if (!findAllSuitParent && temp.clientWidth > 0 && temp.clientWidth < targetWidth * 1.2 && temp.clientHeight > 0 && temp.clientHeight < targetHeight * 1.2) { if (document.fullscreenElement) { suitParents.push(temp); } else { if (temp.scrollHeight < targetHeight * 1.2) { suitParents.push(temp); } else { findAllSuitParent = true; scrollHeightOut = true; } } } if (temp.tagName === "BODY" || temp.tagName === "HTML" || !temp.parentElement) { if (suitParents.length > 0) { biggestContainer = suitParents[suitParents.length - 1]; } else if (target.tagName !== "VIDEO") { return; } suitParents = null; break; } } if (target.tagName !== "VIDEO") { let videoArray = biggestContainer.getElementsByTagName("video"); if (videoArray.length > 0) { videoElement = videoArray[0]; if (!document.fullscreenElement && top === window && !videoElement.controls && scrollHeightOut && target.clientHeight > window.innerHeight * 0.8) { maybeTiktok = true; } if (!maybeTiktok && targetHeight > videoElement.clientHeight * 1.5) { return; } if (videoArray.length > 1) { console.log("触摸位置找到不止一个视频。"); } } else { return; } } else { videoElement = target; } let playing = !videoElement.paused; let sampleVideo = false; let videoReady = false; let videoReadyHandler = function () { videoReady = true; if (videoElement.duration < 30) { sampleVideo = true; } }; if (videoElement.readyState > 0) { videoReadyHandler(); } else { videoElement.addEventListener("loadedmetadata", videoReadyHandler, {once: true}); } let componentContainer = findComponentContainer(); let notice; let timeChange = 0; let direction; makeTagAQuiet(); if (!videoElement.getAttribute("disable_contextmenu")) { videoElement.addEventListener("contextmenu", (e) => { e.preventDefault(); }); videoElement.setAttribute("disable_contextmenu", true); } if (target.tagName === "IMG") { target.draggable = false; if (!target.getAttribute("disable_contextmenu")) { target.addEventListener("contextmenu", (e) => { e.preventDefault(); }); target.setAttribute("disable_contextmenu", true); } } let sharedCSS = "border-radius:4px;z-index:99999;opacity:0.5;background-color:black;color:white;" + "display:flex;justify-content:center;align-items:center;text-align:center;user-select:none;"; let haveControls = videoElement.controls; let longPress = false; let rateTimer = setTimeout(() => { videoElement.playbackRate = settings.rate; videoElement.controls = false; target.removeEventListener("touchmove", touchmoveHandler); notice.innerText = "x" + settings.rate; notice.style.display = "flex"; longPress = true; rateTimer = null; if (!document.fullscreenElement || videoElement.readyState === 0 || !settings.speed) { return; } let speedBtns = componentContainer.getElementsByClassName("me-speed-btn"); let speedBtn; if (speedBtns.length > 0) { speedBtn = speedBtns[0]; speedBtn.style.display = "flex"; } else { speedBtn = document.createElement("div"); speedBtn.className = "me-speed-btn"; speedBtn.style.cssText = sharedCSS + "position:absolute;width:30px;height:30px;font-size:18px;"; speedBtn.style.top = "50px"; speedBtn.style.right = "20px"; speedBtn.textContent = "速"; componentContainer.appendChild(speedBtn); speedBtn.addEventListener("click", showSpeedMenu); } setTimeout(() => { speedBtn.style.display = "none"; }, 3000); window.addEventListener("resize", () => { speedBtn.style.display = "none"; }, {once: true}); function showSpeedMenu() { speedBtn.style.display = "none"; let containers = componentContainer.getElementsByClassName("me-speed-container"); let container; if (containers.length > 0) { container = containers[0]; container.style.display = "flex"; } else { container = document.createElement("div"); container.className = "me-speed-container"; componentContainer.appendChild(container); let css; if (videoElement.videoHeight > videoElement.videoWidth) { css = `flex-direction:column;top:0;bottom:0;left:${(window.innerWidth * 2) / 3 + 40}px`; } else { css = `flex-direction:row;left:0;right:0;top:${(window.innerHeight / 3) - 30}px`; } container.style.cssText = "display:flex;position:absolute;flex-wrap:nowrap;z-index:99999;justify-content:center;" + css; const values = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4, 5, 6]; values.forEach(value => { const button = document.createElement('div'); container.appendChild(button); button.className = 'button'; button.textContent = value + ""; button.style.cssText = sharedCSS + "width:40px;height:30px;margin:2px;font-size:18px;"; button.addEventListener('click', () => { container.style.display = "none"; videoElement.playbackRate = value; }); }); } target.addEventListener("touchstart", () => { container.style.display = "none"; }, {once: true}); window.addEventListener("resize", () => { container.style.display = "none"; }, {once: true}); } }, 800); // 创建自适应宽度且采用最终版本notice提示格式的 notice 元素 let notices = componentContainer.getElementsByClassName("me-notice"); if (notices.length === 0) { notice = document.createElement("div"); notice.className = "me-notice"; let noticeTop = Math.round(componentContainer.clientHeight / 6); notice.style.cssText = sharedCSS + "font-size:16px;position:absolute;display:none;letter-spacing:normal;padding:0 10px;min-height:30px;width:auto;max-width:90vw;white-space:nowrap;"; notice.style.left = "50%"; notice.style.top = noticeTop + "px"; notice.style.transform = "translateX(-50%)"; componentContainer.appendChild(notice); window.addEventListener("resize", () => { notice.remove(); }, {once: true}); } else { notice = notices[0]; } target.addEventListener("touchmove", touchmoveHandler); target.addEventListener("touchend", touchendHandler, {once: true}); function makeTagAQuiet() { for (let element of allParents) { if (element.tagName === "A" && !element.getAttribute("disable_menu_and_drag")) { element.addEventListener("contextmenu", (e) => { e.preventDefault(); }); element.draggable = false; element.setAttribute("disable_menu_and_drag", true); element.target = "_blank"; break; } } allParents = null; } function findComponentContainer() { let temp = videoElement; while (true) { if (temp.parentElement.clientWidth > 0 && temp.parentElement.clientHeight > 0) { return temp.parentElement; } else { temp = temp.parentElement; } } } function touchmoveHandler(moveEvent) { if (rateTimer) { clearTimeout(rateTimer); rateTimer = null; } if (maybeTiktok || sampleVideo || !videoReady) return; moveEvent.preventDefault(); if (moveEvent.touches.length === 1) { let temp = Math.ceil(moveEvent.touches[0].clientX); if (temp === endX) return; endX = temp; endY = Math.ceil(moveEvent.touches[0].screenY); } let containerWidth = listenTarget.clientWidth || window.innerWidth; let deltaX = endX - startX; // 根据模式选择算法 if (settings.usePercentage) { // 百分比模式(长短视频分类) if (Math.abs(deltaX) > 10) { if (!videoElement.duration || isNaN(videoElement.duration)) return; let swipeFraction = (Math.abs(deltaX) - 10) / containerWidth; let skipPercent = videoElement.duration <= settings.threshold ? settings.skipPercentShort : settings.skipPercentLong; timeChange = Math.round(videoElement.duration * swipeFraction * (skipPercent / 100)); if (deltaX < 0) timeChange = -timeChange; direction = deltaX > 0 ? 1 : 2; notice.style.display = "flex"; let newTime = videoElement.currentTime + timeChange; notice.innerText = (direction === 1 ? ">>>" : "<<<") + formatTime(newTime) + "/" + formatTime(videoElement.duration); } else { timeChange = 0; } } else { // 固定模式 if (deltaX > 10) { if (!direction) { direction = 1; } if (direction === 1) { if (videoElement.duration <= settings.threshold) { timeChange = Math.round((deltaX - 10) * settings.sensitivity2); } else { timeChange = Math.round((deltaX - 10) * settings.sensitivity1); } } else { timeChange = 0; } } else if (deltaX < -10) { if (!direction) { direction = 2; } if (direction === 2) { if (videoElement.duration <= settings.threshold) { timeChange = Math.round((deltaX + 10) * settings.sensitivity2); } else { timeChange = Math.round((deltaX + 10) * settings.sensitivity1); } } else { timeChange = 0; } } else if (timeChange !== 0) { timeChange = 0; } else { return; } notice.style.display = "flex"; notice.innerText = (direction === 1 ? ">>>" : "<<<") + getClearTimeChange(timeChange); } } function touchendHandler() { if (notice) notice.style.display = "none"; setTimeout(() => { if (playing && videoElement.paused && !maybeTiktok) { videoElement.play(); } }, 200); if (!longPress && videoElement.controls && !document.fullscreenElement) { let btns = componentContainer.getElementsByClassName("me-fullscreen-btn"); let btn; if (btns.length === 0) { btn = document.createElement("div"); btn.style.cssText = sharedCSS + "position:absolute;width:40px;padding:2px;font-size:14px;font-weight:bold;" + "box-sizing:border-box;border:1px solid white;white-space:normal;line-height:normal;"; btn.innerText = "点我\n全屏"; btn.className = "me-fullscreen-btn"; let divHeight = 40; btn.style.height = divHeight + "px"; btn.style.top = Math.round(componentContainer.clientHeight / 2 - divHeight / 2 - 10) + "px"; btn.style.left = Math.round((componentContainer.clientWidth * 5 / 7)) + "px"; componentContainer.append(btn); btn.addEventListener("touchstart", async function () { btn.style.display = "none"; await componentContainer.requestFullscreen(); }); videoElement.controlsList = ["nofullscreen"]; } else { btn = btns[0]; btn.style.display = "flex"; } setTimeout(() => { btn.style.display = "none"; }, 2000); } if (endX === startX) { if (rateTimer) clearTimeout(rateTimer); if (longPress) { videoElement.controls = haveControls; videoElement.playbackRate = 1; } } else { if (timeChange !== 0) { videoElement.currentTime += timeChange; } } target.removeEventListener("touchmove", touchmoveHandler); } }); } // 全屏横屏模块:拦截网页自带方向锁调用,并在全屏时自动横屏 window.tempLock = screen.orientation.lock; let myLock = function () { console.log("网页自带js试图执行lock()"); }; screen.orientation.lock = myLock; if (top === window) { window.addEventListener("message", async (e) => { if (typeof e.data === 'string' && e.data.includes("MeVideoJS")) { if (document.fullscreenElement) { screen.orientation.lock = window.tempLock; await screen.orientation.lock("landscape"); screen.orientation.lock = myLock; } } }); } let inTimes = 0; window.addEventListener("resize", () => { setTimeout(fullscreenHandler, 500); }); function fullscreenHandler() { let _fullscreenElement = document.fullscreenElement; if (_fullscreenElement) { if (_fullscreenElement.tagName === "IFRAME") return; inTimes++; } else if (inTimes > 0) { inTimes = 0; } else { return; } if (inTimes !== 1) return; let videoElement; if (_fullscreenElement.tagName !== "VIDEO") { let videoArray = _fullscreenElement.getElementsByTagName("video"); if (videoArray.length > 0) { videoElement = videoArray[0]; if (videoArray.length > 1) { console.log("全屏内找到多个视频。"); } } } else { videoElement = _fullscreenElement; } if (videoElement) { let changeHandler = function () { if (videoElement.videoHeight < videoElement.videoWidth) { top.postMessage("MeVideoJS", "*"); } }; if (videoElement.readyState < 1) { videoElement.addEventListener("loadedmetadata", changeHandler, {once: true}); } else { changeHandler(); } } } })();