4chan grid gallery

Browse all images and videos in a 4chan thread using a grid gallery with a cinema-style viewer.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         4chan grid gallery
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Browse all images and videos in a 4chan thread using a grid gallery with a cinema-style viewer.
// @author       apheli0n
// @match        https://boards.4chan.org/*/thread/*
// @match        https://boards.4channel.org/*/thread/*
//
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAExklEQVR4nO2Za4hWVRSGH/OSt7yWlfZlTeqPsiysENKGIv1R2sWQIIqkMDSyfmRUkBWhUxCl1UQ/TCprIC3th12giHAoiyi6aanFmFSWeaOZcoopv1jxntgcv3P2BQca/F44MHP2ete399l77bX2u6GOOlzMBr4C2oHXgDPogbgM+BuoOk87MK27fvACoAXYAXQBe4B3gXlA30SfvYGt6vw9wDjgbf2/D2g4zGNgCXAw99Xc52vg7AS/F4rfpkEZjtbysvcfAIuBLcAB4EvgVuColEFcJac2C48CZwGDgGOB6zVD2XKYGun7QXGX594PA7aXfLjnUwayQeSFBe2DteTMpkMDDcWr4l1To2262mwTmAL0By7RkrP3s2IH0i6idbgINtUvym4T0C/Q9xfiTC5ofwqo5N7dJY59hCj8JOIYj90AxYrZ3hzoe5fsT4jozwRxbOlFYZWI65yA9MXT9sCdLJvtIRH9GSBOZwSHPsCTTpA1e+x7AR/L9qYA/12ytd8JxRhxdoYSjgM2imQJ62lgZADvSidIfTgg29CYMswX53UCYIH9qQg7tN+Hwr7uj+KeFxh/wwN9DwN+EGdOCKHJGcQppCVQ4z/hsdsiu5Df6Ae8JfvW0KTYJkLMTLiYLP7PipsivCc7X11VUZbPYmNsaEd+F2kEaejlLJuy0uVZ2dxQYnMF8IvsbFlNjOnIOhHfBE4kDc/Jx6ISm7tl83iNtvNVlGY75oaAXHYIGpyAtZ1lLbAAaNR6Hq6g7qu/KyojrNR4SB3oCNhdZjrFoWGgZqfVKVJ3A7cF5LBCnAysqXFWiHk2A/eX/MYIdbhTFe9vuTPJkshkWQqbzluAlcCHwHcq3LrUASsztumrtqjsthPf8YH+N+cG/5Fm/xh6GJq0/G7vjoNUHXXUUceRgbHAnco3F9MD0ahc4GpfL3s4I6UyLlZdZrXUt6pu98nHr8A3SrYrdag6tTsGMM4R0+z5S3VWVnYM8chAqc9GVQ+HBfOcwtBqpEeA0Wpr1ftrC7hrnFpsmQrFqcrsVoSaAGgYCozXjFulvF6zlA3opcjz/iEntdWOsxeAUTmbhWp7xVO+mw4Qi4Gq/bLl93CCj3912fVysL9AIURftqoDUa0TYqPaPycd58qHielR6O3Eg3Vwksd+m2xNUKs1qx2KpRRNAO2KUbJQhgdE3BV4EZPpwXM9p1CTQFOOFm3im6wajNOAP3TIuiiQc4cnDmar/bOYjgCnO9Ls1kCt7T88liDnZ2q6XdwUxdveQBUFxdoCR9TbVEPg9iK7VTJBIBSTxLEkV4T7ZPNOgL/m3E5ZdjtQiE45sDuKUJwkjn31Igx1tlHbycow2NG2lpKI/ZHSpqua/+mxu9dJjrbcyjDFsU1CprDPiExcmZxUhv5O8K4ImJVq7JWCi6Vy8EaNfNCsTucxXhxTXXw4x1E3rdQp0nVvdAI9CRVHc8quF87UwKrK9vmLnavVZkk0BJdri89UxemagUG6l2xy2m1AybjOI9a15Aq4FXpvN7cx2Tq7Nqj1HEytrfKYoRLavsz3ujaYo4C2H3pfSnyDs9/H3r/bHYhl+09UxnTo+uEZ1VfdiktzcmfVU/3+r1HRV9upXWVVwSZQxxGFfwAoYqhai0UgxgAAAABJRU5ErkJggg==
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

