8chan Toggle All Media per Post

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

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

})();