Eporner Zippser Filter

Hides watched videos and excludes videos by title phrases on Eporner.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Eporner Zippser Filter
// @namespace    http://tampermonkey.net/
// @version      18.0
// @description  Hides watched videos and excludes videos by title phrases on Eporner.
// @author       https://github.com/zippser
// @match        https://www.eporner.com/*
// @grant        GM_addStyle
// @license      GPL-3.0
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    // --- Enter keywords separated by comma to hide videos ---
    const KEYWORDS_TO_BAN = ['fart', 'trans', 'findom', 
      'foot'];

    const videoContainerSelector = '.mb';
    const watchedIndicatorSelector = 'i[title^="Watched on"]';
    // Keeping the specific wrapper, but prioritizing body scan if it fails
    const feedWrapperSelector = '#vidresults';

    const TITLE_LINK_SELECTOR = '.mbunder .mbtit a';

    const WATCHED_MARKER_CLASS = 'ai-hidden-watched';
    const KEYWORD_MARKER_CLASS = 'ai-keyword-banned';

    let isWatchedHidden = true;
    let styleElement;
    let mainObserver;

    // --- Style Injection Setup ---
    function setupStyleSheet() {
        if (styleElement) return;
        styleElement = document.createElement('style');
        styleElement.id = 'ai-keyword-ban-styles-v17';
        styleElement.textContent = `
            .${KEYWORD_MARKER_CLASS},
            .${WATCHED_MARKER_CLASS} {
                display: none !important;
            }
        `;
        document.head.appendChild(styleElement);
    }

    // --- Utility Functions ---
    function shouldHideDueToKeyword(container) {
        if (KEYWORDS_TO_BAN.length === 0) return false;
        const titleLink = container.querySelector(TITLE_LINK_SELECTOR);

        if (titleLink && titleLink.innerText) {
            const textToCheck = titleLink.innerText.toLowerCase();
            for (const keyword of KEYWORDS_TO_BAN) {
                if (textToCheck.includes(keyword)) {
                    return true;
                }
            }
        }
        return false;
    }

    // --- UI Functions (Simplified for this test run) ---
    function createToggleButton() {
        if (document.getElementById('ai-toggle-videos-btn')) return;
        const button = document.createElement('button');
        button.id = 'ai-toggle-videos-btn';
        button.style.cssText = `position: fixed !important; top: 5px !important; right: 280px !important; z-index: 2147483647 !important; padding: 12px 20px !important; font-size: 16px !important; font-weight: bold !important; background-color: #28a745 !important; color: white !important; border: 1px solid #333 !important; border-radius: 8px !important; cursor: pointer !important; box-shadow: 0 8px 16px rgba(0,0,0,0.5) !important; transition: all 0.3s ease !important;`;
        button.onclick = toggleVideoVisibility;
        document.documentElement.appendChild(button);
        button.onmouseover = () => button.style.backgroundColor = '#218838';
        button.onmouseout = () => button.style.backgroundColor = '#28a745';
        button.textContent = isWatchedHidden ? 'Show' : 'Hide';
    }

    function toggleVideoVisibility() {
        isWatchedHidden = !isWatchedHidden;
        processDOM(document.body); // Process the entire body when toggling
        const button = document.getElementById('ai-toggle-videos-btn');
        if (button) button.textContent = isWatchedHidden ? 'Show' : 'Hide';
    }


    // --- Core Processing Logic ---
    function processElement(container) {
        if (!container || !container.classList.contains('mb')) return;

        const isWatched = container.querySelector(watchedIndicatorSelector);
        const isBanned = shouldHideDueToKeyword(container);

        let shouldHide = false;

        // 1. Keyword Enforcement
        if (isBanned) {
            container.classList.add(KEYWORD_MARKER_CLASS);
            container.classList.remove(WATCHED_MARKER_CLASS);
            shouldHide = true;
        } else {
            container.classList.remove(KEYWORD_MARKER_CLASS);

            // 2. Watched Enforcement
            if (isWatched && isWatchedHidden) {
                container.classList.add(WATCHED_MARKER_CLASS);
                shouldHide = true;
            } else {
                container.classList.remove(WATCHED_MARKER_CLASS);
            }
        }

        // 3. Apply the display property based on the determined visibility
        if (shouldHide) {
            container.style.setProperty('display', 'none', 'important');
        } else {
            container.style.removeProperty('display');
        }
    }

    function processDOM(targetNode) {
        // If a specific target node is provided (like document.body on initial load), use it.
        // Otherwise, try the specific wrapper, falling back to the entire body if the wrapper fails or doesn't exist.
        const rootNode = targetNode || document.querySelector(feedWrapperSelector) || document.body;

        if (!rootNode) return;

        // Process all matching containers found within the designated root node
        rootNode.querySelectorAll(videoContainerSelector).forEach(processElement);
    }

    // --- Main Initialization ---
    function initializeScript() {
        setupStyleSheet();
        createToggleButton();

        // Initial comprehensive scan of the entire body, which covers all page types (home, tag, search)
        processDOM(document.body);

        // Set up a MutationObserver to monitor for changes in the DOM that could introduce new video containers 
        if (!mainObserver) {
            mainObserver = new MutationObserver((mutationsList) => {
                let needsProcessing = false;
                for (const mutation of mutationsList) {
                    // If any nodes were added or removed that could affect the feed area
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        needsProcessing = true;
                        break;
                    }
                }

                if (needsProcessing) {
                    // Re-process the entire body structure to catch newly injected items
                    processDOM(document.body);
                }
            });

            // Observe the entire body for structural changes
            const config = { childList: true, subtree: true };
            mainObserver.observe(document.body, config);
        }
    }

    if (document.readyState === "loading") {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }

})();