PornXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         PornXP Enhanced
// @namespace    https://github.com/quantavil/
// @version      1.2
// @description  Autoplay in frame video previews plus case insensitive tag blocking
// @match        *://*.pornxp.*/*
// @match        *://*.porn-xp.*/*
// @match        *://porn-xp.*/*
// @match        *://pornxp.*/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'pxp_filters_v1';
    const DEFAULTS = { blockedTags: [], mutePreview: true, hideBlocked: true };
    
    let state = loadState();
    let blockedSet = new Set();

    function loadState() {
        try {
            return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(STORAGE_KEY)) };
        } catch { return { ...DEFAULTS }; }
    }

    function syncState() {
        state.blockedTags = [...new Set(state.blockedTags.map(t => t.toLowerCase()))];
        blockedSet = new Set(state.blockedTags);
        localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        applyFilter();
        refreshPanelTagList();
    }

    const $ = (s, c = document) => c.querySelector(s);
    const $$ = (s, c = document) => [...c.querySelectorAll(s)];
    
    const escapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
    const escapeHtml = str => str.replace(/[&<>"']/g, c => escapeMap[c]);

    function parseTagName(href) {
        const m = href?.match(/\/tags\/([^/?#]+)/i);
        return m ? decodeURIComponent(m[1]).toLowerCase() : null;
    }

    function getItemTags(cont) {
        return $$('.item_tags a', cont).map(a => parseTagName(a.getAttribute('href'))).filter(Boolean);
    }

    function isBlocked(cont) {
        return getItemTags(cont).some(t => blockedSet.has(t));
    }

    function applyFilter() {
        $$('.item_cont').forEach(cont => {
            const blocked = isBlocked(cont);
            if (state.hideBlocked && blocked) {
                cont.style.display = 'none';
                cont.style.opacity = '';
                cont.style.filter = '';
                cont.style.pointerEvents = '';
            } else if (!state.hideBlocked && blocked) {
                cont.style.display = '';
                cont.style.opacity = '0.15';
                cont.style.filter = 'blur(4px) grayscale(100%)';
                cont.style.pointerEvents = 'none';
            } else {
                cont.style.display = '';
                cont.style.opacity = '';
                cont.style.filter = '';
                cont.style.pointerEvents = '';
            }
        });
    }

    const visObserver = new IntersectionObserver(entries => {
        entries.forEach(e => {
            const vid = e.target.querySelector('video.pxp-preview');
            if (!vid) return;
            if (e.isIntersecting) {
                vid.style.visibility = 'visible';
                vid.style.opacity = '1';
                vid.play().catch(() => {});
            } else {
                vid.pause();
                vid.style.opacity = '0';
                vid.style.visibility = 'hidden';
            }
        });
    }, { rootMargin: '100px', threshold: 0.2 });

    function initPreviews() {
        $$('.item.preview').forEach(item => {
            if (item.querySelector('video.pxp-preview')) return;
            const previewUrl = item.dataset.preview;
            const thumb = $('.item_thumb', item);
            if (!previewUrl || !thumb) return;

            const video = document.createElement('video');
            video.className = 'pxp-preview';
            video.loop = true;
            video.muted = state.mutePreview;
            video.playsInline = true;
            video.preload = 'none';
            video.src = previewUrl;

            thumb.appendChild(video);
            visObserver.observe(item);
        });

        $$('video.pxp-preview').forEach(v => v.muted = state.mutePreview);
    }

    function addBlockButtons() {
        $$('.item_tags a').forEach(a => {
            if (a.querySelector('.pxp-block-btn')) return;
            const tag = parseTagName(a.getAttribute('href'));
            if (!tag) return;

            const btn = document.createElement('span');
            btn.className = 'pxp-block-btn';
            btn.innerHTML = '&times;';
            btn.title = `Block "${tag}"`;
            btn.onclick = e => {
                e.preventDefault();
                e.stopPropagation();
                if (blockedSet.has(tag)) return;
                state.blockedTags.push(tag);
                syncState();
            };
            a.appendChild(btn);
        });
    }

    function refreshPanelTagList() {
        const list = $('#pxp-taglist');
        if (!list) return;
        if (!state.blockedTags.length) {
            list.innerHTML = '<div class="pxp-empty">No blocked tags</div>';
            return;
        }
        list.innerHTML = state.blockedTags.map(tag =>
            `<div class="pxp-tag"><span>${escapeHtml(tag)}</span><button data-tag="${escapeHtml(tag)}">&times;</button></div>`
        ).join('');
        
        list.querySelectorAll('button').forEach(btn => {
            btn.onclick = () => {
                state.blockedTags = state.blockedTags.filter(t => t !== btn.dataset.tag);
                syncState();
            };
        });
    }

    function buildPanel() {
        const panel = document.createElement('div');
        panel.id = 'pxp-settings';
        panel.innerHTML = `
            <div class="pxp-header"><span>Filters</span><button class="pxp-close">&times;</button></div>
            <div class="pxp-body">
                <label class="pxp-row"><input type="checkbox" id="pxp-mute" ${state.mutePreview ? 'checked' : ''}><span>Mute previews</span></label>
                <label class="pxp-row"><input type="checkbox" id="pxp-hide" ${state.hideBlocked ? 'checked' : ''}><span>Hide blocked items</span></label>
                <div class="pxp-section">Blocked Tags</div>
                <div class="pxp-taglist" id="pxp-taglist"></div>
                <div class="pxp-addtag">
                    <input type="text" id="pxp-newtag" placeholder="Tag name">
                    <button id="pxp-add">Block</button>
                </div>
            </div>`;

        panel.querySelector('.pxp-close').onclick = () => panel.classList.remove('open');
        
        panel.querySelector('#pxp-mute').onchange = e => {
            state.mutePreview = e.target.checked;
            syncState();
            initPreviews();
        };
        
        panel.querySelector('#pxp-hide').onchange = e => {
            state.hideBlocked = e.target.checked;
            syncState();
        };

        const addTag = () => {
            const input = panel.querySelector('#pxp-newtag');
            const tag = input.value.trim().toLowerCase();
            if (!tag || blockedSet.has(tag)) { input.value = ''; return; }
            state.blockedTags.push(tag);
            input.value = '';
            syncState();
        };
        
        panel.querySelector('#pxp-add').onclick = addTag;
        panel.querySelector('#pxp-newtag').onkeydown = e => { if (e.key === 'Enter') addTag(); };

        refreshPanelTagList();
        return panel;
    }

    GM_addStyle(`
        .item_thumb { position: relative !important; overflow: hidden !important; }
        .item_thumb img { display: block !important; position: relative !important; z-index: 1 !important; }
        video.pxp-preview {
            position: absolute !important; top: 0 !important; left: 0 !important;
            width: 100% !important; height: 100% !important; object-fit: cover !important;
            z-index: 2 !important; opacity: 0; visibility: hidden;
            transition: opacity 0.2s ease; pointer-events: none;
        }
        #pxp-settings {
            position:fixed; top:60px; right:-320px; width:300px;
            background:#1a1a1a; border:1px solid #333; border-radius:8px;
            color:#ddd; font-family:system-ui,sans-serif; font-size:13px;
            z-index:99999; transition:right .25s ease; box-shadow:0 8px 32px rgba(0,0,0,.6);
        }
        #pxp-settings.open { right:12px; }
        .pxp-header { display:flex; justify-content:space-between; align-items:center; padding:12px 14px; border-bottom:1px solid #333; font-weight:600; font-size:14px; color:#fff; }
        .pxp-close { background:none; border:none; color:#888; font-size:20px; cursor:pointer; line-height:1; }
        .pxp-close:hover { color:#fff; }
        .pxp-body { padding:12px 14px; }
        .pxp-row { display:flex; align-items:center; gap:8px; margin-bottom:10px; cursor:pointer; }
        .pxp-row input { cursor:pointer; }
        .pxp-section { margin:14px 0 8px; font-weight:600; color:#bbb; text-transform:uppercase; font-size:11px; letter-spacing:.5px; }
        .pxp-taglist { max-height:180px; overflow-y:auto; margin-bottom:10px; }
        .pxp-empty { color:#666; font-style:italic; padding:8px 0; }
        .pxp-tag { display:flex; justify-content:space-between; align-items:center; background:#252525; padding:6px 10px; border-radius:4px; margin-bottom:6px; }
        .pxp-tag button { background:none; border:none; color:#e74c3c; cursor:pointer; font-size:16px; }
        .pxp-tag button:hover { color:#ff6b6b; }
        .pxp-addtag { display:flex; gap:6px; }
        .pxp-addtag input { flex:1; background:#252525; border:1px solid #444; color:#ddd; padding:6px 10px; border-radius:4px; font-size:12px; }
        .pxp-addtag button { background:#e74c3c; color:#fff; border:none; padding:6px 12px; border-radius:4px; cursor:pointer; font-size:12px; font-weight:500; }
        .pxp-addtag button:hover { background:#c0392b; }
        #pxp-toggle { position:fixed; top:14px; right:14px; z-index:99999; background:#e74c3c; color:#fff; border:none; padding:8px 14px; border-radius:6px; cursor:pointer; font-weight:600; font-size:13px; box-shadow:0 2px 12px rgba(231,76,60,.3); }
        #pxp-toggle:hover { background:#c0392b; }
        .item_tags a { position:relative; padding-right:14px!important; }
        .pxp-block-btn { display:inline-block; margin-left:4px; color:#e74c3c; cursor:pointer; font-weight:bold; font-size:13px; opacity:0; transition:opacity .15s; }
        .item_tags a:hover .pxp-block-btn { opacity:1; }
        .pxp-block-btn:hover { color:#ff6b6b; }
    `);

    function clearCache() {
        localStorage.removeItem(STORAGE_KEY);
        state = { ...DEFAULTS };
        blockedSet = new Set();
        syncState();
        initPreviews();
    }

    GM_registerMenuCommand('Clear Cache and Reset Filters', clearCache);

    function init() {
        const toggle = document.createElement('button');
        toggle.id = 'pxp-toggle';
        toggle.textContent = 'Filters';
        toggle.onclick = () => {
            let panel = $('#pxp-settings');
            if (!panel) { panel = buildPanel(); document.body.appendChild(panel); }
            panel.classList.toggle('open');
        };
        document.body.appendChild(toggle);

        syncState();
        initPreviews();
        addBlockButtons();

        let pending;
        const target = $('#content') || document.body;
        new MutationObserver(m => {
            if (!m.some(r => r.addedNodes.length)) return;
            clearTimeout(pending);
            pending = setTimeout(() => { initPreviews(); addBlockButtons(); applyFilter(); }, 150);
        }).observe(target, { childList: true, subtree: true });
    }

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