PornXP Enhanced

Autoplay in frame video previews plus case insensitive tag blocking

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
})();