// ==UserScript== // @name 4chan Gallery // @namespace http://tampermonkey.net/ // @version 2024-06-05 (2.0) // @description 4chan grid based Image Gallery for threads that can browse images, images with sounds, webms with sounds // @author TheDarkEnjoyer // @match https://boards.4chan.org/*/thread/* // @match https://boards.4chan.org/*/archive // @match https://boards.4channel.org/*/thread/* // @match https://boards.4channel.org/*/archive // @match https://warosu.org/*/thread/* // @match https://warosu.org/*/ // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @license GNU GPLv3 // @downloadURL none // ==/UserScript== (function () { "use strict"; // injectVideoJS(); 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]; } } function injectVideoJS() { const link = document.createElement("link"); link.href = "https://vjs.zencdn.net/8.10.0/video-js.css"; link.rel = "stylesheet"; document.head.appendChild(link); // theme const theme = document.createElement("link"); theme.href = "https://unpkg.com/@videojs/themes@1/dist/city/index.css"; theme.rel = "stylesheet"; document.head.appendChild(theme); const script = document.createElement("script"); script.src = "https://vjs.zencdn.net/8.10.0/video.min.js"; document.body.appendChild(script); ("VideoJS injected successfully!"); } const loadButton = () => { const isArchivePage = window.location.pathname.includes("/archive"); const button = document.createElement("button"); button.textContent = "Open Image Gallery"; button.id = "openImageGallery"; 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"); gallery.id = "imageGallery"; 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 ? // Get all checked threads in the archive page or the current link if it's not an archive page Array.from( document.querySelectorAll( ".flashListing input[type='checkbox']:checked" ) ).map((checkbox) => { let archiveSite = checkbox.parentNode.parentNode.querySelector('a').href; return archiveSite; }) : [threadURL]; const loadPostsFromThread = (thread) => { // get the website url without the protocol and next slash const websiteUrl = thread .replace(/(^\w+:|^)\/\//, "") .split("/")[0]; // const board = thread.split("/thread/")[0].split("/").pop(); // const threadNo = `${parseInt(thread.split("thread/").pop())}` fetch(thread) .then((response) => response.text()) .then((html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); let posts; // use a case statement to deal with different websites switch (websiteUrl) { case "warosu.org": posts = doc.querySelectorAll(".comment"); break; case "boards.4chan.org": case "boards.4channel.org": default: posts = doc.querySelectorAll(".postContainer"); break; } posts.forEach((post) => { let mediaLinkFlag = false; let thumbnailUrl; let mediaLink; let fileName; let comment; let isVideo; let isImage; let soundLink; // case statement for different websites switch (websiteUrl) { case "warosu.org": let thumbnailElement = post.querySelector("img"); // File: 3.61 MB, 852x480, awa[sound=files.catbox.moe%2Fshcsjl.ogg].webm fileName = post.querySelector(".fileinfo")?.innerText.split(", ")[2]; thumbnailUrl = thumbnailElement?.src; mediaLink = thumbnailElement?.parentNode.href; comment = post.querySelector("blockquote"); if (mediaLink) { isVideo = mediaLink.includes(".webm"); isImage = mediaLink.includes(".jpg") || mediaLink.includes(".png") || mediaLink.includes(".gif"); soundLink = fileName.match(/\[sound=(.+?)\]/); mediaLinkFlag = true; } else { return; // Skip posts without media links } break; case "boards.4chan.org": case "boards.4channel.org": default: if (post.querySelector(".fileText")) { mediaLink = post.querySelector(".fileText a"); if (mediaLink.href.includes("4cdn") || mediaLink.href.includes("4chan.org")) { if (mediaLink.title) { fileName = mediaLink.title; } else { fileName = mediaLink.innerText; } } else { fileName = mediaLink.innerText; } } else { return; // Skip posts without media links } thumbnailUrl = post.querySelector(".fileThumb img")?.src; comment = post.querySelector(".postMessage"); if (mediaLink) { mediaLink = mediaLink.href; isVideo = mediaLink.includes(".webm"); isImage = mediaLink.includes(".jpg") || mediaLink.includes(".png") || mediaLink.includes(".gif"); soundLink = fileName.match(/\[sound=(.+?)\]/); mediaLinkFlag = true; } break; } if (mediaLinkFlag) { // 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", display: "flex", justifyContent: "center", }); 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; video.muted = true; video.controls = true; video.title = comment.innerText; video.videothumbnailDisplayed = "true"; video.setAttribute("fileName", fileName); setStyles(video, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", display: "none", }); // videoJS stuff (not working for some reason) // video.className = "video-js"; // video.setAttribute("data-setup", "{}"); // const source = document.createElement("source"); // source.src = mediaLink; // source.type = "video/webm"; // video.appendChild(source); 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 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); // html5 video play video.onplay = (event) => { audio.play(); } video.onpause = (event) => { audio.pause(); } 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", block: "center"}); 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", display: "flex", justifyContent: "center", alignItems: "center", }); const image = document.createElement("img"); image.src = mediaLink; image.setAttribute("fileName", fileName); setStyles(image, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", cursor: "pointer", }); let createDarkenBackground = () => { const background = document.createElement("div"); background.id = "darkenBackground"; setStyles(background, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", backgroundColor: "rgba(0, 0, 0, 0.3)", backdropFilter: "blur(5px)", zIndex: "9999", }); return background; } let zoomImage = () => { // have the image pop up centered in front of the screen so that it fills about 80% of the screen image.style = ""; setStyles(image, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", height: "80%", width: "80%", objectFit: "contain", cursor: "pointer", }); // darken and blur the background behind the image without affecting the image const background = createDarkenBackground(); gallery.appendChild(background); // create a container for the buttons, number, and download buttons (even space between them) // position: fixed; bottom: 10px; display: flex; flex-direction: row; justify-content: space-around; z-index: 10000; width: 100%; margin:auto; const bottomContainer = document.createElement("div"); setStyles(bottomContainer, { position: "fixed", bottom: "10px", display: "flex", flexDirection: "row", justifyContent: "space-around", zIndex: "10000", width: "100%", margin: "auto", }); background.appendChild(bottomContainer); // buttons on the bottom left of the screen for reverse image search (SauceNAO, Google Lens, Yandex) const buttonContainer = document.createElement("div"); setStyles(buttonContainer, { display: "flex", gap: "10px", }); buttonContainer.setAttribute("mediaLink", mediaLink); const sauceNAOButton = document.createElement("button"); sauceNAOButton.textContent = "SauceNAO"; setStyles(sauceNAOButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); sauceNAOButton.addEventListener("click", () => { window.open( `https://saucenao.com/search.php?url=${encodeURIComponent( buttonContainer.getAttribute("mediaLink") )}` ); }); buttonContainer.appendChild(sauceNAOButton); const googleLensButton = document.createElement("button"); googleLensButton.textContent = "Google Lens"; setStyles(googleLensButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); googleLensButton.addEventListener("click", () => { window.open( `https://lens.google.com/uploadbyurl?url=${encodeURIComponent( buttonContainer.getAttribute("mediaLink") )}` ); }); buttonContainer.appendChild(googleLensButton); const yandexButton = document.createElement("button"); yandexButton.textContent = "Yandex"; setStyles(yandexButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); yandexButton.addEventListener("click", () => { window.open( `https://yandex.com/images/search?rpt=imageview&url=${encodeURIComponent( buttonContainer.getAttribute("mediaLink") )}` ); }); buttonContainer.appendChild(yandexButton); bottomContainer.appendChild(buttonContainer); // download container for video/img and audio const downloadButtonContainer = document.createElement("div"); setStyles(downloadButtonContainer, { display: "flex", gap: '10px', }); bottomContainer.appendChild(downloadButtonContainer); const downloadButton = document.createElement("a"); downloadButton.textContent = "Download Video/Image"; downloadButton.href = mediaLink; downloadButton.download = fileName; downloadButton.target = "_blank"; setStyles(downloadButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); downloadButtonContainer.appendChild(downloadButton); const audioDownloadButton = document.createElement("a"); audioDownloadButton.textContent = "Download Audio"; audioDownloadButton.target = "_blank"; if (soundLink) { audioDownloadButton.href = decodeURIComponent( soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}` ); audioDownloadButton.download = soundLink[1].split("/").pop(); } else { audioDownloadButton.style.display = "none"; } setStyles(audioDownloadButton, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", }); downloadButtonContainer.appendChild(audioDownloadButton); // number on the bottom right of the screen to show which image is currently being viewed const imageNumber = document.createElement("div"); let currentImageNumber = Array.from(cell.parentNode.children).indexOf(cell) + 1; let imageTotal = cell.parentNode.children.length; imageNumber.textContent = `${currentImageNumber}/${imageTotal}`; setStyles(imageNumber, { backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", zIndex: "10000", }); bottomContainer.appendChild(imageNumber); // title of the image/video on the top left of the screen const imageTitle = document.createElement("div"); imageTitle.textContent = fileName; setStyles(imageTitle, { position: "fixed", top: "10px", left: "10px", backgroundColor: "#1c1c1c", color: "#d9d9d9", padding: "5px 10px", borderRadius: "3px", border: "none", cursor: "pointer", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)", zIndex: "10000", }); background.appendChild(imageTitle); let currentCell = cell; // use left and right arrow keys to navigate between images/videos let keybindHandler = (event) => { if (event.key === "ArrowLeft") { // get the previous cell in the grid const previousCell = currentCell.previousElementSibling; if (previousCell) { if (gallery.querySelector("#zoomedVideo")) { if (gallery.querySelector("#zoomedVideo").querySelector("audio")) { gallery.querySelector("#zoomedVideo").querySelector("audio").pause(); } gallery.removeChild(gallery.querySelector("#zoomedVideo")); } else if (gallery.querySelector("#zoomedImage")) { gallery.removeChild(gallery.querySelector("#zoomedImage")); } else { image.style = ""; setStyles(image, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", }); } // check if it has a video const video = previousCell?.querySelector("video"); if (video) { const video = previousCell.querySelector("video").cloneNode(true); video.id = "zoomedVideo"; video.style = ""; setStyles(video, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", height: "80%", width: "80%", objectFit: "contain", cursor: "pointer", preload: "auto", }); gallery.appendChild(video); // check if there is an audio element let audio = previousCell.querySelector("audio"); if (audio) { audio = audio.cloneNode(true); // same event listeners as the video video.onplay = (event) => { audio.play(); } video.onpause = (event) => { audio.pause(); } let lastVideoTime = 0; video.addEventListener("timeupdate", () => { if ( Math.abs(video.currentTime - lastVideoTime) >= 2 ) { audio.currentTime = video.currentTime; lastVideoTime = video.currentTime; } lastVideoTime = video.currentTime; }); video.appendChild(audio); } } else { // if it doesn't have a video, it must have an image const currentImage = previousCell.querySelector("img").cloneNode(true); currentImage.id = "zoomedImage"; currentImage.style = ""; setStyles(currentImage, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", height: "80%", width: "80%", objectFit: "contain", cursor: "pointer", }); gallery.appendChild(currentImage); currentImage.addEventListener("click", () => { gallery.removeChild(currentImage); gallery.removeChild(background); document.removeEventListener("keydown", keybindHandler); }); let audio = previousCell.querySelector("audio"); if (audio) { audio = audio.cloneNode(true); currentImage.appendChild(audio); // event listeners when hovering over the image currentImage.addEventListener("mouseenter", () => { audio.play(); }); currentImage.addEventListener("mouseleave", () => { audio.pause(); }); } } if (previousCell) { currentCell = previousCell; buttonContainer.setAttribute("mediaLink", previousCell.querySelector("img").src); currentImageNumber -= 1; imageNumber.textContent = `${currentImageNumber}/${imageTotal}`; // filename of the video if it has one, otherwise the filename of the image imageTitle.textContent = video ? video.getAttribute("fileName") : previousCell.querySelector("img").getAttribute("fileName"); // update the download button links downloadButton.href = video ? video.src : previousCell.querySelector("img").src; if (previousCell.querySelector("audio")) { audioDownloadButton.href = previousCell.querySelector("audio").src; audioDownloadButton.download = previousCell.querySelector("audio").src.split("/").pop(); audioDownloadButton.style.display = "block"; } else { audioDownloadButton.style.display = "none"; } } } } else if (event.key === "ArrowRight") { // get the next cell in the grid const nextCell = currentCell.nextElementSibling; if (nextCell) { if (gallery.querySelector("#zoomedVideo")) { if (gallery.querySelector("#zoomedVideo").querySelector("audio")) { gallery.querySelector("#zoomedVideo").querySelector("audio").pause(); } gallery.removeChild(gallery.querySelector("#zoomedVideo")); // ("removed video"); } else if (gallery.querySelector("#zoomedImage")) { gallery.removeChild(gallery.querySelector("#zoomedImage")); // ("removed image"); } else { image.style = ""; setStyles(image, { maxWidth: "100%", maxHeight: "200px", objectFit: "contain", }); } // check if it has a video const video = nextCell?.querySelector("video"); if (video) { const video = nextCell.querySelector("video").cloneNode(true); video.id = "zoomedVideo"; video.style = ""; setStyles(video, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", height: "80%", width: "80%", objectFit: "contain", cursor: "pointer", preload: "auto", }); // check if there is an audio element let audio = nextCell.querySelector("audio"); if (audio) { audio = audio.cloneNode(true); // same event listeners as the video video.onplay = (event) => { audio.play(); } video.onpause = (event) => { audio.pause(); } let lastVideoTime = 0; video.addEventListener("timeupdate", () => { if ( Math.abs(video.currentTime - lastVideoTime) >= 2 ) { audio.currentTime = video.currentTime; lastVideoTime = video.currentTime; } lastVideoTime = video.currentTime; }); video.appendChild(audio); } gallery.appendChild(video); } else { const currentImage = nextCell.querySelector("img").cloneNode(true); currentImage.id = "zoomedImage"; currentImage.style = ""; setStyles(currentImage, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", height: "80%", width: "80%", objectFit: "contain", cursor: "pointer", }); gallery.appendChild(currentImage); currentImage.addEventListener("click", () => { gallery.removeChild(currentImage); gallery.removeChild(background); document.removeEventListener("keydown", keybindHandler); }); let audio = nextCell.querySelector("audio"); if (audio) { audio = nextCell.querySelector("audio").cloneNode(true); currentImage.appendChild(audio); currentImage.addEventListener("mouseenter", () => { audio.play(); }); currentImage.addEventListener("mouseleave", () => { audio.pause(); }); } } if (nextCell) { currentCell = nextCell; buttonContainer.setAttribute("mediaLink", nextCell.querySelector("img").src); currentImageNumber += 1; imageNumber.textContent = `${currentImageNumber}/${imageTotal}`; // filename of the video if it has one, otherwise the filename of the image imageTitle.textContent = video ? video.getAttribute("fileName") : nextCell.querySelector("img").getAttribute("fileName"); // update the download button links downloadButton.href = video ? video.src : nextCell.querySelector("img").src; if (nextCell.querySelector("audio")) { audioDownloadButton.href = nextCell.querySelector("audio").src; audioDownloadButton.download = nextCell.querySelector("audio").src.split("/").pop(); audioDownloadButton.style.display = "block"; } else { audioDownloadButton.style.display = "none"; } } } }}; document.addEventListener("keydown", keybindHandler); image.addEventListener("click", () => { image.style = ""; setStyles(image, { maxWidth: "99%", maxHeight: "199px", objectFit: "contain", }); if (gallery.querySelector("#darkenBackground")) { gallery.removeChild(background); }; document.removeEventListener("keydown", keybindHandler); image.addEventListener("click", zoomImage, { once: true }); }, { once: true }); } image.addEventListener("click", zoomImage, { once: true }); 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]}` ); audio.loop = true; 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)); }; checkedThreads.forEach(loadPostsFromThread); }; loadPosts(mode); gallery.appendChild(gridContainer); const closeButton = document.createElement("button"); closeButton.textContent = "Close"; closeButton.id = "closeGallery"; 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 // (`Last scroll position: ${lastScrollPosition} px`); gridContainer.addEventListener("scroll", () => { lastScrollPosition = gridContainer.scrollTop; // (`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; // (`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 let posts; switch (window.location.hostname) { case "warosu.org": posts = document.querySelectorAll(".comment"); break; case "boards.4chan.org": case "boards.4channel.org": default: posts = document.querySelectorAll(".postContainer"); break; } // Use the "i" key to open and close the gallery/grid document.addEventListener("keydown", (event) => { if (event.key === "i") { if (document.querySelector("#imageGallery")) { gallerySize = { width: document.querySelector("#imageGallery").querySelector("div").offsetWidth, height: document.querySelector("#imageGallery").querySelector("div").offsetHeight, }; document.body.removeChild(document.querySelector("#imageGallery")); } else { if (document.querySelector("#openImageGallery")) { document.querySelector("#openImageGallery").click(); } } } }); loadButton(); ("4chan Gallery loaded successfully!"); })();