PornXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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