Open2ch NG Fix Plus

自動新着NG修正 + 回線・レベル1指定NG機能

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    });

})();