Greasy Fork is available in English.
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 2025-01-12 (3.6)
- // @description 4chan grid-based image gallery with zoom mode support for threads that allows you to browse images, and soundposts (images with sounds, webms with sounds) along with other utility features.
- // @author TheDarkEnjoyer
- // @match https://boards.4chan.org/*/thread/*
- // @match https://boards.4chan.org/*/archive
- // @match https://boards.4channel.org/*/thread/*
- // @match https://boards.4channel.org/*/archive
- // @match https://warosu.org/*/*
- // @match https://archived.moe/*/*
- // @match https://archive.palanq.win/*/*
- // @match https://archive.4plebs.org/*/*
- // @match https://d###archive.org/*/*
- // @match https://thebarchive.com/*/*
- // @match https://archiveofsins.com/*/*
- // @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)",
- },
- Add_Placeholder_Image_For_Zoom_Mode: {
- value: true,
- info: "Add a placeholder image for zoom mode so even if the thread has no images, you can still open the zoom mode",
- },
- Play_Webms_On_Hover: {
- value: true,
- info: "Autoplay webms on hover, pause on mouse leave",
- },
- Switch_Catbox_To_Pixstash_For_Soundposts: {
- value: false,
- info: "Switch all catbox.moe links to pixstash.moe links for soundposts",
- },
- Show_Arrow_Buttons_In_Zoom_Mode: {
- value: true,
- info: "Show clickable arrow buttons on screen edges in zoom mode",
- },
- Grid_Columns: {
- value: 3,
- info: "Number of columns in the grid view",
- },
- Grid_Cell_Max_Height: {
- value: 200,
- info: "Maximum height of each cell in pixels",
- },
- Embed_External_Links: {
- value: false,
- info: "Embed catbox/pixstash links found in post comments",
- },
- Strictly_Load_GIFs_As_Thumbnails_On_Hover: {
- value: false,
- info: "Only load GIF thumbnails until hovered"
- },
- Open_Close_Gallery_Key: {
- value: "i",
- info: "Key to open/close the gallery"
- },
- Hide_Gallery_Button: {
- value: false,
- info: "Hide the gallery button (You can still open the gallery with the keybind, default is 'i')"
- },
- };
- let threadURL = window.location.href;
- let lastScrollPosition = 0;
- let gallerySize = { width: 0, height: 0 };
- let gridContainer; // Add this line
- // store settings in local storage
- if (!localStorage.getItem("gallerySettings")) {
- localStorage.setItem("gallerySettings", JSON.stringify(defaultSettings));
- }
- let settings = JSON.parse(localStorage.getItem("gallerySettings"));
- // check if settings has all the keys from defaultSettings, if not, add the missing keys
- let missingSetting = false;
- for (const setting in defaultSettings) {
- if (!settings.hasOwnProperty(setting)) {
- settings[setting] = defaultSettings[setting];
- missingSetting = true;
- }
- }
- // update the settings in local storage if there are missing settings
- if (missingSetting) {
- localStorage.setItem("gallerySettings", JSON.stringify(settings));
- }
- 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, .highlight");
- case "archived.moe":
- case "archive.palanq.win":
- case "archive.4plebs.org":
- case "d###archive.org":
- case "thebarchive.com":
- case "archiveofsins.com":
- return doc.querySelectorAll(".post, .thread");
- 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!");
- }
- function createArrowButton(direction) {
- const button = document.createElement('button');
- setStyles(button, {
- position: 'fixed',
- top: '50%',
- [direction]: '20px',
- transform: 'translateY(-50%)',
- zIndex: '10001',
- backgroundColor: 'rgba(28, 28, 28, 0.7)',
- color: '#d9d9d9',
- padding: '15px',
- border: 'none',
- borderRadius: '50%',
- cursor: 'pointer',
- display: settings.Show_Arrow_Buttons_In_Zoom_Mode.value ? 'block' : 'none'
- });
- button.innerHTML = direction === 'left' ? '◀' : '▶';
- button.onclick = () => {
- const event = new KeyboardEvent('keydown', { key: direction === 'left' ? 'ArrowLeft' : 'ArrowRight' });
- document.dispatchEvent(event);
- };
- return button;
- }
- // Modify createMediaCell to accept mode and postURL parameters
- function createMediaCell(url, commentText, mode, postURL, board, threadID, postID) {
- if (!gridContainer) {
- gridContainer = document.createElement("div");
- setStyles(gridContainer, {
- display: "grid",
- gridTemplateColumns: `repeat(${settings.Grid_Columns.value}, 1fr)`,
- gap: "10px",
- padding: "20px",
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- maxWidth: "80%",
- maxHeight: "80%",
- overflowY: "auto",
- resize: "both",
- overflow: "auto",
- border: "1px solid #d9d9d9",
- });
- }
- const cell = document.createElement("div");
- setStyles(cell, {
- border: "1px solid #d9d9d9",
- position: "relative",
- });
- // Make the cell draggable
- cell.draggable = true;
- cell.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", [...gridContainer.children].indexOf(cell));
- e.dataTransfer.dropEffect = "move";
- });
- // Allow drops on this cell
- cell.addEventListener("dragover", (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- });
- cell.addEventListener("drop", (e) => {
- e.preventDefault();
- const draggedIndex = e.dataTransfer.getData("text/plain");
- const containerChildren = [...gridContainer.children];
- const draggedCell = containerChildren[draggedIndex];
- if (draggedCell !== cell) {
- const dropIndex = containerChildren.indexOf(cell);
- if (draggedIndex < dropIndex) {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex].nextSibling);
- } else {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex]);
- }
- }
- });
- const mediaContainer = document.createElement("div");
- setStyles(mediaContainer, {
- position: "relative",
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- });
- const buttonDiv = document.createElement("div");
- setStyles(buttonDiv, {
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- padding: "5px",
- });
- // Add view post button for external media
- const viewPostButton = document.createElement("button");
- viewPostButton.textContent = "View Original";
- 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", () => {
- window.location.href = postURL;
- gallerySize = {
- width: gridContainer.offsetWidth,
- height: gridContainer.offsetHeight,
- };
- });
- buttonDiv.appendChild(viewPostButton);
- if (url.match(/\.(webm|mp4)$/i)) {
- const video = document.createElement("video");
- video.src = url;
- video.controls = true;
- video.title = commentText;
- video.setAttribute("fileName", url.split('/').pop());
- video.setAttribute("board", board);
- video.setAttribute("threadID", threadID);
- video.setAttribute("postID", postID);
- setStyles(video, {
- maxWidth: "100%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- objectFit: "contain",
- cursor: "pointer",
- });
- mediaContainer.appendChild(video);
- const openInNewTabButton = document.createElement("button");
- openInNewTabButton.textContent = "Open";
- setStyles(openInNewTabButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- openInNewTabButton.onclick = () => {
- window.open(url, "_blank");
- };
- buttonDiv.appendChild(openInNewTabButton);
- } else if (url.match(/\.(jpg|jpeg|png|gif)$/i)) {
- // Only create image cell if mode is "all"
- if (mode === "all") {
- const image = document.createElement("img");
- image.src = url;
- image.title = commentText;
- image.setAttribute("fileName", url.split('/').pop());
- image.setAttribute("actualSrc", url);
- image.setAttribute("thumbnailUrl", url);
- image.setAttribute("board", board);
- image.setAttribute("threadID", threadID);
- image.setAttribute("postID", postID);
- setStyles(image, {
- maxWidth: "100%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- objectFit: "contain",
- cursor: "pointer",
- });
- image.loading = "lazy";
- if (
- settings.Strictly_Load_GIFs_As_Thumbnails_On_Hover.value &&
- url.match(/\.gif$/i)
- ) {
- image.src = url;
- image.addEventListener("mouseover", () => {
- image.src = url;
- });
- image.addEventListener("mouseout", () => {
- image.src = url;
- });
- }
- mediaContainer.appendChild(image);
- } else {
- return; // Skip non-webm/soundpost media in webm mode
- }
- }
- cell.appendChild(mediaContainer);
- cell.appendChild(buttonDiv);
- gridContainer.appendChild(cell);
- }
- const loadButton = () => {
- const isArchivePage = window.location.pathname.includes("/archive");
- let addFakeImage = settings.Add_Placeholder_Image_For_Zoom_Mode.value;
- 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)",
- visibility: settings.Hide_Gallery_Button.value ? "hidden" : "visible",
- });
- const openImageGallery = () => {
- // new check to see if gallery is already in the DOM
- const existingGallery = document.getElementById("imageGallery");
- if (existingGallery) {
- existingGallery.style.display = "flex";
- return;
- }
- addFakeImage = settings.Add_Placeholder_Image_For_Zoom_Mode.value;
- 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",
- });
- gridContainer = document.createElement("div");
- setStyles(gridContainer, {
- display: "grid",
- gridTemplateColumns: `repeat(${settings.Grid_Columns.value}, 1fr)`,
- gap: "10px",
- padding: "20px",
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- maxWidth: "80%",
- maxHeight: "80%",
- overflowY: "auto",
- resize: "both",
- overflow: "auto",
- border: "1px solid #d9d9d9",
- });
- // Add dragover & drop listeners to the grid container
- gridContainer.addEventListener("dragover", (e) => {
- e.preventDefault();
- });
- gridContainer.addEventListener("drop", (e) => {
- e.preventDefault();
- const draggedIndex = e.dataTransfer.getData("text/plain");
- const targetCell = e.target.closest("div[draggable='true']");
- if (!targetCell) return;
- const containerChildren = [...gridContainer.children];
- const dropIndex = containerChildren.indexOf(targetCell);
- if (draggedIndex >= 0 && dropIndex >= 0) {
- const draggedCell = containerChildren[draggedIndex];
- if (draggedIndex < dropIndex) {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex].nextSibling);
- } else {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex]);
- }
- }
- });
- // 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
- 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, addFakeImage); // 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, addFakeImage); // 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) {
- // remove settings that are not in the default settings
- if (!(setting in defaultSettings)) {
- delete settings[setting];
- continue;
- }
- 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 = {};
- // First copy default settings structure
- for (const key in defaultSettings) {
- newSettings[key] = { ...defaultSettings[key] };
- }
- const inputs = document.querySelectorAll("#settingsList input");
- inputs.forEach((input) => {
- const settingName = input.previousSibling.textContent.replace(
- / /g,
- "_"
- );
- if (settingName in defaultSettings) {
- newSettings[settingName].value = input.type === "checkbox" ? input.checked : input.value;
- }
- });
- localStorage.setItem("gallerySettings", JSON.stringify(newSettings));
- settings = newSettings;
- settingsContainer.remove();
- const gallery = document.querySelector('#imageGallery');
- if (gallery) {
- document.body.removeChild(gallery);
- setTimeout(() => {
- document.querySelector('#openImageGallery').click();
- }, 20);
- }
- });
- // 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, addFakeImage) => {
- 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, addFakeImage) => {
- // get the website url without the protocol and next slash
- let 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);
- // add thread and website url as attributes to every post
- posts.forEach((post) => {
- post.setAttribute("thread", thread);
- post.setAttribute("websiteUrl", websiteUrl);
- });
- if (addFakeImage) {
- // Add a fake image to the grid container to allow zoom mode to open even if the thread has no images
- let placeholder_imageURL = "https://files.pixstash.moe/ecl8vh.png";
- let examplePost = document.createElement("div");
- examplePost.innerHTML = `
- <div class="postContainer", id="1231232">
- <div class="fileText">
- <a href="${placeholder_imageURL}" download="${placeholder_imageURL}">OpenZoomMode[sound=https://files.catbox.moe/brugtt.mp3].jpg</a>
- </div>
- <div class="fileThumb">
- <img src="${placeholder_imageURL}" alt="Thumbnail">
- </div>
- <div class="postMessage">
- Just a placeholder image for zoom mode
- </div>
- </div>
- `;
- examplePost.setAttribute("thread", "https://boards.4chan.org/b/thread/123456789");
- examplePost.setAttribute("websiteUrl", "boards.4chan.org");
- posts = [examplePost, ...posts];
- }
- posts.forEach((post) => {
- let mediaLinkFlag = false;
- let board;
- let threadID;
- let postID;
- let postURL;
- let thumbnailUrl;
- let mediaLink;
- let fileName;
- let comment;
- let isVideo;
- let isImage;
- let soundLink;
- let encodedSoundPostLink;
- let temp;
- let hasEmbeddedMediaLink = false;
- let matches;
- websiteUrl = post.getAttribute("websiteUrl");
- thread = post.getAttribute("thread");
- // case statement for different websites
- switch (websiteUrl) {
- case "warosu.org":
- let thumbnailElement = post.querySelector(".thumb");
- fileName = post
- .querySelector(".fileinfo")
- ?.innerText.split(", ")[2];
- thumbnailUrl = thumbnailElement?.src;
- mediaLink = thumbnailElement?.parentNode.href;
- comment = post.querySelector("blockquote");
- threadID = post.getAttribute("thread").match(/thread\/(\d+)/)
- if (threadID) {
- threadID = threadID[1];
- } else {
- threadID = post.querySelector(".js").href.match(/thread\/(\d+)/)[1];
- }
- postID = post.id.replace("pc", "").replace("p", "");
- break;
- case "archived.moe":
- case "archive.palanq.win":
- case "archive.4plebs.org":
- case "d###archive.org":
- case "thebarchive.com":
- case "archiveofsins.com":
- thumbnailUrl = post.querySelector(".post_image")?.src;
- mediaLink = post.querySelector(".thread_image_link")?.href;
- fileName = post.querySelector(
- ".post_file_filename"
- )?.title;
- comment = post.querySelector(".text");
- threadID = post.querySelector(".post_data > a")?.href.match(
- /thread\/(\d+)/
- )[1];
- postID = post.id
- break;
- case "boards.4chan.org":
- case "boards.4channel.org":
- default:
- if (post.querySelector(".fileText")) {
- // if they have 4chanX installed, there will be a fileText-orignal class
- if (post.querySelector(".download-button")) {
- temp = post.querySelector(".download-button");
- mediaLink = temp.href;
- fileName = temp.download;
- } else {
- if (post.classList.contains("opContainer")) {
- mediaLink = post.querySelector(".fileText a");
- temp = mediaLink;
- } else {
- mediaLink = post.querySelector(".fileText");
- temp = mediaLink.querySelector("a");
- }
- if (mediaLink.title === "") {
- if (temp.title === "") {
- fileName = temp.innerText;
- } else {
- fileName = temp.title;
- }
- } else {
- fileName = mediaLink.title;
- }
- mediaLink = temp.href;
- }
- thumbnailUrl = post.querySelector(".fileThumb img")?.src;
- }
- comment = post.querySelector(".postMessage");
- threadID = thread.match(/thread\/(\d+)/)[1];
- postID = post.id.replace("pc", "").replace("p", "");
- }
- const fileExtRegex = /\.(webm|mp4|jpg|png|gif)$/i;
- const linkRegex = /https:\/\/(files|litter)\.(catbox|pixstash)\.moe\/[a-z0-9]+\.(jpg|png|gif|webm|mp4)/g;
- if (mediaLink) {
- const ext = mediaLink.match(fileExtRegex)?.[1]?.toLowerCase();
- isVideo = ext === 'webm' || ext === 'mp4';
- isImage = ext === 'jpg' || ext === 'png' || ext === 'gif';
- soundLink = fileName.match(/\[sound=(.+?)\]/);
- mediaLinkFlag = true;
- }
- if (settings.Embed_External_Links.value && comment) {
- matches = Array.from(comment.innerText.matchAll(linkRegex)).map(match => match[0]);
- if (matches.length > 0) {
- if (!mediaLinkFlag) {
- mediaLink = matches[0];
- fileName = mediaLink.split("/").pop();
- thumbnailUrl = mediaLink;
- if (hasEmbeddedMediaLink) {
- matches.shift();
- }
- const ext = mediaLink.match(fileExtRegex)?.[1]?.toLowerCase();
- isVideo = ext === 'webm' || ext === 'mp4';
- isImage = ext === 'jpg' || ext === 'png' || ext === 'gif';
- soundLink = fileName.match(/\[sound=(.+?)\]/);
- mediaLinkFlag = true;
- }
- hasEmbeddedMediaLink = matches.length > 0;
- }
- }
- // replace the "#pcXXXXXXX" or "#pXXXXXXX" with an empty string to get the actual thread url
- if (thread.includes("#")) {
- postURL = thread.replace(/#p\d+/, "");
- postURL = postURL.replace(/#pc\d+/, "");
- } else {
- postURL = thread;
- }
- // post info (constant)
- board = thread.match(/\/\/[^\/]+\/([^\/]+)/)[1];
- if (soundLink) {
- encodedSoundPostLink = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
- }
- if (mediaLinkFlag) {
- // Check if the post should be loaded based on the mode
- if (
- mode === "all" ||
- (mode === "webm" && (isVideo || (isImage && soundLink)))
- ) {
- // Insert a button/link to open media in new tab for videos
- const cell = document.createElement("div");
- setStyles(cell, {
- border: "1px solid #d9d9d9",
- position: "relative",
- });
- // Make the cell draggable
- cell.draggable = true;
- cell.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", [...gridContainer.children].indexOf(cell));
- e.dataTransfer.dropEffect = "move";
- });
- // Allow drops on this cell
- cell.addEventListener("dragover", (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- });
- cell.addEventListener("drop", (e) => {
- e.preventDefault();
- const draggedIndex = e.dataTransfer.getData("text/plain");
- const containerChildren = [...gridContainer.children];
- const draggedCell = containerChildren[draggedIndex];
- if (draggedCell !== cell) {
- const dropIndex = containerChildren.indexOf(cell);
- if (draggedIndex < dropIndex) {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex].nextSibling);
- } else {
- gridContainer.insertBefore(draggedCell, containerChildren[dropIndex]);
- }
- }
- });
- 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",
- });
- // if medialink is catbox.moe or pixstash.moe, then video thumbnail is a video element with no controls
- let videoThumbnail;
- if (mediaLink.match(/catbox.moe|pixstash.moe/)) {
- videoThumbnail = document.createElement("video");
- } else {
- videoThumbnail = document.createElement("img");
- }
- videoThumbnail.src = thumbnailUrl;
- videoThumbnail.alt = "Video Thumbnail";
- setStyles(videoThumbnail, {
- width: "100%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- objectFit: "contain",
- cursor: "pointer",
- });
- videoThumbnail.loading = "lazy";
- const video = document.createElement("video");
- video.src = mediaLink;
- video.controls = true;
- video.title = comment.innerText;
- video.videothumbnailDisplayed = "true";
- video.setAttribute("fileName", fileName);
- video.setAttribute("board", board);
- video.setAttribute("threadID", threadID);
- video.setAttribute("postID", postID);
- setStyles(video, {
- maxWidth: "100%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- 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 {
- if (settings.Play_Webms_On_Hover.value) {
- 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]}`
- );
- // if switch catbox to pixstash is enabled, replace catbox.moe with pixstash.moe
- if (settings.Switch_Catbox_To_Pixstash_For_Soundposts.value) {
- audio.src = audio.src.replace("catbox.moe", "pixstash.moe");
- }
- // add attribute to the audio element with the encoded soundpost link
- audio.setAttribute(
- "encodedSoundPostLink",
- encodedSoundPostLink
- );
- videoContainer.appendChild(audio);
- const resetButton = document.createElement("button");
- resetButton.textContent = "Reset";
- setStyles(resetButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- resetButton.addEventListener("click", () => {
- video.currentTime = 0;
- audio.currentTime = 0;
- });
- buttonDiv.appendChild(resetButton);
- // html5 video play
- video.onplay = (event) => {
- audio.play();
- };
- video.onpause = (event) => {
- audio.pause();
- };
- let lastVideoTime = 0;
- // Sync audio with video on timeupdate event only if the difference is 2 seconds or more
- video.addEventListener("timeupdate", () => {
- if (Math.abs(video.currentTime - lastVideoTime) >= 2) {
- audio.currentTime = video.currentTime;
- lastVideoTime = video.currentTime;
- }
- lastVideoTime = video.currentTime;
- });
- }
- cell.appendChild(videoContainer);
- } else if (isImage) {
- const imageContainer = document.createElement("div");
- setStyles(imageContainer, {
- position: "relative",
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- });
- const image = document.createElement("img");
- image.src = thumbnailUrl;
- if (settings.Load_High_Res_Images_By_Default.value) {
- image.src = mediaLink;
- }
- if (mediaLink.includes(".gif")) {
- image.src = mediaLink;
- if (
- settings.Strictly_Load_GIFs_As_Thumbnails_On_Hover.value
- ) {
- mediaLink = thumbnailUrl;
- image.src = thumbnailUrl;
- }
- }
- image.setAttribute("fileName", fileName);
- image.setAttribute("actualSrc", mediaLink);
- image.setAttribute("thumbnailUrl", thumbnailUrl);
- image.setAttribute("board", board);
- image.setAttribute("threadID", threadID);
- image.setAttribute("postID", postID);
- setStyles(image, {
- maxWidth: "100%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- 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();
- background.appendChild(createArrowButton('left'));
- background.appendChild(createArrowButton('right'));
- gallery.appendChild(background);
- // create a container for the buttons, number, and download buttons (even space between them)
- // position: fixed; bottom: 10px; display: flex; flex-direction: row; justify-content: space-around; z-index: 10000; width: 100%; margin:auto;
- const bottomContainer = document.createElement("div");
- setStyles(bottomContainer, {
- position: "fixed",
- bottom: "10px",
- display: "flex",
- flexDirection: "row",
- justifyContent: "space-around",
- zIndex: "10000",
- width: "100%",
- margin: "auto",
- });
- background.appendChild(bottomContainer);
- // buttons on the bottom left of the screen for reverse image search (SauceNAO, Google Lens, Yandex)
- const buttonContainer = document.createElement("div");
- setStyles(buttonContainer, {
- display: "flex",
- gap: "10px",
- });
- buttonContainer.setAttribute("mediaLink", mediaLink);
- const sauceNAOButton = document.createElement("button");
- sauceNAOButton.textContent = "SauceNAO";
- setStyles(sauceNAOButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- });
- sauceNAOButton.addEventListener("click", () => {
- window.open(
- `https://saucenao.com/search.php?url=${encodeURIComponent(
- buttonContainer.getAttribute("mediaLink")
- )}`
- );
- });
- buttonContainer.appendChild(sauceNAOButton);
- const googleLensButton = document.createElement("button");
- googleLensButton.textContent = "Google Lens";
- setStyles(googleLensButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- googleLensButton.addEventListener("click", () => {
- window.open(
- `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(
- buttonContainer.getAttribute("mediaLink")
- )}`
- );
- });
- buttonContainer.appendChild(googleLensButton);
- const yandexButton = document.createElement("button");
- yandexButton.textContent = "Yandex";
- setStyles(yandexButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- yandexButton.addEventListener("click", () => {
- window.open(
- `https://yandex.com/images/search?rpt=imageview&url=${encodeURIComponent(
- buttonContainer.getAttribute("mediaLink")
- )}`
- );
- });
- buttonContainer.appendChild(yandexButton);
- bottomContainer.appendChild(buttonContainer);
- // download container for video/img and audio
- const downloadButtonContainer =
- document.createElement("div");
- setStyles(downloadButtonContainer, {
- display: "flex",
- gap: "10px",
- });
- bottomContainer.appendChild(downloadButtonContainer);
- const viewPostButton = document.createElement("a");
- viewPostButton.textContent = "View Post";
- viewPostButton.href = `https://boards.4chan.org/${board}/thread/${threadID}#p${postID}`;
- setStyles(viewPostButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- downloadButtonContainer.appendChild(viewPostButton);
- const downloadButton = document.createElement("a");
- downloadButton.textContent = "Download Video/Image";
- downloadButton.href = mediaLink;
- downloadButton.download = fileName;
- downloadButton.target = "_blank";
- setStyles(downloadButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- downloadButtonContainer.appendChild(downloadButton);
- const audioDownloadButton = document.createElement("a");
- audioDownloadButton.textContent = "Download Audio";
- audioDownloadButton.target = "_blank";
- setStyles(audioDownloadButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- if (soundLink) {
- audioDownloadButton.href = decodeURIComponent(
- soundLink[1].startsWith("http")
- ? soundLink[1]
- : `https://${soundLink[1]}`
- );
- // if switch catbox to pixstash is enabled, replace catbox.moe with pixstash.moe
- if (settings.Switch_Catbox_To_Pixstash_For_Soundposts.value) {
- audioDownloadButton.href = audioDownloadButton.href.replace(
- "catbox.moe",
- "pixstash.moe"
- );
- }
- audioDownloadButton.download = soundLink[1]
- .split("/")
- .pop();
- } else {
- audioDownloadButton.style.display = "none";
- }
- downloadButtonContainer.appendChild(audioDownloadButton);
- // a button beside the download video and download audio button that says download encoded soundpost which links to the following url in a new tab "https://4chan.mahdeensky.top/<board>/thread/<thread>/<post>" where things between the <>, are variables to be replaced
- const encodedSoundPostButton =
- document.createElement("a");
- encodedSoundPostButton.textContent =
- "Download Encoded Soundpost";
- encodedSoundPostButton.target = "_blank";
- setStyles(encodedSoundPostButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- if (soundLink) {
- encodedSoundPostButton.href = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
- } else {
- encodedSoundPostButton.style.display = "none";
- }
- downloadButtonContainer.appendChild(
- encodedSoundPostButton
- );
- // number on the bottom right of the screen to show which image is currently being viewed
- const imageNumber = document.createElement("div");
- let currentImageNumber =
- Array.from(cell.parentNode.children).indexOf(cell) + 1;
- let imageTotal = cell.parentNode.children.length;
- imageNumber.textContent = `${currentImageNumber}/${imageTotal}`;
- setStyles(imageNumber, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- position: "fixed",
- top: "10px",
- left: "10px",
- });
- background.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",
- right: "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;
- function navigateImage(direction) {
- const targetCell = direction === 'left' ? currentCell.previousElementSibling : currentCell.nextElementSibling;
- if (!targetCell) return;
- // ...existing navigation code using targetCell instead of previousCell/nextCell...
- 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: `${settings.Grid_Cell_Max_Height.value}px`,
- objectFit: "contain",
- });
- }
- // check if it has a video
- const video = targetCell?.querySelector("video");
- if (video) {
- const video = targetCell
- .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 = targetCell.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 =
- targetCell.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 = targetCell.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 (targetCell) {
- currentCell = targetCell;
- buttonContainer.setAttribute(
- "mediaLink",
- targetCell.querySelector("img").src
- );
- currentImageNumber += direction === 'left' ? -1 : 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")
- : targetCell
- .querySelector("img")
- .getAttribute("fileName");
- // update view post button link
- let targetMedia = video || targetCell.querySelector("img");
- let targetBoard = targetMedia.getAttribute("board");
- let targetThreadID = targetMedia.getAttribute("threadID");
- let targetPostID = targetMedia.getAttribute("postID");
- viewPostButton.href = `https://boards.4chan.org/${targetBoard}/thread/${targetThreadID}#p${targetPostID}`;
- // update the download button links
- downloadButton.href = targetMedia.src;
- if (targetCell.querySelector("audio")) {
- // updating audio button download link
- audioDownloadButton.href =
- targetCell.querySelector("audio").src;
- audioDownloadButton.download = targetCell
- .querySelector("audio")
- .src.split("/")
- .pop();
- audioDownloadButton.style.display = "block";
- // updating encoded soundpost button link
- encodedSoundPostButton.href = targetCell.querySelector("audio")
- .getAttribute("encodedSoundPostLink");
- encodedSoundPostButton.style.display = "block";
- } else {
- audioDownloadButton.style.display = "none";
- encodedSoundPostButton.style.display = "none";
- }
- }
- }
- const keybindHandler = (event) => {
- if (event.key === "ArrowLeft") {
- navigateImage('left');
- } else if (event.key === "ArrowRight") {
- navigateImage('right');
- }
- };
- document.addEventListener("keydown", keybindHandler);
- image.addEventListener(
- "click",
- () => {
- image.style = "";
- // image.src = thumbnailUrl;
- setStyles(image, {
- maxWidth: "99%",
- maxHeight: `${settings.Grid_Cell_Max_Height.value}px`,
- 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]}`
- );
- // if switch catbox to pixstash is enabled, replace catbox.moe with pixstash.moe
- if (settings.Switch_Catbox_To_Pixstash_For_Soundposts.value) {
- audio.src = audio.src.replace("catbox.moe", "pixstash.moe");
- }
- audio.loop = true;
- // set the attribute to the audio element with the encoded soundpost link
- audio.setAttribute(
- "encodedSoundPostLink",
- encodedSoundPostLink
- );
- imageContainer.appendChild(audio);
- image.addEventListener("mouseenter", () => {
- audio.play();
- });
- image.addEventListener("mouseleave", () => {
- audio.pause();
- });
- const playPauseButton = document.createElement("button");
- playPauseButton.textContent = "Play/Pause";
- setStyles(playPauseButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- playPauseButton.addEventListener("click", () => {
- if (audio.paused) {
- audio.play();
- } else {
- audio.pause();
- }
- });
- buttonDiv.appendChild(playPauseButton);
- }
- imageContainer.appendChild(image);
- cell.appendChild(imageContainer);
- } else {
- return; // Skip non-video and non-image posts
- }
- // Add button that scrolls to the post in the thread
- const viewPostButton = document.createElement("button");
- viewPostButton.textContent = "View Post";
- setStyles(viewPostButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- viewPostButton.addEventListener("click", () => {
- gallerySize = {
- width: gridContainer.offsetWidth,
- height: gridContainer.offsetHeight,
- };
- lastScrollPosition = gridContainer.scrollTop;
- window.location.href = postURL + "#" + post.id;
- // post id example: "pc77515440"
- gallery.style.display = "none"; // hide instead of removing
- });
- buttonDiv.appendChild(viewPostButton);
- // Add button that opens the media in a new tab if the media
- const openInNewTabButton = document.createElement("button");
- openInNewTabButton.textContent = "Open";
- setStyles(openInNewTabButton, {
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "5px 10px",
- borderRadius: "3px",
- border: "none",
- cursor: "pointer",
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
- });
- openInNewTabButton.addEventListener("click", () => {
- window.open(mediaLink, "_blank");
- });
- buttonDiv.appendChild(openInNewTabButton);
- cell.appendChild(buttonDiv);
- gridContainer.appendChild(cell);
- }
- }
- // In the loadPosts function, update the embedded links section:
- if (hasEmbeddedMediaLink) {
- // Create a proper post link that includes the thread ID and post ID
- const fullPostLink = postURL + "#" + post.id;
- matches.forEach(url => {
- createMediaCell(url, comment.innerText, mode, fullPostLink, board, threadID, postID); // Pass the current post's URL
- });
- }
- });
- });
- };
- // only load the fake image in the first thread
- loadPostsFromThread(checkedThreads[0], addFakeImage);
- // load the rest of the threads with no fake image
- checkedThreads.slice(1).forEach((thread) => {
- loadPostsFromThread(thread, false);
- });
- };
- loadPosts(mode, addFakeImage);
- 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,
- };
- gallery.style.display = "none"; // hide instead of removing
- });
- gallery.appendChild(closeButton);
- // Add scroll to bottom button
- const scrollBottomButton = document.createElement("button");
- scrollBottomButton.textContent = "Scroll to Last";
- setStyles(scrollBottomButton, {
- position: "fixed",
- bottom: "20px",
- left: "20px",
- backgroundColor: "#1c1c1c",
- color: "#d9d9d9",
- padding: "10px 20px",
- borderRadius: "5px",
- border: "none",
- cursor: "pointer",
- zIndex: "10000",
- });
- scrollBottomButton.addEventListener("click", () => {
- const lastCell = gridContainer.lastElementChild;
- if (lastCell) {
- lastCell.scrollIntoView({ behavior: "smooth" });
- }
- });
- gallery.appendChild(scrollBottomButton);
- // Add zoom mode arrow buttons
- const background = document.createElement('div');
- background.appendChild(createArrowButton('left'));
- background.appendChild(createArrowButton('right'));
- 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.includes(threadURL.replace(/#.*$/, ""))) {
- 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;
- }, 100);
- } 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 };
- }
- gallery.addEventListener("click", (event) => {
- if (event.target === gallery) {
- closeButton.click();
- }
- });
- };
- 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.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA") {
- return;
- }
- if (event.key === settings.Open_Close_Gallery_Key.value) {
- if (!document.querySelector("#imageGallery")) {
- document.querySelector("#openImageGallery").click();
- return;
- }
- if (document.querySelector("#imageGallery").style.display === "none") {
- document.querySelector("#openImageGallery").click();
- } else {
- document.querySelector("#closeGallery").click();
- }
- }
- });
- loadButton();
- console.log("4chan Gallery loaded successfully!");
- })();