Eporner Zippser Filter

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

})();