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();