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