PornXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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();
})();