Greasy Fork

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     2.0
// @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 = false;

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