Eporner Zippser Filter

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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

})();