8chan Single ID Post Opacity with Thread-Specific Cross-Domain Toggle

Halves opacity of posts with unique labelId (based on background-color) if CHECK_UNIQUE_IDS is true, adds a toggle icon to adjust opacity for all posts by ID color in the same thread (including OP), persists toggle state across 8chan.moe and 8chan.se, and handles dynamically added posts

2025-04-19 기준 버전입니다. 최신 버전을 확인하세요.

// ==UserScript==
// @name        8chan Single ID Post Opacity with Thread-Specific Cross-Domain Toggle
// @namespace   https://8chan.moe
// @description Halves opacity of posts with unique labelId (based on background-color) if CHECK_UNIQUE_IDS is true, adds a toggle icon to adjust opacity for all posts by ID color in the same thread (including OP), persists toggle state across 8chan.moe and 8chan.se, and handles dynamically added posts
// @match       https://8chan.moe/*/res/*
// @match       https://8chan.se/*/res/*
// @version     1.9
// @author      Anonymous
// @grant       GM_setValue
// @grant       GM_getValue
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Global constant to enable/disable unique ID opacity check
    // Set to true to halve opacity for IDs with only one post; set to false to disable
    const CHECK_UNIQUE_IDS = true;

    // Function to extract board and thread from URL and create a domain-agnostic storage key
    function getThreadInfo() {
        const url = window.location.href;
        const regex = /https:\/\/8chan\.(moe|se)\/([^/]+)\/res\/(\d+)\.html/;
        const match = url.match(regex);
        if (match) {
            return {
                board: match[2], // e.g., 'a'
                thread: match[3], // e.g., '23364'
                storageKey: `toggledColors_${match[2]}_${match[3]}` // e.g., 'toggledColors_a_23364'
            };
        }
        return null;
    }

    // Wait for the DOM to be fully loaded
    window.addEventListener('load', function() {
        // Get thread info from URL
        const threadInfo = getThreadInfo();
        if (!threadInfo) {
            console.error('Could not parse board and thread from URL');
            return;
        }

        // Use the domain-agnostic storage key
        const storageKey = threadInfo.storageKey;

        // Retrieve toggled colors for this thread from storage (or initialize empty array)
        let toggledColors = GM_getValue(storageKey, []);
        if (!Array.isArray(toggledColors)) {
            toggledColors = [];
            GM_setValue(storageKey, toggledColors);
        }

        // Create a map to count occurrences of each background-color
        const colorCount = new Map();

        // Function to update color counts
        function updateColorCounts() {
            colorCount.clear();
            document.querySelectorAll('.labelId').forEach(label => {
                const bgColor = label.style.backgroundColor;
                if (bgColor) {
                    colorCount.set(bgColor, (colorCount.get(bgColor) || 0) + 1);
                }
            });
        }

        // Function to create and handle the toggle icon
        function createToggleIcon(container, bgColor) {
            // Skip if toggle icon already exists
            if (container.querySelector('.opacityToggle')) return;

            const icon = document.createElement('label');
            icon.textContent = '⚪';
            icon.style.cursor = 'pointer';
            icon.style.marginLeft = '5px';
            icon.style.color = toggledColors.includes(bgColor) ? '#00ff00' : '#808080'; // Green if toggled, gray if not
            icon.className = 'opacityToggle glowOnHover coloredIcon';
            icon.title = 'Toggle opacity for this ID in this thread';

            // Insert icon after extraMenuButton
            const extraMenuButton = container.querySelector('.extraMenuButton');
            if (extraMenuButton) {
                extraMenuButton.insertAdjacentElement('afterend', icon);
            }

            // Click handler for toggling opacity
            icon.addEventListener('click', () => {
                // Toggle state for this background-color
                if (toggledColors.includes(bgColor)) {
                    toggledColors = toggledColors.filter(color => color !== bgColor);
                } else {
                    toggledColors.push(bgColor);
                }
                // Update storage for this thread
                GM_setValue(storageKey, toggledColors);

                // Update icon color
                icon.style.color = toggledColors.includes(bgColor) ? '#00ff00' : '#808080';

                // Update opacity for all posts with this background-color (OP and replies)
                document.querySelectorAll('.innerOP, .innerPost').forEach(p => {
                    const label = p.querySelector('.labelId');
                    if (label && label.style.backgroundColor === bgColor) {
                        let shouldBeOpaque = false;
                        // Check if ID is toggled
                        if (toggledColors.includes(bgColor)) {
                            shouldBeOpaque = true;
                        }
                        // Check if ID is unique (controlled by CHECK_UNIQUE_IDS)
                        if (CHECK_UNIQUE_IDS && colorCount.get(bgColor) === 1) {
                            shouldBeOpaque = true;
                        }
                        p.style.opacity = shouldBeOpaque ? '0.5' : '1';
                    }
                });
            });
        }

        // Function to process a single post (OP or regular)
        function processPost(post, isOP = false) {
            const labelId = post.querySelector('.labelId');
            if (labelId) {
                const bgColor = labelId.style.backgroundColor;
                if (bgColor) {
                    let shouldBeOpaque = false;
                    // Check if ID is toggled
                    if (toggledColors.includes(bgColor)) {
                        shouldBeOpaque = true;
                    }
                    // Check if ID is unique (controlled by CHECK_UNIQUE_IDS)
                    if (CHECK_UNIQUE_IDS && colorCount.get(bgColor) === 1) {
                        shouldBeOpaque = true;
                    }
                    // Set initial opacity: 0.5 for toggled or unique IDs, 1 otherwise
                    post.style.opacity = shouldBeOpaque ? '0.5' : '1';

                    // Add toggle icon to .opHead.title (OP) or .postInfo.title (regular)
                    const title = post.querySelector(isOP ? '.opHead.title' : '.postInfo.title');
                    if (title) {
                        createToggleIcon(title, bgColor);
                    }
                }
            }
        }

        // Initial processing: Update color counts and process existing posts
        updateColorCounts();

        // Process OP post (.innerOP)
        const opPost = document.querySelector('.innerOP');
        if (opPost) {
            processPost(opPost, true);
        }

        // Process existing regular posts (.innerPost)
        document.querySelectorAll('.innerPost').forEach(post => {
            processPost(post, false);
        });

        // Set up MutationObserver to detect new posts
        const postsContainer = document.querySelector('.divPosts');
        if (postsContainer) {
            const observer = new MutationObserver((mutations) => {
                let newPosts = false;
                mutations.forEach(mutation => {
                    if (mutation.addedNodes.length) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === Node.ELEMENT_NODE && node.matches('.postCell')) {
                                const innerPost = node.querySelector('.innerPost');
                                if (innerPost) {
                                    newPosts = true;
                                }
                            }
                        });
                    }
                });

                if (newPosts) {
                    // Update color counts to include new posts
                    updateColorCounts();

                    // Process new posts
                    document.querySelectorAll('.innerPost').forEach(post => {
                        // Only process posts without opacity set to avoid reprocessing
                        if (!post.style.opacity) {
                            processPost(post, false);
                        }
                    });

                    // Reapply opacity to all posts for unique IDs and toggled colors
                    document.querySelectorAll('.innerOP, .innerPost').forEach(p => {
                        const label = p.querySelector('.labelId');
                        if (label && label.style.backgroundColor) {
                            const bgColor = label.style.backgroundColor;
                            let shouldBeOpaque = false;
                            // Check if ID is toggled
                            if (toggledColors.includes(bgColor)) {
                                shouldBeOpaque = true;
                            }
                            // Check if ID is unique (controlled by CHECK_UNIQUE_IDS)
                            if (CHECK_UNIQUE_IDS && colorCount.get(bgColor) === 1) {
                                shouldBeOpaque = true;
                            }
                            p.style.opacity = shouldBeOpaque ? '0.5' : '1';
                        }
                    });
                }
            });

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