// ==UserScript== // @name AIMGchan Applying localStorage settings. NG Filter & Tweet Toggle // @namespace http://tampermonkey.net/ // @version 2.0 // @description Hide catalog cells based on NG words and toggle tweet embeds // @author You // @match https://nijiurachan.net/pc/* // @grant none // ==/UserScript== (function() { 'use strict'; const NG_STORAGE_KEY = 'futaba_ng_settings'; const NG_ID_DURATION = 7 * 24 * 60 * 60 * 1000; // 7日間(ミリ秒) // ===== NGワードフィルタ機能 ===== // 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); } // ===== Tweet埋め込み表示切り替え機能 ===== // tweet-embedの表示/非表示を切り替える関数 function toggleTweetEmbeds() { const ogpPreview = localStorage.getItem('futaba_ogp_preview'); const tweetEmbeds = document.querySelectorAll('.tweet-embed'); tweetEmbeds.forEach(embed => { if (ogpPreview === '0') { embed.style.display = 'none'; } else if (ogpPreview === '1') { embed.style.display = ''; } }); } // MutationObserverでtweet-embedを監視 function watchTweetEmbeds() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { toggleTweetEmbeds(); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } // localStorageの変更を監視(他のタブでの変更も検知) function watchOGPPreviewChanges() { window.addEventListener('storage', (e) => { if (e.key === 'futaba_ogp_preview') { toggleTweetEmbeds(); } }); // 同一タブ内でのlocalStorage変更を検知するためのオーバーライド const originalSetItem = localStorage.setItem; localStorage.setItem = function(key, value) { originalSetItem.apply(this, arguments); if (key === 'futaba_ogp_preview') { toggleTweetEmbeds(); } }; } // ===== 初期化 ===== function init() { console.log('Futaba NG Filter & Tweet Toggle 初期化'); const isCatalog = window.location.pathname.includes('catalog.php'); const isThread = window.location.pathname.includes('thread.php'); // DOMが完全に読み込まれるまで待機 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (isCatalog) { setTimeout(applyNGFilter, 500); watchNGSettings(); } if (isThread) { toggleTweetEmbeds(); watchTweetEmbeds(); watchOGPPreviewChanges(); } }); } else { if (isCatalog) { setTimeout(applyNGFilter, 500); watchNGSettings(); } if (isThread) { toggleTweetEmbeds(); watchTweetEmbeds(); watchOGPPreviewChanges(); } } // グローバルに公開(デバッグ用) window.reapplyNGFilter = applyNGFilter; window.toggleTweetEmbeds = toggleTweetEmbeds; } init(); })();