Android Swipe to Seek Video

Adds swipe seeking to Firefox android or any other browser efficiently

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Android Swipe to Seek Video
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds swipe seeking to Firefox android or any other browser efficiently
// @author       Modified by Assistant
// @match        *://*/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration with default values
    const DEFAULT_CONFIG = {
        // Base seconds to seek per 100 pixels of swipe
        seekRate: 10,
        // Maximum seconds that can be seeked in one swipe
        maxSeekAmount: 60
    };

    // Get config with defaults
    function getConfig() {
        const savedConfig = GM_getValue('seekConfig', DEFAULT_CONFIG);
        return { ...DEFAULT_CONFIG, ...savedConfig };
    }

    // Save config
    function saveConfig(config) {
        GM_setValue('seekConfig', { ...DEFAULT_CONFIG, ...config });
    }

    // Config UI
    function showConfigUI() {
        const config = getConfig();
        const dialog = document.createElement('div');
        dialog.style.cssText = `
            position: fixed !important;
            top: 50% !important;
            left: 50% !important;
            transform: translate(-50%, -50%) !important;
            background: white !important;
            padding: 20px !important;
            border-radius: 8px !important;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
            z-index: 2147483647 !important;
            width: 300px !important;
            font-family: Arial, sans-serif !important;
        `;

        dialog.innerHTML = `
            <h2 style="margin: 0 0 15px 0 !important; font-size: 18px !important;">Swipe Seek Configuration</h2>
            <div style="margin-bottom: 15px !important;">
                <label style="display: block !important; margin-bottom: 5px !important;">
                    Seek Rate (seconds per 100px swipe):
                    <input type="number" id="seekRate" value="${config.seekRate}" 
                           style="width: 100% !important; padding: 5px !important; margin-top: 5px !important;">
                </label>
            </div>
            <div style="margin-bottom: 15px !important;">
                <label style="display: block !important; margin-bottom: 5px !important;">
                    Maximum Seek Amount (seconds):
                    <input type="number" id="maxSeekAmount" value="${config.maxSeekAmount}"
                           style="width: 100% !important; padding: 5px !important; margin-top: 5px !important;">
                </label>
            </div>
            <div style="text-align: right !important;">
                <button id="cancelConfig" style="margin-right: 10px !important; padding: 5px 10px !important;">Cancel</button>
                <button id="saveConfig" style="padding: 5px 10px !important;">Save</button>
            </div>
        `;

        document.body.appendChild(dialog);

        document.getElementById('cancelConfig').onclick = () => dialog.remove();
        document.getElementById('saveConfig').onclick = () => {
            const newConfig = {
                seekRate: parseFloat(document.getElementById('seekRate').value),
                maxSeekAmount: parseFloat(document.getElementById('maxSeekAmount').value)
            };
            saveConfig(newConfig);
            dialog.remove();
            showToast('Configuration saved');
        };
    }

    // Register config menu
    GM_registerMenuCommand('Configure Swipe Seek', showConfigUI);

    let isInFullscreen = false;
    let isPlaying = false;
    let initialX = null;
    let currentSeekAmount = 0;
    let seekIndicator = null;
    
    // 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');
        }
    }

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

    function showToast(message) {
        if (!seekIndicator) {
            seekIndicator = createSeekIndicator();
        }
        seekIndicator.textContent = message;
        seekIndicator.style.opacity = '1';
        setTimeout(() => {
            seekIndicator.style.opacity = '0';
        }, 2000);
    }

    function createSeekIndicator() {
        const indicator = document.createElement('div');
        indicator.id = 'video-seek-indicator';
        indicator.style.cssText = `
            position: fixed !important;
            top: 50% !important;
            left: 50% !important;
            transform: translate(-50%, -50%) !important;
            background: rgba(0, 0, 0, 0.8) !important;
            color: white !important;
            padding: 15px 25px !important;
            border-radius: 25px !important;
            font-size: 18px !important;
            font-family: Arial, sans-serif !important;
            z-index: 2147483647 !important;
            pointer-events: none !important;
            opacity: 0 !important;
            transition: opacity 0.2s !important;
            display: flex !important;
            align-items: center !important;
            gap: 10px !important;
        `;
        document.body.appendChild(indicator);
        return indicator;
    }

    function updateSeekIndicator(seekAmount) {
        if (!seekIndicator) {
            seekIndicator = createSeekIndicator();
        }

        const arrow = seekAmount > 0 ? '→' : '←';
        const absAmount = Math.abs(seekAmount);
        seekIndicator.textContent = `${arrow} ${absAmount.toFixed(1)}s`;
        seekIndicator.style.opacity = '1';
        seekIndicator.style.borderLeft = `4px solid ${seekAmount > 0 ? '#4CAF50' : '#f44336'}`;
    }

    function hideSeekIndicator() {
        if (seekIndicator) {
            seekIndicator.style.opacity = '0';
        }
    }

    function calculateSeekAmount(deltaX) {
        const config = getConfig();
        // Convert pixel distance to seconds based on seekRate
        const rawAmount = (deltaX / 100) * config.seekRate;
        // Clamp the seek amount to the configured maximum
        return Math.max(Math.min(rawAmount, config.maxSeekAmount), -config.maxSeekAmount);
    }

    function handleTouch(video, event) {
        if (!isInFullscreen || isBlacklisted()) return;

        switch(event.type) {
            case 'touchstart':
                initialX = event.touches[0].clientX;
                currentSeekAmount = 0;
                break;

            case 'touchmove':
                if (initialX === null) return;

                const currentX = event.touches[0].clientX;
                const deltaX = currentX - initialX;
                
                currentSeekAmount = calculateSeekAmount(deltaX);
                updateSeekIndicator(currentSeekAmount);
                event.preventDefault();
                break;

            case 'touchend':
                if (initialX !== null && currentSeekAmount !== 0) {
                    if (video.player && typeof video.player.currentTime === 'function') {
                        video.player.currentTime(video.player.currentTime() + currentSeekAmount);
                    } else {
                        video.currentTime += currentSeekAmount;
                    }

                    setTimeout(hideSeekIndicator, 500);
                }
                initialX = null;
                currentSeekAmount = 0;
                break;
        }
    }

    function attachTouchHandlers(video) {
        const touchHandler = (event) => handleTouch(video, event);
        
        video.addEventListener('touchstart', touchHandler, { passive: true });
        video.addEventListener('touchmove', touchHandler, { passive: false });
        video.addEventListener('touchend', touchHandler, { passive: true });
    }

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

        if (isNowFullscreen && !isInFullscreen) {
            isInFullscreen = true;
            const video = document.querySelector('video');
            if (video) attachTouchHandlers(video);
        } else if (!isNowFullscreen) {
            isInFullscreen = false;
            hideSeekIndicator();
        }
    }

    // Watch for video elements
    const observer = new MutationObserver((mutations, obs) => {
        const video = document.querySelector('video');
        if (video) {
            attachTouchHandlers(video);

            video.addEventListener('webkitbeginfullscreen', () => {
                isInFullscreen = true;
                attachTouchHandlers(video);
            });

            video.addEventListener('webkitendfullscreen', () => {
                isInFullscreen = false;
                hideSeekIndicator();
            });

            const videoJs = document.querySelector('.video-js');
            if (videoJs) {
                const classObserver = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                            handleFullscreenChange();
                        }
                    });
                });

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

            obs.disconnect();
        }
    });

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

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