您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
4chan grid based Image Gallery for threads that can browse images, images with sounds, webms with sounds (Button on the Bottom Right)
当前为
// ==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 (Button on the Bottom Right) // @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 // ==/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!"); })();