- // ==UserScript==
- // @name 4chan Gallery
- // @namespace http://tampermonkey.net/
- // @version 2024-07-26 (2.6)
- // @description 4chan grid-based image gallery with zoom mode support for threads that allows you to browse images, and soundposts (images with sounds, webms with sounds) along with other utility features.
- // @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/*/
- // @match https://archived.moe/*/thread/*
- // @match https://archived.moe/*/
- // @match https://archive.palanq.win/*/
- // @match https://archive.palanq.win/*/thread/*
- // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
- // @grant none
- // @license GNU GPLv3
- // ==/UserScript==
-
- (function () {
- "use strict";
- // injectVideoJS();
- const defaultSettings = {
- Load_High_Res_Images_By_Default: {
- value: false,
- info: "When opening the gallery, load high quality images by default (no thumbnails)",
- },
- };
-
- let threadURL = window.location.href;
- let lastScrollPosition = 0;
- let gallerySize = { width: 0, height: 0 };
-
- // store settings in local storage
- if (!localStorage.getItem("gallerySettings")) {
- localStorage.setItem("gallerySettings", JSON.stringify(defaultSettings));
- }
- let settings = JSON.parse(localStorage.getItem("gallerySettings"));
-
- function setStyles(element, styles) {
- for (const property in styles) {
- element.style[property] = styles[property];
- }
- }
-
- function getPosts(websiteUrl, doc) {
- switch (websiteUrl) {
- case "warosu.org":
- return doc.querySelectorAll(".comment");
- case "archived.moe":
- case "archive.palanq.win":
- return doc.querySelectorAll(".has_image");
- case "boards.4chan.org":
- case "boards.4channel.org":
- default:
- return doc.querySelectorAll(".postContainer");
- }
- }
-
- function getDocument(thread, threadURL) {
- return new Promise((resolve, reject) => {
- if (thread === threadURL) {
- resolve(document);
- } else {
- fetch(thread)
- .then((response) => response.text())
- .then((html) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, "text/html");
- resolve(doc);
- })
- .catch((error) => {
- reject(error);
- });
- }
- });
- }
-
- 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(3, 1fr)`,
- gridTemplateRows: `repeat(2, 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
-
- // top left corner of the screen
- const mediaTypeButtonContainer = document.createElement("div");
- setStyles(mediaTypeButtonContainer, {
- position: "absolute",
- top: "10px",
- left: "10px",
- display: "flex",
- gap: "10px",
- });
-
- // Toggle mode button
- const toggleModeButton = document.createElement("button");
- toggleModeButton.textContent = "Toggle Mode (All)";
- setStyles(toggleModeButton, {
- 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
- });
-
- // Toggle auto play webms button
- const toggleAutoPlayButton = document.createElement("button");
- toggleAutoPlayButton.textContent = "Auto Play Webms without Sound";
- setStyles(toggleAutoPlayButton, {
- 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
- });
- mediaTypeButtonContainer.appendChild(toggleModeButton);
- mediaTypeButtonContainer.appendChild(toggleAutoPlayButton);
- gallery.appendChild(mediaTypeButtonContainer);
-
- // settings button on the top right corner of the screen
- const settingsButton = document.createElement("button");
- settingsButton.id = "settingsButton";
- settingsButton.textContent = "Settings";
- setStyles(settingsButton, {
- position: "absolute",
- top: "20px",
- right: "20px",
- backgroundColor: "#007bff", // Primary color
- color: "#fff",
- padding: "10px 20px",
- borderRadius: "5px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- transition: "background-color 0.3s ease",
- });
- settingsButton.addEventListener("click", () => {
- const settingsContainer = document.createElement("div");
- settingsContainer.id = "settingsContainer";
- setStyles(settingsContainer, {
- 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",
- animation: "fadeIn 0.3s ease",
- });
-
- const settingsBox = document.createElement("div");
- setStyles(settingsBox, {
- backgroundColor: "#000000", // Background color
- color: "#ffffff", // Text color
- padding: "30px",
- borderRadius: "10px",
- border: "1px solid #6c757d", // Secondary color
- maxWidth: "80%",
- maxHeight: "80%",
- overflowY: "auto",
- boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
- });
-
- const settingsTitle = document.createElement("h2");
- settingsTitle.id = "settingsTitle";
- settingsTitle.textContent = "Settings";
- setStyles(settingsTitle, {
- textAlign: "center",
- marginBottom: "20px",
- });
-
- const settingsList = document.createElement("ul");
- settingsList.id = "settingsList";
- setStyles(settingsList, {
- listStyleType: "none",
- padding: "0",
- margin: "0",
- });
-
- // include default settings as existing settings inside the input fields
- // have an icon next to the setting that explains what the setting does
- for (const setting in settings) {
- const settingItem = document.createElement("li");
- setStyles(settingItem, {
- display: "flex",
- alignItems: "center",
- marginBottom: "15px",
- });
-
- const settingLabel = document.createElement("label");
- settingLabel.textContent = setting.replace(/_/g, " ");
- settingLabel.title = settings[setting].info;
- setStyles(settingLabel, {
- flex: "1",
- display: "flex",
- alignItems: "center",
- });
-
- const settingIcon = document.createElement("span");
- settingIcon.className = "material-icons-outlined";
- settingIcon.textContent = settings[setting].icon;
- settingIcon.style.marginRight = "10px";
- settingLabel.prepend(settingIcon);
-
- settingItem.appendChild(settingLabel);
-
- const settingInput = document.createElement("input");
- const settingValueType = typeof defaultSettings[setting].value;
- if (settingValueType === "boolean") {
- settingInput.type = "checkbox";
- settingInput.checked = settings[setting].value;
- } else if (settingValueType === "number") {
- settingInput.type = "number";
- settingInput.value = settings[setting].value;
- } else {
- settingInput.type = "text";
- settingInput.value = settings[setting].value;
- }
- setStyles(settingInput, {
- padding: "8px 12px",
- borderRadius: "5px",
- border: "1px solid #6c757d", // Secondary color
- flex: "2",
- });
- settingInput.addEventListener("focus", () => {
- setStyles(settingInput, {
- borderColor: "#007bff", // Primary color
- boxShadow: "0 0 0 2px rgba(0, 123, 255, 0.25)",
- outline: "none",
- });
- });
- settingInput.addEventListener("blur", () => {
- setStyles(settingInput, {
- borderColor: "#6c757d", // Secondary color
- boxShadow: "none",
- });
- });
-
- if (settingValueType === "boolean") {
- settingInput.style.marginRight = "10px";
- }
-
- settingItem.appendChild(settingInput);
- settingsList.appendChild(settingItem);
- }
-
- const saveButton = document.createElement("button");
- saveButton.id = "saveButton";
- saveButton.textContent = "Save";
- setStyles(saveButton, {
- backgroundColor: "#007bff", // Primary color
- color: "#fff",
- padding: "10px 20px",
- borderRadius: "5px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- transition: "background-color 0.3s ease",
- marginRight: "10px",
- });
- saveButton.addEventListener("click", () => {
- const newSettings = defaultSettings;
- const inputs = document.querySelectorAll("#settingsList input");
- inputs.forEach((input) => {
- const settingName = input.previousSibling.textContent.replace(
- / /g,
- "_"
- );
- const settingValue =
- typeof defaultSettings[settingName].value === "boolean"
- ? input.checked
- : input.value;
- newSettings[settingName].value = settingValue;
- });
- localStorage.setItem("gallerySettings", JSON.stringify(newSettings));
- settings = newSettings;
- settingsContainer.remove();
- gridContainer.innerHTML = ""; // Clear the grid
- loadPosts(mode); // Reload posts based on the new settings
- });
-
- // Close button
- const closeButton = document.createElement("button");
- closeButton.id = "closeButton";
- closeButton.textContent = "Close";
- setStyles(closeButton, {
- backgroundColor: "#007bff", // Primary color
- color: "#fff",
- padding: "10px 20px",
- borderRadius: "5px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- transition: "background-color 0.3s ease",
- });
- closeButton.addEventListener("click", () => {
- settingsContainer.remove();
- });
-
- settingsBox.appendChild(settingsTitle);
- settingsBox.appendChild(settingsList);
- settingsBox.appendChild(saveButton);
- settingsBox.appendChild(closeButton);
- settingsContainer.appendChild(settingsBox);
- gallery.appendChild(settingsContainer);
- });
-
- // Hover effect for settings button
- settingsButton.addEventListener("mouseenter", () => {
- settingsButton.style.backgroundColor = "#0056b3";
- });
- settingsButton.addEventListener("mouseleave", () => {
- settingsButton.style.backgroundColor = "#007bff";
- });
-
- gallery.appendChild(settingsButton);
-
- 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())}`
- getDocument(thread, threadURL).then((doc) => {
- let posts;
-
- // use a case statement to deal with different websites
- posts = getPosts(websiteUrl, doc);
-
- posts.forEach((post) => {
- let mediaLinkFlag = false;
- let board;
- let threadID;
- let postID;
- let postURL;
- let thumbnailUrl;
- let mediaLink;
- let fileName;
- let comment;
-
- let isVideo;
- let isImage;
- let soundLink;
- let encodedSoundPostLink;
- let temp;
-
- // case statement for different websites
- switch (websiteUrl) {
- case "warosu.org":
- let thumbnailElement = post.querySelector("img");
-
- fileName = post
- .querySelector(".fileinfo")
- ?.innerText.split(", ")[2];
- thumbnailUrl = thumbnailElement?.src;
- mediaLink = thumbnailElement?.parentNode.href;
- comment = post.querySelector("blockquote");
-
- threadID = post.querySelector(".js").href.match(/thread\/(\d+)/)[1];
- postID = post.id.replace("pc", "").replace("p", "");
- break;
- case "archived.moe":
- case "archive.palanq.win":
- thumbnailUrl = post.querySelector(".post_image").src;
- mediaLink = post.querySelector(".thread_image_link").href;
- fileName = post.querySelector(
- ".post_file_filename"
- ).title;
- comment = post.querySelector(".text");
- threadID = post.querySelector(".post_data > a").href.match(
- /thread\/(\d+)/
- )[1];
- postID = post.id
- break;
- case "boards.4chan.org":
- case "boards.4channel.org":
- default:
- if (!post.querySelector(".fileText")) {
- return; // Skip posts without media links
- }
- // if they have 4chanX installed, there will be a fileText-orignal class
- if (post.querySelector(".download-button")) {
- temp = post.querySelector(".download-button");
- mediaLink = temp.href;
- fileName = temp.download;
- } else {
- if (post.classList.contains("opContainer")) {
- mediaLink = post.querySelector(".fileText a");
- temp = mediaLink;
- } else {
- mediaLink = post.querySelector(".fileText");
- temp = mediaLink.querySelector("a");
- }
- if (mediaLink.title === "") {
- if (temp.title === "") {
- fileName = temp.innerText;
- } else {
- fileName = temp.title;
- }
- } else {
- fileName = mediaLink.title;
- }
- mediaLink = temp.href;
- }
-
- thumbnailUrl = post.querySelector(".fileThumb img")?.src;
- comment = post.querySelector(".postMessage");
-
- threadID = thread.match(/thread\/(\d+)/)[1];
- postID = post.id.replace("pc", "").replace("p", "");
- }
-
- 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
- }
-
- // replace the "#pcXXXXXXX" or "#pXXXXXXX" with an empty string to get the actual thread url
- if (thread.includes("#")) {
- postURL = thread.replace(/#p\d+/, "");
- postURL = postURL.replace(/#pc\d+/, "");
- } else {
- postURL = thread;
- }
-
- // post info (constant)
- board = thread.match(/\/\/[^\/]+\/([^\/]+)/)[1];
- if (soundLink) {
- encodedSoundPostLink = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
- }
-
- 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);
- video.setAttribute("board", board);
- video.setAttribute("threadID", threadID);
- video.setAttribute("postID", postID);
- 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();
- });
- }
- }
-
- 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]}`
- );
- // add attribute to the audio element with the encoded soundpost link
- audio.setAttribute(
- "encodedSoundPostLink",
- encodedSoundPostLink
- );
- 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;
- });
- }
-
- 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 = thumbnailUrl;
- if (settings.Load_High_Res_Images_By_Default.value) {
- image.src = mediaLink;
- }
- if (mediaLink.includes(".gif")) {
- image.src = mediaLink;
- }
- image.setAttribute("fileName", fileName);
- image.setAttribute("actualSrc", mediaLink);
- image.setAttribute("thumbnailUrl", thumbnailUrl);
- image.setAttribute("board", board);
- image.setAttribute("threadID", threadID);
- image.setAttribute("postID", postID);
- 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 = "";
- image.src = mediaLink;
- 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",
- });
- 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 viewPostButton = document.createElement("a");
- viewPostButton.textContent = "View Post";
- viewPostButton.href = `https://boards.4chan.org/${board}/thread/${threadID}#p${postID}`;
- setStyles(viewPostButton, {
- 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(viewPostButton);
-
- 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";
- 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)",
- });
- 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";
- }
- downloadButtonContainer.appendChild(audioDownloadButton);
-
- // a button beside the download video and download audio button that says download encoded soundpost which links to the following url in a new tab "https://4chan.mahdeensky.top/<board>/thread/<thread>/<post>" where things between the <>, are variables to be replaced
- const encodedSoundPostButton =
- document.createElement("a");
- encodedSoundPostButton.textContent =
- "Download Encoded Soundpost";
- encodedSoundPostButton.target = "_blank";
- setStyles(encodedSoundPostButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- if (soundLink) {
- encodedSoundPostButton.href = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
- } else {
- encodedSoundPostButton.style.display = "none";
- }
- downloadButtonContainer.appendChild(
- encodedSoundPostButton
- );
-
- // 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 = "";
- // image.src = thumbnailUrl;
- 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 originalImage =
- previousCell.querySelector("img");
- const currentImage =
- originalImage.cloneNode(true);
- currentImage.id = "zoomedImage";
- currentImage.style = "";
- currentImage.src =
- currentImage.getAttribute("actualSrc");
- originalImage.src =
- originalImage.getAttribute("actualSrc");
- 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 view post button link
- let previousMedia = video || previousCell.querySelector("img");
- let previousBoard = previousMedia.getAttribute("board");
- let previousThreadID = previousMedia.getAttribute("threadID");
- let previousPostID = previousMedia.getAttribute("postID");
- viewPostButton.href = `https://boards.4chan.org/${previousBoard}/thread/${previousThreadID}#p${previousPostID}`;
-
- // update the download button links
- downloadButton.href = previousMedia.src;
- if (previousCell.querySelector("audio")) {
- // updating audio button download link
- audioDownloadButton.href =
- previousCell.querySelector("audio").src;
- audioDownloadButton.download = previousCell
- .querySelector("audio")
- .src.split("/")
- .pop();
- audioDownloadButton.style.display = "block";
-
- // updating encoded soundpost button link
- encodedSoundPostButton.href = previousCell.querySelector("audio")
- .getAttribute("encodedSoundPostLink");
- encodedSoundPostButton.style.display = "block";
-
- } else {
- audioDownloadButton.style.display = "none";
- encodedSoundPostButton.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 originalImage =
- nextCell.querySelector("img");
- const currentImage =
- originalImage.cloneNode(true);
- currentImage.id = "zoomedImage";
- currentImage.style = "";
- currentImage.src =
- currentImage.getAttribute("actualSrc");
- originalImage.src =
- originalImage.getAttribute("actualSrc");
- 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 view post button link
- let nextMedia = video || nextCell.querySelector("img");
- let nextBoard = nextMedia.getAttribute("board");
- let nextThreadID = nextMedia.getAttribute("threadID");
- let nextPostID = nextMedia.getAttribute("postID");
- viewPostButton.href = `https://boards.4chan.org/${nextBoard}/thread/${nextThreadID}#p${nextPostID}`;
-
- // update the download button links
- downloadButton.href = nextMedia.src;
- if (nextCell.querySelector("audio")) {
- audioDownloadButton.href =
- nextCell.querySelector("audio").src;
- audioDownloadButton.download = nextCell
- .querySelector("audio")
- .src.split("/")
- .pop();
- audioDownloadButton.style.display = "block";
-
- encodedSoundPostButton.href = nextCell.querySelector("audio")
- .getAttribute("encodedSoundPostLink");
- encodedSoundPostButton.style.display = "block";
- } else {
- audioDownloadButton.style.display = "none";
- encodedSoundPostButton.style.display = "none";
- }
- }
- }
- }
- };
- document.addEventListener("keydown", keybindHandler);
-
- image.addEventListener(
- "click",
- () => {
- image.style = "";
- // image.src = thumbnailUrl;
- 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;
- // set the attribute to the audio element with the encoded soundpost link
- audio.setAttribute(
- "encodedSoundPostLink",
- encodedSoundPostLink
- );
- 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
- }
-
- // Add button that scrolls to the post in the thread
- const viewPostButton = document.createElement("button");
- viewPostButton.textContent = "View Post";
- setStyles(viewPostButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
-
- viewPostButton.addEventListener("click", () => {
- // post id example: "pc77515440"
- window.location.href = postURL + "#" + post.id;
- document.body.removeChild(gallery);
- });
- buttonDiv.appendChild(viewPostButton);
-
- cell.appendChild(buttonDiv);
- gridContainer.appendChild(cell);
- }
- }
- });
- });
- };
- checkedThreads.forEach(loadPostsFromThread);
- };
-
- loadPosts(mode);
-
- gallery.appendChild(gridContainer);
-
- const closeButton = document.createElement("button");
- closeButton.textContent = "Close";
- closeButton.id = "closeGallery";
- setStyles(closeButton, {
- position: "absolute",
- bottom: "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(() => {
- if (gallerySize.width > 0 && gallerySize.height > 0) {
- gridContainer.style.width = `${gallerySize.width}px`;
- gridContainer.style.height = `${gallerySize.height}px`;
- }
- // (`Restored scroll position: ${lastScrollPosition} px`);
- gridContainer.scrollTop = lastScrollPosition;
- }, 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);
- });
- }
- };
-
- // Use the "i" key to open and close the gallery/grid
- document.addEventListener("keydown", (event) => {
- if (event.key === "i") {
- // Prevent the gallery from opening when typing in an input or textarea
- if (
- event.target.tagName == "INPUT" ||
- event.target.tagName == "TEXTAREA"
- ) {
- return;
- }
-
- if (document.querySelector("#imageGallery")) {
- document.body.removeChild(document.querySelector("#imageGallery"));
- } else {
- if (document.querySelector("#openImageGallery")) {
- document.querySelector("#openImageGallery").click();
- }
- }
- }
- });
-
- loadButton();
- ("4chan Gallery loaded successfully!");
- })();