NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name e621 Pool Viewer // @namespace http://tampermonkey.net/ // @version 0.4 // @description Add carousel for viewing image pools // @author bobsacramento // @license MIT // @match https://e621.net/posts/* // @icon https://e621.net/favicon.ico // @run-at document-idle // @grant GM_xmlhttpRequest // @updateURL https://openuserjs.org/meta/bobsacramento/e621_Pool_Viewer.meta.js // @downloadURL https://openuserjs.org/install/bobsacramento/e621_Pool_Viewer.user.js // ==/UserScript== /* jshint esversion: 8 */ const previewCount = 3; const loadDelay = 500; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Function to check if the post is part of a pool async function checkForPool(postId) { const postApiUrl = `https://e621.net/posts/${postId}.json`; return new Promise((resolve, reject) => { const poolRequest = { method: 'GET', url: postApiUrl, onload: response => resolve(handlePoolRequest(response)), onerror: error => reject(`Error fetching post details: ${error}`) }; GM_xmlhttpRequest(poolRequest); }); } async function handlePoolRequest(response) { const postDetails = JSON.parse(response.responseText); const pools = postDetails.post.pools; if (pools && pools.length > 0) { return await findLargestPool(pools); } else { throw new Error('This post is not part of any pool.'); } } async function findLargestPool(poolIds) { let poolSizes = poolIds.map(getPoolPostCount); poolSizes = await Promise.allSettled(poolSizes); poolSizes = poolSizes.map(result => result.status === 'fulfilled' ? result.value : 0); let [index, count] = indexOfMax(poolSizes); return [poolIds[index], count]; } function indexOfMax(arr) { if (arr.length === 0) { throw new Error("Empty array!"); } let max = arr[0]; let maxIndex = 0; for (let i = 1; i < arr.length; i++) { if (arr[i] > max) { maxIndex = i; max = arr[i]; } } return [maxIndex, max]; } async function getPoolPostCount(poolId) { const poolDetailsApiUrl = `https://e621.net/pools/${poolId}.json`; return new Promise((resolve, reject) => { const poolDetailsRequest = { method: 'GET', url: poolDetailsApiUrl, onload: response => { const poolDetails = JSON.parse(response.responseText); const postCount = poolDetails.post_count; resolve(postCount); }, onerror: error => reject(`Error fetching pool details: ${error}`) }; GM_xmlhttpRequest(poolDetailsRequest); }); } // Function to load images from the pool async function loadPoolIds(poolId) { const poolApiUrl = `https://e621.net/pools/${poolId}.json`; const response = await fetch(poolApiUrl); const poolDetails = await response.json(); return poolDetails.post_ids; } // Function to create a carousel/slider async function createCarousel(currentPost, poolIds) { const poolSize = poolIds.length; var currentIndex = poolIds.indexOf(currentPost); if (currentIndex === -1) { throw new Error("Current post not in pool"); } const imageDisplayArea = document.getElementById("image"); if (imageDisplayArea.tagName !== 'image') throw new Error("Element is not an image. Aborting"); const carouselContainer = document.createElement('div'); carouselContainer.id = 'carouselContainer'; carouselContainer.style.width = '100%'; carouselContainer.style.overflow = 'hidden'; // Create placeholders for all images in the pool poolIds.forEach(id => { const placeholder = createPlaceholder(id); carouselContainer.appendChild(placeholder); }); // The current image is already loaded. Just move the src carouselContainer.childNodes[currentIndex].src = imageDisplayArea.src; carouselContainer.childNodes[currentIndex].setAttribute("data-loaded", ""); const priorityList = buildPriorityList(currentIndex, poolSize); loadImages(priorityList, poolIds, carouselContainer); // Replace the current image display with the carousel imageDisplayArea.replaceWith(carouselContainer); document.addEventListener('keydown', event => { let prevIndex = currentIndex; if (event.key === 'ArrowLeft') { if (currentIndex > 0) { currentIndex = currentIndex - 1; } } if (event.key === 'ArrowRight') { if (currentIndex < poolSize - 1) currentIndex = currentIndex + 1; } updateCarouselImages(prevIndex, currentIndex); }); function updateCarouselImages(prevIndex, currIndex) { const indexesToLoad = buildPriorityList(currIndex, poolSize); loadImages(indexesToLoad, poolIds, carouselContainer); const imgs = carouselContainer.childNodes; imgs[prevIndex].hidden = true; imgs[prevIndex].id = ""; imgs[currIndex].hidden = false; imgs[currIndex].id = "image"; imgs[currIndex].className = imgs[prevIndex].className; } } // Retrieves the image url from a post async function getImgUrl(postId) { const postApiUrl = `https://e621.net/posts/${postId}.json`; const postResponse = await fetch(postApiUrl); const postData = await postResponse.json(); return postData.post.file.url; } function optimizeImage(imageUrl) { const imgPreloader = document.createElement('link'); imgPreloader.rel = "preload"; imgPreloader.as = "image"; imgPreloader.href = imageUrl; document.head.appendChild(imgPreloader); } function createPlaceholder(postId) { const imgPlaceholder = document.createElement('img'); imgPlaceholder.dataset.postId = postId; imgPlaceholder.className = "fit-window"; imgPlaceholder.hidden = true; return imgPlaceholder; } async function loadImages(order, poolIds, parent) { for (let index of order) { if (parent.childNodes[index].hasAttribute("data-loaded")) { continue; } const postId = poolIds[index]; const imageUrl = await getImgUrl(postId); parent.childNodes[index].src = imageUrl; // Mark element as loaded parent.childNodes[index].setAttribute("data-loaded", ""); // Optimization to force browser to load image even when hidden optimizeImage(imageUrl); // Load slowly not hit the rate limit await sleep(loadDelay); } } function buildPriorityList(current, maxLength) { // Calculate range of indices to load const start = Math.max(0, current - previewCount); const end = Math.min(maxLength, current + previewCount + 1); const priorityList = [current]; // Next image if (current + 1 < end) { priorityList.push(current + 1); } // Previous image if (current - 1 >= start) { priorityList.push(current - 1); } // Rest of images for (let i = current + 2; i < end; i++) { priorityList.push(i); } // All previous images for (let i = current - 2; i >= start; i--) { priorityList.push(i); } return priorityList; } // Main function to start the script async function main() { // Get the id of the current post const postId = parseInt(window.location.pathname.split('/')[2]); // Check if the post is part of a pool const poolId = await checkForPool(postId).catch(console.log); const poolIds = await loadPoolIds(poolId); createCarousel(postId, poolIds).catch(console.log); } // Run the main function when the page is loaded main();