// ==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
// ==/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!");
})();