☰

Video Detector & Player (Universal)

Detects videos on any site with universal network interception, HLS support, pagination, resizable UI, and optimized players.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Video Detector & Player (Universal)
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  Detects videos on any site with universal network interception, HLS support, pagination, resizable UI, and optimized players.
// @author       Manus
// @match        *://*/*
// @grant        GM_download
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js
// @connect      *
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CONFIG = {
        autoSaveDir: GM_getValue('autoSaveDir', 'Video_Downloads'),
        maxConcurrentDownloads: 3,
        videoExtensions: new Set(['.mp4', '.webm', '.mkv', '.mov', '.m4v', '.m3u8', '.mpd', '.ts', '.avi', '.flv', '.wmv']),
        videoKeywords: new Set(['video', 'media', 'stream', 'play', 'movie', 'film', 'clip', 'content']),
        batchSize: 10,
        scanDebounce: 500,
        itemsPerPage: GM_getValue('itemsPerPage', 6),
        currentPage: 1,
        panelSize: {
            width: GM_getValue('panelWidth', 380),
            height: GM_getValue('panelHeight', 600)
        }
    };

    // --- Styles (optimized with CSS containment and custom player controls) ---
    GM_addStyle(`
        #vd-root-container {
            all: initial;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            box-sizing: border-box;
            contain: content;
        }
        #vd-root-container * {
            box-sizing: border-box !important;
            margin: 0;
            padding: 0;
        }

        /* Main Panel */
        .vd-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: #1a1a1a;
            color: #ffffff;
            border: 1px solid #333;
            border-radius: 12px;
            padding: 16px;
            z-index: 2147483640;
            width: ${CONFIG.panelSize.width}px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.5);
            display: flex;
            flex-direction: column;
            gap: 12px;
            transition: transform 0.2s ease, opacity 0.2s ease;
            will-change: transform, opacity;
            contain: layout style;
            resize: both;
            overflow: auto;
            min-width: 280px;
            min-height: 200px;
            max-width: 90vw;
            max-height: 90vh;
        }
        .vd-panel.vd-hidden {
            transform: translateY(150%);
            opacity: 0;
            pointer-events: none;
        }

        /* Panel Resize Controls */
        .vd-resize-controls {
            display: flex;
            gap: 8px;
            align-items: center;
            justify-content: flex-end;
            padding: 4px 0;
        }
        .vd-resize-btn {
            background: #333;
            border: 1px solid #555;
            color: #fff;
            padding: 4px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s;
        }
        .vd-resize-btn:hover {
            background: #444;
        }

        .vd-panel .header { display: flex; justify-content: space-between; align-items: center; }
        .vd-panel .title { font-weight: bold; font-size: 16px; color: #4CAF50; }
        .vd-panel .close-btn { cursor: pointer; font-size: 18px; color: #888; padding: 4px; }
        .vd-panel .close-btn:hover { color: #fff; }
        .vd-panel .stats { font-size: 13px; color: #aaa; }
        .vd-panel .action-btn {
            background: #4CAF50;
            color: white;
            border: none;
            padding: 10px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: 14px;
            text-align: center;
            transition: background 0.2s;
        }
        .vd-panel .action-btn:hover { background: #45a049; }
        .vd-panel .secondary-btn { background: #333; margin-top: -4px; }
        .vd-panel .secondary-btn:hover { background: #444; }

        /* Overlay/Modal */
        .vd-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0,0,0,0.85);
            z-index: 2147483647;
            display: none;
            justify-content: center;
            align-items: center;
            backdrop-filter: blur(4px);
            contain: strict;
        }
        .vd-modal {
            background: #111;
            width: 95%;
            height: 95%;
            max-width: 1600px;
            border-radius: 12px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
            border: 1px solid #333;
            box-shadow: 0 20px 60px rgba(0,0,0,0.8);
            contain: content;
        }
        .vd-modal-header {
            padding: 16px 24px;
            background: #1a1a1a;
            border-bottom: 1px solid #333;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
        }
        .vd-modal-title { font-size: 18px; font-weight: bold; color: #fff; }
        .vd-modal-close { cursor: pointer; font-size: 24px; color: #888; }

        /* Grid Layout */
        .vd-grid {
            flex: 1;
            padding: 24px;
            overflow-y: auto;
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 20px;
            background: #0a0a0a;
            align-content: start;
            contain: content;
            will-change: scroll-position;
        }

        /* Video Card */
        .vd-card {
            background: #1a1a1a;
            border: 1px solid #333;
            border-radius: 8px;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            height: fit-content;
            contain: layout style;
            transition: border-color 0.2s;
        }
        .vd-card:hover {
            border-color: #555;
        }

        /* Custom Video Player */
        .vd-video-container {
            width: 100%;
            aspect-ratio: 16/9;
            background: #000;
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
            contain: strict;
            cursor: pointer;
        }
        .vd-video-container video {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: contain;
            max-height: 100%;
        }

        /* Custom Controls */
        .vd-player-controls {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            background: linear-gradient(transparent, rgba(0,0,0,0.8));
            padding: 20px 10px 5px;
            opacity: 0;
            transition: opacity 0.3s;
            pointer-events: none;
        }
        .vd-video-container:hover .vd-player-controls,
        .vd-video-container.vd-controls-visible .vd-player-controls {
            opacity: 1;
            pointer-events: auto;
        }

        .vd-progress-bar {
            width: 100%;
            height: 4px;
            background: rgba(255,255,255,0.2);
            cursor: pointer;
            margin-bottom: 8px;
            position: relative;
        }
        .vd-progress-filled {
            height: 100%;
            background: #4CAF50;
            width: 0%;
            transition: width 0.1s;
        }
        .vd-controls-row {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
        .vd-control-btn {
            background: rgba(255,255,255,0.1);
            border: none;
            color: white;
            padding: 4px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s;
            white-space: nowrap;
        }
        .vd-control-btn:hover {
            background: rgba(255,255,255,0.2);
        }
        .vd-time-display {
            color: white;
            font-size: 11px;
            margin-left: auto;
        }
        .vd-speed-menu {
            position: absolute;
            bottom: 40px;
            right: 10px;
            background: #333;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 4px 0;
            display: none;
            z-index: 10;
        }
        .vd-speed-menu.active {
            display: block;
        }
        .vd-speed-option {
            padding: 6px 16px;
            cursor: pointer;
            font-size: 12px;
            color: #fff;
        }
        .vd-speed-option:hover {
            background: #444;
        }

        .vd-placeholder {
            position: absolute;
            color: #555;
            font-size: 14px;
            text-align: center;
            z-index: 1;
            pointer-events: none;
        }

        .vd-card-info { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
        .vd-card-name { font-size: 13px; color: #eee; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; }
        .vd-card-url { font-size: 11px; color: #666; word-break: break-all; max-height: 32px; overflow: hidden; line-height: 1.4; }
        .vd-url-copy-btn {
            background: #2a2a2a;
            border: 1px solid #444;
            color: #ccc;
            padding: 4px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 11px;
            margin-top: 4px;
        }
        .vd-url-copy-btn:hover {
            background: #3a3a3a;
        }
        .vd-card-actions { display: flex; gap: 8px; margin-top: 4px; }
        .vd-card-btn {
            flex: 1;
            padding: 8px 4px;
            font-size: 12px;
            border-radius: 4px;
            border: 1px solid #444;
            background: #2a2a2a;
            color: #ccc;
            cursor: pointer;
            text-align: center;
            font-weight: 500;
            transition: all 0.15s;
        }
        .vd-card-btn:hover { background: #3a3a3a; color: #fff; border-color: #666; }
        .vd-card-btn.primary { background: #4CAF50; border-color: #4CAF50; color: #fff; }
        .vd-card-btn.primary:hover { background: #45a049; }

        /* Pagination */
        .vd-pagination {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            padding: 12px;
            background: #1a1a1a;
            border-top: 1px solid #333;
            flex-shrink: 0;
        }
        .vd-page-btn {
            background: #333;
            border: 1px solid #555;
            color: #fff;
            padding: 6px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 13px;
            transition: background 0.2s;
            min-width: 35px;
        }
        .vd-page-btn:hover:not(:disabled) {
            background: #444;
        }
        .vd-page-btn.active {
            background: #4CAF50;
            border-color: #4CAF50;
        }
        .vd-page-btn:disabled {
            opacity: 0.3;
            cursor: not-allowed;
        }
        .vd-page-info {
            color: #aaa;
            font-size: 13px;
            margin: 0 8px;
        }

        .vd-modal-footer {
            padding: 16px 24px;
            background: #1a1a1a;
            border-top: 1px solid #333;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
        }
        .vd-stats-footer { font-size: 14px; color: #aaa; }

        /* Mobile adjustments */
        @media (max-width: 600px) {
            .vd-grid { grid-template-columns: 1fr; padding: 12px; }
            .vd-modal { width: 100%; height: 100%; border-radius: 0; }
            .vd-panel { width: calc(100% - 40px); left: 20px; right: 20px; bottom: 10px; }
            .vd-player-controls { padding: 15px 5px 3px; }
            .vd-control-btn { padding: 3px 6px; font-size: 10px; }
        }
    `);

    // --- State Management ---
    const state = {
        videos: new Map(),
        isModalOpen: false,
        observer: null,
        intersectionObserver: null,
        hlsPlayers: new Set(),
        scanTimer: null,
        lastScanTime: 0,
        currentPage: CONFIG.currentPage,
        itemsPerPage: CONFIG.itemsPerPage,
        totalPages: 1,
        activePlayers: new Set(),
        playerTimers: new Map()
    };

    // --- Helper Functions ---
    const getFileName = (url, index) => {
        try {
            const urlObj = new URL(url);
            let name = urlObj.pathname.split('/').pop() || '';
            name = name.split('?')[0];

            if (!name || !name.includes('.') || name.length < 3) {
                const ext = url.includes('.m3u8') ? '.m3u8' : (url.includes('.mpd') ? '.mpd' : '.mp4');
                name = `video_${index}${ext}`;
            }
            return decodeURIComponent(name).replace(/[^a-zA-Z0-9._-]/g, '_');
        } catch {
            return `video_${index}.mp4`;
        }
    };

    const copyToClipboard = async (text) => {
        try {
            if (navigator.clipboard && window.isSecureContext) {
                await navigator.clipboard.writeText(text);
                return true;
            }
        } catch {}

        const el = document.createElement('textarea');
        el.value = text;
        el.style.position = 'fixed';
        el.style.opacity = '0';
        document.body.appendChild(el);
        el.select();
        document.execCommand('copy');
        document.body.removeChild(el);
        return true;
    };

    // --- Network Interception (optimized) ---
    function initNetworkSniffer() {
        const videoCheckBuffer = [];
        let bufferTimer = null;

        const processVideoBuffer = () => {
            if (videoCheckBuffer.length === 0) return;
            const urlsToCheck = videoCheckBuffer.splice(0);
            urlsToCheck.forEach(url => checkAndAddVideo(url));
        };

        const queueVideoCheck = (url) => {
            if (!url || typeof url !== 'string') return;
            const lowerUrl = url.toLowerCase();
            if (!isPotentialVideoUrl(lowerUrl)) return;

            videoCheckBuffer.push(url);
            if (videoCheckBuffer.length > 100) {
                processVideoBuffer();
                return;
            }

            if (!bufferTimer) {
                bufferTimer = setTimeout(() => {
                    bufferTimer = null;
                    processVideoBuffer();
                }, 100);
            }
        };

        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            try {
                const url = typeof args[0] === 'string' ? args[0] :
                           (args[0] instanceof URL ? args[0].href :
                           (args[0]?.url || ''));
                queueVideoCheck(url);
            } catch {}
            return originalFetch.apply(this, args);
        };

        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            try {
                queueVideoCheck(url);
            } catch {}
            return originalOpen.apply(this, arguments);
        };
    }

    const isPotentialVideoUrl = (lowerUrl) => {
        if (CONFIG.videoExtensions.has(lowerUrl.slice(-4)) ||
            CONFIG.videoExtensions.has(lowerUrl.slice(-5))) {
            return true;
        }

        return lowerUrl.includes('.m3u8') ||
               lowerUrl.includes('.mpd') ||
               lowerUrl.includes('googlevideo.com') ||
               lowerUrl.includes('/video/') ||
               lowerUrl.includes('videoplayback') ||
               lowerUrl.includes('video_url');
    };

    function checkAndAddVideo(url) {
        if (!url || typeof url !== 'string' || state.videos.has(url)) return;

        const lowerUrl = url.toLowerCase();

        if (lowerUrl.includes('analytics') ||
            lowerUrl.includes('log') ||
            lowerUrl.includes('tracking') ||
            lowerUrl.length < 10) return;

        if (!isPotentialVideoUrl(lowerUrl)) return;

        if (CONFIG.videoExtensions.has(lowerUrl.slice(-4))) {
            if (lowerUrl.endsWith('.m4a') || lowerUrl.endsWith('.mp3')) return;
        }

        state.videos.set(url, {
            url,
            addedAt: Date.now(),
            selected: true,
            playerInstance: null
        });

        updateUI();
    }

    // --- Core Logic ---
    function scanForVideos() {
        const now = Date.now();
        if (now - state.lastScanTime < CONFIG.scanDebounce) return;
        state.lastScanTime = now;

        if (window.requestIdleCallback) {
            requestIdleCallback(() => performScan());
        } else {
            setTimeout(performScan, 0);
        }
    }

    function performScan() {
        const videoUrls = new Set();

        document.querySelectorAll('video[src], video source[src]').forEach(el => {
            const src = el.tagName === 'SOURCE' ? el.src : el.currentSrc || el.src;
            if (src && !src.startsWith('blob:')) {
                videoUrls.add(src);
            }
        });

        const links = document.querySelectorAll('a[href]');
        for (let i = 0; i < links.length; i++) {
            const href = links[i].href;
            if (href && isPotentialVideoUrl(href.toLowerCase())) {
                videoUrls.add(href);
            }
        }

        const pageText = document.documentElement.innerHTML;
        const videoRegex = /https?:\/\/[^\s"'<>]+?\.(mp4|m3u8|webm|mkv|mov|mpd|avi|flv)(?:\?[^\s"'<>]*)?/gi;
        let match;
        while ((match = videoRegex.exec(pageText)) !== null) {
            videoUrls.add(match[0]);
        }

        const urlsArray = Array.from(videoUrls);
        for (let i = 0; i < urlsArray.length; i += CONFIG.batchSize) {
            const batch = urlsArray.slice(i, i + CONFIG.batchSize);
            setTimeout(() => {
                batch.forEach(url => checkAndAddVideo(url));
            }, 0);
        }
    }

    function initObservers() {
        state.observer = new MutationObserver((mutations) => {
            let shouldScan = false;
            for (let i = 0; i < mutations.length; i++) {
                const mutation = mutations[i];
                if (mutation.addedNodes.length > 0) {
                    for (let j = 0; j < mutation.addedNodes.length; j++) {
                        const node = mutation.addedNodes[j];
                        if (node.nodeType === 1) {
                            const tagName = node.tagName?.toLowerCase();
                            if (tagName === 'video' || tagName === 'source' ||
                                tagName === 'a' || tagName === 'div' ||
                                tagName === 'iframe' || tagName === 'script') {
                                shouldScan = true;
                                break;
                            }
                        }
                    }
                    if (shouldScan) break;
                }
            }

            if (shouldScan) {
                clearTimeout(state.scanTimer);
                state.scanTimer = setTimeout(scanForVideos, CONFIG.scanDebounce);
            }
        });

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

        state.intersectionObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                const video = entry.target;
                if (entry.isIntersecting && video.dataset.src && !video.dataset.loaded) {
                    loadVideoSource(video, video.dataset.src);
                    state.intersectionObserver.unobserve(video);
                } else if (!entry.isIntersecting && !video.paused) {
                    // Pause video when out of view to save resources
                    video.pause();
                }
            });
        }, {
            threshold: 0.1,
            rootMargin: '100px'
        });
    }

    function loadVideoSource(videoEl, url) {
        videoEl.dataset.loaded = "true";
        if (url.includes('.m3u8') && typeof Hls !== 'undefined' && Hls.isSupported()) {
            const hls = new Hls({
                enableWorker: true,
                lowLatencyMode: true,
                maxBufferLength: 30,
                maxMaxBufferLength: 60
            });
            hls.loadSource(url);
            hls.attachMedia(videoEl);
            state.hlsPlayers.add(hls);
        } else {
            videoEl.src = url;
            videoEl.load();
        }
    }

    // --- UI Components ---
    let panelEl, overlayEl, gridEl;

    function createUI() {
        if (document.getElementById('vd-root-container')) return;

        const root = document.createElement('div');
        root.id = 'vd-root-container';
        document.body.appendChild(root);

        // Panel with resize controls
        panelEl = document.createElement('div');
        panelEl.className = 'vd-panel vd-hidden';
        root.appendChild(panelEl);

        // Overlay/Modal
        overlayEl = document.createElement('div');
        overlayEl.className = 'vd-overlay';
        overlayEl.innerHTML = `
            <div class="vd-modal">
                <div class="vd-modal-header">
                    <div class="vd-modal-title">Detected Videos</div>
                    <div class="vd-modal-close">✕</div>
                </div>
                <div class="vd-grid"></div>
                <div class="vd-pagination"></div>
                <div class="vd-modal-footer">
                    <div class="vd-stats-footer">0 videos selected</div>
                    <div style="display: flex; gap: 12px;">
                        <button class="vd-card-btn" id="vd-btn-select-all">Select All</button>
                        <button class="vd-card-btn primary" id="vd-btn-download-zip">Download Selected (ZIP)</button>
                    </div>
                </div>
            </div>
        `;
        root.appendChild(overlayEl);
        gridEl = overlayEl.querySelector('.vd-grid');

        // Event listeners
        overlayEl.addEventListener('click', handleOverlayClick);
        overlayEl.querySelector('#vd-btn-select-all').addEventListener('click', handleSelectAll);
        overlayEl.querySelector('#vd-btn-download-zip').addEventListener('click', downloadZip);

        initNetworkSniffer();
        scanForVideos();
    }

    function updateUI() {
        if (!panelEl) return;
        const count = state.videos.size;
        if (count > 0) {
            panelEl.classList.remove('vd-hidden');
            panelEl.innerHTML = `
                <div class="header">
                    <div class="title">Video Detector</div>
                    <div class="close-btn">✕</div>
                </div>
                <div class="stats">${count} video(s) detected</div>
                <div class="vd-resize-controls">
                    <button class="vd-resize-btn" data-action="width-minus">W-</button>
                    <button class="vd-resize-btn" data-action="width-plus">W+</button>
                    <button class="vd-resize-btn" data-action="height-minus">H-</button>
                    <button class="vd-resize-btn" data-action="height-plus">H+</button>
                </div>
                <button class="action-btn">Review Videos</button>
                <button class="action-btn secondary-btn">Download All</button>
            `;

            panelEl.querySelector('.close-btn').addEventListener('click', (e) => {
                e.stopPropagation();
                panelEl.classList.add('vd-hidden');
            });

            panelEl.querySelector('.action-btn').addEventListener('click', () => toggleModal(true));
            panelEl.querySelector('.secondary-btn').addEventListener('click', downloadZip);

            // Resize controls
            panelEl.querySelectorAll('.vd-resize-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    handleResize(e.target.dataset.action);
                });
            });
        }
    }

    function handleResize(action) {
        const currentWidth = panelEl.offsetWidth;
        const currentHeight = panelEl.offsetHeight;
        const step = 50;

        switch(action) {
            case 'width-plus':
                CONFIG.panelSize.width = Math.min(currentWidth + step, window.innerWidth * 0.9);
                GM_setValue('panelWidth', CONFIG.panelSize.width);
                break;
            case 'width-minus':
                CONFIG.panelSize.width = Math.max(currentWidth - step, 280);
                GM_setValue('panelWidth', CONFIG.panelSize.width);
                break;
            case 'height-plus':
                CONFIG.panelSize.height = Math.min(currentHeight + step, window.innerHeight * 0.9);
                GM_setValue('panelHeight', CONFIG.panelSize.height);
                break;
            case 'height-minus':
                CONFIG.panelSize.height = Math.max(currentHeight - step, 200);
                GM_setValue('panelHeight', CONFIG.panelSize.height);
                break;
        }

        panelEl.style.width = CONFIG.panelSize.width + 'px';
        panelEl.style.height = CONFIG.panelSize.height + 'px';
    }

    function handleOverlayClick(e) {
        const target = e.target;

        if (target.classList.contains('vd-modal-close') || target === overlayEl) {
            toggleModal(false);
            return;
        }

        // Player controls
        if (target.classList.contains('vd-control-btn')) {
            handlePlayerControl(target);
            return;
        }

        if (target.classList.contains('vd-progress-bar') || target.classList.contains('vd-progress-filled')) {
            handleProgressBarClick(target, e);
            return;
        }

        if (target.classList.contains('vd-speed-option')) {
            handleSpeedSelection(target);
            return;
        }

        if (target.classList.contains('vd-url-copy-btn')) {
            handleUrlCopy(target);
            return;
        }

        const card = target.closest('.vd-card');
        if (!card) return;

        const videoUrl = card.dataset.url;
        const video = state.videos.get(videoUrl);
        if (!video) return;

        if (target.classList.contains('vd-copy')) {
            copyToClipboard(videoUrl);
            const oldText = target.textContent;
            target.textContent = 'Copied!';
            setTimeout(() => target.textContent = oldText, 2000);
        } else if (target.classList.contains('vd-open')) {
            window.open(videoUrl, '_blank');
        } else if (target.classList.contains('vd-dl')) {
            GM_download({
                url: videoUrl,
                name: getFileName(videoUrl, Array.from(state.videos.keys()).indexOf(videoUrl)),
                saveAs: true
            });
        } else if (target.classList.contains('vd-checkbox')) {
            video.selected = target.checked;
            updateStats();
        }
    }

    function handlePlayerControl(btn) {
        const videoContainer = btn.closest('.vd-video-container');
        const video = videoContainer?.querySelector('video');
        if (!video) return;

        const action = btn.dataset.action;
        const card = btn.closest('.vd-card');
        const videoUrl = card?.dataset.url;

        switch(action) {
            case 'play-pause':
                if (video.paused) {
                    video.play();
                    btn.textContent = '⏸';
                } else {
                    video.pause();
                    btn.textContent = '▶';
                }
                break;
            case 'fullscreen':
                if (videoContainer.requestFullscreen) {
                    videoContainer.requestFullscreen();
                } else if (video.webkitRequestFullscreen) {
                    video.webkitRequestFullscreen();
                }
                break;
            case 'speed':
                toggleSpeedMenu(btn);
                break;
            case 'zoom-in':
                video.style.transform = `scale(${Math.min(getVideoScale(video) + 0.1, 2)})`;
                break;
            case 'zoom-out':
                video.style.transform = `scale(${Math.max(getVideoScale(video) - 0.1, 0.5)})`;
                break;
            case 'color':
                cycleColorFilter(video);
                break;
        }
    }

    function getVideoScale(video) {
        const transform = video.style.transform;
        const match = transform.match(/scale\(([\d.]+)\)/);
        return match ? parseFloat(match[1]) : 1;
    }

    function toggleSpeedMenu(btn) {
        const container = btn.closest('.vd-video-container');
        const speedMenu = container.querySelector('.vd-speed-menu');
        if (speedMenu) {
            speedMenu.classList.toggle('active');
        }
    }

    function handleSpeedSelection(btn) {
        const speed = parseFloat(btn.dataset.speed);
        const container = btn.closest('.vd-video-container');
        const video = container.querySelector('video');
        const speedMenu = container.querySelector('.vd-speed-menu');

        if (video) {
            video.playbackRate = speed;
        }
        if (speedMenu) {
            speedMenu.classList.remove('active');
        }

        const speedBtn = container.querySelector('[data-action="speed"]');
        if (speedBtn) {
            speedBtn.textContent = `${speed}x`;
        }
    }

    function cycleColorFilter(video) {
        const filters = [
            'none',
            'brightness(1.2) contrast(1.1)',
            'saturate(1.5)',
            'sepia(0.5)',
            'hue-rotate(90deg)',
            'grayscale(1)'
        ];

        const currentFilter = video.style.filter || 'none';
        const currentIndex = filters.indexOf(currentFilter);
        const nextIndex = (currentIndex + 1) % filters.length;
        video.style.filter = filters[nextIndex];
    }

    function handleProgressBarClick(target, e) {
        const progressBar = target.classList.contains('vd-progress-bar') ? target : target.parentElement;
        const container = progressBar.closest('.vd-video-container');
        const video = container.querySelector('video');
        if (!video) return;

        const rect = progressBar.getBoundingClientRect();
        const clickX = e.clientX - rect.left;
        const percentage = clickX / rect.width;
        video.currentTime = percentage * video.duration;
    }

    function handleUrlCopy(btn) {
        const card = btn.closest('.vd-card');
        const videoUrl = card?.dataset.url;
        if (videoUrl) {
            copyToClipboard(videoUrl);
            const oldText = btn.textContent;
            btn.textContent = 'Copied!';
            setTimeout(() => btn.textContent = oldText, 2000);
        }
    }

    function handleSelectAll() {
        const allSelected = Array.from(state.videos.values()).every(v => v.selected);
        state.videos.forEach(v => v.selected = !allSelected);
        renderGrid();
    }

    function toggleModal(show) {
        state.isModalOpen = show;
        overlayEl.style.display = show ? 'flex' : 'none';
        if (show) {
            renderGrid();
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = '';
            // Cleanup all players
            cleanupPlayers();
        }
    }

    function cleanupPlayers() {
        state.hlsPlayers.forEach(p => p.destroy());
        state.hlsPlayers.clear();

        state.activePlayers.forEach(video => {
            if (!video.paused) {
                video.pause();
            }
            video.removeAttribute('src');
            video.load();
        });
        state.activePlayers.clear();

        state.playerTimers.forEach(timer => clearInterval(timer));
        state.playerTimers.clear();
    }

    function updateStats() {
        const selectedCount = Array.from(state.videos.values()).filter(v => v.selected).length;
        overlayEl.querySelector('.vd-stats-footer').textContent = `${selectedCount} videos selected`;
    }

    function updatePagination() {
        const paginationEl = overlayEl.querySelector('.vd-pagination');
        if (!paginationEl) return;

        const totalVideos = state.videos.size;
        state.totalPages = Math.ceil(totalVideos / state.itemsPerPage) || 1;

        if (state.currentPage > state.totalPages) {
            state.currentPage = state.totalPages;
        }

        let paginationHTML = `
            <button class="vd-page-btn" data-page="prev" ${state.currentPage === 1 ? 'disabled' : ''}>←</button>
        `;

        // Show page numbers with ellipsis for many pages
        const maxVisiblePages = 7;
        let startPage = Math.max(1, state.currentPage - Math.floor(maxVisiblePages / 2));
        let endPage = Math.min(state.totalPages, startPage + maxVisiblePages - 1);

        if (endPage - startPage < maxVisiblePages - 1) {
            startPage = Math.max(1, endPage - maxVisiblePages + 1);
        }

        if (startPage > 1) {
            paginationHTML += `<button class="vd-page-btn" data-page="1">1</button>`;
            if (startPage > 2) {
                paginationHTML += `<span class="vd-page-info">...</span>`;
            }
        }

        for (let i = startPage; i <= endPage; i++) {
            paginationHTML += `<button class="vd-page-btn ${i === state.currentPage ? 'active' : ''}" data-page="${i}">${i}</button>`;
        }

        if (endPage < state.totalPages) {
            if (endPage < state.totalPages - 1) {
                paginationHTML += `<span class="vd-page-info">...</span>`;
            }
            paginationHTML += `<button class="vd-page-btn" data-page="${state.totalPages}">${state.totalPages}</button>`;
        }

        paginationHTML += `
            <button class="vd-page-btn" data-page="next" ${state.currentPage === state.totalPages ? 'disabled' : ''}>→</button>
            <span class="vd-page-info">Page ${state.currentPage} of ${state.totalPages}</span>
        `;

        paginationEl.innerHTML = paginationHTML;

        // Add event listeners
        paginationEl.querySelectorAll('.vd-page-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const page = btn.dataset.page;
                if (page === 'prev' && state.currentPage > 1) {
                    state.currentPage--;
                } else if (page === 'next' && state.currentPage < state.totalPages) {
                    state.currentPage++;
                } else if (page !== 'prev' && page !== 'next') {
                    state.currentPage = parseInt(page);
                }
                renderGrid();
            });
        });
    }

    function createPlayerControls(videoUrl, fileName) {
        return `
            <div class="vd-player-controls">
                <div class="vd-progress-bar">
                    <div class="vd-progress-filled"></div>
                </div>
                <div class="vd-controls-row">
                    <button class="vd-control-btn" data-action="play-pause">▶</button>
                    <button class="vd-control-btn" data-action="fullscreen">⛶</button>
                    <button class="vd-control-btn" data-action="speed">1x</button>
                    <button class="vd-control-btn" data-action="zoom-in">🔍+</button>
                    <button class="vd-control-btn" data-action="zoom-out">🔍-</button>
                    <button class="vd-control-btn" data-action="color">🎨</button>
                    <span class="vd-time-display">0:00 / 0:00</span>
                </div>
                <div class="vd-speed-menu">
                    <div class="vd-speed-option" data-speed="0.25">0.25x</div>
                    <div class="vd-speed-option" data-speed="0.5">0.5x</div>
                    <div class="vd-speed-option" data-speed="0.75">0.75x</div>
                    <div class="vd-speed-option" data-speed="1">1x</div>
                    <div class="vd-speed-option" data-speed="1.25">1.25x</div>
                    <div class="vd-speed-option" data-speed="1.5">1.5x</div>
                    <div class="vd-speed-option" data-speed="2">2x</div>
                </div>
            </div>
        `;
    }

    function renderGrid() {
        const fragment = document.createDocumentFragment();
        const videoList = Array.from(state.videos.values()).sort((a, b) => b.addedAt - a.addedAt);

        const startIndex = (state.currentPage - 1) * state.itemsPerPage;
        const endIndex = Math.min(startIndex + state.itemsPerPage, videoList.length);
        const pageVideos = videoList.slice(startIndex, endIndex);

        updateStats();
        updatePagination();

        pageVideos.forEach((video, idx) => {
            const card = document.createElement('div');
            card.className = 'vd-card';
            card.dataset.url = video.url;

            const globalIdx = startIndex + idx;
            const fileName = getFileName(video.url, globalIdx);

            card.innerHTML = `
                <div class="vd-video-container">
                    <div class="vd-placeholder">Loading player...</div>
                    ${createPlayerControls(video.url, fileName)}
                </div>
                <div class="vd-card-info">
                    <div class="vd-card-name" title="${fileName}">${fileName}</div>
                    <div class="vd-card-url" title="${video.url}">${video.url}</div>
                    <button class="vd-url-copy-btn">Copy URL</button>
                    <div class="vd-card-actions">
                        <button class="vd-card-btn vd-open">Open</button>
                        <button class="vd-card-btn primary vd-dl">Download</button>
                    </div>
                    <label style="display: flex; align-items: center; gap: 8px; margin-top: 4px; cursor: pointer;">
                        <input type="checkbox" ${video.selected ? 'checked' : ''} class="vd-checkbox">
                        <span style="font-size: 12px; color: #aaa;">Select for ZIP</span>
                    </label>
                </div>
            `;

            const videoContainer = card.querySelector('.vd-video-container');
            const videoEl = document.createElement('video');
            videoEl.controls = false; // Hide native controls
            videoEl.muted = true;
            videoEl.preload = 'metadata';
            videoEl.dataset.src = video.url;
            videoEl.playsInline = true;

            state.intersectionObserver.observe(videoEl);
            state.activePlayers.add(videoEl);

            setupPlayerEvents(videoEl, videoContainer);

            videoContainer.querySelector('.vd-placeholder').after(videoEl);
            fragment.appendChild(card);
        });

        gridEl.innerHTML = '';
        gridEl.appendChild(fragment);
    }

    function setupPlayerEvents(videoEl, container) {
        videoEl.addEventListener('loadedmetadata', () => {
            const placeholder = container.querySelector('.vd-placeholder');
            if (placeholder) placeholder.style.display = 'none';

            const timeDisplay = container.querySelector('.vd-time-display');
            if (timeDisplay) {
                timeDisplay.textContent = `0:00 / ${formatTime(videoEl.duration)}`;
            }
        });

        videoEl.addEventListener('timeupdate', () => {
            const progressFilled = container.querySelector('.vd-progress-filled');
            const timeDisplay = container.querySelector('.vd-time-display');

            if (progressFilled && videoEl.duration) {
                const percentage = (videoEl.currentTime / videoEl.duration) * 100;
                progressFilled.style.width = percentage + '%';
            }

            if (timeDisplay) {
                timeDisplay.textContent = `${formatTime(videoEl.currentTime)} / ${formatTime(videoEl.duration)}`;
            }
        });

        videoEl.addEventListener('play', () => {
            const playBtn = container.querySelector('[data-action="play-pause"]');
            if (playBtn) playBtn.textContent = '⏸';

            // Auto-hide controls
            container.classList.add('vd-controls-visible');
            setTimeout(() => {
                if (!videoEl.paused) {
                    container.classList.remove('vd-controls-visible');
                }
            }, 3000);
        });

        videoEl.addEventListener('pause', () => {
            const playBtn = container.querySelector('[data-action="play-pause"]');
            if (playBtn) playBtn.textContent = '▶';
            container.classList.add('vd-controls-visible');
        });

        videoEl.addEventListener('error', () => {
            const placeholder = container.querySelector('.vd-placeholder');
            if (placeholder) placeholder.textContent = 'Error loading video';
        });

        videoEl.addEventListener('click', () => {
            if (videoEl.paused) {
                videoEl.play();
            } else {
                videoEl.pause();
            }
        });

        // Show controls on mouse move
        container.addEventListener('mousemove', () => {
            container.classList.add('vd-controls-visible');
            clearTimeout(container._controlsTimeout);
            container._controlsTimeout = setTimeout(() => {
                if (!videoEl.paused) {
                    container.classList.remove('vd-controls-visible');
                }
            }, 3000);
        });
    }

    function formatTime(seconds) {
        if (!seconds || isNaN(seconds)) return '0:00';
        const mins = Math.floor(seconds / 60);
        const secs = Math.floor(seconds % 60);
        return `${mins}:${secs.toString().padStart(2, '0')}`;
    }

    async function downloadZip() {
        const selectedVideos = Array.from(state.videos.values()).filter(v => v.selected);
        if (selectedVideos.length === 0) {
            alert('No videos selected!');
            return;
        }

        const btn = document.querySelector('#vd-btn-download-zip') || document.querySelector('.vd-panel .secondary-btn');
        const originalText = btn.textContent;
        btn.textContent = 'Preparing ZIP...';
        btn.disabled = true;

        try {
            const zip = new JSZip();
            const folder = zip.folder(CONFIG.autoSaveDir);

            const batchSize = CONFIG.maxConcurrentDownloads;
            for (let i = 0; i < selectedVideos.length; i += batchSize) {
                const batch = selectedVideos.slice(i, i + batchSize);
                const promises = batch.map(async (video, batchIdx) => {
                    try {
                        const response = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: video.url,
                                responseType: 'blob',
                                onload: resolve,
                                onerror: reject,
                                timeout: 30000
                            });
                        });
                        const fileName = getFileName(video.url, i + batchIdx);
                        folder.file(fileName, response.response);
                    } catch (e) {
                        console.error('Failed to download:', video.url, e);
                    }
                });

                await Promise.all(promises);
                btn.textContent = `Downloading... ${Math.min(i + batchSize, selectedVideos.length)}/${selectedVideos.length}`;
            }

            btn.textContent = 'Creating ZIP...';
            const content = await zip.generateAsync({
                type: 'blob',
                compression: 'DEFLATE',
                compressionOptions: { level: 6 }
            });

            const url = URL.createObjectURL(content);
            const a = document.createElement('a');
            a.href = url;
            a.download = `${CONFIG.autoSaveDir}_${new Date().getTime()}.zip`;
            a.click();
            URL.revokeObjectURL(url);

        } catch (e) {
            alert('Error creating ZIP: ' + e.message);
        } finally {
            btn.textContent = originalText;
            btn.disabled = false;
        }
    }

    function init() {
        createUI();
        initObservers();

        GM_registerMenuCommand('Open Video Detector', () => toggleModal(true));
        GM_registerMenuCommand('Reset UI', () => {
            const root = document.getElementById('vd-root-container');
            if (root) root.remove();
            createUI();
        });

        if (document.body) {
            scanForVideos();
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
    } else {
        setTimeout(init, 100);
    }
})();