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