(function () {
    "use strict";

    const COLUMNS      = 3;
    const THUMB_HEIGHT = 300;   // px

    /* ─── utilidad ─────────────────────────────────────────────────── */
    function css(el, styles) {
        Object.assign(el.style, styles);
    }

    /* ─── modo cinema ───────────────────────────────────────────────── */
    let cinemaOverlay = null;
    let cinemaItems   = [];   // [{ type, src, thumbSrc, soundSrc }]
    let cinemaIndex   = 0;
    let cinemaKeyFn   = null;

    function openCinema(items, index) {
        cinemaItems = items;
        cinemaIndex = index;
        renderCinema();
    }

    function closeCinema() {
        if (cinemaOverlay) { cinemaOverlay.remove(); cinemaOverlay = null; }
        if (cinemaKeyFn)   { document.removeEventListener("keydown", cinemaKeyFn); cinemaKeyFn = null; }
    }

    function renderCinema() {
        closeCinema();

        const item = cinemaItems[cinemaIndex];

        /* overlay oscuro */
        cinemaOverlay = document.createElement("div");
        css(cinemaOverlay, {
            position: "fixed", inset: "0",
            backgroundColor: "rgba(0,0,0,0.88)",
            display: "flex", justifyContent: "center", alignItems: "center",
            zIndex: "99999",
        });

        /* elemento media */
        let media;
        if (item.type === "video") {
            media = document.createElement("video");
            media.src      = item.src;
            media.controls = true;
            media.autoplay = true;

            /* soundpost */
            if (item.soundSrc) {
                const audio = document.createElement("audio");
                audio.src  = item.soundSrc;
                media.onplay  = () => { audio.currentTime = media.currentTime; audio.play(); };
                media.onpause = () => audio.pause();
                media.addEventListener("seeked", () => { audio.currentTime = media.currentTime; });
                cinemaOverlay.appendChild(audio);
            }
        } else {
            media = document.createElement("img");
            media.src = item.src;
        }

        css(media, {
            maxWidth: "90vw", maxHeight: "90vh",
            objectFit: "contain",
            borderRadius: "4px",
            zIndex: "100000",
            boxShadow: "0 8px 40px rgba(0,0,0,0.7)",
        });
        cinemaOverlay.appendChild(media);

        /* contador  n / total */
        const counter = document.createElement("div");
        counter.textContent = `${cinemaIndex + 1} / ${cinemaItems.length}`;
        css(counter, {
            position: "absolute", top: "14px", left: "50%",
            transform: "translateX(-50%)",
            color: "#fff", fontSize: "13px",
            background: "rgba(0,0,0,0.55)",
            padding: "3px 12px", borderRadius: "20px",
            userSelect: "none",
        });
        cinemaOverlay.appendChild(counter);

        /* flecha izquierda */
        if (cinemaIndex > 0) {
            const btn = arrowBtn("◀", "left");
            btn.addEventListener("click", (e) => { e.stopPropagation(); cinemaIndex--; renderCinema(); });
            cinemaOverlay.appendChild(btn);
        }

        /* flecha derecha */
        if (cinemaIndex < cinemaItems.length - 1) {
            const btn = arrowBtn("▶", "right");
            btn.addEventListener("click", (e) => { e.stopPropagation(); cinemaIndex++; renderCinema(); });
            cinemaOverlay.appendChild(btn);
        }

        /* cerrar con clic en el fondo */
        cinemaOverlay.addEventListener("click", (e) => {
    if (e.target === cinemaOverlay || e.target.tagName === "IMG") closeCinema();
});

        /* teclado */
        cinemaKeyFn = (e) => {
            if (e.key === "ArrowLeft"  && cinemaIndex > 0)                       { cinemaIndex--; renderCinema(); }
            if (e.key === "ArrowRight" && cinemaIndex < cinemaItems.length - 1)  { cinemaIndex++; renderCinema(); }
            if (e.key === "Escape")    closeCinema();
        };
        document.addEventListener("keydown", cinemaKeyFn);

        document.body.appendChild(cinemaOverlay);
    }

    function arrowBtn(symbol, side) {
        const b = document.createElement("button");
        b.textContent = symbol;
        css(b, {
            position: "absolute", top: "50%", [side]: "18px",
            transform: "translateY(-50%)",
            background: "rgba(0,0,0,0.5)", color: "#fff",
            border: "none", borderRadius: "50%",
            width: "48px", height: "48px",
            fontSize: "20px", cursor: "pointer",
            zIndex: "100001", transition: "background 0.2s",
        });
        b.addEventListener("mouseenter", () => { b.style.background = "rgba(255,255,255,0.2)"; });
        b.addEventListener("mouseleave", () => { b.style.background = "rgba(0,0,0,0.5)"; });
        return b;
    }

    /* ─── galería ───────────────────────────────────────────────────── */
    function buildGallery() {
        /* si ya existe, solo mostrarla */
        const prev = document.getElementById("imageGallery");
        if (prev) { prev.style.display = "flex"; return; }

        /* fondo */
        const gallery = document.createElement("div");
        gallery.id = "imageGallery";
        css(gallery, {
            position: "fixed", inset: "0",
            backgroundColor: "rgba(0,0,0,0.82)",
            display: "flex", justifyContent: "center", alignItems: "center",
            zIndex: "9999",
        });

        /* grid scrolleable */
        const grid = document.createElement("div");
        css(grid, {
            display: "grid",
            gridTemplateColumns: `repeat(${COLUMNS}, 1fr)`,
            gap: "8px",
            padding: "20px",
            backgroundColor: "#1c1c1c",
            borderRadius: "10px",
            border: "1px solid #333",
            maxWidth: "85vw", maxHeight: "85vh",
            overflowY: "auto",
            boxSizing: "border-box",
        });

        /* botón cerrar */
        const closeBtn = document.createElement("button");
        closeBtn.id          = "closeGallery";
        closeBtn.textContent = "✕  Cerrar";
        css(closeBtn, {
            position: "absolute", bottom: "18px", right: "20px",
            zIndex: "10000",
            background: "rgba(255,255,255,0.1)", color: "#fff",
            padding: "8px 18px", borderRadius: "6px",
            border: "1px solid rgba(255,255,255,0.2)", cursor: "pointer",
            fontSize: "14px", transition: "background 0.2s",
        });
        closeBtn.addEventListener("mouseenter", () => { closeBtn.style.background = "rgba(255,255,255,0.22)"; });
        closeBtn.addEventListener("mouseleave", () => { closeBtn.style.background = "rgba(255,255,255,0.1)"; });
        const refreshBtn = document.createElement("button"); refreshBtn.textContent = "↺ Refresh"; css(refreshBtn, { position: "absolute", bottom: "18px", left: "20px", zIndex: "10000", background: "rgba(255,255,255,0.1)", color: "#fff", padding: "8px 18px", borderRadius: "6px", border: "1px solid rgba(255,255,255,0.2)", cursor: "pointer", fontSize: "14px" }); refreshBtn.addEventListener("click", () => { gallery.remove(); buildGallery(); }); gallery.appendChild(refreshBtn);

        gallery.addEventListener("click", (e) => { if (e.target === gallery) closeBtn.click(); });

        /* ── recolectar media del thread ── */
        const items = [];

        document.querySelectorAll(".postContainer").forEach((post) => {
            let mediaLink, thumbnailUrl, fileName;

            if (!post.querySelector(".fileText")) return;

            if (post.querySelector(".download-button")) {
                const dl = post.querySelector(".download-button");
                mediaLink = dl.href;
                fileName  = dl.download || dl.href.split("/").pop();
            } else {
                const anchor = post.querySelector(".fileText a");
                if (!anchor) return;
                mediaLink = anchor.href;
                fileName  = anchor.title || anchor.innerText || anchor.href.split("/").pop();
            }

            thumbnailUrl = post.querySelector(".fileThumb img")?.src || "";

            const extMatch = mediaLink.match(/\.(webm|mp4|jpg|jpeg|png|gif)$/i);
            if (!extMatch) return;
            const ext     = extMatch[1].toLowerCase();
            const isVideo = ext === "webm" || ext === "mp4";

            const soundMatch = fileName.match(/\[sound=(.+?)\]/);
            const soundSrc   = soundMatch
                ? decodeURIComponent(soundMatch[1].startsWith("http") ? soundMatch[1] : `https://${soundMatch[1]}`)
                : null;

            items.push({ type: isVideo ? "video" : "image", src: mediaLink, thumbSrc: thumbnailUrl, soundSrc });

            /* ── celda del grid ── */
            const cell = document.createElement("div");
            css(cell, {
                position: "relative", cursor: "pointer",
                overflow: "hidden", borderRadius: "5px",
                backgroundColor: "#000",
                transition: "opacity 0.15s",
            });
            cell.addEventListener("mouseenter", () => { cell.style.opacity = "0.82"; });
            cell.addEventListener("mouseleave", () => { cell.style.opacity = "1"; });

            /* thumbnail (siempre img estática) */
            const thumb = document.createElement("img");
            thumb.src     = isVideo ? thumbnailUrl : mediaLink;
            thumb.loading = "lazy";
            css(thumb, {
                width: "100%", height: `${THUMB_HEIGHT}px`,
                objectFit: "contain", display: "block",
                pointerEvents: "none",
            });
            cell.appendChild(thumb);

            /* overlay ▶ en videos */
            if (isVideo) {
                const play = document.createElement("div");
                play.textContent = "▶";
                css(play, {
                    position: "absolute", top: "50%", left: "50%",
                    transform: "translate(-50%, -50%)",
                    fontSize: "36px",
                    color: "rgba(255,255,255,0.90)",
                    textShadow: "0 2px 12px rgba(0,0,0,0.9)",
                    pointerEvents: "none",
                    lineHeight: "1",
                });
                cell.appendChild(play);

                /* badge "SOUND" si es soundpost */
                if (soundSrc) {
                    const badge = document.createElement("div");
                    badge.textContent = "♪";
                    css(badge, {
                        position: "absolute", top: "6px", right: "6px",
                        background: "rgba(0,0,0,0.65)", color: "#fff",
                        fontSize: "13px", padding: "2px 7px", borderRadius: "10px",
                        pointerEvents: "none",
                    });
                    cell.appendChild(badge);
                }
            }

            const idx = items.length - 1;
            cell.addEventListener("click", () => openCinema(items, idx));

            grid.appendChild(cell);
        });

        gallery.appendChild(grid);
        gallery.appendChild(closeBtn);
        document.body.appendChild(gallery);
    }

    /* ─── botón flotante ────────────────────────────────────────────── */
    function loadButton() {
        const btn = document.createElement("button");
        btn.id          = "openImageGallery";
        btn.textContent = "🖼 Gallery";
        css(btn, {
            position: "fixed", bottom: "20px", right: "20px",
            zIndex: "1000",
            backgroundColor: "#1c1c1c", color: "#d9d9d9",
            padding: "10px 20px", borderRadius: "6px",
            border: "1px solid #444", cursor: "pointer",
            boxShadow: "0 2px 8px rgba(0,0,0,0.4)",
            fontSize: "14px", transition: "background 0.2s",
        });
        btn.addEventListener("mouseenter", () => { btn.style.backgroundColor = "#2e2e2e"; });
        btn.addEventListener("mouseleave", () => { btn.style.backgroundColor = "#1c1c1c"; });

        btn.addEventListener("click", () => {
            const g = document.getElementById("imageGallery");
            if (!g)                            { buildGallery(); return; }
            if (g.style.display === "none")    { g.style.display = "flex"; }
            else                               { g.style.display = "none"; }
        });

        document.body.appendChild(btn);

        /* tecla "i" para abrir/cerrar */
        document.addEventListener("keydown", (e) => {
            if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
            if (e.key === "i") btn.click(); if (e.key === "Escape" && !cinemaOverlay) { const g = document.getElementById("imageGallery"); if (g) g.style.display = "none"; }
        });
    }

    /* ─── init ──────────────────────────────────────────────────────── */
    loadButton();
    console.log("4chan Gallery (cinema) loaded!");
})();