Eporner Zippser Filter

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

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

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

})();