// ==UserScript== // @name HTML5视频手势 // @namespace http://tampermonkey.net/ // @version 68.85 // @description 集多家之长的手机浏览器触摸手势插件。 // @author Gemini & 仙 // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/570331/HTML5%E8%A7%86%E9%A2%91%E6%89%8B%E5%8A%BF.user.js // @updateURL https://update.greasyfork.icu/scripts/570331/HTML5%E8%A7%86%E9%A2%91%E6%89%8B%E5%8A%BF.meta.js // ==/UserScript== (function() { 'use strict'; const CFG = { minDist: 10, longPress: 500, rateBase: 2.0, senseX: 0.25, senseY: 1.0, progressBarColor: '#FF6699', uiTimeout: 2500, maxScale: 8.0, senseRate: 0.015 }; const FS_BTN_SELECTORS = '.art-icon-fullscreenOn, .art-control-fullscreen, .dplayer-full-icon, .plyr__control[data-plyr="fullscreen"], .vjs-fullscreen-control, .xgplayer-fullscreen, .tcplayer-fullscreen-btn, .prism-fullscreen-btn, [aria-label*="全屏"], [title*="全屏"], .fullscreen-btn, .bilibili-player-video-btn-fullscreen, [data-testid="btn-fullscreen"], [aria-label="На весь экран"]'; let seekSec = GM_getValue('gt_seek_sec', 10); let seekMode = GM_getValue('gt_seek_mode', 'sec'); let fpsMode = GM_getValue('gt_fps', 30); // 仅保留跨视频公用的瞬时触控交互特征 let startX, startY, initVol, initTime, initRate, targetV, targetP, isTouch = false, action = null, lpTimer = null, lastTapTime = 0, tapCount = 0; let activeSeekSide = null, seekAccumulator = 0, seekSessionTimer = null, wasPlayingBeforeSequence = false; let initPinchDist = 0, initScale = 1.0, initPanX = 0, initPanY = 0, initSpeed = 1.0, initCenterX = 0, initCenterY = 0, originDx = 0, originDy = 0; let blockGestureUntil = 0; let enforceStateUntil = 0; let enforceTarget = null; let isSimulatingFS = false; // 性能优化:虚拟时间(用于滑动虚实分离)与节流时间戳 let virtualTime = null; let lastThrottledTime = 0; // [原生直链媒体页探针]:精准探测极其纯净的系统合成页,防止普通建站逻辑导致 DOM 坍缩 const checkNativeMediaPage = () => { const vids = document.querySelectorAll('video'); if (vids.length === 1 && vids[0].parentNode === document.body) { let validSiblings = 0; for (let el of document.body.children) { const tag = el.tagName.toUpperCase(); // 忽略非视觉渲染的核心组件及其他插件可能注入的零散 div if (tag !== 'SCRIPT' && tag !== 'STYLE' && tag !== 'LINK' && tag !== 'META') { if (el.classList && el.classList.contains('gt-ui-layer')) continue; validSiblings++; } } // 原生合成页特征:几乎没有有效的 DOM 兄弟节点,或者携带 name="media" 这一绝杀标志 if (validSiblings <= 3 || vids[0].getAttribute('name') === 'media') { return vids[0]; } } return null; }; // 全局持有原生直链对象 let nativeVideoEl = checkNativeMediaPage(); // 预览动图/推荐小窗口嗅探器 const isPreviewVideo = (v) => { const rect = v.getBoundingClientRect(); if (rect.width === 0) return false; if (rect.width < 300) return true; const isSilentAuto = !v.hasAttribute('controls') && (v.muted || v.hasAttribute('muted')) && (v.autoplay || v.hasAttribute('autoplay')); let p = v.parentNode; let depth = 0; while (p && p !== document.body && depth < 3) { if (p.tagName === 'A' && rect.width < 600) return true; if (typeof p.className === 'string' && /(hover-?play|preview-?vid)/i.test(p.className)) { if (rect.width < 600) return true; } p = p.parentNode; depth++; } if (isSilentAuto && rect.width < 450) return true; return false; }; // 【核心改造】:新增 isNativeFull 特权通行证,原生页无视视口挤压,强制满血倍率 const getDeviceScale = (containerH, isPreview, isNativeFull = false) => { if (isPreview) return 0.5; let logicalW = GM_getValue('gt_logical_w', 0); let innerW = window.innerWidth; let screenW = Math.min(window.screen.width, window.screen.height); if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { if (logicalW === 0 || Math.abs(logicalW - screenW) > 50) { if (screenW < 600) { logicalW = screenW; GM_setValue('gt_logical_w', logicalW); } else if (logicalW === 0) { logicalW = 400; } } } let scale = 1.0; const isMobileHW = logicalW > 0 && logicalW < 600; if (isMobileHW && innerW >= 800) { let fullScale = innerW / logicalW; // 如果不是原生全屏页,才启用高度防坍缩压制;如果是原生页,敞开给全倍率 if (containerH > 0 && !isNativeFull) { let maxAllowedScale = (containerH - 40) / 180; scale = Math.min(fullScale, Math.max(1.0, maxAllowedScale)); } else { scale = fullScale; } } return scale; }; // 静态硬注入钩子 const applyFixedScale = (root, video, uiLayer, forceFixed = false) => { if (!uiLayer || !uiLayer.id) return; const containerH = (root ? root.clientHeight : 0) || video.clientHeight || window.innerHeight; const isPreview = video.dataset.gtIsPreview === 'true'; const isNativeFull = nativeVideoEl === video; const S = getDeviceScale(containerH, isPreview, isNativeFull); let styleEl = uiLayer.querySelector('style'); if (styleEl) { let newCss = getUICss(S, uiLayer.id, isNativeFull); if (forceFixed) { newCss = newCss.replace('position: absolute !important; top: 0; left: 0; width: 100%; height: 100%;', 'position: fixed !important; top: 0; left: 0; width: 100vw; height: 100dvh;'); } styleEl.textContent = newCss; } }; window.addEventListener('message', (e) => { if (e.data && e.data.type === 'gt_lock_orientation') { if (screen.orientation && screen.orientation.lock) screen.orientation.lock(e.data.dir).catch(()=>{}); } else if (e.data && e.data.type === 'gt_unlock_orientation') { if (screen.orientation && screen.orientation.unlock) screen.orientation.unlock(); } }); const VIP_SELECTORS = '.video-js, .vjs-custom-skin, .player-container, .art-video-player, .xgplayer, .tcplayer, .prism-player, .mui-player, [data-testid="videoComponent"], [data-testid="video-container"], .player-wrapper, .one-video-player_display-w, .one-video-player, vk-video-player, [aria-label="Видео плеер"], [aria-label="Video Player"], .plyr, #html5video, #movie_player, .html5-video-player, .bpx-player-container, .dplayer, .artplayer-app, .MacPlayer, .ckplayer, #playleft, iframe'; const IGNORE_TOUCH_SELECTORS = '.gt-btn-base, .dplayer-controller, .dplayer-bar-wrap, .vjs-control-bar, .art-bottom, .art-controls, .bpx-player-control-wrap, .plyr__controls, .xgplayer-controls, .tcplayer-controls, .prism-controlbar, .mui-player-controls, .wrapper-bottom, [data-testid="player_controls"], [data-testid="progress_bar"], [data-testid="volume-slider"], input[type="range"], .buttons-bar, .progress-bar-container'; const findUp = (el, selector) => { while (el && el !== document.body && el !== document.documentElement) { if (el.matches && el.matches(selector)) return el; el = el.parentNode || el.host; } return null; }; const isExcludedZone = (e) => { const path = (typeof e.composedPath === 'function') ? e.composedPath() : []; if (path.length > 0) { for (let el of path) { if (el.matches && el.matches(IGNORE_TOUCH_SELECTORS)) return true; } return false; } return !!findUp(e.target, IGNORE_TOUCH_SELECTORS); }; const getValidPlayerRoot = (video) => { let current = video.parentNode || (video.getRootNode && video.getRootNode().host) || video; let bestMatch = null; let fallbackMatch = null; let depth = 0; while (current && current !== document.body && current !== document.documentElement && depth < 15) { if (current.getBoundingClientRect) { const rect = current.getBoundingClientRect(); if (rect.width > 50 && rect.height > 50) { if (!fallbackMatch) fallbackMatch = current; if (current.matches && current.matches(VIP_SELECTORS)) { bestMatch = current; } } } current = current.parentNode || current.host; depth++; } return bestMatch || fallbackMatch || video.parentNode || video; }; const isNakedForumVideo = (video) => { if (findUp(video, VIP_SELECTORS.replace(', iframe', ''))) return false; let p = video.parentNode; if (!p || p === document.body || p === document.documentElement) return true; const className = (p.className || '').toLowerCase(); if (/(bbs|thread|post|article|content|message|text)/.test(className)) return true; const pRect = p.getBoundingClientRect(); const vRect = video.getBoundingClientRect(); if (pRect.height - vRect.height > 50 || pRect.width - vRect.width > 50) return true; return false; }; const findDeepVid = (root) => { if (!root) return null; let v = root.querySelector ? root.querySelector('video') : null; if (v) return v; let els = root.querySelectorAll ? root.querySelectorAll('*') : []; for (let el of els) { if (el.shadowRoot) { v = findDeepVid(el.shadowRoot); if (v) return v; } } return null; }; const hijackFullscreenAPI = () => { const fsMethods = ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen']; fsMethods.forEach(method => { if (Element.prototype[method]) { const originalMethod = Element.prototype[method]; Element.prototype[method] = function(...args) { let target = this; let v = target.tagName === 'VIDEO' ? target : findDeepVid(target); if (!v) v = findDeepVid(document); if (target.tagName === 'VIDEO' && target.parentNode && target.parentNode.classList && target.parentNode.classList.contains('gt-video-wrapper')) { target = target.parentNode; } else if (target.tagName === 'VIDEO' && v && v.gtRoot && target !== v.gtRoot) { target = v.gtRoot; } if (target.classList && (target.classList.contains('gt-video-wrapper') || target.tagName !== 'VIDEO')) { target.classList.add('gt-fullscreen-active'); } if (v && v.gtRoot && target.tagName !== 'VIDEO' && v.gtUI) { let oldRoot = v.gtRoot; const isUIInsideTarget = target.contains(v.gtUI) || (target.shadowRoot && target.shadowRoot.contains(v.gtUI)); if (!isUIInsideTarget) { target.appendChild(v.gtUI); v.gtRoot = target; } } const promise = originalMethod.apply(target, args); if (v && screen.orientation && screen.orientation.lock) { const dir = (v.videoWidth === 0 || v.videoWidth >= v.videoHeight) ? 'landscape' : 'portrait'; screen.orientation.lock(dir).catch(()=>{ try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){} }); } return promise; }; } }); }; hijackFullscreenAPI(); const toggleNativeFullscreen = (container, video) => { const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement) || container.classList.contains('gt-fullscreen-active'); const fsBtn = container.querySelector(FS_BTN_SELECTORS); isSimulatingFS = true; if (isFS) { if (fsBtn) { try { fsBtn.click(); } catch(e){} } if (document.exitFullscreen) document.exitFullscreen().catch(()=>{}); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); container.classList.remove('gt-fullscreen-active'); if (screen.orientation?.unlock) { screen.orientation.unlock(); try { window.top.postMessage({ type: 'gt_unlock_orientation' }, '*'); } catch(e){} } } else { const forceLockLandscape = () => { let dir = 'landscape'; if (video && video.videoWidth > 0 && video.videoHeight > 0) { dir = (video.videoWidth < video.videoHeight) ? 'portrait' : 'landscape'; } if (screen.orientation?.lock) { screen.orientation.lock(dir).catch(()=>{ try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){} }); } else { try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){} } }; if (fsBtn) { try { fsBtn.click(); } catch(e){} } container.classList.add('gt-fullscreen-active'); const reqFs = container.requestFullscreen || container.webkitRequestFullscreen || container.mozRequestFullScreen; if (reqFs) { const p = reqFs.call(container); if (p && p.then) { p.then(() => setTimeout(forceLockLandscape, 150)).catch(()=>{ if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); setTimeout(forceLockLandscape, 150); }); } else { setTimeout(forceLockLandscape, 150); } } else if (video.webkitEnterFullscreen) { video.webkitEnterFullscreen(); setTimeout(forceLockLandscape, 150); } } setTimeout(() => { isSimulatingFS = false; }, 100); }; const showMsg = (txt, video = null) => { let uiLayer = (video && video.gtUI) ? video.gtUI : (targetV ? targetV.gtUI : null); let t; if (uiLayer) { t = uiLayer.querySelector('.gt-toast'); if (!t) { t = document.createElement('div'); t.className = 'gt-toast'; uiLayer.appendChild(t); } t.style.position = 'absolute'; } else { t = document.getElementById('gt-toast-global'); if (!t) { t = document.createElement('div'); t.id = 'gt-toast-global'; t.className = 'gt-toast'; document.body.appendChild(t); } t.style.position = 'fixed'; t.style.zIndex = '2147483647'; const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active')); const baseS = getDeviceScale(window.innerHeight, false); const TS = baseS > 1.0 ? baseS * 0.75 : baseS; const FS = baseS > 1.0 ? baseS * 0.5 : 1.0; const activeS = isFS ? FS : TS; t.style.padding = `${4 * activeS}px ${10 * activeS}px`; t.style.borderRadius = `${4 * activeS}px`; t.style.fontSize = `${14 * activeS}px`; t.style.textShadow = `0 0 ${2 * activeS}px #000`; } t.innerText = txt; t.classList.add('show'); if (t.gtTimer) clearTimeout(t.gtTimer); t.gtTimer = setTimeout(() => t.classList.remove('show'), 800); }; GM_registerMenuCommand('🔗 拷贝视频源(直链/页面)', () => { if (sniffedUrl) { GM_setClipboard(sniffedUrl); showMsg('已复制嗅探流媒体直链'); return; } let v = targetV || findDeepVid(document); if (!v) { showMsg('未找到视频元素'); return; } let src = v.src; if (!src || src.startsWith('blob:')) { const s = v.querySelector('source'); if (s && s.src) src = s.src; } if (src && !src.startsWith('blob:')) { GM_setClipboard(src); showMsg('已复制视频直链'); } else { GM_setClipboard(window.location.href); showMsg('已复制网页源地址'); } }); let menuIdResume = null; const updateResumeMenu = () => { let resume = GM_getValue('gt_resume_playback', true); if (menuIdResume !== null) { GM_unregisterMenuCommand(menuIdResume); } const title = resume ? '🟢 [已开启] 从历史位置播放' : '⚪ [已关闭] 从历史位置播放'; menuIdResume = GM_registerMenuCommand(title, () => { GM_setValue('gt_resume_playback', !resume); showMsg(`历史位置恢复已${!resume ? '开启' : '关闭'}`); updateResumeMenu(); }); }; updateResumeMenu(); const toggleAdBlockStyles = (enable) => { let styleEl = document.getElementById('gt-adblock-style'); if (enable) { if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = 'gt-adblock-style'; styleEl.textContent = ` .dplayer-pause-ad, .dplayer-notice, .dplayer-ad, .artplayer-plugin-ads, .art-ad, .art-notice, .play-ad, .pause-ad, .player-ad, .ad-box, .ad-mask, .pause-html, #pause-html, [class*="pause-html"], [id*="pause-html"], [class*="pause-ad"], [id*="pause-ad"], [class*="play-ad"], [id*="play-ad"], [class*="ad_box"], [id*="ad_box"], [class*="ad-wrap"], [id*="ad-wrap"], [class*="player_ad"], [id*="player_ad"], [id*="player_pause"], [class*="player_pause"], [title*="关闭广告"], [title*="广告"], a[href*="mms0.baidu.com"], img[src*="mms0.baidu.com"] { opacity: 0 !important; pointer-events: none !important; position: absolute !important; z-index: -2147483648 !important; } `; const injectSafe = () => { if (document.head) document.head.appendChild(styleEl); else if (document.documentElement) document.documentElement.appendChild(styleEl); else setTimeout(injectSafe, 10); }; injectSafe(); } } else { if (styleEl) styleEl.remove(); } }; let menuIdAdBlock = null; const updateAdBlockMenu = () => { let blockAds = GM_getValue('gt_block_ads', true); if (menuIdAdBlock !== null) { GM_unregisterMenuCommand(menuIdAdBlock); } const title = blockAds ? '🟢 [已开启] 拦截暂停广告' : '⚪ [已关闭] 拦截暂停广告'; menuIdAdBlock = GM_registerMenuCommand(title, () => { GM_setValue('gt_block_ads', !blockAds); toggleAdBlockStyles(!blockAds); showMsg(`暂停广告拦截已${!blockAds ? '开启' : '关闭'}`); updateAdBlockMenu(); }); }; toggleAdBlockStyles(GM_getValue('gt_block_ads', true)); updateAdBlockMenu(); const getVideoKey = (v, isStrict = true) => { let searchParams = window.location.search.replace(/&?(t|time|start)=[^&]+/ig, ''); if (searchParams === '?') searchParams = ''; let hashParams = window.location.hash.replace(/&?(t|time|start)=[^&]+/ig, ''); if (hashParams === '#') hashParams = ''; let route = window.location.host + window.location.pathname + searchParams + hashParams; let fp = ''; let dataStr = ''; for (let key in v.dataset) { if (typeof v.dataset[key] === 'string' && !key.startsWith('gt')) { dataStr += v.dataset[key]; } } if (dataStr.length > 5) fp += dataStr; if (!fp) { let posterAttr = v.getAttribute('poster'); if (posterAttr && posterAttr.length > 2) fp += posterAttr; } if (!fp) { let srcAttr = v.getAttribute('src') || ''; if (srcAttr && !srcAttr.startsWith('blob:')) { fp += srcAttr.replace(/(token|auth|sign|expires|timestamp|ts|key|mac)=[^&]+/ig, ''); } } if (!fp) fp = 'stream_media'; fp = fp.replace(/[^a-zA-Z0-9]/g, '').slice(-30); let key = route + '_' + fp; if (isStrict && v.duration && !isNaN(v.duration) && v.duration !== Infinity) { key += '_' + Math.floor(v.duration); } return key; }; const saveProgress = (v) => { if (!v || isNaN(v.duration) || v.duration <= 5 || v.currentTime < 2) return; if (v.dataset.gtIsPreview === 'true') return; const key = getVideoKey(v, true); let history = GM_getValue('gt_video_history', {}); history[key] = { t: Math.floor(v.currentTime), ts: Date.now() }; const keys = Object.keys(history); if (keys.length > 1000) { keys.sort((a, b) => history[b].ts - history[a].ts); let newHistory = {}; for (let i = 0; i < 1000; i++) { newHistory[keys[i]] = history[keys[i]]; } history = newHistory; } GM_setValue('gt_video_history', history); }; let sniffedUrl = ''; const mediaReg = /\.(m3u8|mpd|mp4|webm|flv)(\?|$)/i; const sniff = (url) => { try { if (url && typeof url === 'string' && mediaReg.test(url) && url.startsWith('http')) sniffedUrl = url; } catch(e){} }; const oFetch = window.fetch; window.fetch = function(...args) { try { let u = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : ''); sniff(u); } catch(e){} return oFetch.apply(window, args); }; const oOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, ...rest) { try { sniff(url); } catch(e){} return oOpen.call(this, method, url, ...rest); }; const toggleOrientation = () => { if (!screen.orientation) return; const dir = screen.orientation.type.startsWith('landscape') ? 'portrait' : 'landscape'; if (screen.orientation.lock) { screen.orientation.lock(dir).catch(()=>{ try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(e){} }); } }; // 绝对定死的静态插值渲染层:增加特权判定 (isNativeFull) const getUICss = (S, uid, isNativeFull = false) => { const TS = S > 1.0 ? S * 0.75 : S; const FS = S > 1.0 ? S * 0.5 : 1.0; // 【核心修复】:原生页进度条加高到 4px,并且抬升安全距离,防底部小白条遮挡 const nativeProgStyle = isNativeFull ? `height: 4px !important; bottom: env(safe-area-inset-bottom, 2px) !important;` : ``; return ` #${uid} { position: absolute !important; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none !important; z-index: 2147483647 !important; } #${uid} .gt-mini-progress { position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 2147483640; pointer-events: none; overflow: hidden; opacity: 0.9; transition: height 0.2s, opacity 0.3s; box-shadow: 0 -1px 1px rgba(0,0,0,0.2); ${nativeProgStyle} } #${uid} .gt-mini-progress .gt-fill { height: 100%; width: 0%; background: ${CFG.progressBarColor}; transition: width 0.1s linear; box-shadow: 0 0 4px ${CFG.progressBarColor}; } :fullscreen #${uid} .gt-mini-progress, .gt-fullscreen-active #${uid} .gt-mini-progress { height: 3px !important; } #${uid} .gt-lock-shield { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2147483645; background: rgba(0,0,0,0); touch-action: none; display: none; pointer-events: auto; } :fullscreen #${uid} .gt-lock-shield, .gt-fullscreen-active #${uid} .gt-lock-shield { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; } #${uid} .gt-btn-base { position: absolute; width: ${26 * S}px; height: ${26 * S}px; display: flex; align-items: center; justify-content: center; z-index: 2147483647; opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.15s ease; border: none; background: transparent; color: rgba(255, 255, 255, 0.95); filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.8)); } #${uid} .gt-btn-base * { pointer-events: none !important; } #${uid} .gt-btn-base svg { width: ${15 * S}px; height: ${15 * S}px; transition: all 0.2s ease; transform-origin: center center; fill: none !important; stroke: currentColor !important; stroke-width: 2 !important; stroke-linecap: round !important; stroke-linejoin: round !important; } #${uid} .gt-btn-base svg * { fill: none !important; stroke: currentColor !important; stroke-width: 2 !important; stroke-linecap: round !important; stroke-linejoin: round !important; } #${uid} .gt-btn-base span { font-size: ${11 * S}px; font-weight: 800; font-family: system-ui; letter-spacing: 0.5px; transform-origin: center center; display: inline-block; } #${uid}.gt-ui-visible .gt-btn-base { opacity: 0.65 !important; pointer-events: auto !important; } #${uid}.gt-ui-visible .gt-btn-base.hidden-by-state { display: none !important; pointer-events: none !important; } #${uid} .gt-btn-base:active { opacity: 0.9 !important; color: #fff; } #${uid} .gt-rotate-btn { top: ${24 * S}px; left: ${10 * S}px; } #${uid} .gt-seek-mode-btn { top: ${24 * S}px; left: ${10 * S}px; } #${uid} .gt-seek-val-btn { top: ${(24 + 28) * S}px; left: ${10 * S}px; } #${uid} .gt-fit-btn { top: ${(24 + 56) * S}px; left: ${10 * S}px; } #${uid} .gt-shot-btn { top: ${(24 + 84) * S}px; left: ${10 * S}px; } #${uid} .gt-reset-speed-btn { top: ${(24 + 112) * S}px; left: ${10 * S}px; } #${uid} .gt-pip-btn { top: ${10 * S}px; right: ${10 * S}px; } #${uid} .gt-lock-btn { top: calc(50% - ${32 * S}px); right: ${10 * S}px; transform: translateY(-50%); } #${uid} .gt-mode-btn { top: calc(50% + ${32 * S}px); right: ${10 * S}px; transform: translateY(-50%); } #${uid} .gt-reset-zoom-btn { top: calc(50% + ${62 * S}px); right: ${10 * S}px; transform: translateY(-50%); } :fullscreen #${uid} .gt-btn-base, .gt-fullscreen-active #${uid} .gt-btn-base { width: ${38 * FS}px !important; height: ${38 * FS}px !important; } :fullscreen #${uid}.gt-ui-visible .gt-btn-base, .gt-fullscreen-active #${uid}.gt-ui-visible .gt-btn-base { opacity: 0.5 !important; } :fullscreen #${uid} .gt-btn-base svg, .gt-fullscreen-active #${uid} .gt-btn-base svg { width: ${22 * FS}px !important; height: ${22 * FS}px !important; } :fullscreen #${uid} .gt-btn-base span, .gt-fullscreen-active #${uid} .gt-btn-base span { font-size: ${14 * FS}px !important; } :fullscreen #${uid} .gt-rotate-btn, .gt-fullscreen-active #${uid} .gt-rotate-btn { top: 20px !important; left: 20px !important; } :fullscreen #${uid} .gt-seek-mode-btn, .gt-fullscreen-active #${uid} .gt-seek-mode-btn { top: calc(20px + ${48 * FS}px) !important; left: 20px !important; } :fullscreen #${uid} .gt-seek-val-btn, .gt-fullscreen-active #${uid} .gt-seek-val-btn { top: calc(20px + ${96 * FS}px) !important; left: 20px !important; } :fullscreen #${uid} .gt-fit-btn, .gt-fullscreen-active #${uid} .gt-fit-btn { top: calc(20px + ${144 * FS}px) !important; left: 20px !important; } :fullscreen #${uid} .gt-shot-btn, .gt-fullscreen-active #${uid} .gt-shot-btn { top: calc(20px + ${192 * FS}px) !important; left: 20px !important; } :fullscreen #${uid} .gt-reset-speed-btn, .gt-fullscreen-active #${uid} .gt-reset-speed-btn { top: calc(20px + ${240 * FS}px) !important; left: 20px !important; } :fullscreen #${uid} .gt-pip-btn, .gt-fullscreen-active #${uid} .gt-pip-btn { top: 20px !important; right: 20px !important; } :fullscreen #${uid} .gt-lock-btn, .gt-fullscreen-active #${uid} .gt-lock-btn { top: calc(50% - ${35 * FS}px) !important; right: 20px !important; transform: translateY(-50%) !important; } :fullscreen #${uid} .gt-mode-btn, .gt-fullscreen-active #${uid} .gt-mode-btn { top: calc(50% + ${35 * FS}px) !important; right: 20px !important; transform: translateY(-50%) !important; } :fullscreen #${uid} .gt-reset-zoom-btn, .gt-fullscreen-active #${uid} .gt-reset-zoom-btn { top: calc(50% + ${75 * FS}px) !important; right: 20px !important; transform: translateY(-50%) !important; } #${uid} .gt-seek-msg { position: absolute !important; top: 45% !important; color: rgba(255, 255, 255, 0.95) !important; z-index: 2147483647 !important; pointer-events: none !important; opacity: 0; transition: opacity 0.15s ease-out; display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: center !important; gap: ${6 * TS}px !important; font-family: system-ui, -apple-system, sans-serif !important; white-space: nowrap !important; text-shadow: 0 0 ${10 * TS}px rgba(0,0,0,0.8), 0 0 ${4 * TS}px rgba(0,0,0,0.6), 0 ${2 * TS}px ${4 * TS}px rgba(0,0,0,0.5) !important; } #${uid} .gt-seek-msg.left { left: 15%; transform: translateY(-50%); } #${uid} .gt-seek-msg.right { right: 15%; transform: translateY(-50%); } #${uid} .gt-seek-msg.show { opacity: 1; } #${uid} .gt-seek-text { display: block !important; font-size: ${15 * TS}px !important; font-weight: 500 !important; line-height: 1 !important; white-space: nowrap !important; transform-origin: center center !important; will-change: transform; } #${uid} .gt-arrows { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: center !important; font-size: ${22 * TS}px !important; font-weight: 400 !important; line-height: 1 !important; } #${uid} .gt-arrows span { display: block !important; line-height: 1 !important; white-space: nowrap !important; } #${uid} .gt-toast { position: absolute; top: 10%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.15); color: #fff; padding: ${4 * TS}px ${10 * TS}px; border-radius: ${4 * TS}px; font: 700 ${14 * TS}px system-ui; z-index: 2147483647; pointer-events: none; opacity: 0; transition: opacity 0.2s; text-shadow: 0 0 ${2 * TS}px #000; border: 1px solid rgba(255,255,255,0.05); } #${uid} .gt-toast.show { opacity: 1; } :fullscreen #${uid} .gt-seek-msg, .gt-fullscreen-active #${uid} .gt-seek-msg { top: 45% !important; gap: ${6 * FS}px !important; text-shadow: 0 0 ${10 * FS}px rgba(0,0,0,0.8), 0 0 ${4 * FS}px rgba(0,0,0,0.6), 0 ${2 * FS}px ${4 * FS}px rgba(0,0,0,0.5) !important; } :fullscreen #${uid} .gt-seek-text, .gt-fullscreen-active #${uid} .gt-seek-text { font-size: ${15 * FS}px !important; } :fullscreen #${uid} .gt-arrows, .gt-fullscreen-active #${uid} .gt-arrows { font-size: ${22 * FS}px !important; } :fullscreen #${uid} .gt-toast, .gt-fullscreen-active #${uid} .gt-toast { padding: ${4 * FS}px ${10 * FS}px !important; border-radius: ${4 * FS}px !important; font-size: ${14 * FS}px !important; text-shadow: 0 0 ${2 * FS}px #000 !important; } `; }; GM_addStyle(` :fullscreen { background-color: #000 !important; } .gt-pop-anim { animation: gt-pop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes gt-pop { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } } .gt-arrow-slide-r { animation: gt-slide-r 0.6s infinite; } @keyframes gt-slide-r { 0% { transform: translateX(-4px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateX(4px); opacity: 0; } } .gt-arrow-slide-l { animation: gt-slide-l 0.6s infinite; } @keyframes gt-slide-l { 0% { transform: translateX(4px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateX(-4px); opacity: 0; } } #gt-toast-global { position: fixed; top: 10%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.15); color: #fff; padding: 4px 10px; border-radius: 4px; font: 700 14px system-ui; z-index: 2147483647; pointer-events: none; opacity: 0; transition: opacity 0.2s; text-shadow: 0 0 2px #000; border: 1px solid rgba(255,255,255,0.05); } #gt-toast-global.show { opacity: 1; } `); const SVG_LOCK = ``; const SVG_UNLOCK = ``; const SVG_SPEED = ``; const SVG_ZOOM = ``; const SVG_RESET_ZOOM = ``; const SVG_SEC = ``; const SVG_FRAME = ``; const SVG_FIT = ``; const SVG_SHOT = ``; const SVG_PIP = ``; const getFS = () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; const identify = (e) => { let targetVideo = null; let rootContainer = null; let cx = e.clientX, cy = e.clientY; if (e.touches && e.touches.length > 0) { cx = e.touches[0].clientX; cy = e.touches[0].clientY; } else if (e.changedTouches && e.changedTouches.length > 0) { cx = e.changedTouches[0].clientX; cy = e.changedTouches[0].clientY; } const path = (e.composedPath && typeof e.composedPath === 'function') ? e.composedPath() : []; for (let el of path) { if (el.tagName === 'VIDEO') { targetVideo = el; break; } } if (!targetVideo && cx !== undefined && cy !== undefined) { const scanStrictGeometry = (root) => { let vids = root.querySelectorAll ? root.querySelectorAll('video') : []; for (let v of vids) { if (v.getBoundingClientRect) { const r = v.getBoundingClientRect(); if (r.width > 50 && r.height > 50 && cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom) { return v; } } } let els = root.querySelectorAll ? root.querySelectorAll('*') : []; for (let el of els) { if (el.shadowRoot) { let res = scanStrictGeometry(el.shadowRoot); if (res) return res; } } return null; }; targetVideo = scanStrictGeometry(document); } if (!targetVideo) return null; if (targetVideo.gtRoot && document.contains(targetVideo.gtRoot)) { rootContainer = targetVideo.gtRoot; } else { rootContainer = getValidPlayerRoot(targetVideo); if (rootContainer && rootContainer !== document.body && rootContainer !== document.documentElement) { targetVideo.gtRoot = rootContainer; } } if (!rootContainer || rootContainer === document.body || rootContainer === document.documentElement) return null; return { root: rootContainer, video: targetVideo }; }; const updateUIState = (root, video) => { if(!video || !video.gtUI) return; let uiLayer = video.gtUI; const btnLock = uiLayer.querySelector('.gt-lock-btn'), btnMode = uiLayer.querySelector('.gt-mode-btn'), btnRot = uiLayer.querySelector('.gt-rotate-btn'), btnRst = uiLayer.querySelector('.gt-reset-speed-btn'), btnZoomRst = uiLayer.querySelector('.gt-reset-zoom-btn'), btnSeekMode = uiLayer.querySelector('.gt-seek-mode-btn'), btnSeekVal = uiLayer.querySelector('.gt-seek-val-btn'), btnFit = uiLayer.querySelector('.gt-fit-btn'), btnShot = uiLayer.querySelector('.gt-shot-btn'), btnPip = uiLayer.querySelector('.gt-pip-btn'), shield = uiLayer.querySelector('.gt-lock-shield'); const isFS = !!getFS() || root.classList.contains('gt-fullscreen-active'); const vState = video.gtState; if(btnLock) btnLock.innerHTML = vState.isScreenLocked ? SVG_LOCK : SVG_UNLOCK; if(btnMode) btnMode.innerHTML = vState.pinchMode === 'speed' ? SVG_SPEED : SVG_ZOOM; if(btnSeekMode) btnSeekMode.innerHTML = seekMode === 'sec' ? SVG_SEC : SVG_FRAME; if(btnSeekVal) btnSeekVal.innerHTML = `${seekMode === 'sec' ? seekSec + 's' : fpsMode + 'f'}`; if(btnFit) btnFit.innerHTML = SVG_FIT; if(btnShot) btnShot.innerHTML = SVG_SHOT; if(btnPip) btnPip.innerHTML = SVG_PIP; if (vState.isScreenLocked) { if(shield) shield.style.display = 'block'; [btnMode, btnRot, btnRst, btnZoomRst, btnSeekMode, btnSeekVal, btnFit, btnShot, btnPip].forEach(b => b?.classList.add('hidden-by-state')); } else { if(shield) shield.style.display = 'none'; [btnMode, btnSeekMode, btnSeekVal, btnFit, btnShot, btnPip].forEach(b => b?.classList.remove('hidden-by-state')); if(btnRot) { if(isFS) btnRot.classList.remove('hidden-by-state'); else btnRot.classList.add('hidden-by-state'); } if(btnRst) { if(video && video.playbackRate !== 1.0) btnRst.classList.remove('hidden-by-state'); else btnRst.classList.add('hidden-by-state'); } if(btnZoomRst) { if(vState.scale > 1.0) btnZoomRst.classList.remove('hidden-by-state'); else btnZoomRst.classList.add('hidden-by-state'); } } }; const wakeUpUI = (root, video) => { if (!video || !video.gtUI) return; const uiLayer = video.gtUI; uiLayer.classList.add('gt-ui-visible'); updateUIState(root, video); if (video.gtUITimer) clearTimeout(video.gtUITimer); video.gtUITimer = setTimeout(() => { uiLayer.classList.remove('gt-ui-visible'); }, CFG.uiTimeout); }; const hideUI = (video) => { if (video && video.gtUI) { video.gtUI.classList.remove('gt-ui-visible'); if (video.gtUITimer) clearTimeout(video.gtUITimer); } }; const applyTransform = (video) => { if(!video || !video.gtState) return; const vState = video.gtState; if (vState.scale <= 1.05) { vState.scale = 1.0; vState.panX = 0; vState.panY = 0; } else { const mX = (video.clientWidth * vState.scale - video.clientWidth) / 2, mY = (video.clientHeight * vState.scale - video.clientHeight) / 2; vState.panX = Math.max(-mX, Math.min(mX, vState.panX)); vState.panY = Math.max(-mY, Math.min(mY, vState.panY)); } video.style.setProperty('transition', 'none', 'important'); video.style.setProperty('will-change', 'transform', 'important'); video.style.transform = `translate(${vState.panX}px, ${vState.panY}px) scale(${vState.scale})`; }; let pendingRate = null; let lastRateUpdateTime = 0; const setRateThrottled = (v, rate) => { pendingRate = rate; const now = Date.now(); if (now - lastRateUpdateTime > 150) { v.playbackRate = pendingRate; lastRateUpdateTime = now; pendingRate = null; } }; const bindTap = (btn, handler) => { let lastExec = 0; const wrap = (e) => { e.stopPropagation(); e.stopImmediatePropagation(); if (e.type === 'touchend' && e.cancelable) e.preventDefault(); const now = Date.now(); if (now - lastExec < 300) return; lastExec = now; handler(e); const icon = btn.querySelector('svg') || btn.querySelector('span'); if (icon) { icon.classList.remove('gt-pop-anim'); void icon.offsetWidth; icon.classList.add('gt-pop-anim'); } }; btn.addEventListener('touchend', wrap, {passive: false, capture: true}); btn.addEventListener('click', wrap, {capture: true}); ['touchstart', 'mousedown', 'pointerdown', 'contextmenu', 'dblclick'].forEach(evt => { btn.addEventListener(evt, (e)=>{e.stopPropagation(); e.stopImmediatePropagation();}, { capture: true, passive: false }); }); }; const buildUI = (root, video) => { const uid = 'gt-ui-' + Math.random().toString(36).substr(2, 9); let uiLayer = document.createElement('div'); uiLayer.id = uid; uiLayer.className = 'gt-ui-layer'; const isPreview = video.dataset.gtIsPreview === 'true'; const containerH = root.clientHeight || video.clientHeight || window.innerHeight; const isNativeFull = nativeVideoEl === video; const S = getDeviceScale(containerH, isPreview, isNativeFull); const styleEl = document.createElement('style'); let initialCss = getUICss(S, uid, isNativeFull); if (isNativeFull) { initialCss = initialCss.replace('position: absolute !important; top: 0; left: 0; width: 100%; height: 100%;', 'position: fixed !important; top: 0; left: 0; width: 100vw; height: 100dvh;'); } styleEl.textContent = initialCss; uiLayer.appendChild(styleEl); const bar = document.createElement('div'); bar.className = 'gt-mini-progress'; bar.innerHTML = '
'; uiLayer.appendChild(bar); const shield = document.createElement('div'); shield.className = 'gt-lock-shield'; const blk = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); wakeUpUI(root, video); }; ['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'dblclick', 'touchstart', 'touchend'].forEach(evt => shield.addEventListener(evt, blk, {capture:true, passive:false})); uiLayer.appendChild(shield); const rBtn = document.createElement('div'); rBtn.className = 'gt-btn-base gt-rotate-btn'; rBtn.innerHTML = ``; bindTap(rBtn, () => { toggleOrientation(); wakeUpUI(root, video); }); uiLayer.appendChild(rBtn); const smBtn = document.createElement('div'); smBtn.className = 'gt-btn-base gt-seek-mode-btn'; bindTap(smBtn, () => { seekMode = seekMode === 'sec' ? 'frame' : 'sec'; GM_setValue('gt_seek_mode', seekMode); updateUIState(root, video); showMsg(`切换为: ${seekMode==='sec'?'按秒':'按帧'}`, video); wakeUpUI(root, video); }); uiLayer.appendChild(smBtn); const svBtn = document.createElement('div'); svBtn.className = 'gt-btn-base gt-seek-val-btn'; bindTap(svBtn, () => { if(seekMode==='sec'){ const a=[10,15,30,1,5]; seekSec=a[(a.indexOf(seekSec)+1)%a.length]; GM_setValue('gt_seek_sec', seekSec); showMsg(`步进: ${seekSec}s`, video);} else { fpsMode=fpsMode===30?60:30; GM_setValue('gt_fps', fpsMode); showMsg(`帧率: ${fpsMode}`, video); } updateUIState(root, video); wakeUpUI(root, video); }); uiLayer.appendChild(svBtn); const fitBtn = document.createElement('div'); fitBtn.className = 'gt-btn-base gt-fit-btn'; fitBtn.innerHTML = SVG_FIT; bindTap(fitBtn, () => { const fits = ['contain', 'cover', 'fill']; const current = video.style.objectFit || 'contain'; const next = fits[(fits.indexOf(current) + 1) % fits.length]; video.style.objectFit = next; showMsg(next === 'cover' ? '画面: 铺满裁切' : (next === 'fill' ? '画面: 暴力拉伸' : '画面: 原始比例'), video); wakeUpUI(root, video); }); uiLayer.appendChild(fitBtn); const shotBtn = document.createElement('div'); shotBtn.className = 'gt-btn-base gt-shot-btn'; shotBtn.innerHTML = SVG_SHOT; bindTap(shotBtn, () => { try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = dataURL; a.download = `Screenshot_${Math.floor(Date.now()/1000)}.png`; a.click(); showMsg('截图已保存', video); } catch (e) { showMsg('截图失败: 跨域限制', video); } wakeUpUI(root, video); }); uiLayer.appendChild(shotBtn); if (document.pictureInPictureEnabled) { const pipBtn = document.createElement('div'); pipBtn.className = 'gt-btn-base gt-pip-btn'; pipBtn.innerHTML = SVG_PIP; bindTap(pipBtn, () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); else video.requestPictureInPicture().catch(() => showMsg('小窗启动失败', video)); wakeUpUI(root, video); }); uiLayer.appendChild(pipBtn); } const lBtn = document.createElement('div'); lBtn.className = 'gt-btn-base gt-lock-btn'; bindTap(lBtn, () => { if(!video.gtState)return; video.gtState.isScreenLocked = !video.gtState.isScreenLocked; if(video.gtState.isScreenLocked){ const r=video.getBoundingClientRect(); const clk=new MouseEvent('click', {bubbles:true, cancelable:true, clientX:r.left+r.width/2, clientY:r.top+r.height/2}); video.dispatchEvent(clk); } wakeUpUI(root, video); }); uiLayer.appendChild(lBtn); const mBtn = document.createElement('div'); mBtn.className = 'gt-btn-base gt-mode-btn'; bindTap(mBtn, () => { if(!video.gtState)return; video.gtState.pinchMode = video.gtState.pinchMode==='speed'?'zoom':'speed'; wakeUpUI(root, video); showMsg(video.gtState.pinchMode==='speed'?'双指: 变速':'双指: 缩平移', video); }); uiLayer.appendChild(mBtn); const rstBtn = document.createElement('div'); rstBtn.className = 'gt-btn-base gt-reset-speed-btn'; rstBtn.innerHTML = `1.0x`; bindTap(rstBtn, () => { video.playbackRate = 1.0; showMsg('恢复原速', video); wakeUpUI(root, video); }); uiLayer.appendChild(rstBtn); const zoomRstBtn = document.createElement('div'); zoomRstBtn.className = 'gt-btn-base gt-reset-zoom-btn'; zoomRstBtn.innerHTML = SVG_RESET_ZOOM; bindTap(zoomRstBtn, () => { if(!video.gtState)return; video.gtState.scale = 1.0; video.gtState.panX = 0; video.gtState.panY = 0; if (video.parentNode && video.parentNode.dataset.gtOverflow) { video.parentNode.style.overflow = video.parentNode.dataset.gtOverflow; } if (video) video.style.transform = `translate(0px, 0px) scale(1)`; showMsg('恢复大小', video); wakeUpUI(root, video); }); uiLayer.appendChild(zoomRstBtn); if (root.shadowRoot) { root.shadowRoot.appendChild(uiLayer); } else { const style = window.getComputedStyle(root); if (style.position === 'static' && nativeVideoEl !== video) root.style.position = 'relative'; root.appendChild(uiLayer); } return uiLayer; }; const checkAndBuildUI = (root, video) => { if (!video.gtState) { video.gtState = { isScreenLocked: false, pinchMode: 'speed', scale: 1.0, panX: 0, panY: 0, looseKey: null }; } let uiLayer = root.shadowRoot ? root.shadowRoot.querySelector('.gt-ui-layer') : root.querySelector('.gt-ui-layer'); if (uiLayer && video.gtUI && uiLayer !== video.gtUI) { uiLayer.remove(); uiLayer = null; } if (!uiLayer) { const container = root.shadowRoot || root; Array.from(container.children).forEach(child => { if (child.classList && child.classList.contains('gt-ui-layer') && child !== video.gtUI) { child.remove(); } }); uiLayer = buildUI(root, video); video.gtUI = uiLayer; } return uiLayer; }; const initVideoCore = (video) => { if (!video || video.dataset.gtCoreInit) return; video.dataset.gtCoreInit = 'true'; if (!video.gtState) video.gtState = { isScreenLocked: false, pinchMode: 'speed', scale: 1.0, panX: 0, panY: 0, looseKey: null }; if (nativeVideoEl === video) { video.dataset.gtIsPreview = 'false'; } else { video.dataset.gtIsPreview = isPreviewVideo(video) ? 'true' : 'false'; const root = getValidPlayerRoot(video); if (root) { video.gtRoot = root; checkAndBuildUI(root, video); } } if (video.dataset.gtIsPreview !== 'true') { let savedVol = GM_getValue('gt_volume', null); let savedMuted = GM_getValue('gt_is_muted', null); if (savedVol !== null && !isNaN(savedVol)) video.volume = savedVol; if (savedMuted === false && video.muted) video.muted = false; const checkAndRestore = () => { if (video.currentTime >= 2) return; const exactKey = getVideoKey(video, true); const looseKey = getVideoKey(video, false); const history = GM_getValue('gt_video_history', {}); let targetT = 0; if (history[exactKey] && history[exactKey].t > 3) { targetT = history[exactKey].t; } else { let newestTs = 0; for (let k in history) { if (k.startsWith(looseKey) && history[k].ts > newestTs) { newestTs = history[k].ts; targetT = history[k].t; } } } if (targetT > 3) { let hasToasted = false; const formatTime = (t) => { let h = Math.floor(t / 3600).toString().padStart(2, '0'); let m = Math.floor((t % 3600) / 60).toString().padStart(2, '0'); let s = Math.floor(t % 60).toString().padStart(2, '0'); return h !== '00' ? `${h}:${m}:${s}` : `${m}:${s}`; }; const doRestore = () => { applyFixedScale(video.gtRoot, video, video.gtUI, nativeVideoEl === video); if (Math.abs(video.currentTime - targetT) > 2) { video.currentTime = targetT; } if (!hasToasted) { hasToasted = true; showMsg(`已恢复至 ${formatTime(targetT)}`, video); } }; if (video.currentTime < 2) doRestore(); let defenseTicks = 0; const defenseInterval = setInterval(() => { if (video.currentTime < 2) doRestore(); defenseTicks++; if (defenseTicks > 20) clearInterval(defenseInterval); }, 250); const clearDefense = () => { if (video.currentTime > 2 && video.currentTime >= targetT - 2) { video.removeEventListener('timeupdate', clearDefense); } }; video.addEventListener('timeupdate', clearDefense); } }; if (GM_getValue('gt_resume_playback', true)) { if (video.readyState >= 1) checkAndRestore(); video.addEventListener('loadedmetadata', () => { if (video.gtState) video.gtState.looseKey = null; applyFixedScale(video.gtRoot, video, video.gtUI, nativeVideoEl === video); checkAndRestore(); }); } video.addEventListener('emptied', () => { if (video.currentTime > 2) saveProgress(video); if (video.gtState) video.gtState.looseKey = null; }); const restoreVol = () => { let savedVol = GM_getValue('gt_volume', null); let savedMuted = GM_getValue('gt_is_muted', null); if (savedVol !== null && !isNaN(savedVol)) video.volume = savedVol; if (savedMuted === false && video.muted) video.muted = false; video.removeEventListener('play', restoreVol); video.removeEventListener('touchstart', restoreVol, {capture: true}); }; video.addEventListener('play', restoreVol); video.addEventListener('touchstart', restoreVol, {capture: true, passive: true}); } let lastSaveTime = 0; video.addEventListener('timeupdate', () => { if (video.dataset.gtIsPreview !== 'true' && Math.abs(video.currentTime - lastSaveTime) > 5) { saveProgress(video); lastSaveTime = video.currentTime; } if (video.gtUI && video.duration) { const fill = video.gtUI.querySelector('.gt-mini-progress .gt-fill'); if (fill) fill.style.width = `${(video.currentTime / video.duration) * 100}%`; } }); video.addEventListener('pause', () => { if (Date.now() < enforceStateUntil && enforceTarget === 'playing') video.play().catch(()=>{}); }); video.addEventListener('play', () => { if (Date.now() < enforceStateUntil && enforceTarget === 'paused') video.pause(); }); }; if (nativeVideoEl) { nativeVideoEl.dataset.gtIsNaked = 'false'; nativeVideoEl.gtRoot = document.body; document.body.style.setProperty('touch-action', 'none', 'important'); nativeVideoEl.style.setProperty('touch-action', 'none', 'important'); nativeVideoEl.style.setProperty('overscroll-behavior', 'none', 'important'); let uiLayer = buildUI(document.body, nativeVideoEl); nativeVideoEl.gtUI = uiLayer; initVideoCore(nativeVideoEl); } else { const scanAndInitCore = () => { document.querySelectorAll('video').forEach(initVideoCore); }; const domObserver = new MutationObserver((mutations) => { let hasNew = false; for (let m of mutations) { if (m.addedNodes.length) { hasNew = true; break; } } if (hasNew) scanAndInitCore(); }); domObserver.observe(document, { childList: true, subtree: true }); ['play', 'loadedmetadata'].forEach(evt => { document.addEventListener(evt, (e) => { if (e.target && e.target.tagName === 'VIDEO') initVideoCore(e.target); }, { capture: true, passive: true }); }); scanAndInitCore(); } const setupPlayer = (video) => { if (!video) return false; let root = getValidPlayerRoot(video); if (!root || root === document.body || root === document.documentElement) return false; const isPreview = isPreviewVideo(video); video.dataset.gtIsPreview = isPreview ? 'true' : 'false'; video.style.setProperty('touch-action', 'none', 'important'); video.style.setProperty('overscroll-behavior', 'none', 'important'); video.style.setProperty('transition', 'none', 'important'); video.style.setProperty('will-change', 'transform', 'important'); const isNaked = isNakedForumVideo(video); video.dataset.gtIsNaked = isNaked ? 'true' : 'false'; if (isNaked && video.parentNode && !video.parentNode.classList.contains('gt-video-wrapper')) { const wrapper = document.createElement('div'); wrapper.className = 'gt-video-wrapper'; const cStyle = window.getComputedStyle(video); wrapper.style.position = 'relative'; wrapper.style.display = (cStyle.display === 'inline' || cStyle.display === 'inline-block') ? 'inline-block' : 'block'; let w = video.style.width || video.getAttribute('width') || cStyle.width; let h = video.style.height || video.getAttribute('height') || cStyle.height; wrapper.style.width = (!w || w === 'auto' || w === '0px') ? '100%' : w; wrapper.style.height = (!h || h === 'auto' || h === '0px') ? '100%' : h; wrapper.style.margin = cStyle.margin; wrapper.style.setProperty('touch-action', 'none', 'important'); wrapper.style.setProperty('overscroll-behavior', 'none', 'important'); wrapper.style.background = '#000'; video.parentNode.insertBefore(wrapper, video); wrapper.appendChild(video); video.style.margin = '0'; video.style.width = '100%'; video.style.height = '100%'; if (!video.style.objectFit) video.style.objectFit = 'contain'; root = wrapper; video.setAttribute('controlslist', 'nofullscreen'); } if (!isNaked) { root.style.setProperty('touch-action', 'none', 'important'); root.style.setProperty('overscroll-behavior', 'none', 'important'); } if (!video.dataset.gtSetupDone || video.gtRoot !== root) { video.dataset.gtSetupDone = 'true'; video.gtRoot = root; initVideoCore(video); } checkAndBuildUI(root, video); return true; }; const handleAccumulatedSeek = (dir, uiLayer, video) => { activeSeekSide = dir; const stepVal = seekMode === 'sec' ? seekSec : (1 / fpsMode); seekAccumulator += stepVal; if (video) { video.currentTime = dir === 'left' ? Math.max(0, video.currentTime - stepVal) : Math.min(video.duration || 0, video.currentTime + stepVal); } let t = uiLayer.querySelector('.gt-seek-msg'); if (!t) { t = document.createElement('div'); t.className = `gt-seek-msg ${dir}`; uiLayer.appendChild(t); } t.className = `gt-seek-msg ${dir}`; let textNode = t.querySelector('.gt-seek-text'); if (!textNode) { t.innerHTML = dir === 'left' ? `
-${seekMode==='sec'?seekAccumulator:Math.round(seekAccumulator*fpsMode)}${seekMode==='sec'?'':'帧'}` : `+${seekMode==='sec'?seekAccumulator:Math.round(seekAccumulator*fpsMode)}${seekMode==='sec'?'':'帧'}
`; } else { textNode.classList.remove('gt-pop-anim'); void textNode.offsetWidth; textNode.classList.add('gt-pop-anim'); textNode.innerText = (dir === 'left' ? '-' : '+') + (seekMode === 'sec' ? seekAccumulator : Math.round(seekAccumulator * fpsMode)) + (seekMode === 'sec' ? '' : '帧'); } t.classList.add('show'); clearTimeout(seekSessionTimer); seekSessionTimer = setTimeout(() => { t.classList.remove('show'); activeSeekSide = null; seekAccumulator = 0; setTimeout(() => { if (t && t.parentNode && !t.classList.contains('show')) t.innerHTML = ''; }, 200); }, 800); }; const getPinchData = (touches) => { const dx = touches[0].clientX - touches[1].clientX, dy = touches[0].clientY - touches[1].clientY; return { dist: Math.hypot(dx, dy), cx: (touches[0].clientX + touches[1].clientX) / 2, cy: (touches[0].clientY + touches[1].clientY) / 2 }; }; const onStart = (e) => { if (!getFS()) { document.querySelectorAll('.plyr--fullscreen-active, .jw-flag-fullscreen, .gt-fullscreen-active, .gt-ui-visible').forEach(el => { el.classList.remove('plyr--fullscreen-active', 'jw-flag-fullscreen', 'gt-fullscreen-active', 'gt-ui-visible'); el.style.cssText = ''; }); } const isEx = isExcludedZone(e); const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active')); if (nativeVideoEl) { targetV = nativeVideoEl; targetP = document.body; } else { let hit = identify(e); if (!hit || !hit.video) return; targetV = hit.video; if (targetV.gtState && targetV.gtState.isScreenLocked && !isEx) { if (isFS || (targetP && (e.composedPath().includes(targetP) || targetP.contains(e.target)))) { if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (targetP && targetV) wakeUpUI(targetP, targetV); lastTapTime = Date.now(); return; } } if (isEx) { clearTimeout(lpTimer); return; } setupPlayer(targetV); targetP = targetV.gtRoot; } if (!targetV || !targetP) return; if (!targetV.gtState) targetV.gtState = { isScreenLocked: false, pinchMode: 'speed', scale: 1.0, panX: 0, panY: 0, looseKey: null }; if (targetV.gtState.isScreenLocked && !isEx) { if (isFS || (targetP && (e.composedPath().includes(targetP) || targetP.contains(e.target)))) { if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (targetP && targetV) wakeUpUI(targetP, targetV); lastTapTime = Date.now(); return; } } let uiLayer = targetV.gtUI || (targetP.shadowRoot ? targetP.shadowRoot.querySelector('.gt-ui-layer') : targetP.querySelector('.gt-ui-layer')); if (!uiLayer) return; clearTimeout(lpTimer); const now = Date.now(); const isRapid = (now - lastTapTime < 350); if (!isRapid) { tapCount = 1; wasPlayingBeforeSequence = targetV ? !targetV.paused : false; wakeUpUI(targetP, targetV); } else { tapCount++; } lastTapTime = now; if (e.touches && e.touches.length > 1) { if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); tapCount = 0; enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = now + 800; if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{}); else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause(); } if (tapCount >= 2) { blockGestureUntil = now + 500; if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const rRect = targetP.getBoundingClientRect(); const rClientX = (e.touches ? e.touches[0].clientX : e.clientX); const rWidth = rRect.width || window.innerWidth; const rLeft = rRect.width ? rRect.left : 0; const r = (rClientX - rLeft) / rWidth; if (r < 0.3) handleAccumulatedSeek('left', uiLayer, targetV); else if (r > 0.7) handleAccumulatedSeek('right', uiLayer, targetV); else if (tapCount === 2 && targetV.dataset.gtIsPreview !== 'true') { toggleNativeFullscreen(targetP, targetV); } enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = now + 800; if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{}); else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause(); isTouch = false; if (getFS()) hideUI(targetV); return; } isTouch = true; action = null; startX = e.touches[0].clientX; startY = e.touches[0].clientY; initVol = targetV.volume; initTime = targetV.currentTime; initRate = targetV.playbackRate; virtualTime = null; if (e.touches.length === 2) { const p = getPinchData(e.touches); initPinchDist = p.dist; initCenterX = p.cx; initCenterY = p.cy; initScale = targetV.gtState.scale; initPanX = targetV.gtState.panX; initPanY = targetV.gtState.panY; initSpeed = targetV.playbackRate; const rect = targetV.getBoundingClientRect(); originDx = initCenterX - (rect.left + rect.width/2 - initPanX); originDy = initCenterY - (rect.top + rect.height/2 - initPanY); action = 'pinch'; if (getFS()) hideUI(targetV); } else if (e.touches.length === 1 && targetV.gtState.scale === 1.0) { lpTimer = setTimeout(() => { if (isTouch && targetV) { action = 'rate'; targetV.playbackRate = Math.max(0.1, initRate + CFG.rateBase - 1.0); showMsg(`${targetV.playbackRate.toFixed(1)}x`, targetV); if (getFS()) hideUI(targetV); } }, CFG.longPress); } }; const onMove = (e) => { if (!targetV || !targetV.gtState) return; const vState = targetV.gtState; if (vState.isScreenLocked) { if (!isExcludedZone(e) && (!!getFS() || (targetP && (e.composedPath().includes(targetP) || targetP.contains(e.target))))) { if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } return; } if (!isTouch) return; if (action === 'pinch' || action === 'rate' || action === 'seek' || action === 'vol' || action === 'bri') { if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } if (action === 'pinch' && e.touches.length === 2) { const p = getPinchData(e.touches); if (vState.pinchMode === 'zoom') { vState.scale = Math.max(1.0, Math.min(CFG.maxScale, initScale * (p.dist/initPinchDist))); if (vState.scale > 1.0) { const ds = vState.scale/initScale; vState.panX = (p.cx-initCenterX)+initPanX*ds+originDx*(1-ds); vState.panY = (p.cy-initCenterY)+initPanY*ds+originDy*(1-ds); } else { vState.panX = 0; vState.panY = 0; } applyTransform(targetV); } else { let s = initSpeed + ((p.dist-initPinchDist) * 0.005); let finalSpeed = s; if (finalSpeed > 0.95 && finalSpeed < 1.05) finalSpeed = 1.0; finalSpeed = Math.max(0.1, Math.min(4.0, finalSpeed)); setRateThrottled(targetV, finalSpeed); showMsg(finalSpeed === 1.0 ? '1.0x' : `${finalSpeed.toFixed(2)}x`, targetV); } return; } if (action === 'pinch' || action === 'pinch_wait') return; const dx = e.touches[0].clientX - startX, dy = startY - e.touches[0].clientY; if (action === 'rate') { targetV.playbackRate = Math.max(0.1, Math.min(4.0, initRate + (CFG.rateBase + dx * CFG.senseRate) - 1.0)); showMsg(`${targetV.playbackRate.toFixed(1)}x`, targetV); return; } if (!action) { if (Math.abs(dx) > CFG.minDist || Math.abs(dy) > CFG.minDist) { clearTimeout(lpTimer); action = Math.abs(dx) > Math.abs(dy) ? 'seek' : (startX < innerWidth/2 ? 'bri' : 'vol'); enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = Date.now() + 800; if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{}); else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause(); if (getFS()) hideUI(targetV); } else { if (e.cancelable && targetV.dataset.gtIsNaked === 'true') { e.preventDefault(); } return; } } const now = Date.now(); const canUpdateVisual = (now - lastThrottledTime > 32); if (action === 'seek') { virtualTime = Math.max(0, Math.min(targetV.duration||0, initTime + dx * CFG.senseX)); if (canUpdateVisual) { const formatTime = (t) => { let h = Math.floor(t / 3600).toString().padStart(2, '0'); let m = Math.floor((t % 3600) / 60).toString().padStart(2, '0'); let s = Math.floor(t % 60).toString().padStart(2, '0'); return h !== '00' ? `${h}:${m}:${s}` : `${m}:${s}`; }; showMsg(`${formatTime(virtualTime)}`, targetV); lastThrottledTime = now; } } else if (action === 'vol') { if (canUpdateVisual) { targetV.volume = Math.max(0, Math.min(1, initVol + dy/innerHeight * 2 * CFG.senseY)); showMsg(`Vol: ${Math.round(targetV.volume*100)}%`, targetV); lastThrottledTime = now; } } else if (action === 'bri') { if (canUpdateVisual) { let b = Math.max(0.1, Math.min(2.0, 1 + dy/innerHeight * 2 * CFG.senseY)); targetV.style.filter = `brightness(${b})`; showMsg(`Bri: ${Math.round(b*100)}%`, targetV); lastThrottledTime = now; } } }; const onEnd = (e) => { const now = Date.now(); const isEx = isExcludedZone(e); if (now < blockGestureUntil && !isEx) { e.stopPropagation(); e.stopImmediatePropagation(); if (e.cancelable) e.preventDefault(); isTouch = false; return; } if (targetV && targetV.gtState && targetV.gtState.isScreenLocked) { if (!isEx && (!!getFS() || (targetP && (e.composedPath().includes(targetP) || targetP.contains(e.target))))) { e.stopPropagation(); e.stopImmediatePropagation(); } isTouch = false; return; } if (!isTouch) return; if (e.touches.length > 0) { if (action === 'pinch') action = 'pinch_wait'; return; } if (pendingRate !== null && targetV) { targetV.playbackRate = pendingRate; pendingRate = null; } if (action === 'seek' && virtualTime !== null && targetV) { targetV.currentTime = virtualTime; virtualTime = null; } clearTimeout(lpTimer); if (action === 'rate' && targetV) { targetV.playbackRate = initRate; showMsg('', targetV); wakeUpUI(targetP, targetV); } if ((action === 'pinch' || action === 'pinch_wait' || action === 'pan') && targetV) { wakeUpUI(targetP, targetV); } if (!action) { if (!activeSeekSide) wakeUpUI(targetP, targetV); } else { blockGestureUntil = now + 500; } isTouch = false; targetV = null; action = null; }; const pOpt = { passive: false, capture: true }; document.addEventListener('touchstart', onStart, pOpt); document.addEventListener('touchmove', onMove, pOpt); document.addEventListener('touchend', onEnd, pOpt); document.addEventListener('touchcancel', onEnd, pOpt); ['pointerdown', 'pointerup', 'pointercancel', 'click', 'dblclick'].forEach(evt => { document.addEventListener(evt, (e) => { const isEx = isExcludedZone(e); if (evt === 'dblclick' && !isEx && (nativeVideoEl || identify(e))) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return; } if (Date.now() < blockGestureUntil) { if (!isEx) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } } else if (evt === 'click') { const isL = targetV && targetV.gtState && targetV.gtState.isScreenLocked && !isEx && ((!!getFS()) || (targetP && (e.composedPath().includes(targetP) || targetP.contains(e.target)))); if (activeSeekSide || isL) { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); if (isL && targetP && targetV) wakeUpUI(targetP, targetV); } } }, { capture: true, passive: false }); }); })();