4chan Gallery

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.

// ==UserScript==
// @name         4chan Gallery
// @namespace    http://tampermonkey.net/
// @version      2024-06-08 (2.5)
// @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 postURL;
                            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");

                                    fileName = post
                                        .querySelector(".fileinfo")
                                        ?.innerText.split(", ")[2];
                                    thumbnailUrl = thumbnailElement?.src;
                                    mediaLink = thumbnailElement?.parentNode.href;
                                    comment = post.querySelector("blockquote");
                                    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"
                                    ).innerText;
                                    comment = post.querySelector(".text");
                                    break;
                                case "boards.4chan.org":
                                case "boards.4channel.org":
                                default:
                                    mediaLink = post.querySelector(".fileText a")
                                    if (post.querySelector(".fileText-original a")) {
                                        mediaLink = post.querySelector(".fileText-original a");
                                    }
                                    if (!mediaLink) {
                                        return;
                                    }

                                    if (
                                        mediaLink.href.includes("4cdn") ||
                                        mediaLink.href.includes("4chan.org")
                                    ) {
                                        if (mediaLink.title) {
                                            fileName = mediaLink.title;
                                        } else {
                                            fileName = mediaLink.innerText;
                                        }
                                    } else {
                                        fileName = mediaLink.innerText;
                                    }
                                    mediaLink = mediaLink.href;

                                    thumbnailUrl = post.querySelector(".fileThumb img")?.src;
                                    comment = post.querySelector(".postMessage");
                            }

                            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;
                            }

                            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();
                                                });
                                            }
                                        }

                                        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;
                                            });
                                        }

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