Greasy Fork

Greasy Fork is available in English.

HTML5视频手势

集多家之长特色用ai写的一个触摸屏播放器插件

当前为 2026-03-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         HTML5视频手势
// @namespace    http://tampermonkey.net/
// @version      64.7
// @description  集多家之长特色用ai写的一个触摸屏播放器插件
// @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
// ==/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 
    };

    let seekSec = GM_getValue('gt_seek_sec', 10);
    let seekMode = GM_getValue('gt_seek_mode', 'sec');
    let fpsMode = GM_getValue('gt_fps', 30);
    
    let state = {
        isScreenLocked: false,
        pinchMode: 'speed',
        scale: 1.0, panX: 0, panY: 0
    };

    let startX, startY, initVol, initTime, initRate, targetV, targetP, isTouch = false, action = null, lpTimer = null, toastTimer = null, lastTap = 0, cleanupId = null, uiTimer = null;
    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;

    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) {
                    const promise = originalMethod.apply(this, args);
                    let v = this.tagName === 'VIDEO' ? this : (this.querySelector('video') || document.querySelector('video'));
                    if (v && screen.orientation && screen.orientation.lock) {
                        const dir = v.videoWidth > v.videoHeight ? 'landscape' : 'portrait';
                        screen.orientation.lock(dir).catch(()=>{});
                    }
                    return promise;
                };
            }
        });
    };
    hijackFullscreenAPI();

    const toggleNativeFullscreen = (container, video) => {
        const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement) || container.classList.contains('gt-fullscreen-active');
        if (isFS) {
            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();
        } else {
            container.classList.add('gt-fullscreen-active');
            const reqFs = container.requestFullscreen || container.webkitRequestFullscreen || container.mozRequestFullScreen;
            if (reqFs) {
                reqFs.call(container).catch(()=>{ if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); });
            } else if (video.webkitEnterFullscreen) {
                video.webkitEnterFullscreen();
            }
        }
    };

    // 严格精简的菜单,仅保留直链功能
    GM_registerMenuCommand('🔗 拷贝视频源(直链/页面)', () => {
        if (sniffedUrl) { GM_setClipboard(sniffedUrl); showMsg('已复制嗅探流媒体直链'); return; }
        let v = targetV || (document.querySelectorAll('video').length > 0 ? Array.from(document.querySelectorAll('video')).sort((a,b) => (b.clientWidth*b.clientHeight) - (a.clientWidth*a.clientHeight))[0] : null);
        if (!v) { showMsg('未找到视频元素'); return; }
        let src = v.src;
        if (!src || src.startsWith('blob:')) { const source = v.querySelector('source'); if (source && source.src) src = source.src; }
        if (src && !src.startsWith('blob:')) { GM_setClipboard(src); showMsg('已复制视频直链'); } 
        else { GM_setClipboard(window.location.href); showMsg('已复制网页源地址 (Blob流)'); }
    });
    
    let sniffedUrl = '';
    const mediaReg = /\.(m3u8|mpd|mp4|webm|flv)(\?|$)/i;
    const sniff = (url) => { try { if (typeof url === 'string' && mediaReg.test(url) && url.startsWith('http')) sniffedUrl = url; } catch(e){} };
    const oFetch = window.fetch;
    window.fetch = function(...args) { if (args[0]) sniff(typeof args[0] === 'string' ? args[0] : args[0].url); return oFetch.apply(this, args); };
    const oOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url, ...rest) { sniff(url); return oOpen.call(this, method, url, ...rest); };

    const toggleOrientation = () => {
        if (!screen.orientation) return;
        if (screen.orientation.type.startsWith('landscape')) screen.orientation.lock('portrait').catch(()=>{});
        else screen.orientation.lock('landscape').catch(()=>{});
    };

    GM_addStyle(`
        .dplayer-pause-ad, .dplayer-notice, .dplayer-ad, .artplayer-plugin-ads, .art-ad, .art-notice, .MacPlayer .play-ad, #playleft .pause-ad, .player-ad, .ad-box, .pause-ad, .ad-mask, .pause-html, #pause-html, [class*="pause-html"], [id*="pause-html"] { display: none !important; pointer-events: none !important; opacity: 0 !important; z-index: -2147483648 !important; width: 0 !important; height: 0 !important; }
        .gt-toast { 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.show { opacity: 1; }

        .gt-seek-msg { position: absolute !important; top: 50% !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: 6px !important; font-family: system-ui, -apple-system, sans-serif !important; white-space: nowrap !important; text-shadow: 0 0 10px rgba(0,0,0,0.8), 0 0 4px rgba(0,0,0,0.6), 0 2px 4px rgba(0,0,0,0.5) !important; }
        .gt-seek-msg.left { left: 15%; transform: translateY(-50%); }
        .gt-seek-msg.right { right: 15%; transform: translateY(-50%); }
        .gt-seek-msg.show { opacity: 1; }
        .gt-seek-text { display: block !important; font-size: 15px !important; font-weight: 500 !important; line-height: 1 !important; white-space: nowrap !important; transform-origin: center center !important; -webkit-backface-visibility: hidden !important; backface-visibility: hidden !important; will-change: transform; }
        .gt-arrows { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: center !important; font-size: 22px !important; font-weight: 400 !important; line-height: 1 !important; -webkit-backface-visibility: hidden !important; backface-visibility: hidden !important; }
        .gt-arrows span { display: block !important; line-height: 1 !important; white-space: nowrap !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-lock-touch { touch-action: none !important; overscroll-behavior: none !important; }
        :fullscreen { background-color: #000 !important; }
        .gt-video-wrapper { position: relative !important; display: inline-block; line-height: 0; max-width: 100%; overflow: hidden !important; }
        .gt-video-wrapper video { transform-origin: center center; will-change: transform; }

        .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); }
        .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 .gt-mini-progress, .gt-video-wrapper:fullscreen .gt-mini-progress, .plyr--fullscreen-active .gt-mini-progress, .jw-flag-fullscreen .gt-mini-progress, [data-testid="videoComponent"]:fullscreen .gt-mini-progress { height: 3px !important; }

        .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; }
        :fullscreen .gt-lock-shield, .gt-fullscreen-active .gt-lock-shield { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; }

        .gt-btn-base {
            position: absolute; width: 26px; height: 26px;
            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));
        }
        
        .gt-btn-base * { pointer-events: none !important; }

        .gt-btn-base svg { width: 15px; height: 15px; transition: all 0.2s ease; transform-origin: center center; will-change: transform; }
        .gt-btn-base span { font-size: 11px; font-weight: 800; font-family: system-ui; letter-spacing: 0.5px; transition: font-size 0.2s; transform-origin: center center; display: inline-block; will-change: transform; }
        
        .gt-ui-visible .gt-btn-base { opacity: 0.65 !important; pointer-events: auto !important; }
        .gt-ui-visible .gt-btn-base.hidden-by-state { display: none !important; pointer-events: none !important; }
        .gt-btn-base:active { opacity: 0.9 !important; color: #fff; }

        .gt-rotate-btn { top: 10px; left: 10px; }
        .gt-seek-mode-btn { top: calc(10px + 38px); left: 10px; }
        .gt-seek-val-btn { top: calc(10px + 76px); left: 10px; }
        
        .gt-lock-btn { top: calc(50% - 38px); right: 10px; transform: translateY(-50%); }
        .gt-mode-btn { top: 50%; right: 10px; transform: translateY(-50%); }
        .gt-reset-zoom-btn { top: calc(50% + 38px); right: 10px; transform: translateY(-50%); }
        .gt-reset-speed-btn { top: 50%; left: 10px; transform: translateY(-50%); }

        :fullscreen .gt-btn-base, .gt-fullscreen-active .gt-btn-base { width: 38px; height: 38px; }
        :fullscreen .gt-ui-visible .gt-btn-base, .gt-fullscreen-active .gt-ui-visible .gt-btn-base { opacity: 0.5 !important; }
        :fullscreen .gt-btn-base svg, .gt-fullscreen-active .gt-btn-base svg { width: 22px; height: 22px; }
        :fullscreen .gt-btn-base span, .gt-fullscreen-active .gt-btn-base span { font-size: 14px; }

        :fullscreen .gt-rotate-btn, .gt-fullscreen-active .gt-rotate-btn { top: 20px; left: 20px; transform: none; }
        :fullscreen .gt-seek-mode-btn, .gt-fullscreen-active .gt-seek-mode-btn { top: calc(20px + 60px); left: 20px; transform: none; }
        :fullscreen .gt-seek-val-btn, .gt-fullscreen-active .gt-seek-val-btn { top: calc(20px + 120px); left: 20px; transform: none; }
        
        :fullscreen .gt-lock-btn, .gt-fullscreen-active .gt-lock-btn { top: 50%; right: 20px; transform: translateY(-50%); }
        :fullscreen .gt-mode-btn, .gt-fullscreen-active .gt-mode-btn { top: calc(50% + 60px); right: 20px; transform: translateY(-50%); }
        :fullscreen .gt-reset-zoom-btn, .gt-fullscreen-active .gt-reset-zoom-btn { top: calc(50% + 120px); right: 20px; transform: translateY(-50%); }
        :fullscreen .gt-reset-speed-btn, .gt-fullscreen-active .gt-reset-speed-btn { top: calc(50% + 60px); left: 20px; transform: translateY(-50%); }

        .gt-fullscreen-active#movie_player:fullscreen .html5-video-container { width: 100% !important; height: 100% !important; }
        .gt-fullscreen-active#movie_player:fullscreen video { width: 100% !important; height: 100% !important; object-fit: contain !important; top: 0 !important; left: 0 !important; }
        .gt-fullscreen-active#movie_player:fullscreen .ytp-chrome-bottom { opacity: 1 !important; z-index: 2147483647 !important; }
        :fullscreen.plyr { width: 100vw !important; height: 100vh !important; display: flex !important; flex-direction: column !important; background: #000 !important; position: fixed !important; top: 0 !important; left: 0 !important; margin: 0 !important; }
        :fullscreen.plyr .plyr__video-wrapper { height: 100% !important; width: 100% !important; background: #000 !important; }
        :fullscreen .plyr__controls { z-index: 2147483647 !important; opacity: 1 !important; visibility: visible !important; }
        [data-testid="videoComponent"]:fullscreen { width: 100vw !important; height: 100vh !important; background: #000 !important; margin: 0 !important; display: block !important; }
        [data-testid="videoComponent"]:fullscreen video { width: 100% !important; height: 100% !important; object-fit: contain !important; position: absolute !important; top: 0; left: 0; z-index: 0 !important; }
        [data-testid="videoComponent"]:fullscreen [data-testid^="immersive-video-controls"] { display: flex !important; visibility: visible !important; opacity: 1 !important; z-index: 2147483647 !important; position: absolute !important; bottom: 0 !important; width: 100% !important; pointer-events: auto !important; background: rgba(0,0,0,0.3) !important; }
        #html5video:fullscreen { width: 100vw !important; height: 100vh !important; background: #000 !important; margin: 0 !important; }
        #html5video:fullscreen .buttons-bar, #html5video:fullscreen .progress-bar-container, #html5video:fullscreen .big-buttons { z-index: 2147483647 !important; opacity: 1 !important; visibility: visible !important; }
        .jw-controls, .bpx-player-control-wrap { z-index: 2147483647 !important; }
    `);

    const SVG_LOCK = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`;
    const SVG_UNLOCK = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`;
    const SVG_SPEED = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`;
    const SVG_ZOOM = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`;
    const SVG_RESET_ZOOM = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke-dasharray="4 2"></rect></svg>`;
    const SVG_SEC = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
    const SVG_FRAME = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>`;

    const VIP_SELECTORS = '[data-testid="videoComponent"], .plyr, #html5video, #movie_player, .html5-video-player, .bpx-player-container, .dplayer, .artplayer-app, .MacPlayer, .ckplayer, #playleft';

    const findUp = (el, selector) => { while (el && el !== document.body) { if (el.matches && el.matches(selector)) return el; el = el.parentNode; } return null; };
    const getFS = () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;

    const identify = (e) => {
        const t = e.target;
        let targetVideo = null;
        let rootContainer = null;

        const vip = findUp(t, VIP_SELECTORS);
        if (vip) {
            targetVideo = vip.querySelector('video');
            if (!targetVideo && vip.shadowRoot) targetVideo = vip.shadowRoot.querySelector('video');
            rootContainer = vip;
        }

        if (!targetVideo) {
            let c = t; 
            for(let i=0; i<8; i++) { 
                if (!c || c === document.body) break; 
                if (c.tagName === 'VIDEO') { targetVideo = c; rootContainer = c.parentNode; break; }
                const cls = (c.className || '').toString().toLowerCase(); const id = (c.id || '').toString().toLowerCase();
                if (c.classList?.contains('gt-video-wrapper') || cls.match(/artplayer|dplayer|plyr/)) { 
                    targetVideo = c.shadowRoot ? c.shadowRoot.querySelector('video') : c.querySelector('video'); 
                    if (targetVideo) { rootContainer = c; break; }
                } 
                c = c.parentNode; 
            }
        }
        
        if (!targetVideo) {
            const videos = document.querySelectorAll('video');
            if (videos.length > 0) {
                targetVideo = Array.from(videos).sort((a,b) => (b.clientWidth*b.clientHeight) - (a.clientWidth*a.clientHeight))[0];
                rootContainer = findUp(targetVideo, VIP_SELECTORS) || targetVideo.parentNode;
            }
        }

        if (!targetVideo) return null;

        if (e.touches && e.touches.length > 0) {
            const checkBox = rootContainer || targetVideo;
            const rect = checkBox.getBoundingClientRect();
            const touch = e.touches[0];
            if (touch.clientX < rect.left - 10 || touch.clientX > rect.right + 10 ||
                touch.clientY < rect.top - 10 || touch.clientY > rect.bottom + 10) {
                return null; 
            }
        }

        return { root: rootContainer, video: targetVideo, isNaked: !rootContainer.classList?.contains('gt-video-wrapper') && !findUp(rootContainer, VIP_SELECTORS) };
    };

    const exterminateAds = () => { document.querySelectorAll('#pause-html, #player_pause, .pause-html, .MacPlayer .play-ad').forEach(n => { try { n.remove(); } catch(e){} }); };
    setInterval(exterminateAds, 500);

    const showMsg = (txt) => {
        let t = document.getElementById('gt-toast'); if (!t) { t = document.createElement('div'); t.id = 'gt-toast'; t.className = 'gt-toast'; }
        const parent = document.fullscreenElement || document.body; if (t.parentNode !== parent) parent.appendChild(t);
        t.innerText = txt; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove('show'), 800);
    };

    let activeUIEl = null;
    const updateUIState = (root, video) => {
        if(!root) return;
        const btnLock = root.querySelector('.gt-lock-btn');
        const btnMode = root.querySelector('.gt-mode-btn');
        const btnRot = root.querySelector('.gt-rotate-btn');
        const btnRst = root.querySelector('.gt-reset-speed-btn');
        const btnZoomRst = root.querySelector('.gt-reset-zoom-btn');
        const btnSeekMode = root.querySelector('.gt-seek-mode-btn');
        const btnSeekVal = root.querySelector('.gt-seek-val-btn');
        const shield = root.querySelector('.gt-lock-shield');
        
        const isFS = !!getFS() || root.classList.contains('gt-fullscreen-active');

        if(btnLock) btnLock.innerHTML = state.isScreenLocked ? SVG_LOCK : SVG_UNLOCK;
        if(btnMode) btnMode.innerHTML = state.pinchMode === 'speed' ? SVG_SPEED : SVG_ZOOM;
        if(btnSeekMode) btnSeekMode.innerHTML = seekMode === 'sec' ? SVG_SEC : SVG_FRAME;
        if(btnSeekVal) btnSeekVal.innerHTML = `<span>${seekMode === 'sec' ? seekSec + 's' : fpsMode + 'f'}</span>`;
        
        if (state.isScreenLocked) {
            if(shield) shield.style.display = 'block';
            if(btnMode) btnMode.classList.add('hidden-by-state');
            if(btnRot) btnRot.classList.add('hidden-by-state');
            if(btnRst) btnRst.classList.add('hidden-by-state');
            if(btnZoomRst) btnZoomRst.classList.add('hidden-by-state');
            if(btnSeekMode) btnSeekMode.classList.add('hidden-by-state');
            if(btnSeekVal) btnSeekVal.classList.add('hidden-by-state');
        } else {
            if(shield) shield.style.display = 'none';
            if(btnMode) btnMode.classList.remove('hidden-by-state');
            if(btnSeekMode) btnSeekMode.classList.remove('hidden-by-state');
            if(btnSeekVal) btnSeekVal.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(state.scale > 1.0) btnZoomRst.classList.remove('hidden-by-state');
                else btnZoomRst.classList.add('hidden-by-state');
            }
        }
    };

    const wakeUpUI = (el, video) => {
        if (!el) return; 
        if (activeUIEl && activeUIEl !== el) activeUIEl.classList.remove('gt-ui-visible');
        activeUIEl = el;
        el.classList.add('gt-ui-visible'); updateUIState(el, video);
        if (uiTimer) clearTimeout(uiTimer);
        uiTimer = setTimeout(() => { 
            if (activeUIEl) activeUIEl.classList.remove('gt-ui-visible');
            activeUIEl = null;
        }, CFG.uiTimeout);
    };
    const hideUI = (el) => { 
        if (!el) return; 
        el.classList.remove('gt-ui-visible'); 
        if (activeUIEl === el) activeUIEl = null;
        if (uiTimer) clearTimeout(uiTimer); 
    };

    const applyTransform = () => {
        if(!targetV) return;
        if (state.scale <= 1.05) { state.scale = 1.0; state.panX = 0; state.panY = 0; } 
        else {
            const maxPanX = (targetV.clientWidth * state.scale - targetV.clientWidth) / 2;
            const maxPanY = (targetV.clientHeight * state.scale - targetV.clientHeight) / 2;
            state.panX = Math.max(-maxPanX, Math.min(maxPanX, state.panX));
            state.panY = Math.max(-maxPanY, Math.min(maxPanY, state.panY));
        }
        targetV.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.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});
        const squelch = (e) => { e.stopPropagation(); e.stopImmediatePropagation(); };
        ['touchstart', 'mousedown', 'pointerdown', 'contextmenu', 'dblclick'].forEach(evt => {
            btn.addEventListener(evt, squelch, { capture: true, passive: false });
        });
    };

    const ensureUIAndWrapper = (hit) => {
        let { root, video, isNaked } = hit;
        if (isNaked && !root.classList.contains('gt-video-wrapper')) {
            const wrapper = document.createElement('div'); wrapper.className = 'gt-video-wrapper';
            if (video.style.width) wrapper.style.width = video.style.width;
            if (video.getAttribute('width')) wrapper.style.width = video.getAttribute('width');
            video.parentNode.insertBefore(wrapper, video); wrapper.appendChild(video);
            root = wrapper; hit.root = wrapper;
        }
        if (!root.querySelector('.gt-mini-progress')) {
            const bar = document.createElement('div'); bar.className = 'gt-mini-progress'; bar.innerHTML = '<div class="gt-fill"></div>';
            root.appendChild(bar); 
        }
        
        if (!video.dataset.gtTimeupdate) {
            video.addEventListener('timeupdate', () => { 
                const currentRoot = findUp(video, VIP_SELECTORS) || video.parentNode;
                const fill = currentRoot ? currentRoot.querySelector('.gt-mini-progress .gt-fill') : null;
                if (fill && video.duration) fill.style.width = `${(video.currentTime / video.duration) * 100}%`;
            });
            video.dataset.gtTimeupdate = 'true';
        }
        
        if (!root.querySelector('.gt-lock-shield')) {
            const shield = document.createElement('div');
            shield.className = 'gt-lock-shield';
            const block = (e) => { 
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); 
                wakeUpUI(getFS() || root, video);
            };
            ['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'dblclick', 'touchstart', 'touchend'].forEach(evt => shield.addEventListener(evt, block, {capture:true, passive:false}));
            root.appendChild(shield);
        }

        if (!root.querySelector('.gt-lock-btn')) {
            const rBtn = document.createElement('div'); rBtn.className = 'gt-btn-base gt-rotate-btn'; rBtn.innerHTML = `<svg viewBox="0 0 24 24" width="100%" height="100%" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><polyline points="3 3 3 8 8 8"></polyline></svg>`;
            bindTap(rBtn, () => { toggleOrientation(); wakeUpUI(root, video); }); root.appendChild(rBtn);

            const seekModeBtn = document.createElement('div'); seekModeBtn.className = 'gt-btn-base gt-seek-mode-btn';
            bindTap(seekModeBtn, () => {
                seekMode = seekMode === 'sec' ? 'frame' : 'sec';
                GM_setValue('gt_seek_mode', seekMode);
                updateUIState(root, video);
                showMsg(`已切换为: ${seekMode === 'sec' ? '按秒步进' : '逐帧步进'}`);
                wakeUpUI(root, video);
            }); root.appendChild(seekModeBtn);

            const seekValBtn = document.createElement('div'); seekValBtn.className = 'gt-btn-base gt-seek-val-btn';
            bindTap(seekValBtn, () => {
                if (seekMode === 'sec') {
                    const arr = [10, 15, 30, 1, 5];
                    let idx = arr.indexOf(seekSec);
                    seekSec = arr[(idx + 1) % arr.length];
                    GM_setValue('gt_seek_sec', seekSec);
                    showMsg(`双击步进: ${seekSec}s`);
                } else {
                    fpsMode = fpsMode === 30 ? 60 : 30;
                    GM_setValue('gt_fps', fpsMode);
                    showMsg(`帧率标准: ${fpsMode} FPS`);
                }
                updateUIState(root, video);
                wakeUpUI(root, video);
            }); root.appendChild(seekValBtn);

            const lBtn = document.createElement('div'); lBtn.className = 'gt-btn-base gt-lock-btn';
            bindTap(lBtn, () => { 
                state.isScreenLocked = !state.isScreenLocked; 
                if (state.isScreenLocked) {
                    const rect = video.getBoundingClientRect();
                    const clk = new MouseEvent('click', { bubbles: true, cancelable: true, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 });
                    video.dispatchEvent(clk);
                }
                wakeUpUI(root, video); 
            }); root.appendChild(lBtn);

            const sBtn = document.createElement('div'); sBtn.className = 'gt-btn-base gt-mode-btn';
            bindTap(sBtn, () => { state.pinchMode = state.pinchMode === 'speed' ? 'zoom' : 'speed'; wakeUpUI(root, video); showMsg(state.pinchMode === 'speed' ? '双指模式: 变速' : '双指模式: 缩平移'); }); root.appendChild(sBtn);

            const rstBtn = document.createElement('div'); rstBtn.className = 'gt-btn-base gt-reset-speed-btn'; rstBtn.innerHTML = `<span>1.0x</span>`;
            bindTap(rstBtn, () => { video.playbackRate = 1.0; showMsg('已恢复原速'); wakeUpUI(root, video); }); root.appendChild(rstBtn);

            const zoomRstBtn = document.createElement('div'); zoomRstBtn.className = 'gt-btn-base gt-reset-zoom-btn'; zoomRstBtn.innerHTML = SVG_RESET_ZOOM;
            bindTap(zoomRstBtn, () => { 
                state.scale = 1.0; state.panX = 0; state.panY = 0;
                if (video) video.style.transform = `translate(0px, 0px) scale(1)`; 
                showMsg('已恢复原大小'); wakeUpUI(root, video); 
            }); root.appendChild(zoomRstBtn);
        }
        
        const style = window.getComputedStyle(root);
        if (style.position === 'static') root.style.position = 'relative';
        return root;
    };

    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 handleAccumulatedSeek = (dir, container, video) => {
        activeSeekSide = dir;
        const stepVal = seekMode === 'sec' ? seekSec : (1 / fpsMode);
        seekAccumulator += stepVal;
        
        let displayVal = seekMode === 'sec' ? seekAccumulator : Math.round(seekAccumulator * fpsMode);
        let unit = seekMode === 'sec' ? 's' : '帧';
        
        video.currentTime = dir === 'left' ? Math.max(0, video.currentTime - stepVal) : Math.min(video.duration || 0, video.currentTime + stepVal);
        
        const oppDir = dir === 'left' ? 'right' : 'left';
        let opp = container.querySelector('#gt-seek-' + oppDir); if (opp) opp.classList.remove('show');
        let t = container.querySelector('#gt-seek-' + dir);
        if (!t) { t = document.createElement('div'); t.id = 'gt-seek-' + dir; t.className = `gt-seek-msg ${dir}`; container.appendChild(t); }
        
        if (dir === 'left') t.innerHTML = `<div class="gt-arrows"><span>‹</span><span class="gt-arrow-slide-l">‹</span></div><span class="gt-seek-text gt-pop-anim">-${displayVal}${unit}</span>`;
        else t.innerHTML = `<span class="gt-seek-text gt-pop-anim">+${displayVal}${unit}</span><div class="gt-arrows"><span class="gt-arrow-slide-r">›</span><span>›</span></div>`;
        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); 
    };

    // [核心] 黑洞捕获级 onStart,将双击事件前置处理并抹杀后遗症
    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 isBtn = findUp(e.target, '.gt-btn-base');
        const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active'));
        
        if (state.isScreenLocked && !isBtn) {
            if (isFS || (targetP && targetP.contains(e.target))) {
                if (e.cancelable) e.preventDefault(); 
                e.stopPropagation(); e.stopImmediatePropagation();
                if (targetP && targetV) wakeUpUI(getFS() || targetP, targetV); 
                lastTap = Date.now(); 
                return;
            }
        }
        
        if (isBtn) { clearTimeout(lpTimer); return; }

        let hit = identify(e); if (!hit || !hit.video) return;
        targetP = ensureUIAndWrapper(hit); targetV = hit.video;

        clearTimeout(lpTimer);
        const now = Date.now();

        // [核心绝杀机制] 如果检测到多指落下,直接将当前引发质变的 touchstart 拦截掉,YouTube 根本收不到第二根手指的信号。
        if (e.touches && e.touches.length > 1) {
            if (e.cancelable) e.preventDefault();
            e.stopPropagation(); 
            e.stopImmediatePropagation();
            lastTap = 0; // 重置可能存在的单指残留时间
        }

        // 绝对时间黑洞防御机制
        if (lastTap && (now - lastTap < 300)) {
            blockGestureUntil = now + 500; // 布置 500ms 屏障
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            
            // 强制状态回溯:弥补第一下 Tap 造成的 YouTube 意外暂停
            if (targetV) {
                if (wasPlayingBeforeSequence && targetV.paused) targetV.play().catch(()=>{});
                else if (!wasPlayingBeforeSequence && !targetV.paused) targetV.pause();
            }

            // 直接在 onStart 执行双击判定,极致跟手
            const touchX = e.touches ? e.touches[0].clientX : e.clientX;
            const ratio = touchX / window.innerWidth;
            if (ratio < 0.3) handleAccumulatedSeek('left', targetP, targetV);
            else if (ratio > 0.7) handleAccumulatedSeek('right', targetP, targetV);
            else toggleNativeFullscreen(targetP, targetV);

            lastTap = 0; 
            if (getFS()) hideUI(getFS());
            return; // 切断后续流转
        }

        wasPlayingBeforeSequence = targetV ? !targetV.paused : false;
        
        targetP.classList.add('gt-lock-touch'); targetV.classList.add('gt-lock-touch');
        isTouch = true; action = null; startX = e.touches[0].clientX; startY = e.touches[0].clientY;
        initVol = targetV.volume; initTime = targetV.currentTime; initRate = targetV.playbackRate;

        if (e.touches.length === 2) {
            const pData = getPinchData(e.touches);
            initPinchDist = pData.dist; initCenterX = pData.cx; initCenterY = pData.cy;
            initScale = state.scale; initPanX = state.panX; initPanY = state.panY; initSpeed = targetV.playbackRate;
            
            const rect = targetV.getBoundingClientRect();
            const layoutCenterX = rect.left + rect.width / 2 - initPanX;
            const layoutCenterY = rect.top + rect.height / 2 - initPanY;
            originDx = initCenterX - layoutCenterX;
            originDy = initCenterY - layoutCenterY;

            action = 'pinch'; if (getFS()) hideUI(getFS());
        } else if (e.touches.length === 1 && state.scale === 1.0) {
            lpTimer = setTimeout(() => { 
                if (isTouch) { 
                    action = 'rate'; 
                    let finalRate = Math.max(0.1, initRate + CFG.rateBase - 1.0);
                    targetV.playbackRate = finalRate; 
                    showMsg(`${finalRate.toFixed(1)}x`); 
                    if (getFS()) hideUI(getFS()); 
                } 
            }, CFG.longPress);
        }
    };

    const onMove = (e) => {
        if (state.isScreenLocked) {
            const isBtn = findUp(e.target, '.gt-btn-base');
            if (!isBtn) {
                const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active'));
                if (isFS || (targetP && targetP.contains(e.target))) {
                    if (e.cancelable) e.preventDefault();
                    e.stopPropagation(); e.stopImmediatePropagation();
                }
            }
            return;
        }

        if (!isTouch || !targetV) return;
        if (e.cancelable) e.preventDefault(); 
        
        if (action === 'pinch' && e.touches.length === 2) {
            const pData = getPinchData(e.touches);
            if (state.pinchMode === 'zoom') {
                let newScale = initScale * (pData.dist / initPinchDist);
                state.scale = Math.max(1.0, Math.min(CFG.maxScale, newScale));
                
                if (state.scale > 1.0) { 
                    const deltaS = state.scale / initScale;
                    state.panX = (pData.cx - initCenterX) + initPanX * deltaS + originDx * (1 - deltaS);
                    state.panY = (pData.cy - initCenterY) + initPanY * deltaS + originDy * (1 - deltaS);
                } else { state.panX = 0; state.panY = 0; }
                applyTransform();
            } else {
                let rawSpeed = initSpeed + ((pData.dist - initPinchDist) * 0.005);
                let finalSpeed;
                if (rawSpeed > 0.85 && rawSpeed < 1.15) finalSpeed = 1.0;
                else if (rawSpeed >= 1.15) finalSpeed = 1.0 + (rawSpeed - 1.15);
                else finalSpeed = 1.0 - (0.85 - rawSpeed);
                
                finalSpeed = Math.max(0.1, Math.min(4.0, finalSpeed));
                setRateThrottled(targetV, finalSpeed);
                showMsg(finalSpeed === 1.0 ? '1.0x (原速)' : `${finalSpeed.toFixed(2)}x`);
            }
            return;
        }

        if (action === 'pinch' || action === 'pinch_wait') return; 

        const dx = e.touches[0].clientX - startX, dy = startY - e.touches[0].clientY;

        if (action === 'rate') { 
            let gestureRate = CFG.rateBase + dx * CFG.senseRate;
            gestureRate = Math.max(0.1, Math.min(4.0, gestureRate)); 
            let finalRate = Math.max(0.1, initRate + gestureRate - 1.0); 
            targetV.playbackRate = finalRate; 
            showMsg(`${finalRate.toFixed(1)}x`); 
            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'); 
                if (getFS()) hideUI(getFS());
            } else return; 
        }

        if (action === 'seek') { targetV.currentTime = Math.max(0, Math.min(targetV.duration||0, initTime + dx * CFG.senseX)); showMsg(`${Math.floor(targetV.currentTime/60)}:${(Math.floor(targetV.currentTime%60)+'').padStart(2,'0')}`); }
        else if (action === 'vol') { targetV.volume = Math.max(0, Math.min(1, initVol + dy/innerHeight * 2 * CFG.senseY)); showMsg(`Vol: ${Math.round(targetV.volume*100)}%`); }
        else if (action === 'bri') { 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)}%`); }
    };

    const onEnd = (e) => {
        if (state.isScreenLocked) {
            const isBtn = findUp(e.target, '.gt-btn-base');
            if (!isBtn) {
                const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active'));
                if (isFS || (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; }

        clearTimeout(lpTimer); 
        if (action === 'rate' && targetV) { targetV.playbackRate = initRate; showMsg(''); wakeUpUI(targetP, targetV); }
        if ((action === 'pinch' || action === 'pinch_wait' || action === 'pan') && targetV) { wakeUpUI(targetP, targetV); }
        
        const now = Date.now(); 
        if (!action) { 
            // 单击唤醒逻辑
            lastTap = now; 
            if (!activeSeekSide) wakeUpUI(getFS() || targetP, targetV);
        }

        setTimeout(() => { if(targetP && !getFS()) targetP.classList.remove('gt-lock-touch'); if(targetV) targetV.classList.remove('gt-lock-touch'); }, 100);
        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);
    
    // [全局防御矩阵] 彻底抹除 YouTube 双击第二下的所有遗留信标
    ['pointerdown', 'pointerup', 'pointercancel', 'click', 'dblclick'].forEach(evt => {
        document.addEventListener(evt, (e) => {
            if (Date.now() < blockGestureUntil) {
                const isBtn = findUp(e.target, '.gt-btn-base');
                if (!isBtn) {
                    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                }
            } else if (evt === 'click') {
                // 正常的原生点击处理
                const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active'));
                const isLockedTarget = state.isScreenLocked && !findUp(e.target, '.gt-btn-base') && (isFS || (targetP && targetP.contains(e.target)));
                if (activeSeekSide || isLockedTarget) { 
                    e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); 
                    if (isLockedTarget && targetP && targetV) wakeUpUI(getFS() || targetP, targetV);
                }
            }
        }, { capture: true, passive: false });
    });
    
    ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'].forEach(evt => {
        document.addEventListener(evt, () => { 
            const fsEl = getFS();
            if (!fsEl) { 
                hideUI(targetP); 
                document.querySelectorAll('.gt-lock-touch').forEach(el => el.classList.remove('gt-lock-touch'));
                if (screen.orientation?.unlock) screen.orientation.unlock(); 
            } else { 
                setTimeout(() => {
                    let v = targetV || document.querySelector('video');
                    wakeUpUI(fsEl, v);
                }, 200);
            } 
        });
    });
})();