// ==UserScript== // @name 8chan Catalog Filter // @version 1.4 // @description Filter catalog threads using regex patterns with per-filter board settings and bumplock hide option // @match *://8chan.moe/*/catalog.* // @grant none // @license MIT // @namespace https://greasyfork.org/users/1459581 // @downloadURL none // ==/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 board: 'gacha', // Board for this filter (optional) action: 'setTop' // 'setTop' or 'remove' } // More filters can be added by the user ], hideBumplocked: true // Default to hide bumplocked threads }; // 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 board: filter.board || '', // Board setting for this filter action: filter.action })); // Handle bumplocked setting if it exists if (parsedConfig.hideBumplocked !== undefined) { config.hideBumplocked = parsedConfig.hideBumplocked; } 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, board: filter.board || '', action: filter.action })), hideBumplocked: config.hideBumplocked }; localStorage.setItem('8chanCatalogFilterConfig', JSON.stringify(serializedConfig)); } // Get current board from URL function getCurrentBoard() { const match = window.location.pathname.match(/\/([^\/]+)\/catalog/); return match ? match[1] : ''; } // Check if a filter should apply on the current board function shouldApplyFilter(filter) { const currentBoard = getCurrentBoard(); // If filter has no board specified or matches current board, apply it return !filter.board || filter.board === '' || filter.board === currentBoard; } // 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 = ` Filters: (${config.filters.length} active) Current board: ${getCurrentBoard() || 'unknown'} `; // 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(); }); document.getElementById('hideBumplockedCheck').addEventListener('change', function() { config.hideBumplocked = this.checked; saveConfig(); processCatalog(); }); } function updateFilterManagerContent(filterManager) { let content = `

Current Filters

`; config.filters.forEach((filter, index) => { content += ` `; }); content += `
Pattern Board Action Remove
${filter.patternText || filter.pattern.source} ${filter.board || 'All boards'} ${filter.action}

Add New Filter

`; 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 boardInput = document.getElementById('newFilterBoard'); 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 boardValue = boardInput.value.trim(); const newFilter = { pattern: new RegExp(patternText, flags), patternText: patternText, // Store the raw text board: boardValue, // Board specific to this filter action: actionSelect.value }; config.filters.push(newFilter); saveConfig(); updateFilterManagerContent(filterManager); updateActiveFiltersCount(); patternInput.value = ''; boardInput.value = ''; } catch (e) { alert('Invalid regex pattern: ' + e.message); } } }); }, 0); } function updateActiveFiltersCount() { const countElement = document.getElementById('activeFiltersCount'); if (countElement) { // Count only filters applicable to the current board const currentBoard = getCurrentBoard(); const applicableFilters = config.filters.filter(filter => !filter.board || filter.board === '' || filter.board === currentBoard ); countElement.textContent = `(${applicableFilters.length} active on this board)`; } } 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}`; let shouldHide = false; // Check for bumplocked threads first if option is enabled if (config.hideBumplocked) { const bumpLockIndicator = cell.querySelector('.bumpLockIndicator'); if (bumpLockIndicator) { cell.style.display = 'none'; shouldHide = true; } } // If not already hidden by bumplocked filter, check other filters if (!shouldHide) { config.filters.forEach(filter => { // Only apply filter if it's for the current board or for all boards if (shouldApplyFilter(filter) && 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(); updateActiveFiltersCount(); // 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(); } })();