R34 Thumbnail always active

Обложки видео всегда активны на всех страницах

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         R34 Thumbnail always active
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Обложки видео всегда активны на всех страницах
// @author       Grok
// @match        https://rule34video.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34video.com
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // === НАСТРОЙКИ ===
    const DELAY_START = 0;       // мс
    const BACKGROUND_PLAY = 1;   // 1 = играть всегда, 0 = только в зоне видимости
    const FIX_INTERVAL = 3000;   // мс
    const STUCK_THRESHOLD = 2;   // сек без прогресса -> фикс
    const PROCESS_DELAY = 150;   // debounce для AJAX/ленивой подгрузки
    // =================

    const ITEM_SELECTOR = '.item.thumb';
    const WRAP_SELECTOR = '.img.wrap_image';
    const OVERLAY_SELECTOR = '.sound, .quality, .time, .custom-play';

    let started = false;
    let fixInterval = null;
    let processTimer = null;
    let pendingRoots = new Set();

    const trackedVideos = new Set();
    const visibilityState = new WeakMap();
    const visibilityObserver = BACKGROUND_PLAY === 0 ? new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            const item = entry.target;
            visibilityState.set(item, entry.isIntersecting);

            const video = item.querySelector('video[our-trailer]');
            if (!video) return;

            if (entry.isIntersecting) {
                video.play().catch(() => {});
            } else {
                video.pause();
            }
        });
    }, { threshold: 0.35 }) : null;

    function isInViewport(el) {
        if (!el) return false;
        const rect = el.getBoundingClientRect();
        return rect.top < window.innerHeight && rect.bottom > 0 &&
            rect.left < window.innerWidth && rect.right > 0;
    }

    function shouldPlay(item) {
        if (BACKGROUND_PLAY) return true;
        return visibilityState.get(item) ?? isInViewport(item);
    }

    function blockHoverOnce(item) {
        if (item.dataset.trailerHoverBlocked === '1') return;
        item.dataset.trailerHoverBlocked = '1';
    }

    function ensureOverlayZIndex(item) {
        if (item.dataset.trailerOverlayFixed === '1') return;
        item.dataset.trailerOverlayFixed = '1';
        item.querySelectorAll(OVERLAY_SELECTOR).forEach(el => {
            el.style.zIndex = '2';
        });
    }

    function registerVideo(item, video) {
        trackedVideos.add(video);
        video.lastTime = 0;
        video.stuckCheckTime = Date.now();

        video.addEventListener('canplay', () => {
            if (shouldPlay(item)) video.play().catch(() => {});
        });

        video.addEventListener('timeupdate', () => {
            if (video.currentTime > video.lastTime + 0.1) {
                video.lastTime = video.currentTime;
                video.stuckCheckTime = Date.now();
            }
        });

        video.addEventListener('error', () => {
            setTimeout(() => {
                if (!video.isConnected) return;
                video.load();
                if (shouldPlay(item)) video.play().catch(() => {});
            }, 1000);
        });
    }

    function activateTrailer(item) {
        const wrap = item.querySelector(WRAP_SELECTOR);
        const img = wrap?.querySelector('img.thumb');
        const previewUrl = wrap?.getAttribute('data-preview');
        if (!wrap || !img || !previewUrl) return;

        blockHoverOnce(item);

        wrap.querySelectorAll('video').forEach(video => {
            if (!video.hasAttribute('our-trailer')) {
                video.remove();
            }
        });

        let video = wrap.querySelector('video[our-trailer]');
        if (video) {
            if (video.dataset.previewUrl !== previewUrl) {
                video.pause();
                video.src = previewUrl;
                video.dataset.previewUrl = previewUrl;
                video.load();
            }

            img.style.display = 'none';
            video.style.display = 'block';
            if (shouldPlay(item) && video.paused) {
                video.play().catch(() => {});
            }
            return;
        }

        video = document.createElement('video');
        video.setAttribute('our-trailer', '1');
        video.dataset.previewUrl = previewUrl;
        video.src = previewUrl;
        video.loop = true;
        video.muted = true;
        video.playsInline = true;
        video.preload = BACKGROUND_PLAY ? 'metadata' : 'none';
        video.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;z-index:1;display:none;background:#000;pointer-events:none;';

        if (getComputedStyle(wrap).position === 'static') {
            wrap.style.position = 'relative';
        }

        wrap.appendChild(video);
        img.style.display = 'none';
        video.style.display = 'block';

        ensureOverlayZIndex(item);
        registerVideo(item, video);

        if (visibilityObserver) {
            visibilityObserver.observe(item);
        }

        if (shouldPlay(item)) {
            video.play().catch(() => {});
        }
    }

    function processItem(item) {
        if (!(item instanceof HTMLElement) || !item.matches(ITEM_SELECTOR)) return;

        const wrap = item.querySelector(WRAP_SELECTOR);
        const previewUrl = wrap?.getAttribute('data-preview');
        if (!wrap || !previewUrl) return;

        if (item.dataset.trailerPreviewUrl === previewUrl && wrap.querySelector('video[our-trailer]')) {
            return;
        }

        activateTrailer(item);
        item.dataset.trailerPreviewUrl = previewUrl;
    }

    function processRoot(root) {
        if (!root) return;

        if (root instanceof Element && root.matches(ITEM_SELECTOR)) {
            processItem(root);
        }

        if (root.querySelectorAll) {
            root.querySelectorAll(ITEM_SELECTOR).forEach(processItem);
        }
    }

    function flushPendingRoots() {
        const roots = Array.from(pendingRoots);
        pendingRoots.clear();
        roots.forEach(processRoot);
    }

    function scheduleProcess(root = document) {
        pendingRoots.add(root);
        if (processTimer) return;

        processTimer = setTimeout(() => {
            processTimer = null;
            flushPendingRoots();
        }, PROCESS_DELAY);
    }

    function fixStuckVideos() {
        if (!started) return;

        trackedVideos.forEach(video => {
            if (!video.isConnected) {
                trackedVideos.delete(video);
                return;
            }

            const item = video.closest(ITEM_SELECTOR);
            if (!item) return;

            if (!BACKGROUND_PLAY && !shouldPlay(item)) {
                return;
            }

            if (video.ended || video.paused) {
                if (shouldPlay(item)) video.play().catch(() => {});
                return;
            }

            const now = Date.now();
            if (!video.stuckCheckTime) {
                video.stuckCheckTime = now;
                return;
            }

            if (now - video.stuckCheckTime > STUCK_THRESHOLD * 1000 && video.currentTime === video.lastTime) {
                video.currentTime = 0;
                video.load();
                setTimeout(() => {
                    if (video.isConnected && shouldPlay(item)) {
                        video.play().catch(() => {});
                    }
                }, 100);
            }
        });
    }

    function start() {
        if (started) return;
        started = true;
        scheduleProcess(document);
        fixInterval = setInterval(fixStuckVideos, FIX_INTERVAL);
    }

    setTimeout(start, DELAY_START);

    ['scroll', 'click', 'keydown'].forEach(eventName => {
        document.addEventListener(eventName, start, { once: true, passive: true });
    });

    ['mouseenter', 'mouseover', 'mouseleave', 'mouseout'].forEach(eventName => {
        document.addEventListener(eventName, (event) => {
            const item = event.target instanceof Element ? event.target.closest(`${ITEM_SELECTOR}[data-trailer-hover-blocked="1"]`) : null;
            if (item) event.stopPropagation();
        }, true);
    });

    new MutationObserver((mutations) => {
        if (!started) return;

        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    scheduleProcess(node);
                }
            }
        }
    }).observe(document.body, { childList: true, subtree: true });

    document.addEventListener('lazyloaded', () => {
        if (started) scheduleProcess(document);
    }, true);

    console.log('R34 Trailer Always Active v2.4 optimized');
})();