Greasy Fork

8chan Catalog Filter

Filter catalog threads using regex patterns

目前为 2025-04-22 提交的版本。查看 最新版本

// ==UserScript==
// @name         8chan Catalog Filter
// @version      1.2
// @description  Filter catalog threads using regex patterns
// @match        *://8chan.moe/*/catalog.*
// @grant        none
// @license MIT
// @namespace https://greasyfork.org/users/1459581
// ==/UserScript==

(function() {
    'use strict';

    // Initial configuration - can be modified by the user through the dashboard
    let config = {
        filters: [
            {
                pattern: /Arknights|AKG/i, // Example regex pattern
                action: 'setTop' // 'setTop' or 'remove'
            }
            // More filters can be added by the user
        ]
    };

    // Load saved configuration from localStorage if available
    function loadConfig() {
        const savedConfig = localStorage.getItem('8chanCatalogFilterConfig');
        if (savedConfig) {
            try {
                const parsedConfig = JSON.parse(savedConfig);
                // Convert string patterns back to RegExp objects
                parsedConfig.filters = parsedConfig.filters.map(filter => ({
                    pattern: new RegExp(filter.patternText, filter.flags),
                    patternText: filter.patternText, // Store the raw text pattern
                    action: filter.action
                }));
                config = parsedConfig;
            } catch (e) {
                console.error('Failed to load saved filters:', e);
            }
        }
    }

    // Save configuration to localStorage
    function saveConfig() {
        // Convert RegExp objects to a serializable format
        const serializedConfig = {
            filters: config.filters.map(filter => ({
                patternText: filter.patternText || filter.pattern.source,
                flags: filter.pattern.flags,
                action: filter.action
            }))
        };
        localStorage.setItem('8chanCatalogFilterConfig', JSON.stringify(serializedConfig));
    }

    // Create and inject the filter dashboard
    function createDashboard() {
        const toolsDiv = document.getElementById('divTools');
        if (!toolsDiv) return;

        // Create container for the filter dashboard
        const dashboardContainer = document.createElement('div');
        dashboardContainer.id = 'filterDashboard';
        dashboardContainer.style.marginBottom = '10px';

        // Create the dashboard controls
        const dashboardControls = document.createElement('div');
        dashboardControls.className = 'catalogLabel';
        dashboardControls.innerHTML = `
            <span style="font-weight: bold;">Filters:</span>
            <button id="showFilterManager" class="catalogLabel" style="margin-left: 5px;">Manage Filters</button>
            <button id="applyFilters" class="catalogLabel" style="margin-left: 5px;">Apply Filters</button>
            <span id="activeFiltersCount" style="margin-left: 5px;">(${config.filters.length} active)</span>
        `;

        // Create the filter manager panel (initially hidden)
        const filterManager = document.createElement('div');
        filterManager.id = 'filterManagerPanel';
        filterManager.style.display = 'none';
        filterManager.style.border = '1px solid #ccc';
        filterManager.style.padding = '10px';
        filterManager.style.marginTop = '5px';

        updateFilterManagerContent(filterManager);

        // Add everything to the dashboard container
        dashboardContainer.appendChild(dashboardControls);
        dashboardContainer.appendChild(filterManager);

        // Insert dashboard before the search box
        const searchDiv = toolsDiv.querySelector('div[style="float: right; margin-top: 6px;"]');
        if (searchDiv) {
            toolsDiv.insertBefore(dashboardContainer, searchDiv);
        } else {
            toolsDiv.appendChild(dashboardContainer);
        }

        // Add event listeners
        document.getElementById('showFilterManager').addEventListener('click', function() {
            const panel = document.getElementById('filterManagerPanel');
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        });

        document.getElementById('applyFilters').addEventListener('click', function() {
            processCatalog();
        });
    }

    function updateFilterManagerContent(filterManager) {
        let content = `
            <div style="margin-bottom: 10px;">
                <h4 style="margin: 0 0 5px 0;">Current Filters</h4>
                <table style="width: 100%; border-collapse: collapse;">
                    <thead>
                        <tr>
                            <th style="text-align: left; padding: 2px 5px;">Pattern</th>
                            <th style="text-align: left; padding: 2px 5px;">Action</th>
                            <th style="text-align: center; padding: 2px 5px;">Remove</th>
                        </tr>
                    </thead>
                    <tbody id="filtersTableBody">
        `;

        config.filters.forEach((filter, index) => {
            content += `
                <tr>
                    <td style="padding: 2px 5px;">${filter.patternText || filter.pattern.source}</td>
                    <td style="padding: 2px 5px;">${filter.action}</td>
                    <td style="text-align: center; padding: 2px 5px;">
                        <button class="removeFilterBtn" data-index="${index}" style="cursor: pointer;">X</button>
                    </td>
                </tr>
            `;
        });

        content += `
                    </tbody>
                </table>
            </div>
            <div style="margin-bottom: 10px;">
                <h4 style="margin: 0 0 5px 0;">Add New Filter</h4>
                <div style="display: flex; gap: 5px; align-items: center;">
                    <input type="text" id="newFilterPattern" placeholder="Regex pattern (e.g. anime|manga)" style="flex-grow: 1;">
                    <label style="white-space: nowrap;">
                        <input type="checkbox" id="caseInsensitive" checked> Case insensitive
                    </label>
                    <select id="newFilterAction">
                        <option value="setTop">Move to Top</option>
                        <option value="remove">Hide</option>
                    </select>
                    <button id="addNewFilter" style="cursor: pointer;">Add</button>
                </div>
            </div>
        `;

        filterManager.innerHTML = content;

        // Add event listeners after updating content
        setTimeout(() => {
            // Remove filter buttons
            document.querySelectorAll('.removeFilterBtn').forEach(btn => {
                btn.addEventListener('click', function() {
                    const index = parseInt(this.dataset.index);
                    config.filters.splice(index, 1);
                    saveConfig();
                    updateFilterManagerContent(filterManager);
                    updateActiveFiltersCount();
                });
            });

            // Add new filter button
            document.getElementById('addNewFilter').addEventListener('click', function() {
                const patternInput = document.getElementById('newFilterPattern');
                const caseInsensitive = document.getElementById('caseInsensitive').checked;
                const actionSelect = document.getElementById('newFilterAction');

                if (patternInput.value.trim()) {
                    try {
                        const patternText = patternInput.value.trim();
                        const flags = caseInsensitive ? 'i' : '';

                        const newFilter = {
                            pattern: new RegExp(patternText, flags),
                            patternText: patternText, // Store the raw text
                            action: actionSelect.value
                        };

                        config.filters.push(newFilter);
                        saveConfig();
                        updateFilterManagerContent(filterManager);
                        updateActiveFiltersCount();
                        patternInput.value = '';
                    } catch (e) {
                        alert('Invalid regex pattern: ' + e.message);
                    }
                }
            });
        }, 0);
    }

    function updateActiveFiltersCount() {
        const countElement = document.getElementById('activeFiltersCount');
        if (countElement) {
            countElement.textContent = `(${config.filters.length} active)`;
        }
    }

    function processCatalog() {
        const catalogDiv = document.getElementById('divThreads');
        if (!catalogDiv) return;

        // Reset all cells visibility first
        const allCells = Array.from(catalogDiv.querySelectorAll('.catalogCell'));
        allCells.forEach(cell => {
            cell.style.display = '';
            cell.style.order = '';
        });

        // Apply filters
        const cells = Array.from(catalogDiv.querySelectorAll('.catalogCell'));
        const matchedCells = [];

        cells.forEach(cell => {
            const subject = cell.querySelector('.labelSubject')?.textContent || '';
            const message = cell.querySelector('.divMessage')?.textContent || '';
            const text = `${subject} ${message}`;

            config.filters.forEach(filter => {
                if (filter.pattern.test(text)) {
                    if (filter.action === 'remove') {
                        cell.style.display = 'none';
                    } else if (filter.action === 'setTop') {
                        matchedCells.push(cell);
                        cell.style.order = -1;
                    }
                }
            });
        });

        // Bring matched cells to top
        matchedCells.reverse().forEach(cell => {
            catalogDiv.insertBefore(cell, catalogDiv.firstChild);
        });
    }

    // Initialize the script
    function init() {
        loadConfig();
        createDashboard();
        processCatalog();

        // Optional: Add mutation observer to handle dynamic updates
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList' &&
                    (mutation.target.id === 'divThreads' ||
                     mutation.target.classList.contains('catalogCell'))) {
                    processCatalog();
                    break;
                }
            }
        });

        const threadsDiv = document.getElementById('divThreads');
        if (threadsDiv) {
            observer.observe(threadsDiv, { childList: true, subtree: true });
        }
    }

    // Wait for page to load completely
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();