Fictionmania Filter

A filter list where you can set Keywords/phrases to automatically hide stories that match from view.

Você precisará 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.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Fictionmania Filter
// @namespace    http://tampermonkey.net/
// @version      2026-09-17
// @description  A filter list where you can set Keywords/phrases to automatically hide stories that match from view.
// @author       Silent J
// @license MIT
// @match        *://*.fictionmania.tv/searchdisplay*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// ==/UserScript==

// This takes the first story on the page and surrounds it with a p tag so the filter below works for all stories on the page
(function() {
    'use strict';

    // Find only the first <hr> tag on the page
    const firstHr = document.querySelector('hr');

    // Exit early if no <hr> exists on the page
    if (!firstHr) return;

    let currentNode = firstHr.nextSibling;
    const nodesToWrap = [];

    // Gather all sibling nodes until we hit a <p> tag or run out of siblings
    while (currentNode) {
        // Stop if we encounter a P tag
        if (currentNode.nodeType === Node.ELEMENT_NODE && currentNode.tagName.toLowerCase() === 'p') {
            break;
        }

        // Also stop if we hit another HR tag
        if (currentNode.nodeType === Node.ELEMENT_NODE && currentNode.tagName.toLowerCase() === 'hr') {
            break;
        }

        nodesToWrap.push(currentNode);
        currentNode = currentNode.nextSibling;
    }

    // Only proceed if we actually found nodes to wrap
    if (nodesToWrap.length > 0) {
        // Create the wrapper <p> element
        const newParagraph = document.createElement('p');

        // Insert the new <p> right after the first <hr>
        firstHr.parentNode.insertBefore(newParagraph, firstHr.nextSibling);

        // Move the gathered nodes inside the new <p>
        nodesToWrap.forEach(node => {
            newParagraph.appendChild(node);
        });
    }
})();

