P0rnXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name         P0rnXP Enhanced
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  Autoplay in frame video previews plus case insensitive tag blocking
// @include      /^https?:\/\/([^\/.]+\.)?porn-xp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?pornxp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?xpxp\.[^\/]+\/.*$/
// @include      /^https?:\/\/([^\/.]+\.)?pxp\.[^\/]+\/.*$/
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

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

    // Safe state loading to prevent crashes from corrupted storage
    let state = Object.assign({}, DEFAULTS);
    try {
        const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        if (Array.isArray(saved.blockedTags)) state.blockedTags = saved.blockedTags;
        if (typeof saved.hideBlocked === 'boolean') state.hideBlocked = saved.hideBlocked;
    } catch (e) {
        localStorage.removeItem(STORAGE_KEY);
    }
    let blockedSet = new Set(state.blockedTags.map(t => String(t).toLowerCase()));

    const syncState = () => {
        state.blockedTags = [...blockedSet];
        localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        applyFilter();
        refreshPanelTagList();
    };

    // Wrapped in try-catch to prevent crashes on malformed URL tags
    const parseTagName = href => {
        try {
            const m = href?.match(/\/tags\/([^/?#]+)/i);
            return m ? decodeURIComponent(m[1]).toLowerCase() : null;
        } catch { return null; }
    };

    const getItemTags = cont => $$('.item_tags a', cont).map(a => parseTagName(a.getAttribute('href'))).filter(Boolean);

    function applyFilter() {
        $$('.item_cont').forEach(cont => {
            const blocked = getItemTags(cont).some(t => blockedSet.has(t));
            if (blocked) {
                cont.style.display = state.hideBlocked ? 'none' : '';
                cont.style.opacity = state.hideBlocked ? '' : '0.2';
                cont.style.filter = state.hideBlocked ? '' : 'blur(5px) grayscale(100%)';
                cont.style.pointerEvents = state.hideBlocked ? '' : 'none';
            } else {
                cont.style.cssText = '';
            }
        });
    }

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

    function initPreviews() {
        $$('.item.preview').forEach(item => {
            if (item.querySelector('video.pxp-preview')) return;
            const thumb = $('.item_thumb', item);
            if (!item.dataset.preview || !thumb) return;
            const video = document.createElement('video');
            video.className = 'pxp-preview';
            video.loop = video.muted = video.playsInline = true;
            video.preload = 'none';
            thumb.appendChild(video);
            visObserver.observe(item);
        });
    }

    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.textContent = '×';
            btn.title = `Block "${tag}"`;
            btn.addEventListener('click', e => { 
                e.preventDefault(); e.stopPropagation(); 
                blockedSet.add(tag); syncState(); 
            });
            a.appendChild(btn);
        });
    }

    function refreshPanelTagList() {
        const list = $('#pxp-taglist');
        if (!list) return;
        const tags = [...blockedSet].filter(Boolean);
        list.innerHTML = tags.length ? tags.map(tag => 
            `<div class="pxp-tag"><span>${escapeHtml(tag)}</span><button data-tag="${escapeHtml(tag)}">×</button></div>`
        ).join('') : '<div class="pxp-empty">No blocked tags</div>';
        
        list.querySelectorAll('button').forEach(btn => btn.onclick = () => {
            blockedSet.delete(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">×</button></div>
            <div class="pxp-body">
                <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="Enter tag name">
                    <button id="pxp-add">Block</button>
                </div>
            </div>`;

        panel.querySelector('.pxp-close').onclick = () => panel.classList.remove('open');
        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)) { blockedSet.add(tag); syncState(); }
            input.value = '';
        };
        
        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; transition: opacity .25s ease; pointer-events: none;
        }
        #pxp-settings {
            position:fixed; top:60px; right:-320px; width:280px; max-width:calc(100vw - 32px); z-index:99999;
            background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%);
            border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#ccc;
            font-family: -apple-system, BlinkMacSystemFont, "system-ui", sans-serif; font-size:13px;
            transition: right .35s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow:0 10px 40px rgba(0,0,0,.8); overflow:hidden;
            box-sizing: border-box;
        }
        #pxp-settings.open { right:16px; }
        .pxp-header { display:flex; justify-content:space-between; align-items:center; padding:14px 16px; border-bottom:1px solid rgba(255,255,255,0.08); font-weight:600; font-size:15px; color:#fff; background:rgba(255,255,255,0.03);}
        .pxp-close { background:rgba(255,255,255,0.1); border:none; color:#fff; font-size:14px; cursor:pointer; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; transition:background .2s; }
        .pxp-close:hover { background:rgba(255,255,255,0.2); }
        .pxp-body { padding:16px; }
        .pxp-row { display:flex; align-items:center; gap:10px; margin-bottom:12px; cursor:pointer; color:#ddd; }
        .pxp-section { margin:16px 0 8px; font-weight:600; color:rgba(255,255,255,0.4); text-transform:uppercase; font-size:11px; letter-spacing:1px; }
        .pxp-taglist { max-height:180px; overflow-y:auto; margin-bottom:12px; display:flex; flex-direction:column; gap:8px; -webkit-overflow-scrolling:touch; }
        .pxp-taglist::-webkit-scrollbar { width:4px; } .pxp-taglist::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.2); border-radius:2px; }
        .pxp-empty { color:rgba(255,255,255,0.3); font-style:italic; padding:8px 0; text-align:center; }
        .pxp-tag { display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.06); padding:8px 12px; border-radius:8px; font-size:12px; color:#eee; }
        .pxp-tag button { background:none; border:none; color:#ff6b6b; cursor:pointer; font-size:16px; line-height:1; padding:0 0 0 8px; }
        .pxp-addtag { display:flex; gap:8px; }
        .pxp-addtag input { flex:1; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.1); color:#fff; padding:10px 12px; border-radius:8px; font-size:12px; outline:none; box-sizing:border-box; }
        .pxp-addtag input:focus { border-color:rgba(255,255,255,0.3); background:rgba(255,255,255,0.12); }
        .pxp-addtag button { background:rgba(255,255,255,0.12); color:#fff; border:1px solid rgba(255,255,255,0.15); padding:8px 16px; border-radius:8px; cursor:pointer; font-size:12px; font-weight:500; transition:.2s; }
        .pxp-addtag button:hover { background:rgba(255,255,255,0.2); }
        #pxp-toggle { position:fixed; top:16px; right:16px; z-index:99999; background:rgba(0,0,0,0.6); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); color:#fff; border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:20px; cursor:pointer; font-weight:600; font-size:13px; box-shadow:0 4px 15px rgba(0,0,0,.4); transition:.2s; }
        #pxp-toggle:hover { background:rgba(0,0,0,0.8); transform:scale(1.05); }
        .item_tags a { position:relative; padding-right:14px!important; }
        .pxp-block-btn { display:inline-block; margin-left:4px; color:#ff6b6b; cursor:pointer; font-weight:bold; font-size:12px; opacity:0; transition:opacity .15s; }
        .item_tags a:hover .pxp-block-btn { opacity:1; }
        @media (max-width: 480px) {
            #pxp-toggle { padding:8px 14px; font-size:12px; top:10px; right:10px; }
            #pxp-settings { top:50px; }
        }
    `);

    GM_registerMenuCommand('Reset Filters', () => {
        localStorage.removeItem(STORAGE_KEY);
        state = { ...DEFAULTS };
        blockedSet = new Set();
        syncState();
        initPreviews();
    });

    function init() {
        let toggle = $('#pxp-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');
        };
        if (!toggle.parentElement) document.body.appendChild(toggle);

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

        let pending;
        new MutationObserver(m => {
            // Only trigger if new video containers or tags are added to the DOM
            const hasRelevantMutations = m.some(r => [...r.addedNodes].some(n => 
                n.nodeType === 1 && 
                (n.matches?.('.item_cont, .item.preview, .item_tags') || n.querySelector?.('.item_cont, .item.preview, .item_tags'))
            ));
            if (!hasRelevantMutations) return;
            
            clearTimeout(pending);
            pending = setTimeout(() => { 
                if (!$('#pxp-toggle')) document.body.appendChild(toggle); // Re-add toggle if site SPA removes it
                initPreviews(); 
                addBlockButtons(); 
                applyFilter(); 
            }, 150);
        }).observe(document.body, { childList: true, subtree: true });
    }

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