NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name 4chan Gallery // @namespace http://tampermonkey.net/ // @version 2024-03-30 (1.6) // @description 4chan grid based Image Gallery for threads that can load images, images with sounds, webms with sounds (Button on the Bottom Right) // @author MahdeenSky // @match https://boards.4chan.org/*/thread/* // @match https://boards.4chan.org/*/archive // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @license MIT // ==/UserScript== // ==OpenUserJS== // @author MahdeenSky // ==/OpenUserJS== (function () { "use strict"; let threadURL = window.location.href; let lastScrollPosition = 0; let gallerySize = { width: 0, height: 0 }; function setStyles(element, styles) { for (const property in styles) { element.style[property] = styles[property]; } } const loadButton = () => { const isArchivePage = window.location.pathname.includes("/archive"); const button = document.createElement("button"); button.textContent = "Open Image Gallery"; setStyles(button, { position: "fixed", bottom: "20px", right: "20px", zIndex: "1000", backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "10px 20px", borderRadius: "5px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); const openImageGallery = () => { const gallery = document.createElement("div"); setStyles(gallery, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", backgroundColor: "rgba(0, 0, 0, 0.8)", display: "flex", justifyContent: "center", alignItems: "center", zIndex: "9999", }); const gridContainer = document.createElement("div"); setStyles(gridContainer, { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "10px", padding: "20px", backgroundColor: "#1c1c1c", color: "#d9d9d9", maxWidth: "80%", maxHeight: "80%", overflowY: "auto", resize: "both", overflow: "auto", border: "1px solid #d9d9d9", }); // Restore the previous grid container size if (gallerySize.width > 0 && gallerySize.height > 0) { gridContainer.style.width = `${gallerySize.width}px`; gridContainer.style.height = `${gallerySize.height}px`; } let mode = "all"; // Default mode is "all" let autoPlayWebms = false; // Default auto play webms without sound is false // Toggle mode button const toggleModeButton = document.createElement("button"); toggleModeButton.textContent = "Toggle Mode (All)"; setStyles(toggleModeButton, { position: "absolute", top: "10px", left: "10px", backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "10px 20px", borderRadius: "5px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); toggleModeButton.addEventListener("click", () => { mode = mode === "all" ? "webm" : "all"; toggleModeButton.textContent = `Toggle Mode (${mode === "all" ? "All" : "Webm & Images with Sound"})`; gridContainer.innerHTML = ""; // Clear the grid loadPosts(mode); // Reload posts based on the new mode }); gallery.appendChild(toggleModeButton); // Toggle auto play webms button const toggleAutoPlayButton = document.createElement("button"); toggleAutoPlayButton.textContent = "Auto Play Webms without Sound"; setStyles(toggleAutoPlayButton, { position: "absolute", top: "10px", left: "350px", backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "10px 20px", borderRadius: "5px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); toggleAutoPlayButton.addEventListener("click", () => { autoPlayWebms = !autoPlayWebms; toggleAutoPlayButton.textContent = autoPlayWebms ? "Stop Auto Play Webms" : "Auto Play Webms without Sound"; gridContainer.innerHTML = ""; // Clear the grid loadPosts(mode); // Reload posts based on the new mode and auto play setting }); gallery.appendChild(toggleAutoPlayButton); const loadPosts = (mode) => { const checkedThreads = isArchivePage ? Array.from(document.querySelectorAll(".flashListing input[type='checkbox']:checked")).map((checkbox) => checkbox.parentNode.parentNode) : []; // Use an empty array for non-archive pages const loadPostsFromThread = (thread) => { // thread number is the 2nd child of the parent node const threadNo = thread.children[1].textContent; // get current board const board = window.location.pathname.split("/")[1]; const threadURL = `https://boards.4chan.org/${board}/thread/${threadNo}`; fetch(threadURL) .then((response) => response.text()) .then((html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const posts = doc.querySelectorAll(".postContainer"); posts.forEach((post) => { let mediaLink = post.querySelector(".fileText a"); if (post.querySelector(".fileText-original")) { mediaLink = post.querySelector(".fileText-original a"); } // thumbnailUrl is in the img element inside the mediaLink let thumbnailUrl = post.querySelector(".fileThumb img")?.src; const comment = post.querySelector(".postMessage"); if (mediaLink) { const isVideo = mediaLink.href.includes(".webm"); const isImage = mediaLink.href.includes(".jpg") || mediaLink.href.includes(".png") || mediaLink.href.includes(".gif"); const fileName = mediaLink.href.split("/").pop(); const soundLink = mediaLink.title.match(/\[sound=(.+?)\]/); // Check if the post should be loaded based on the mode if (mode === "all" || (mode === "webm" && (isVideo || (isImage && soundLink)))) { const cell = document.createElement("div"); setStyles(cell, { border: "1px solid #d9d9d9", position: "relative", }); const buttonDiv = document.createElement("div"); setStyles(buttonDiv, { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "5px", }); if (isVideo) { const videoContainer = document.createElement("div"); setStyles(videoContainer, { position: "relative", }); const videoThumbnail = document.createElement("img"); videoThumbnail.src = thumbnailUrl; videoThumbnail.alt = "Video Thumbnail"; setStyles(videoThumbnail, { width: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", }); videoThumbnail.loading = "lazy"; const video = document.createElement("video"); video.src = mediaLink.href; video.muted = true; video.controls = true; video.title = comment.innerText; video.videothumbnailDisplayed = "true"; setStyles(video, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", display: "none", }); videoThumbnail.addEventListener("click", () => { videoThumbnail.style.display = "none"; video.style.display = "block"; video.videothumbnailDisplayed = "false"; video.load(); }); // hide the video thumbnail and show the video when hovered videoThumbnail.addEventListener("mouseenter", () => { videoThumbnail.style.display = "none"; video.style.display = "block"; video.videothumbnailDisplayed = "false"; video.load(); }); // Play webms without sound automatically on hover or if autoPlayWebms is true if (!soundLink) { if (autoPlayWebms) { video.addEventListener("canplaythrough", () => { video.play(); video.loop = true; // Loop webms when autoPlayWebms is true }); } else { video.addEventListener("mouseenter", () => { video.play(); }); video.addEventListener("mouseleave", () => { video.pause(); }); } } video.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); videoContainer.appendChild(videoThumbnail); videoContainer.appendChild(video); if (soundLink) { video.preload = "none"; // Disable video preload for better performance const audio = document.createElement("audio"); audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`); videoContainer.appendChild(audio); const playPauseButton = document.createElement("button"); playPauseButton.textContent = "Play/Pause"; setStyles(playPauseButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); playPauseButton.addEventListener("click", () => { // hide the video thumbnail and show the video if (video.videothumbnailDisplayed === "true") { video.videothumbnailDisplayed = "false"; videoThumbnail.style.display = "none"; video.style.display = "block"; video.load(); } if (video.paused && audio.paused) { video.play(); audio.play(); } else { video.pause(); audio.pause(); } }); buttonDiv.appendChild(playPauseButton); const resetButton = document.createElement("button"); resetButton.textContent = "Reset"; setStyles(resetButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); resetButton.addEventListener("click", () => { video.currentTime = 0; audio.currentTime = 0; }); buttonDiv.appendChild(resetButton); let lastVideoTime = 0; // Sync audio with video on timeupdate event only if the difference is 2 seconds or more video.addEventListener("timeupdate", () => { if (Math.abs(video.currentTime - lastVideoTime) >= 2) { audio.currentTime = video.currentTime; lastVideoTime = video.currentTime; } lastVideoTime = video.currentTime; }); } const cellButton = document.createElement("button"); cellButton.textContent = "View Post"; setStyles(cellButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); cellButton.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); buttonDiv.appendChild(cellButton); cell.appendChild(videoContainer); } else if (isImage) { const imageContainer = document.createElement("div"); setStyles(imageContainer, { position: "relative", }); const image = document.createElement("img"); image.src = mediaLink.href; setStyles(image, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", }); image.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); image.title = comment.innerText; image.loading = "lazy"; if (soundLink) { const audio = document.createElement("audio"); audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`); imageContainer.appendChild(audio); image.addEventListener("mouseenter", () => { audio.play(); }); image.addEventListener("mouseleave", () => { audio.pause(); }); const playPauseButton = document.createElement("button"); playPauseButton.textContent = "Play/Pause"; setStyles(playPauseButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); playPauseButton.addEventListener("click", () => { if (audio.paused) { audio.play(); } else { audio.pause(); } }); buttonDiv.appendChild(playPauseButton); } imageContainer.appendChild(image); cell.appendChild(imageContainer); } else { return; // Skip non-video and non-image posts } cell.appendChild(buttonDiv); gridContainer.appendChild(cell); } } }); }) .catch((error) => console.error(error)); }; if (isArchivePage) { checkedThreads.forEach(loadPostsFromThread); } else { const posts = document.querySelectorAll(".postContainer"); posts.forEach((post) => { let mediaLink = post.querySelector(".fileText a"); if (post.querySelector(".fileText-original")) { mediaLink = post.querySelector(".fileText-original a"); } let thumbnailUrl = post.querySelector(".fileThumb img")?.src; const comment = post.querySelector(".postMessage"); if (mediaLink) { const isVideo = mediaLink.href.includes(".webm"); const isImage = mediaLink.href.includes(".jpg") || mediaLink.href.includes(".png") || mediaLink.href.includes(".gif"); const fileName = mediaLink.href.split("/").pop(); const soundLink = mediaLink.title.match(/\[sound=(.+?)\]/); // Check if the post should be loaded based on the mode if (mode === "all" || (mode === "webm" && (isVideo || (isImage && soundLink)))) { const cell = document.createElement("div"); setStyles(cell, { border: "1px solid #d9d9d9", position: "relative", }); const buttonDiv = document.createElement("div"); setStyles(buttonDiv, { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "5px", }); if (isVideo) { const videoContainer = document.createElement("div"); setStyles(videoContainer, { position: "relative", }); const videoThumbnail = document.createElement("img"); videoThumbnail.src = thumbnailUrl; videoThumbnail.alt = "Video Thumbnail"; setStyles(videoThumbnail, { width: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", }); videoThumbnail.loading = "lazy"; const video = document.createElement("video"); video.src = mediaLink.href; video.muted = true; video.controls = true; video.title = comment.innerText; video.videothumbnailDisplayed = "true"; setStyles(video, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", display: "none", }); videoThumbnail.addEventListener("click", () => { videoThumbnail.style.display = "none"; video.style.display = "block"; video.videothumbnailDisplayed = "false"; video.load(); }); // hide the video thumbnail and show the video when hovered videoThumbnail.addEventListener("mouseenter", () => { videoThumbnail.style.display = "none"; video.style.display = "block"; video.videothumbnailDisplayed = "false"; video.load(); }); // Play webms without sound automatically on hover or if autoPlayWebms is true if (!soundLink) { if (autoPlayWebms) { video.addEventListener("canplaythrough", () => { video.play(); video.loop = true; // Loop webms when autoPlayWebms is true }); } else { video.addEventListener("mouseenter", () => { video.play(); }); video.addEventListener("mouseleave", () => { video.pause(); }); } } video.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); videoContainer.appendChild(videoThumbnail); videoContainer.appendChild(video); if (soundLink) { video.preload = "none"; // Disable video preload for better performance const audio = document.createElement("audio"); audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`); videoContainer.appendChild(audio); const playPauseButton = document.createElement("button"); playPauseButton.textContent = "Play/Pause"; setStyles(playPauseButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); playPauseButton.videothumbnailDisplayed = "true"; playPauseButton.addEventListener("click", () => { // hide the video thumbnail and show the video if (video.videothumbnailDisplayed === "true") { video.videothumbnailDisplayed = "false"; videoThumbnail.style.display = "none"; video.style.display = "block"; video.load(); } if (video.paused && audio.paused) { video.play(); audio.play(); } else { video.pause(); audio.pause(); } }); buttonDiv.appendChild(playPauseButton); const resetButton = document.createElement("button"); resetButton.textContent = "Reset"; setStyles(resetButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); resetButton.addEventListener("click", () => { video.currentTime = 0; audio.currentTime = 0; }); buttonDiv.appendChild(resetButton); let lastVideoTime = 0; // Sync audio with video on timeupdate event only if the difference is 2 seconds or more video.addEventListener("timeupdate", () => { if (Math.abs(video.currentTime - lastVideoTime) >= 2) { audio.currentTime = video.currentTime; lastVideoTime = video.currentTime; } lastVideoTime = video.currentTime; }); } const cellButton = document.createElement("button"); cellButton.textContent = "View Post"; setStyles(cellButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); cellButton.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); buttonDiv.appendChild(cellButton); cell.appendChild(videoContainer); } else if (isImage) { const imageContainer = document.createElement("div"); setStyles(imageContainer, { position: "relative", }); const image = document.createElement("img"); image.src = mediaLink.href; setStyles(image, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", }); image.addEventListener("click", () => { post.scrollIntoView({ behavior: "smooth" }); gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); image.title = comment.innerText; image.loading = "lazy"; if (soundLink) { const audio = document.createElement("audio"); audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`); imageContainer.appendChild(audio); image.addEventListener("mouseenter", () => { audio.play(); }); image.addEventListener("mouseleave", () => { audio.pause(); }); const playPauseButton = document.createElement("button"); playPauseButton.textContent = "Play/Pause"; setStyles(playPauseButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); playPauseButton.addEventListener("click", () => { if (audio.paused) { audio.play(); } else { audio.pause(); } }); buttonDiv.appendChild(playPauseButton); } imageContainer.appendChild(image); cell.appendChild(imageContainer); } else { return; // Skip non-video and non-image posts } cell.appendChild(buttonDiv); gridContainer.appendChild(cell); } } }); } }; loadPosts(mode); // Load posts based on the initial mode gallery.appendChild(gridContainer); const closeButton = document.createElement("button"); closeButton.textContent = "Close"; setStyles(closeButton, { position: "absolute", top: "10px", right: "10px", zIndex: "10000", backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "10px 20px", borderRadius: "5px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); closeButton.addEventListener("click", () => { gallerySize = { width: gridContainer.offsetWidth, height: gridContainer.offsetHeight, }; document.body.removeChild(gallery); }); gallery.appendChild(closeButton); document.body.appendChild(gallery); // Store the current scroll position and grid container size when closing the gallery // console.log(`Last scroll position: ${lastScrollPosition} px`); gridContainer.addEventListener("scroll", () => { lastScrollPosition = gridContainer.scrollTop; // console.log(`Current scroll position: ${lastScrollPosition} px`); }); // Restore the last scroll position and grid container size when opening the gallery after a timeout if the url is the same if (window.location.href === threadURL) { setTimeout(() => { gridContainer.scrollTop = lastScrollPosition; // console.log(`Restored scroll position: ${lastScrollPosition} px`); if (gallerySize.width > 0 && gallerySize.height > 0) { gridContainer.style.width = `${gallerySize.width}px`; gridContainer.style.height = `${gallerySize.height}px`; } }, 200); } else { // Reset the last scroll position and grid container size if the url is different threadURL = window.location.href; lastScrollPosition = 0; gallerySize = { width: 0, height: 0 }; } }; button.addEventListener("click", openImageGallery); // Append the button to the body document.body.appendChild(button); if (isArchivePage) { // adds the category to thead const thead = document.querySelector(".flashListing thead tr"); const checkboxCell = document.createElement("td"); checkboxCell.className = "postblock"; checkboxCell.textContent = "Selected"; thead.insertBefore(checkboxCell, thead.firstChild); // Add checkboxes to each thread row const threadRows = document.querySelectorAll(".flashListing tbody tr"); threadRows.forEach((row) => { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; const checkboxCell = document.createElement("td"); checkboxCell.appendChild(checkbox); row.insertBefore(checkboxCell, row.firstChild); }); } }; // Check if there are at least two posts before loading the button const posts = document.querySelectorAll(".postContainer"); if (posts.length >= 2) { loadButton(); } else { // If there are less than two posts, try again after 5 seconds setTimeout(loadButton, 1000); } console.log("4chan Gallery loaded successfully!"); })();