8chan Toggle All Media per Post

Adds a [+]/[-] button to expand/collapse all media in a single post on 8chan.moe/se.

// ==UserScript==
// @name         8chan Toggle All Media per Post
// @namespace    sneed
// @version      1.5
// @description  Adds a [+]/[-] button to expand/collapse all media in a single post on 8chan.moe/se.
// @author       Gemini 2.5
// @license      MIT
// @match        https://8chan.moe/*/res/*.html*
// @match        https://8chan.se/*/res/*.html*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const EXPAND_TEXT = '[+]';
    const COLLAPSE_TEXT = '[-]';
    const BUTTON_CLASS = 'toggle-all-media-btn'; // Class for the button

    /**
     * Cleans up extra visible a.hideLink elements in a media cell after collapse.
     * @param {HTMLElement} uploadCell - The figure.uploadCell element.
     */
    function cleanupExtraHideLinks(uploadCell) {
        // We only expect one *functional* hide link per audio/video player when expanded.
        // When collapsed, there should ideally be zero *visible* hide links.
        // This function targets cells that are NOT expanded and might have leftover links.
        if (uploadCell.classList.contains('expandedCell')) {
            return; // Only clean up collapsed cells
        }

        const hideLinks = uploadCell.querySelectorAll('a.hideLink');

        if (hideLinks.length > 1) {
             // console.log(`Cleanup: Found ${hideLinks.length} hide links in a non-expanded cell. Hiding extras.`);
             // Keep the first one potentially, or just hide all visible extras
             // Let's hide all except the first one found, as the first one might be the "correct" one if any interaction happened.
             for (let i = 1; i < hideLinks.length; i++) {
                 hideLinks[i].style.display = 'none';
             }
             // Even the first one shouldn't be visible if the cell isn't expanded, based on normal behavior.
             // Let's ensure all hide links are hidden if the cell is not expanded.
             hideLinks.forEach(link => link.style.display = 'none');
        } else if (hideLinks.length === 1) {
             // If there's exactly one hide link, ensure it's hidden if the cell is not expanded
             hideLinks[0].style.display = 'none';
        }
        // If length is 0 or 1 (and handled above), nothing more needed.
    }


    /**
     * Adds a toggle button to expand/collapse all media in a post if it has multiple uploads.
     * @param {HTMLElement} postElement - The post element (.postCell or .opCell).
     */
    function addExpandToggleButton(postElement) {
        const panelUploads = postElement.querySelector('.panelUploads');

        if (!panelUploads || postElement.querySelector(`.${BUTTON_CLASS}`)) {
            return;
        }

        const uploadCells = panelUploads.querySelectorAll('.uploadCell');
        if (uploadCells.length <= 1) {
            return;
        }

        const button = document.createElement('span');
        button.textContent = EXPAND_TEXT;
        button.title = 'Toggle expand/collapse all media in this post';
        button.classList.add(BUTTON_CLASS);
        button.dataset.state = 'collapsed'; // Initial state assumes things start collapsed

        // button.style.display = 'block';
        button.style.marginBottom = '5px';
        button.style.cursor = 'pointer';
        button.style.fontSize = '0.9em';
        button.style.fontWeight = 'bold';
        button.style.color = 'var(--link-color, blue)';
        button.style.userSelect = 'none';

        button.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();

            const currentPost = event.target.closest('.postCell, .opCell');
            if (!currentPost) return;

            const currentPanelUploads = currentPost.querySelector('.panelUploads');
            if (!currentPanelUploads) return;

            const mediaItems = currentPanelUploads.querySelectorAll('.uploadCell');
            const currentState = button.dataset.state;

            if (currentState === 'collapsed') {
                // --- Action: Expand currently collapsed items ---
                mediaItems.forEach(cell => {
                    // Find items that are NOT expanded
                    if (!cell.classList.contains('expandedCell')) {
                         const link = cell.querySelector('a.imgLink');
                         if (link) {
                             // console.log('Expanding:', link.href);
                             link.click(); // Click the main link to trigger expansion
                         }
                    }
                });
                // Update state AFTER action
                button.dataset.state = 'expanded';
                button.textContent = COLLAPSE_TEXT;

            } else { // currentState === 'expanded'
                // --- Action: Collapse currently expanded items ---
                mediaItems.forEach(cell => {
                    // Find items that ARE expanded
                    if (cell.classList.contains('expandedCell')) {
                        // For expanded audio/video, the collapse button is a.hideLink
                        const hideLink = cell.querySelector('a.hideLink');
                        if (hideLink) {
                            // console.log('Collapsing (via hideLink):', cell.querySelector('a.imgLink')?.href);
                            hideLink.click(); // Click the hide link
                        } else {
                            // For expanded images, the collapse action is clicking a.imgLink again
                            const mainLink = cell.querySelector('a.imgLink');
                             if (mainLink) {
                                 // console.log('Collapsing (via imgLink):', mainLink.href);
                                 mainLink.click();
                            }
                        }
                    }
                });

                // Update state AFTER action
                button.dataset.state = 'collapsed';
                button.textContent = EXPAND_TEXT;

                // --- Cleanup: Hide any extra, visible hide links after collapse ---
                // Add a small delay to allow the native script's collapse animation/DOM changes to finish
                // before cleaning up.
                 setTimeout(() => {
                     mediaItems.forEach(cell => {
                          cleanupExtraHideLinks(cell);
                     });
                 }, 50); // 50ms delay should be sufficient
            }
        });

        const firstUploadCell = panelUploads.querySelector('.uploadCell');
        if (firstUploadCell) {
            panelUploads.insertBefore(button, firstUploadCell);
        } else {
             panelUploads.appendChild(button);
        }
    }

    /**
     * Observes the main post container for newly added posts and adds buttons to them.
     */
    function observeNewPosts() {
        const targetNode = document.querySelector('#divThreads .divPosts');
        if (!targetNode) {
            console.warn('Toggle All Media: Could not find target node for MutationObserver.');
            return;
        }

        const config = { childList: true };

        const callback = function(mutationsList, observer) {
            for(const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches('.postCell, .opCell')) {
                                addExpandToggleButton(node);
                            } else {
                                // Check for posts potentially nested within the added node (e.g. in a wrapper)
                                node.querySelectorAll('.postCell, .opCell').forEach(addExpandToggleButton);
                            }
                        }
                    });
                }
            }
        };

        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
    }

    // --- Main Execution ---
    document.querySelectorAll('.postCell, .opCell').forEach(addExpandToggleButton);
    requestAnimationFrame(observeNewPosts);

})();