そうだねソート

ふたば掲示板のレスをそうだね順にソートしたりハイライトやフィルタ機能を追加する(過去ログサイト対応)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         そうだねソート
// @namespace    http://2chan.net/
// @version      1.0.0
// @description  ふたば掲示板のレスをそうだね順にソートしたりハイライトやフィルタ機能を追加する(過去ログサイト対応)
// @author       futaba
// @match        http://*.2chan.net/*/res/*
// @match        https://*.2chan.net/*/res/*
// @match        http://kako.futakuro.com/futa/*/*/
// @match        https://kako.futakuro.com/futa/*/*/
// @match        https://*.ftbucket.info/*/*/
// @match        https://tsumanne.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=2chan.net
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ========================================
    // 定数定義
    // ========================================

    const DEFAULT_STATE = {
        sortOrder: 'original',
        enableFilter: false,
        minSodane: 1,
        highlightPositive: false,
        highlightNegative: false,
        highlightText: false,
        autoUpdate: false,
        updateInterval: 30,
        buttonPosition: 'center'
    };

    // フィルターベースのハイライト(相対的な色調整)
    const HIGHLIGHT_FILTERS = {
        positive: {
            high: 'brightness(1.08) saturate(1.08) contrast(1.00)',   // 10以上: 明るく鮮やか
            medium: 'brightness(1.04) saturate(1.04) contrast(1.00)', // 5-9: やや明るく
            low: 'brightness(1.02) saturate(1.02) contrast(1.00)'                    // 3-4: ほんのり明るく
        },
        negative: {
            zero: 'brightness(0.92) saturate(0.70) contrast(1.00)',   // 0: 暗く地味に
            low: 'brightness(0.96) saturate(0.85) contrast(1.00)'     // 1-2: やや暗く
        }
    };

    // テキストハイライトは絶対色指定(視認性重視)
    const TEXT_COLORS = {
        high: '#B00000',
        medium: '#C04000',
        low: '#A04000'
    };

    const SODANE_THRESHOLD = {
        HIGH: 10,
        MEDIUM: 5,
        LOW: 3
    };

    const UI_COLORS = {
        primary: '#800000',
        background: 'rgb(254, 255, 238)',
        border: '#800000'
    };

    const KAKOLOG_HOSTS = ['kako.futakuro.com', 'ftbucket.info', 'tsumanne.net'];
    const STORAGE_KEY = 'futaba_sodane_settings';
    const TIMING = {
        DOM_UPDATE_WAIT: 500,
        PANEL_CLICK_DELAY: 100
    };

    // ========================================
    // 状態管理
    // ========================================

    let state = { ...DEFAULT_STATE };
    let updateTimer = null;
    let isUpdating = false;

    // ========================================
    // ユーティリティ関数
    // ========================================

    function isKakologSite() {
        return KAKOLOG_HOSTS.some(host => location.hostname.includes(host));
    }

    function parseSodaneCount(text) {
        if (!text) return 0;
        const match = text.match(/そうだね[x×](\d+)/);
        if (match) return parseInt(match[1]);
        return text.trim() === '+' ? 0 : -1;
    }

    function formatSodaneCount(count) {
        return count === 0 ? '+' : `そうだねx${count}`;
    }

    function safeQuerySelector(selector, parent = document) {
        try {
            return parent.querySelector(selector);
        } catch (e) {
            return null;
        }
    }

    function safeQuerySelectorAll(selector, parent = document) {
        try {
            return parent.querySelectorAll(selector);
        } catch (e) {
            return [];
        }
    }

    /**
     * レス番号を取得(data-sno優先、フォールバックとしてid属性)
     */
    function getPostNumber(sodaneElement) {
        const dataSno = sodaneElement.getAttribute('data-sno');
        if (dataSno) return dataSno;

        const id = sodaneElement.getAttribute('id');
        if (id && id.startsWith('sd')) {
            return id.replace('sd', '');
        }

        return null;
    }

    // ========================================
    // コア機能
    // ========================================

    function getThreadContainer() {
        return safeQuerySelector('.thre');
    }

    function getAllPosts() {
        const posts = [];
        const sodaneMap = new Map();

        safeQuerySelectorAll('a.sod').forEach(sodane => {
            const postNum = getPostNumber(sodane);
            if (!postNum) return;

            const count = parseSodaneCount(sodane.textContent);
            if (count >= 0) {
                sodaneMap.set(postNum, count);
            }
        });

        const threDiv = getThreadContainer();
        if (!threDiv) return posts;

        safeQuerySelectorAll('table[border="0"]', threDiv).forEach(table => {
            const td = safeQuerySelector('td.rtd', table);
            const cno = safeQuerySelector('.cno', td);
            const postNum = cno?.textContent.replace('No.', '');

            if (!postNum) return;

            const sodaneCount = sodaneMap.get(postNum);
            let sodaneElement = safeQuerySelector(`a.sod[data-sno="${postNum}"]`, td);
            if (!sodaneElement) {
                sodaneElement = safeQuerySelector(`a.sod[id="sd${postNum}"]`, td);
            }

            posts.push({
                postNum: postNum,
                count: sodaneCount !== undefined ? sodaneCount : 0,
                element: table,
                sodaneElement: sodaneElement,
                td: td
            });
        });

        return posts;
    }

    /**
     * 個別のレスにハイライトを適用(フィルターベース)
     */
    function applyHighlightToPost(post) {
        if (!post.td) return;

        const count = post.count;

        // クリア
        post.td.style.filter = '';
        if (post.sodaneElement) {
            post.sodaneElement.style.color = '';
            post.sodaneElement.style.fontWeight = '';
        }

        // 背景フィルター(ポジティブ: 3以上)
        if (state.highlightPositive && count >= SODANE_THRESHOLD.LOW) {
            if (count >= SODANE_THRESHOLD.HIGH) {
                post.td.style.filter = HIGHLIGHT_FILTERS.positive.high;
            } else if (count >= SODANE_THRESHOLD.MEDIUM) {
                post.td.style.filter = HIGHLIGHT_FILTERS.positive.medium;
            } else {
                post.td.style.filter = HIGHLIGHT_FILTERS.positive.low;
            }
        }

        // 背景フィルター(ネガティブ: 0-2)
        if (state.highlightNegative && count <= 2) {
            if (count === 0) {
                post.td.style.filter = HIGHLIGHT_FILTERS.negative.zero;
            } else {
                post.td.style.filter = HIGHLIGHT_FILTERS.negative.low;
            }
        }

        // テキストハイライト(3以上、絶対色指定)
        if (state.highlightText && post.sodaneElement && count >= SODANE_THRESHOLD.LOW) {
            if (count >= SODANE_THRESHOLD.HIGH) {
                post.sodaneElement.style.color = TEXT_COLORS.high;
                post.sodaneElement.style.fontWeight = 'bold';
            } else if (count >= SODANE_THRESHOLD.MEDIUM) {
                post.sodaneElement.style.color = TEXT_COLORS.medium;
                post.sodaneElement.style.fontWeight = 'bold';
            } else {
                post.sodaneElement.style.color = TEXT_COLORS.low;
            }
        }
    }

    function cleanupDuplicateExtensionElements(element) {
        const titles = safeQuerySelectorAll('.userjs-title', element);
        if (titles.length > 1) {
            for (let i = 1; i < titles.length; i++) {
                titles[i].remove();
            }
        }

        const iframes = safeQuerySelectorAll('iframe[src*="youtube"]', element);
        if (iframes.length > 1) {
            for (let i = 1; i < iframes.length; i++) {
                const parent = iframes[i].parentElement;
                if (parent && parent.tagName === 'DIV' && parent.hasAttribute('*pageexpand*')) {
                    parent.remove();
                } else {
                    iframes[i].remove();
                }
            }
        }
    }

    function cleanupAllDuplicates() {
        const posts = getAllPosts();
        posts.forEach(post => {
            cleanupDuplicateExtensionElements(post.element);
        });
    }

    function applySettings(skipSort = false) {
        const posts = getAllPosts();
        if (posts.length === 0) return;

        // フィルタリング
        posts.forEach(post => {
            const shouldHide = state.enableFilter && post.count < state.minSodane;
            post.element.style.display = shouldHide ? 'none' : '';
        });

        // ハイライト
        posts.forEach(post => applyHighlightToPost(post));

        // ソート
        if (!skipSort) {
            sortPosts(posts);
        }
    }

    function sortPosts(posts) {
        const sorted = [...posts].sort((a, b) => {
            if (state.sortOrder === 'original') {
                return parseInt(a.postNum) - parseInt(b.postNum);
            }
            return (b.count - a.count) || (parseInt(a.postNum) - parseInt(b.postNum));
        });

        const threDiv = getThreadContainer();
        const firstTable = safeQuerySelector('table[border="0"]', threDiv);
        if (!threDiv || !firstTable) return;

        cleanupAllDuplicates();

        let insertAfter = firstTable;
        sorted.forEach(post => {
            threDiv.insertBefore(post.element, insertAfter.nextSibling);
            insertAfter = post.element;
        });
    }

    // ========================================
    // UI関連
    // ========================================

    function getButtonPositionStyle() {
        switch (state.buttonPosition) {
            case 'top':
                return 'top: 10px; transform: translateY(0);';
            case 'bottom':
                return 'bottom: 10px; top: auto; transform: translateY(0);';
            default:
                return 'top: 50%; transform: translateY(-50%);';
        }
    }

    function addMainButton() {
        const existing = document.getElementById('sodane-main-button');
        if (existing) existing.remove();

        const container = document.createElement('div');
        container.id = 'sodane-main-button';
        container.style.cssText = `
            position: fixed;
            right: 10px;
            z-index: 9999;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            cursor: pointer;
            font-size: 12px;
            font-weight: bold;
            text-align: center;
            ${getButtonPositionStyle()}
        `;

        container.innerHTML = `
            <div id="sodane-sort-toggle" style="padding: 6px 10px; border-bottom: 1px solid rgba(255,255,255,0.3);"></div>
            <div id="sodane-filter-toggle" style="display: flex; align-items: center; padding: 6px 10px;">
                <input type="checkbox" id="sodane-filter-checkbox" style="margin-right: 6px; cursor: pointer;">
                <span id="sodane-panel-opener">フィルタ</span>
            </div>
        `;

        document.body.appendChild(container);
        updateMainButtonAppearance();
        bindMainButtonEvents();
    }

    function bindMainButtonEvents() {
        const sortToggle = document.getElementById('sodane-sort-toggle');
        if (sortToggle) {
            sortToggle.onclick = () => {
                state.sortOrder = (state.sortOrder === 'desc') ? 'original' : 'desc';
                updateMainButtonAppearance();
                applySettings();
                saveSettings();
            };
        }

        const filterCheckbox = document.getElementById('sodane-filter-checkbox');
        if (filterCheckbox) {
            filterCheckbox.checked = state.enableFilter;
            filterCheckbox.onclick = (e) => {
                e.stopPropagation();
                state.enableFilter = filterCheckbox.checked;

                const panelCheckbox = document.getElementById('panel-filter-enable');
                if (panelCheckbox) panelCheckbox.checked = state.enableFilter;

                updateMainButtonAppearance();
                applySettings(true);
                saveSettings();
            };
        }

        const panelOpener = document.getElementById('sodane-panel-opener');
        if (panelOpener) {
            panelOpener.onclick = () => {
                const panel = document.getElementById('sodane-control-panel');
                if (panel) {
                    panel.remove();
                } else {
                    createControlPanel();
                }
            };
        }
    }

    function updateMainButtonAppearance() {
        const sortToggle = document.getElementById('sodane-sort-toggle');
        const filterToggle = document.getElementById('sodane-filter-toggle');
        if (!sortToggle || !filterToggle) return;

        sortToggle.textContent = 'そうだね順';
        if (state.sortOrder === 'desc') {
            sortToggle.style.backgroundColor = UI_COLORS.primary;
            sortToggle.style.color = 'white';
            sortToggle.style.border = `1px solid ${UI_COLORS.border}`;
        } else {
            sortToggle.style.backgroundColor = UI_COLORS.background;
            sortToggle.style.color = UI_COLORS.primary;
            sortToggle.style.border = `1px solid ${UI_COLORS.border}`;
        }

        if (state.enableFilter) {
            filterToggle.style.backgroundColor = UI_COLORS.primary;
            filterToggle.style.color = 'white';
            filterToggle.style.border = `1px solid ${UI_COLORS.border}`;
        } else {
            filterToggle.style.backgroundColor = UI_COLORS.background;
            filterToggle.style.color = UI_COLORS.primary;
            filterToggle.style.border = `1px solid ${UI_COLORS.border}`;
        }
    }

    function createControlPanel() {
        const existing = document.getElementById('sodane-control-panel');
        if (existing) existing.remove();

        const panel = document.createElement('div');
        panel.id = 'sodane-control-panel';
        panel.style.cssText = `
            position: fixed;
            right: 130px;
            width: 340px;
            background: white;
            border: 2px solid ${UI_COLORS.primary};
            padding: 12px;
            z-index: 10000;
            font-family: sans-serif;
            font-size: 12px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3);
            border-radius: 5px;
            ${getButtonPositionStyle()}
        `;

        const isKakolog = isKakologSite();
        const disabledAttr = isKakolog ? 'disabled' : '';
        const kakoLogNote = isKakolog
            ? '<span style="font-size: 9px; color: #999;"> (過去ログ無効)</span>'
            : '<span id="update-status" style="font-size: 10px; color: #666; margin-left: 5px;"></span>';

        panel.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ccc;">
                <strong style="font-size: 14px;">設定パネル</strong>
                <button id="close-panel" style="cursor: pointer; background: ${UI_COLORS.primary}; color: white; border: none; padding: 2px 8px; border-radius: 3px;">✕</button>
            </div>
            <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
                <div>
                    <div style="margin-bottom: 15px;">
                        <strong style="display: block; margin-bottom: 8px;">並び順</strong>
                        <label style="display: block; cursor: pointer;"><input type="radio" name="sort" value="original"> デフォルト</label>
                        <label style="display: block; cursor: pointer;"><input type="radio" name="sort" value="desc"> そうだね順</label>
                    </div>
                    <div>
                        <strong style="display: block; margin-bottom: 8px;">ハイライト</strong>
                        <label style="display: block; cursor: pointer;"><input type="checkbox" name="highlight" value="positive"> 背景+</label>
                        <label style="display: block; cursor: pointer;"><input type="checkbox" name="highlight" value="negative"> 背景-</label>
                        <label style="display: block; cursor: pointer;"><input type="checkbox" name="highlight" value="text"> そうだね色</label>
                    </div>
                </div>
                <div>
                    <div style="margin-bottom: 15px;">
                        <label style="display: flex; align-items: center; cursor: pointer; white-space: nowrap;">
                            <input type="checkbox" id="panel-filter-enable">
                            <strong style="margin: 0 4px;">そうだね</strong>
                            <input type="number" id="panel-filter-value" min="0" style="width: 40px; margin-right: 4px;"> 未満を非表示
                        </label>
                    </div>
                    <div style="margin-bottom: 15px;">
                        <label style="display: flex; align-items: center; cursor: pointer; white-space: nowrap;">
                            <input type="checkbox" id="panel-autoupdate-enable" ${disabledAttr}>
                            <strong style="margin: 0 4px;">自動更新</strong>
                            <input type="number" id="panel-autoupdate-interval" min="10" style="width: 40px; margin-right: 4px;" ${disabledAttr}> 秒間隔
                        </label>
                        ${kakoLogNote}
                    </div>
                    <div>
                        <strong style="display: block; margin-bottom: 8px;">ボタン位置</strong>
                        <label style="display: block; cursor: pointer;"><input type="radio" name="button-position" value="top"> 右上</label>
                        <label style="display: block; cursor: pointer;"><input type="radio" name="button-position" value="center"> 右中央</label>
                        <label style="display: block; cursor: pointer;"><input type="radio" name="button-position" value="bottom"> 右下</label>
                    </div>
                </div>
            </div>
            <div style="display: flex; gap: 8px; margin-top: 15px; border-top: 1px solid #ccc; padding-top: 10px;">
                <button id="re-apply-settings" style="flex: 3; padding: 6px; background: ${UI_COLORS.primary}; color: white; border: none; cursor: pointer; border-radius: 4px; font-size: 12px; font-weight: bold;">再適用</button>
                <button id="reset-settings" style="flex: 1; padding: 6px; background: #666; color: white; border: none; cursor: pointer; border-radius: 4px; font-size: 11px;">リセット</button>
            </div>
        `;

        document.body.appendChild(panel);
        bindPanelEvents();
        loadSettingsToPanel();
        setupPanelOutsideClick(panel);

        const closeBtn = document.getElementById('close-panel');
        if (closeBtn) {
            closeBtn.onclick = () => panel.remove();
        }

        const resetBtn = document.getElementById('reset-settings');
        if (resetBtn) {
            resetBtn.onclick = resetSettings;
        }
    }

    function setupPanelOutsideClick(panel) {
        setTimeout(() => {
            function closeOnOutsideClick(e) {
                const mainButton = document.getElementById('sodane-main-button');
                if (!panel.contains(e.target) && !mainButton?.contains(e.target)) {
                    panel.remove();
                    document.removeEventListener('click', closeOnOutsideClick);
                }
            }
            document.addEventListener('click', closeOnOutsideClick);
        }, TIMING.PANEL_CLICK_DELAY);
    }

    // ========================================
    // イベント処理
    // ========================================

    function bindPanelEvents() {
        const applyAndSave = () => {
            applySettings();
            saveSettings();
        };

        const applyFilterOnly = () => {
            applySettings(true);
            saveSettings();
        };

        // 並び順
        safeQuerySelectorAll('input[name="sort"]').forEach(radio => {
            radio.onchange = () => {
                state.sortOrder = radio.value;
                updateMainButtonAppearance();
                applyAndSave();
            };
        });

        // ハイライト
        safeQuerySelectorAll('input[name="highlight"]').forEach(checkbox => {
            checkbox.onchange = () => {
                state.highlightPositive = safeQuerySelector('input[name="highlight"][value="positive"]')?.checked || false;
                state.highlightNegative = safeQuerySelector('input[name="highlight"][value="negative"]')?.checked || false;
                state.highlightText = safeQuerySelector('input[name="highlight"][value="text"]')?.checked || false;
                applyFilterOnly();
            };
        });

        // フィルタ
        const filterEnable = document.getElementById('panel-filter-enable');
        const filterValue = document.getElementById('panel-filter-value');

        if (filterEnable) {
            filterEnable.onchange = () => {
                state.enableFilter = filterEnable.checked;
                const mainCheckbox = document.getElementById('sodane-filter-checkbox');
                if (mainCheckbox) mainCheckbox.checked = state.enableFilter;
                updateMainButtonAppearance();
                applyFilterOnly();
            };
        }

        if (filterValue) {
            filterValue.oninput = () => {
                state.minSodane = parseInt(filterValue.value) || 0;
                if (state.enableFilter) {
                    applyFilterOnly();
                } else {
                    saveSettings();
                }
            };
        }

        // 自動更新
        const autoUpdateEnable = document.getElementById('panel-autoupdate-enable');
        const autoUpdateInterval = document.getElementById('panel-autoupdate-interval');

        if (autoUpdateEnable) {
            autoUpdateEnable.onchange = () => {
                state.autoUpdate = autoUpdateEnable.checked;
                toggleAutoUpdate();
                saveSettings();
            };
        }

        if (autoUpdateInterval) {
            autoUpdateInterval.oninput = () => {
                state.updateInterval = parseInt(autoUpdateInterval.value) || 30;
                if (state.autoUpdate) {
                    toggleAutoUpdate();
                }
                saveSettings();
            };
        }

        // ボタン位置
        safeQuerySelectorAll('input[name="button-position"]').forEach(radio => {
            radio.onchange = () => {
                state.buttonPosition = radio.value;
                addMainButton();
                const panel = document.getElementById('sodane-control-panel');
                if (panel) panel.remove();
                createControlPanel();
                saveSettings();
            };
        });

        // 再適用ボタン
        const reApplyBtn = document.getElementById('re-apply-settings');
        if (reApplyBtn) {
            reApplyBtn.onclick = () => {
                loadSettingsFromPanel();
                applyAndSave();
            };
        }
    }

    // ========================================
    // データ管理
    // ========================================

    function loadSettingsToPanel() {
        const sortRadio = safeQuerySelector(`input[name="sort"][value="${state.sortOrder}"]`);
        if (sortRadio) sortRadio.checked = true;

        const highlightPositive = safeQuerySelector('input[name="highlight"][value="positive"]');
        const highlightNegative = safeQuerySelector('input[name="highlight"][value="negative"]');
        const highlightText = safeQuerySelector('input[name="highlight"][value="text"]');
        if (highlightPositive) highlightPositive.checked = state.highlightPositive;
        if (highlightNegative) highlightNegative.checked = state.highlightNegative;
        if (highlightText) highlightText.checked = state.highlightText;

        const filterEnable = document.getElementById('panel-filter-enable');
        const filterValue = document.getElementById('panel-filter-value');
        if (filterEnable) filterEnable.checked = state.enableFilter;
        if (filterValue) filterValue.value = state.minSodane;

        const autoUpdateEnable = document.getElementById('panel-autoupdate-enable');
        const autoUpdateInterval = document.getElementById('panel-autoupdate-interval');
        if (autoUpdateEnable) autoUpdateEnable.checked = state.autoUpdate;
        if (autoUpdateInterval) autoUpdateInterval.value = state.updateInterval;

        const buttonPosRadio = safeQuerySelector(`input[name="button-position"][value="${state.buttonPosition}"]`);
        if (buttonPosRadio) buttonPosRadio.checked = true;
    }

    function loadSettingsFromPanel() {
        const sortRadio = safeQuerySelector('input[name="sort"]:checked');
        if (sortRadio) state.sortOrder = sortRadio.value;

        const highlightPositive = safeQuerySelector('input[name="highlight"][value="positive"]');
        const highlightNegative = safeQuerySelector('input[name="highlight"][value="negative"]');
        const highlightText = safeQuerySelector('input[name="highlight"][value="text"]');
        state.highlightPositive = highlightPositive?.checked || false;
        state.highlightNegative = highlightNegative?.checked || false;
        state.highlightText = highlightText?.checked || false;

        const filterEnable = document.getElementById('panel-filter-enable');
        const filterValue = document.getElementById('panel-filter-value');
        if (filterEnable) state.enableFilter = filterEnable.checked;
        if (filterValue) state.minSodane = parseInt(filterValue.value) || 0;

        const autoUpdateEnable = document.getElementById('panel-autoupdate-enable');
        const autoUpdateInterval = document.getElementById('panel-autoupdate-interval');
        if (autoUpdateEnable) state.autoUpdate = autoUpdateEnable.checked;
        if (autoUpdateInterval) state.updateInterval = parseInt(autoUpdateInterval.value) || 30;

        const buttonPosRadio = safeQuerySelector('input[name="button-position"]:checked');
        if (buttonPosRadio) state.buttonPosition = buttonPosRadio.value;
    }

    function saveSettings() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        } catch (e) {
            console.error('設定の保存に失敗:', e);
        }
    }

    function loadSettings() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                const parsed = JSON.parse(saved);
                state = { ...DEFAULT_STATE, ...parsed };
            }
        } catch (e) {
            console.error('設定の読み込みに失敗:', e);
            state = { ...DEFAULT_STATE };
        }
    }

    function resetSettings() {
        try {
            localStorage.removeItem(STORAGE_KEY);
            state = { ...DEFAULT_STATE };
            location.reload();
        } catch (e) {
            console.error('設定のリセットに失敗:', e);
        }
    }

    // ========================================
    // 自動更新機能
    // ========================================

    function toggleAutoUpdate() {
        if (updateTimer) {
            clearInterval(updateTimer);
            updateTimer = null;
        }

        if (state.autoUpdate && !isKakologSite()) {
            updateTimer = setInterval(fetchLatestSodane, state.updateInterval * 1000);
        }
    }

    async function fetchLatestSodane() {
        if (isUpdating) return;
        isUpdating = true;

        const statusEl = document.getElementById('update-status');
        if (statusEl) {
            statusEl.textContent = '更新中...';
            statusEl.style.color = '#0066cc';
        }

        try {
            const response = await fetch(location.href, { cache: 'no-cache' });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            const buffer = await response.arrayBuffer();
            const html = new TextDecoder('shift_jis').decode(buffer);
            const doc = new DOMParser().parseFromString(html, 'text/html');

            // スレ落ちチェック
            const bodyText = doc.body.textContent || '';
            if (bodyText.includes('スレッドがありません') || bodyText.includes('Not Found')) {
                state.autoUpdate = false;
                toggleAutoUpdate();
                if (statusEl) {
                    statusEl.textContent = 'スレ落ち';
                    statusEl.style.color = '#cc0000';
                }
                return;
            }

            // サーバーから取得したそうだね要素をマップ化
            const newSodaneElementsMap = new Map();
            doc.querySelectorAll('a.sod').forEach(sodane => {
                const postNum = getPostNumber(sodane);
                if (!postNum) return;

                const count = parseSodaneCount(sodane.textContent);
                if (count >= 0) {
                    newSodaneElementsMap.set(postNum, {
                        count: count,
                        element: sodane.cloneNode(true)
                    });
                }
            });

            // 現在のページの全レスを走査
            let hasUpdates = false;
            const threDiv = getThreadContainer();
            if (!threDiv) return;

            safeQuerySelectorAll('table[border="0"]', threDiv).forEach(table => {
                const td = safeQuerySelector('td.rtd', table);
                if (!td) return;

                const cno = safeQuerySelector('.cno', td);
                const postNum = cno?.textContent.replace('No.', '');
                if (!postNum) return;

                // 既存のそうだね要素を検索
                let existingSodane = safeQuerySelector(`a.sod[data-sno="${postNum}"]`, td);
                if (!existingSodane) {
                    existingSodane = safeQuerySelector(`a.sod[id="sd${postNum}"]`, td);
                }

                const newSodaneData = newSodaneElementsMap.get(postNum);

                // サーバーにもクライアントにもそうだね要素が無い場合(投稿直後の新規レス)
                if (!newSodaneData && !existingSodane) {
                    // デフォルトのそうだね要素を作成(data-sno属性は設定しない)
                    const defaultSodane = document.createElement('a');
                    defaultSodane.className = 'sod';
                    defaultSodane.id = `sd${postNum}`;
                    defaultSodane.href = 'javascript:void(0);';
                    defaultSodane.rel = 'noreferrer';
                    defaultSodane.textContent = '+';

                    if (cno && cno.nextSibling) {
                        td.insertBefore(document.createTextNode(' '), cno.nextSibling);
                        td.insertBefore(defaultSodane, cno.nextSibling.nextSibling);
                    } else if (cno) {
                        td.appendChild(document.createTextNode(' '));
                        td.appendChild(defaultSodane);
                    }

                    hasUpdates = true;
                    return;
                }

                // サーバーにそうだね情報が無い場合はスキップ
                if (!newSodaneData) return;

                if (existingSodane) {
                    // 既存レス: そうだね数が変わっていれば更新
                    const currentCount = parseSodaneCount(existingSodane.textContent);
                    if (currentCount !== newSodaneData.count) {
                        existingSodane.textContent = formatSodaneCount(newSodaneData.count);
                        hasUpdates = true;
                    }
                } else {
                    // 新規レス: そうだね要素自体を挿入
                    const newSodaneElement = newSodaneData.element;

                    if (cno && cno.nextSibling) {
                        td.insertBefore(document.createTextNode(' '), cno.nextSibling);
                        td.insertBefore(newSodaneElement, cno.nextSibling.nextSibling);
                    } else if (cno) {
                        td.appendChild(document.createTextNode(' '));
                        td.appendChild(newSodaneElement);
                    }

                    hasUpdates = true;
                }
            });

            // DOM更新が完全に反映されるのを待ってから
            // 常にハイライトとフィルタを再適用(更新の有無に関わらず)
            setTimeout(() => {
                applySettings(true); // skipSort = true
                cleanupAllDuplicates();
            }, TIMING.DOM_UPDATE_WAIT);

            // ステータス更新
            if (statusEl) {
                const timeStr = new Date().toLocaleTimeString('ja-JP');
                statusEl.textContent = `最終更新: ${timeStr}`;
                statusEl.style.color = '#008000';
            }

        } catch (error) {
            console.error('自動更新エラー:', error);
            if (statusEl) {
                statusEl.textContent = '更新失敗';
                statusEl.style.color = '#cc0000';
            }
        } finally {
            isUpdating = false;
        }
    }

    // ========================================
    // 初期化
    // ========================================

    /**
     * MutationObserverでDOM変更を監視(リアルタイム削除)
     */
    function startDOMMutationObserver() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // タイトル要素の追加を検出して即座に重複削除
                        if (node.className?.includes('userjs-title')) {
                            const parent = node.closest('table[border="0"]');
                            if (parent) {
                                const titles = parent.querySelectorAll('.userjs-title');
                                if (titles.length > 1) {
                                    Array.from(titles).slice(1).forEach(t => t.remove());
                                }
                            }
                        }

                        // YouTube iframe重複も即削除
                        if (node.tagName === 'IFRAME' && node.src?.includes('youtube')) {
                            const parent = node.closest('table[border="0"]');
                            if (parent) {
                                const iframes = parent.querySelectorAll('iframe[src*="youtube"]');
                                if (iframes.length > 1) {
                                    Array.from(iframes).slice(1).forEach(iframe => {
                                        const iframeParent = iframe.parentElement;
                                        if (iframeParent && iframeParent.tagName === 'DIV' && iframeParent.hasAttribute('*pageexpand*')) {
                                            iframeParent.remove();
                                        } else {
                                            iframe.remove();
                                        }
                                    });
                                }
                            }
                        }
                    }
                });
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    /**
     * スクリプトの初期化
     */
    function initialize() {
        try {
            loadSettings();
            startDOMMutationObserver();
            addMainButton();
            applySettings();
            toggleAutoUpdate();
        } catch (e) {
            console.error('初期化エラー:', e);
        }
    }

    // ======================================
    // 他スクリプトとの競合を避け、確実に実行
    // ======================================

    // 即時実行関数で囲む(グローバル汚染防止)
    (function () {
        'use strict';

        // ページ構築が落ち着くまで少し待つ
        // (ふたば系など、赤福spや他の拡張と競合しないように)
        setTimeout(() => {
            try {
                initialize();
            } catch (e) {
                console.error('遅延初期化エラー:', e);
            }
        }, 2000); // ← 2秒は実戦的な安全マージン

    })();

})();