☰

Fictionmania Filter

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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