P0rnXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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