// ==UserScript== // @name Futaba NG Word Filter // @namespace http://tampermonkey.net/ // @version 1.0 // @description Hide catalog cells based on NG words from localStorage // @author You // @match https://nijiurachan.net/pc/catalog.php* // @grant none // ==/UserScript== (function() { 'use strict'; const NG_STORAGE_KEY = 'futaba_ng_settings'; const NG_ID_DURATION = 7 * 24 * 60 * 60 * 1000; // 7日間(ミリ秒) // NG設定を取得 function getNGSettings() { try { const saved = localStorage.getItem(NG_STORAGE_KEY); if (saved) { const settings = JSON.parse(saved); // デフォルト値を設定 if (!settings.words) settings.words = []; if (!settings.regexes) settings.regexes = []; if (!settings.ids) settings.ids = []; if (settings.enabled === undefined) settings.enabled = true; // 期限切れのIDを削除 const now = Date.now(); settings.ids = settings.ids.filter(item => { const elapsed = now - (item.addedAt || 0); return elapsed < NG_ID_DURATION; }); // クリーンアップ後の設定を保存 if (saved !== JSON.stringify(settings)) { localStorage.setItem(NG_STORAGE_KEY, JSON.stringify(settings)); } return settings; } } catch(e) { console.error('NG設定の読み込みエラー:', e); } return { words: [], regexes: [], ids: [], enabled: true }; } // テキストがNGに該当するかチェック function isNGText(text, settings) { if (!settings.enabled) return false; // ワードチェック(部分一致) for (const word of settings.words) { if (text.includes(word)) { console.log('NG Word matched:', word, 'in text:', text.substring(0, 50)); return true; } } // 正規表現チェック for (const pattern of settings.regexes) { try { const regex = new RegExp(pattern, 'i'); if (regex.test(text)) { console.log('NG Regex matched:', pattern, 'in text:', text.substring(0, 50)); return true; } } catch(e) { console.error('正規表現エラー:', pattern, e); } } return false; } // IDがNGに該当するかチェック function isNGId(threadId, opPostId, settings) { if (!settings.enabled) return false; for (const item of settings.ids) { if (item.id === threadId || item.id === opPostId) { console.log('NG ID matched:', item.id); return true; } } return false; } // NGフィルタを適用 function applyNGFilter() { const settings = getNGSettings(); if (!settings.enabled) { console.log('NGフィルタは無効です'); return; } console.log('NGフィルタ適用開始:', settings); const cells = document.querySelectorAll('.cat-cell[data-text]'); let hiddenCount = 0; cells.forEach(cell => { const text = cell.dataset.text || ''; const threadId = cell.dataset.threadId || ''; const opPostId = cell.dataset.opPostId || ''; // 既に非表示の場合はスキップ if (cell.classList.contains('cat-hidden') || cell.classList.contains('ng-hidden')) { return; } // NGチェック if (isNGText(text, settings) || isNGId(threadId, opPostId, settings)) { cell.classList.add('ng-hidden'); cell.classList.add('cat-hidden'); cell.style.display = 'none'; hiddenCount++; } }); console.log(`${hiddenCount}件のスレッドを非表示にしました`); // テーブルを再配置 rearrangeCatalogTable(); } // カタログテーブルを再配置(空いたスペースを詰める) function rearrangeCatalogTable() { const table = document.getElementById('cattable'); if (!table) return; const cols = 15; const allCells = Array.from(table.querySelectorAll('td.cat-cell')); const visibleCells = allCells.filter(cell => !cell.classList.contains('cat-hidden')); // 既存の行を取得 const tbody = table.querySelector('tbody') || table; const rows = Array.from(tbody.querySelectorAll('tr')); // 全ての行をクリア rows.forEach(row => row.innerHTML = ''); // 可視セルを再配置 visibleCells.forEach((cell, idx) => { const rowIdx = Math.floor(idx / cols); if (!rows[rowIdx]) { const newRow = document.createElement('tr'); tbody.appendChild(newRow); rows.push(newRow); } rows[rowIdx].appendChild(cell); }); // 最後の行の空きセルを埋める if (visibleCells.length > 0) { const lastRowIdx = Math.floor((visibleCells.length - 1) / cols); const lastRow = rows[lastRowIdx]; if (lastRow) { while (lastRow.children.length < cols) { const emptyCell = document.createElement('td'); lastRow.appendChild(emptyCell); } } } // 非表示セルは別の非表示行に移動 const hiddenCells = allCells.filter(cell => cell.classList.contains('cat-hidden')); if (hiddenCells.length > 0) { let hiddenRow = tbody.querySelector('tr.ng-hidden-row'); if (!hiddenRow) { hiddenRow = document.createElement('tr'); hiddenRow.className = 'ng-hidden-row'; hiddenRow.style.display = 'none'; tbody.appendChild(hiddenRow); } hiddenCells.forEach(cell => hiddenRow.appendChild(cell)); } } // NG設定の変更を監視 function watchNGSettings() { let lastSettings = JSON.stringify(getNGSettings()); setInterval(() => { const currentSettings = JSON.stringify(getNGSettings()); if (currentSettings !== lastSettings) { console.log('NG設定が変更されました。フィルタを再適用します。'); lastSettings = currentSettings; // 全てのNG非表示を解除 document.querySelectorAll('.ng-hidden').forEach(cell => { cell.classList.remove('ng-hidden'); cell.classList.remove('cat-hidden'); cell.style.display = ''; }); // フィルタを再適用 applyNGFilter(); } }, 1000); } // 初期化 function init() { console.log('Futaba NG Word Filter 初期化'); // DOMが完全に読み込まれるまで待機 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(applyNGFilter, 500); watchNGSettings(); }); } else { setTimeout(applyNGFilter, 500); watchNGSettings(); } // グローバルに公開(デバッグ用) window.reapplyNGFilter = applyNGFilter; } init(); })();