Open2ch NG Fix Plus

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();