// ==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 // @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 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 = ` Filters: (${config.filters.length} active) `; // 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 = `

Current Filters

`; config.filters.forEach((filter, index) => { content += ` `; }); content += `
Pattern Action Remove
${filter.patternText || filter.pattern.source} ${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 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(); } })();