Browse all images and videos in a 4chan thread using a grid gallery with a cinema-style viewer.
// ==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!");
})();