自動新着NG修正 + 回線・レベル1指定NG機能
// ==UserScript==
// @name Open2ch NG Fix Plus
// @version 2.1
// @description 自動新着NG修正 + 回線・レベル1指定NG機能
// @author 七色の彩り
// @match https://*.open2ch.net/test/read.cgi/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=open2ch.net
// @run-at document-start
// @grant none
// @license MIT
// @namespace https://greasyfork.org/ja/users/864059
// ==/UserScript==
(function() {
'use strict';
const match = location.pathname.match(/\/([^\/]+)\/\d+/);
if (!match) return;
const board = match[1];
const storageKey = 'ignv4' + board;
const customKey = '_opt_flt_' + board; // 広域NG用の独自キー
let styleEl = null;
// --- 1. CSS更新関数 ---
const updateNGStyle = () => {
const ignoreList = (localStorage.getItem(storageKey) || '').split('<D>').filter(id => id);
const customList = JSON.parse(localStorage.getItem(customKey) || '[]');
// 1. 標準NGの処理(既存レスのクラス名 idXXXX にも対応)
let cssRules = ignoreList.map(id => {
const idNoDot = id.replace(/\./g, '');
return `dl[uid="${id}"], dl:has(.id${idNoDot}), .id${idNoDot} { display: none !important; }`;
}).join('\n');
// 2. 拡張(広域)NGの処理
customList.forEach(pattern => {
// pattern は "ng.L1" 形式。これを "ngL1" に変換
const noDot = pattern.replace(/\./g, '');
// uid属性(新着)と、クラス名(既存)の両方を狙い撃ち
cssRules += `\ndl[uid$=".${pattern}"] { display: none !important; }`;
cssRules += `\ndl[uid$="${noDot}"] { display: none !important; }`;
// クラス名に "ngL1" を含む dl 要素を丸ごと消す
cssRules += `\ndl:has([class*="${noDot}"]), dt[class*="${noDot}"], dd[class*="${noDot}"] { display: none !important; }`;
});
if (!styleEl) {
styleEl = document.createElement('style');
(document.head || document.documentElement).appendChild(styleEl);
}
styleEl.textContent = cssRules;
};
// --- 2. 広域NG登録関数 ---
const addCustomNG = (fullId) => {
let pattern = "";
if (fullId.includes('.')) {
// 通常レス:ab.ng.L1 形式
const parts = fullId.split('.');
if (parts.length >= 3) {
pattern = `${parts[1]}.${parts[2]}`; // ng.L1
}
} else if (fullId.length >= 4) {
// 自動新着:abngL1 形式
// 後ろから4文字目〜3文字目を回線、後ろ2文字をレベルとして抽出
const line = fullId.slice(-4, -2); // vb
const level = fullId.slice(-2); // L1
pattern = `${line}.${level}`; // ng.L1 に統一して保存
}
if (!pattern || !pattern.endsWith('.L1')) return;
let list = JSON.parse(localStorage.getItem(customKey) || '[]');
if (!list.includes(pattern)) {
list.push(pattern);
if (list.length > 50) list.shift();
localStorage.setItem(customKey, JSON.stringify(list));
updateNGStyle();
if (typeof updateIgnoreBox === 'function') updateIgnoreBox();
}
};
// --- 3. UI操作の監視 ---
document.addEventListener('click', (e) => {
const target = e.target;
// A. サイトの「×」ボタンが押されたとき → 独自ボタンを注入
const ignoreBtn = target.closest('.ignore');
if (ignoreBtn) {
const fullId = ignoreBtn.getAttribute('val'); // ドットあり:ab.ng.L1 / ドットなし:abngL1
// 判定条件:ドットありなら「.L1」で終わる、ドットなしなら「L1」で終わる
const isL1 = fullId.includes('.') ? fullId.endsWith('.L1') : fullId.endsWith('L1');
if (isL1) {
setTimeout(() => {
const checkDiv = ignoreBtn.parentElement.querySelector('.ignore_check');
if (checkDiv && !checkDiv.querySelector('.custom-ng-btn')) {
const btn = document.createElement('a');
btn.href = "#";
btn.className = "custom-ng-btn iobtton";
btn.style = "color: #f60; font-size: 10.7px; margin: 1px -2px 0px 0; padding: 5px; height: 15.6px; width: 12px; border: 1.1px solid rgba(0, 0, 0, 0.1); border-radius: 3px; background-color: rgba(255, 255, 255, 0.5); display: inline-flex; align-items: center; justify-content: center; box-sizing: content-box; vertical-align: top; text-decoration: none; line-height: 1;";
btn.innerHTML = '<span class="fas" title="回線・L1指定NG"></span>';
btn.onclick = (ev) => {
ev.preventDefault();
addCustomNG(fullId);
// 本家の「閉じる」ボタンを探してクリックさせる
const closeBtn = checkDiv.querySelector('.ino');
if (closeBtn) {
closeBtn.click();
} else {
checkDiv.remove();
}
};
const closeBtn = checkDiv.querySelector('.ino');
if (closeBtn) {
checkDiv.insertBefore(btn, closeBtn);
} else {
checkDiv.appendChild(btn);
}
}
}, 10);
}
}
// B. サイト標準のNG確定ボタン(iok)が押されたとき
if (target.closest('.iok')) {
setTimeout(() => {
updateNGStyle(); // CSSを更新
updateIgnoreBox(); // 管理ボックスのUIを再描画
}, 100);
}
// C. 本家の「全クリア」ボタンが押されたとき
if (target.closest('.clear_ignore')) {
// 本家の要素(ignore_box)を特定
const ignoreBox = document.querySelector('.ignore_box');
// 本家がアニメーションで隠し始めるのを待ってから処理
setTimeout(() => {
// 1. 本家が残したままにしている古いHTML(「無視設定:n件」など)を強制消去
if (ignoreBox) {
// 自分のUI(custom_info)以外をすべて消す
const myInfo = ignoreBox.querySelector('.custom_info');
ignoreBox.innerHTML = '';
if (myInfo) ignoreBox.appendChild(myInfo);
}
// 2. その後、自分自身のUIを最新状態で描画・維持する
let count = 0;
const retry = setInterval(() => {
updateIgnoreBox();
count++;
if (count > 5) clearInterval(retry);
}, 200);
}, 600); // 本家のアニメーション時間を考慮して0.6秒待機
}
}, true);
// --- 4. 監視と初期化 ---
updateNGStyle();
const updateIgnoreBox = () => {
const customList = JSON.parse(localStorage.getItem(customKey) || '[]');
let ignoreDiv = document.getElementById('ignore_div');
// 回線NGが0件の場合の処理
if (customList.length === 0) {
const oldInfo = document.querySelector('.custom_info');
if (oldInfo) oldInfo.remove();
// 本家NGもなければ隠す
if (ignoreDiv) {
const ignoreBox = ignoreDiv.querySelector('.ignore_box');
if (!ignoreBox || ignoreBox.children.length === 0) ignoreDiv.style.display = 'none';
}
return;
}
// --- 親要素のチェックと強制表示 ---
if (!ignoreDiv) {
const footer = document.querySelector('.field_footer') || document.body;
ignoreDiv = document.createElement('div');
ignoreDiv.id = 'ignore_div';
footer.appendChild(ignoreDiv);
}
// ここで強制表示。本家が消えていても、自分がいれば枠を出す
ignoreDiv.style.display = 'block';
let ignoreBox = ignoreDiv.querySelector('.ignore_box');
if (!ignoreBox) {
ignoreBox = document.createElement('div');
ignoreBox.className = 'ignore_box';
ignoreDiv.appendChild(ignoreBox);
}
// --- 本家のエリアと自分のエリアを上下に分ける ---
ignoreBox.style.textAlign = 'center';
ignoreBox.style.padding = '8px';
// 既存の自分のUIを掃除
const oldInfo = document.querySelector('.custom_info');
if (oldInfo) oldInfo.remove();
// 回線NG用のエリア(div)を作成
const container = document.createElement('div');
container.className = 'custom_info';
// 本家との間に薄い線と余白を入れ、自分専用の1行(ブロック)として表示
container.style = "margin-top: 8px; padding-top: 5px; border-top: 1px solid #ccc; display: block; line-height: 1.5;";
// 本家と全く同じ「2段構成」を再現
container.innerHTML = `
<div>回線NG:<span id="custom_count">${customList.length}</span>件</div>
<a class="custom_undo_btn" href="#" style="color: #0000ff; text-decoration: underline; font-size: 1em;">一個戻す</a>
<a class="custom_clear_btn" href="#" style="color: #0000ff; text-decoration: underline; font-size: 1em;">全クリア</a>
`;
// A. 「一個戻す」の動作
container.querySelector('.custom_undo_btn').onclick = (e) => {
e.preventDefault();
if (customList.length > 0) {
customList.pop(); // 最後の1件を削除
localStorage.setItem(customKey, JSON.stringify(customList));
updateNGStyle(); // レス表示を更新
updateIgnoreBox(); // UIの件数表示を更新
}
};
// B. 「全クリア」の動作
container.querySelector('.custom_clear_btn').onclick = (e) => {
e.preventDefault();
if (confirm('回線NG設定をすべてクリアしますか?')) {
localStorage.removeItem(customKey);
updateNGStyle();
container.remove();
if (!ignoreBox.textContent.includes('無視設定')) ignoreDiv.style.display = 'none';
}
};
ignoreBox.appendChild(container);
};
const observer = new MutationObserver(() => {
updateNGStyle();
});
window.addEventListener('DOMContentLoaded', () => {
const target = document.getElementById('res_field') || document.body;
observer.observe(target, { childList: true });
updateNGStyle();
let initCount = 0;
const initRetry = setInterval(() => {
updateIgnoreBox();
initCount++;
// 0.5秒おきに10回(約5秒間)実行して、本家の遅延描画を追いかける
if (initCount > 10) clearInterval(initRetry);
}, 500);
});
})();