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