(function() {
    'use strict';

    // 1. Initialize keywords list from localStorage (defaults to ['unwanted text'] if empty)
    const STORAGE_KEY = 'suppressor_forbidden_keywords';
    let forbiddenKeywords = JSON.parse(localStorage.getItem(STORAGE_KEY)) || ["unwanted text here"];

    let suppressedParagraphs = [];
    let isHidden = true;

    // 2. Main function to scan and hide paragraphs based on current keywords
    function scanAndFilter() {
        // Reset display and styling of previously handled paragraphs
        suppressedParagraphs.forEach(p => {
            p.style.display = '';
            p.style.outline = '';
            p.style.backgroundColor = '';
        });
        suppressedParagraphs = [];

        // If no keywords exist, stop filtering
        if (forbiddenKeywords.length === 0) {
            updateBannerText();
            return;
        }

        const paragraphs = document.querySelectorAll('p');
        paragraphs.forEach(p => {
            const text = p.textContent.toLowerCase();
            // Check if paragraph contains ANY of the keywords (case-insensitive)
            const matches = forbiddenKeywords.some(keyword => text.includes(keyword.toLowerCase()));

            if (matches) {
                suppressedParagraphs.push(p);
                applyParagraphState(p);
            }
        });

        updateBannerText();
    }

    // Helper to manage visibility and highlighting styles based on toggle state
    function applyParagraphState(p) {
        if (isHidden) {
            p.style.display = 'none';
            p.style.outline = '';
            p.style.backgroundColor = '';
        } else {
            p.style.display = '';
            // Apply distinct highlight styling when items are revealed
            p.style.outline = '2px dashed #ff4d4d';
            p.style.outlineOffset = '2px';
            p.style.backgroundColor = 'rgba(255, 77, 77, 0.15)';
        }
    }

    // 3. UI Construction: Main Floating Container
    const container = document.createElement('div');
    container.style.position = 'fixed';
    container.style.top = '10px';
    container.style.right = '10px';
    container.style.backgroundColor = '#222';
    container.style.color = '#fff';
    container.style.fontSize = '12px';
    container.style.fontFamily = 'sans-serif';
    container.style.borderRadius = '6px';
    container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.4)';
    container.style.zIndex = '99999';
    container.style.width = '240px';
    container.style.overflow = 'hidden';
    document.body.appendChild(container);

    // 4. Banner Component (Clickable Toggle header)
    const banner = document.createElement('div');
    banner.style.padding = '10px';
    banner.style.cursor = 'pointer';
    banner.style.userSelect = 'none';
    banner.style.fontWeight = 'bold';
    banner.style.textAlign = 'center';
    banner.style.transition = 'background-color 0.2s';
    container.appendChild(banner);

    // 5. Dashboard Component (Hidden by default, reveals on hover)
    const dashboard = document.createElement('div');
    dashboard.style.padding = '0px 10px';
    dashboard.style.borderTop = '0px solid #444';
    dashboard.style.backgroundColor = '#2d2d2d';
    dashboard.style.maxHeight = '0px';
    dashboard.style.opacity = '0';
    dashboard.style.overflow = 'hidden';
    dashboard.style.transition = 'max-height 0.25s ease-out, opacity 0.2s ease-out, padding 0.25s ease-out';
    container.appendChild(dashboard);

    // Form structure inside dashboard
    dashboard.innerHTML = `
        <div style="margin-top: 8px; margin-bottom: 8px; font-weight: bold; color: #aaa;">Manage Keywords:</div>
        <div id="keyword-list" style="max-height: 100px; overflow-y: auto; margin-bottom: 8px; padding-right: 5px;"></div>
        <div style="display: flex; gap: 4px; margin-bottom: 8px;">
            <input type="text" id="new-keyword-input" placeholder="Add keyword..." style="flex: 1; padding: 4px; border: 1px solid #555; background: #444; color: #fff; font-size: 11px; border-radius: 3px;">
            <button id="add-keyword-btn" style="padding: 4px 8px; background: #0066cc; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Add</button>
        </div>
    `;

    const keywordListContainer = dashboard.querySelector('#keyword-list');
    const inputField = dashboard.querySelector('#new-keyword-input');
    const addBtn = dashboard.querySelector('#add-keyword-btn');

    // Helper to refresh the textual summary inside the banner header
    function updateBannerText() {
        const count = suppressedParagraphs.length;
        if (isHidden) {
            banner.textContent = `Hidden Stories: ${count} 🙈`;
            banner.style.backgroundColor = '#333';
        } else {
            banner.textContent = `UN-Hidden Stories: ${count} 👁️`;
            banner.style.backgroundColor = '#0066cc';
        }
    }

    // Helper to render the interactive list of terms in the dashboard
    function renderKeywordList() {
        keywordListContainer.innerHTML = '';

        if (forbiddenKeywords.length === 0) {
            keywordListContainer.innerHTML = '<div style="color: #777; font-style: italic;">No active filters.</div>';
            return;
        }

        forbiddenKeywords.forEach((keyword, index) => {
            const item = document.createElement('div');
            item.style.display = 'flex';
            item.style.justifyContent = 'between';
            item.style.alignItems = 'center';
            item.style.marginBottom = '4px';
            item.style.background = '#3c3c3c';
            item.style.padding = '3px 6px';
            item.style.borderRadius = '3px';

            item.innerHTML = `
                <span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${keyword}">${keyword}</span>
                <span class="del-btn" data-index="${index}" style="color: #ff4d4d; cursor: pointer; font-weight: bold; margin-left: 6px; padding: 0 2px;">×</span>
            `;
            keywordListContainer.appendChild(item);
        });
    }

    // 6. Hover Mechanics (Show/Hide Dashboard)
    container.addEventListener('mouseenter', () => {
        dashboard.style.maxHeight = '200px';
        dashboard.style.opacity = '1';
        dashboard.style.padding = '4px 10px';
        dashboard.style.borderTop = '1px solid #444';
    });

    container.addEventListener('mouseleave', () => {
        if (document.activeElement !== inputField) {
            dashboard.style.maxHeight = '0px';
            dashboard.style.opacity = '0';
            dashboard.style.padding = '0px 10px';
            dashboard.style.borderTop = '0px solid #444';
        }
    });

    inputField.addEventListener('blur', () => {
        setTimeout(() => {
            if (!container.matches(':hover')) {
                dashboard.style.maxHeight = '0px';
                dashboard.style.opacity = '0';
                dashboard.style.padding = '0px 10px';
                dashboard.style.borderTop = '0px solid #444';
            }
        }, 100);
    });

    // 7. Interactive Logic and Event Handlers

    // Toggle content visibility and highlighting styles when clicking the banner
    banner.addEventListener('click', () => {
        isHidden = !isHidden;
        suppressedParagraphs.forEach(p => applyParagraphState(p));
        updateBannerText();
    });

    // Add a new keyword action
    function handleAddKeyword() {
        const val = inputField.value.trim();
        if (val && !forbiddenKeywords.includes(val)) {
            forbiddenKeywords.push(val);
            localStorage.setItem(STORAGE_KEY, JSON.stringify(forbiddenKeywords));
            inputField.value = '';
            renderKeywordList();
            scanAndFilter();
        }
    }

    addBtn.addEventListener('click', handleAddKeyword);
    inputField.addEventListener('keypress', (e) => { e.key === 'Enter' && handleAddKeyword(); });

    // Remove keyword action
    keywordListContainer.addEventListener('click', (e) => {
        if (e.target.classList.contains('del-btn')) {
            const idx = parseInt(e.target.getAttribute('data-index'), 10);
            forbiddenKeywords.splice(idx, 1);
            localStorage.setItem(STORAGE_KEY, JSON.stringify(forbiddenKeywords));
            renderKeywordList();
            scanAndFilter();
        }
    });

    // Initial Engine Kickoff
    renderKeywordList();
    scanAndFilter();
})();