Android Double Tap to Seek Video

Adds double-tap seeking to Firefox android or any other browser efficiently

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Android Double Tap to Seek Video
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds double-tap seeking to Firefox android or any other browser efficiently
// @author       Faisal Bhuiyan
// @match        *://*/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let lastTap = 0;
    const DOUBLE_TAP_DELAY = 300;
    let overlayContainer = null;
    let isInFullscreen = false;
    let isPlaying = false;
    const CONTROLS_SAFE_ZONE = 80; // Height in pixels for controls area
    const SIDE_ZONE_WIDTH = 25; // Width percentage for side touch areas
    let toastTimeout = null;
    let lastTouchTime = 0;
    let lastTouchX = 0;
    let lastTouchY = 0;

    // Get current domain
    function getCurrentDomain() {
        return window.location.hostname;
    }

    // Check if current site is blacklisted
    function isBlacklisted() {
        const blacklist = GM_getValue('blacklistedSites', []);
        return blacklist.includes(getCurrentDomain());
    }

    // Toggle blacklist for current site
    function toggleBlacklist() {
        const domain = getCurrentDomain();
        const blacklist = GM_getValue('blacklistedSites', []);
        const isCurrentlyBlacklisted = blacklist.includes(domain);

        if (isCurrentlyBlacklisted) {
            const newBlacklist = blacklist.filter(site => site !== domain);
            GM_setValue('blacklistedSites', newBlacklist);
            showToast('Site removed from blacklist');
        } else {
            blacklist.push(domain);
            GM_setValue('blacklistedSites', blacklist);
            showToast('Site added to blacklist');
            removeExistingOverlays();
        }
    }

    // Register menu command
    GM_registerMenuCommand('Toggle Blacklist for Current Site', toggleBlacklist);

    function showToast(message) {
        let toast = document.getElementById('video-seeker-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'video-seeker-toast';
            toast.style.cssText = `
                position: fixed !important;
                top: 50% !important;
                left: 50% !important;
                transform: translate(-50%, -50%) !important;
                background: rgba(0, 0, 0, 0.7) !important;
                color: white !important;
                padding: 10px 20px !important;
                border-radius: 5px !important;
                z-index: 2147483647 !important;
                pointer-events: none !important;
                transition: opacity 0.3s !important;
                font-family: Arial, sans-serif !important;
            `;
            document.body.appendChild(toast);
        }

        toast.textContent = message;
        toast.style.opacity = '1';

        if (toastTimeout) clearTimeout(toastTimeout);
        toastTimeout = setTimeout(() => {
            toast.style.opacity = '0';
        }, 2000);
    }

    function removeExistingOverlays() {
        const existingOverlays = document.querySelectorAll('#video-overlay-container');
        existingOverlays.forEach(overlay => overlay.remove());
        overlayContainer = null;
    }

    function createOverlayContainer() {
        removeExistingOverlays();

        const container = document.createElement('div');
        container.id = 'video-overlay-container';

        container.style.cssText = `
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            right: 0 !important;
            bottom: ${CONTROLS_SAFE_ZONE}px !important;
            width: 100% !important;
            height: calc(100% - ${CONTROLS_SAFE_ZONE}px) !important;
            pointer-events: auto !important;
            z-index: 2147483647 !important;
            background: transparent !important;
            touch-action: manipulation !important;
            display: ${isPlaying ? 'block' : 'none'} !important;
        `;

        const touchAreas = ['left', 'right'].map((position) => {
            const area = document.createElement('div');
            area.id = `touch-area-${position}`;

            area.style.cssText = `
                position: absolute !important;
                ${position}: 0 !important;
                top: 0 !important;
                width: ${SIDE_ZONE_WIDTH}% !important;
                height: 100% !important;
                pointer-events: auto !important;
                background: rgba(255, 255, 255, 0.01) !important;
                z-index: 2147483647 !important;
                touch-action: manipulation !important;
            `;

            function handleTouch(e) {
                try {
                    if (!isPlaying || !isInFullscreen || isBlacklisted()) return;

                    const currentTime = new Date().getTime();
                    const touch = e.touches && e.touches[0];

                    if (!touch && e.type === 'touchstart') return;

                    if (e.type === 'touchstart') {
                        const tapLength = currentTime - lastTap;
                        const touchX = touch.clientX;
                        const touchY = touch.clientY;

                        // Check if this is a double tap (time and position)
                        const isDoubleTap = tapLength < DOUBLE_TAP_DELAY &&
                            Math.abs(touchX - lastTouchX) < 30 &&
                            Math.abs(touchY - lastTouchY) < 30;

                        if (isDoubleTap) {
                            const video = document.querySelector('video');
                            if (video) {
                                const seekAmount = position === 'right' ? 10 : -10;

                                if (video.player && typeof video.player.currentTime === 'function') {
                                    video.player.currentTime(video.player.currentTime() + seekAmount);
                                } else {
                                    video.currentTime += seekAmount;
                                }

                                showToast(`${seekAmount > 0 ? '+' : ''}${seekAmount} seconds`);
                            }
                            e.preventDefault();
                            e.stopPropagation();
                        }

                        // Update last touch info
                        lastTap = currentTime;
                        lastTouchX = touchX;
                        lastTouchY = touchY;
                    }
                } catch (err) {
                    console.log('Touch handler error:', err);
                }
            }

            ['touchstart', 'touchend'].forEach(eventType => {
                area.addEventListener(eventType, handleTouch, {
                    passive: false,
                    capture: true
                });
            });

            return area;
        });

        touchAreas.forEach(area => container.appendChild(area));
        return container;
    }

    function updateOverlayVisibility() {
        if (overlayContainer) {
            overlayContainer.style.display = (isInFullscreen && isPlaying) ? 'block' : 'none';
        }
    }

    function attachOverlay() {
        const fullscreenElement =
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.querySelector('.video-js.vjs-fullscreen') ||
            document.querySelector('video');

        if (fullscreenElement && isInFullscreen) {
            overlayContainer = createOverlayContainer();

            const container =
                fullscreenElement.querySelector('.vjs-tech-container') ||
                fullscreenElement.querySelector('.video-js') ||
                fullscreenElement;

            if (container) {
                container.appendChild(overlayContainer);
                updateOverlayVisibility();
            }
        }
    }

    function handleFullscreenChange() {
        const isNowFullscreen = !!(
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.querySelector('.video-js.vjs-fullscreen')
        );

        if (isNowFullscreen && !isInFullscreen) {
            isInFullscreen = true;
            setTimeout(attachOverlay, 100);
            showToast('Double-tap controls enabled');
        } else if (!isNowFullscreen && isInFullscreen) {
            isInFullscreen = false;
            removeExistingOverlays();
            showToast('Double-tap controls disabled');
        }
    }

    function handlePlayPause(video) {
        isPlaying = !(video.paused || video.ended || video.seeking || video.readyState < 3);
        updateOverlayVisibility();
    }

    // Cleanup function for removing events
    function cleanup() {
        removeExistingOverlays();
        isInFullscreen = false;
        isPlaying = false;
    }

    // Listen for fullscreen changes
    document.addEventListener('fullscreenchange', handleFullscreenChange, true);
    document.addEventListener('webkitfullscreenchange', handleFullscreenChange, true);

    // Listen for page visibility changes
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            cleanup();
        }
    });

    // Watch for video elements
    const observer = new MutationObserver((mutations, obs) => {
        const video = document.querySelector('video');
        if (video) {
            // Listen for all playback state changes
            ['play', 'pause', 'seeking', 'seeked', 'waiting', 'playing'].forEach(eventType => {
                video.addEventListener(eventType, () => handlePlayPause(video));
            });

            video.addEventListener('webkitbeginfullscreen', () => {
                isInFullscreen = true;
                setTimeout(attachOverlay, 100);
            });

            video.addEventListener('webkitendfullscreen', () => {
                cleanup();
            });

            // Monitor video.js fullscreen class changes
            const videoJs = document.querySelector('.video-js');
            if (videoJs) {
                const classObserver = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                            const isNowFullscreen = videoJs.classList.contains('vjs-fullscreen');
                            if (isNowFullscreen !== isInFullscreen) {
                                handleFullscreenChange();
                            }
                        }
                    });
                });

                classObserver.observe(videoJs, {
                    attributes: true,
                    attributeFilter: ['class']
                });
            }

            // Initial state check
            handlePlayPause(video);

            obs.disconnect();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();