// ==UserScript==
// @name Adulttime Grid View
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Adds a "Grid View" button next to the scene title that, when pressed, automatically opens the carousel (if needed) and then launches a grid overlay. In the grid overlay the images are shown inside cells with a fixed height increased by 50% so that both vertical and landscape images appear larger and are not cut off. The grid overlay also retains its scroll position between openings. The carousel overlay includes mouse-wheel zoom (centered on the mouse pointer) and drag functionality.
// @match https://members.adulttime.com/*
// @license GPL-3.0
// @grant none
// ==/UserScript==
(function() {
'use strict';
let currentCarouselImages = [];
let gridScrollPosition = 0; // To store grid overlay scroll position
// Hide the default carousel container (if any)
function hideDefaultCarousel() {
const defaultCarousel = document.querySelector('#pageOverlaySlot');
if (defaultCarousel) {
defaultCarousel.style.display = 'none';
}
}
// Extract image URLs from the carousel (if already open)
function extractCarouselImages() {
const imageNodes = document.querySelectorAll('#pageOverlaySlot .image-gallery-slide img.image-gallery-image');
const urls = [];
imageNodes.forEach(img => {
if (img.src && !urls.includes(img.src)) {
urls.push(img.src);
}
});
return urls;
}
// Recursively click the "next" arrow until all images have been loaded.
function loadAllCarouselImages(callback) {
const activeImg = document.querySelector('.image-gallery-slide.image-gallery-center img.image-gallery-image');
if (!loadAllCarouselImages.firstSrc && activeImg) {
loadAllCarouselImages.firstSrc = activeImg.src;
}
if (activeImg && loadAllCarouselImages.called && activeImg.src === loadAllCarouselImages.firstSrc) {
callback();
return;
}
loadAllCarouselImages.called = true;
const nextArrow = document.querySelector('a.next-Link:not(.disabled-Link)');
if (nextArrow) {
nextArrow.click();
setTimeout(() => {
loadAllCarouselImages(callback);
}, 1000);
} else {
callback();
}
}
// Create grid overlay container with fixed cell height (increased by 50%) and scroll retention.
function createOverlayContainer(images) {
let container = document.getElementById('gridOverlayContainer');
if (container) container.remove();
container = document.createElement('div');
container.id = 'gridOverlayContainer';
Object.assign(container.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
backgroundColor: 'black',
zIndex: '9999',
display: 'grid',
// Set fixed cell width using auto-fit; using minmax with 600px as minimum (50% more than 400px)
gridTemplateColumns: 'repeat(auto-fit, minmax(600px, 1fr))',
gap: '10px',
padding: '20px',
overflowY: 'auto'
});
// Listen for scroll events to update scroll retention.
container.addEventListener('scroll', () => {
gridScrollPosition = container.scrollTop;
});
// For each image, wrap it in a cell container.
images.forEach(src => {
const cell = document.createElement('div');
// Fixed cell height: assume original vertical cell height was 375px; increased by 50% becomes 562.5px.
cell.style.height = '562.5px';
cell.style.overflow = 'hidden';
cell.style.display = 'flex';
cell.style.alignItems = 'center';
cell.style.justifyContent = 'center';
const img = document.createElement('img');
img.src = src;
// Ensure image fits inside the cell without cropping.
img.style.maxHeight = '100%';
img.style.maxWidth = '100%';
img.style.objectFit = 'contain';
img.style.cursor = 'pointer';
// On clicking an image, remove grid overlay and open custom carousel at that image.
img.addEventListener('click', () => {
container.remove();
const closeBtn = document.getElementById('gridOverlayCloseButton');
if (closeBtn) closeBtn.remove();
createCustomCarousel(images.indexOf(src));
});
cell.appendChild(img);
container.appendChild(cell);
});
// Reapply stored scroll position, if any.
setTimeout(() => {
container.scrollTop = gridScrollPosition;
}, 0);
document.body.appendChild(container);
// Add a persistent close button for grid overlay.
const closeBtn = document.createElement('button');
closeBtn.id = 'gridOverlayCloseButton';
closeBtn.textContent = 'Close Grid';
closeBtn.style.cssText =
'position: fixed; top: 20px; right: 20px; z-index: 10000; padding: 10px 20px; font-size: 16px; cursor: pointer;';
closeBtn.addEventListener('click', () => {
container.remove();
closeBtn.remove();
});
document.body.appendChild(closeBtn);
}
// Create a custom carousel overlay with zoom (based on mouse position) and drag.
function createCustomCarousel(startIndex) {
hideDefaultCarousel();
let currentIndex = startIndex;
const carouselContainer = document.createElement('div');
carouselContainer.id = 'customCarouselContainer';
Object.assign(carouselContainer.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
backgroundColor: 'black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '10000',
overflow: 'hidden'
});
const carouselImage = document.createElement('img');
carouselImage.id = 'customCarouselImage';
carouselImage.src = currentCarouselImages[currentIndex];
carouselImage.style.maxWidth = '90%';
carouselImage.style.maxHeight = '90%';
carouselImage.style.objectFit = 'contain';
carouselImage.draggable = false;
// Setup transform parameters for zoom and drag.
let scale = 1, translateX = 0, translateY = 0, startX, startY;
carouselImage.style.transformOrigin = '0 0';
carouselImage.style.transition = 'transform 0.1s ease-out';
// Mouse wheel zoom (centered on mouse pointer)
carouselImage.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = carouselImage.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = Math.max(1, scale + delta);
const ratio = newScale / scale;
// Adjust translation so that zoom centers on mouse pointer.
translateX = offsetX - ratio * (offsetX - translateX);
translateY = offsetY - ratio * (offsetY - translateY);
scale = newScale;
carouselImage.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
});
// Drag functionality: update translation on mouse move.
carouselImage.addEventListener('mousedown', (e) => {
e.preventDefault();
startX = e.clientX - translateX;
startY = e.clientY - translateY;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
translateX = e.clientX - startX;
translateY = e.clientY - startY;
carouselImage.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
carouselContainer.appendChild(carouselImage);
// Left arrow button for carousel navigation.
const leftButton = document.createElement('button');
leftButton.textContent = '<';
leftButton.style.cssText =
'position: absolute; left: 20px; top: 50%; transform: translateY(-50%); font-size: 30px; background: transparent; border: none; color: white; cursor: pointer;';
leftButton.addEventListener('click', () => {
// Reset zoom/drag on image change.
scale = 1; translateX = 0; translateY = 0;
currentIndex = (currentIndex - 1 + currentCarouselImages.length) % currentCarouselImages.length;
carouselImage.src = currentCarouselImages[currentIndex];
carouselImage.style.transform = `translate(0px, 0px) scale(1)`;
});
carouselContainer.appendChild(leftButton);
// Right arrow button for carousel navigation.
const rightButton = document.createElement('button');
rightButton.textContent = '>';
rightButton.style.cssText =
'position: absolute; right: 20px; top: 50%; transform: translateY(-50%); font-size: 30px; background: transparent; border: none; color: white; cursor: pointer;';
rightButton.addEventListener('click', () => {
scale = 1; translateX = 0; translateY = 0;
currentIndex = (currentIndex + 1) % currentCarouselImages.length;
carouselImage.src = currentCarouselImages[currentIndex];
carouselImage.style.transform = `translate(0px, 0px) scale(1)`;
});
carouselContainer.appendChild(rightButton);
// Close carousel button.
const closeCarouselBtn = document.createElement('button');
closeCarouselBtn.textContent = 'Close Carousel';
closeCarouselBtn.style.cssText =
'position: fixed; top: 20px; right: 20px; z-index: 11000; padding: 10px 20px; font-size: 16px; cursor: pointer;';
closeCarouselBtn.addEventListener('click', () => {
carouselContainer.remove();
closeCarouselBtn.remove();
// When closing carousel, reopen the grid overlay with scroll retention.
createOverlayContainer(currentCarouselImages);
});
document.body.appendChild(carouselContainer);
document.body.appendChild(closeCarouselBtn);
// Keyboard navigation for left/right arrows and escape.
function keyHandler(e) {
if (e.key === 'ArrowLeft') leftButton.click();
else if (e.key === 'ArrowRight') rightButton.click();
else if (e.key === 'Escape') closeCarouselBtn.click();
}
document.addEventListener('keydown', keyHandler);
}
// Toggle grid overlay: load carousel images then open grid overlay.
function toggleOverlay() {
hideDefaultCarousel();
loadAllCarouselImages(() => {
const images = extractCarouselImages();
currentCarouselImages = images.slice();
if (images.length > 0) {
createOverlayContainer(images);
} else {
alert('No carousel images found.');
}
});
}
// Launch grid overlay automatically – if no carousel image is active, simulate a click on first thumbnail.
function launchGridOverlay() {
let activeImg = document.querySelector('.image-gallery-slide.image-gallery-center img.image-gallery-image');
if (!activeImg) {
const firstThumb = document.querySelector('.PhotosetGallery-Image-BackgroundBox img');
if (firstThumb) {
firstThumb.click();
const checkActive = setInterval(() => {
activeImg = document.querySelector('.image-gallery-slide.image-gallery-center img.image-gallery-image');
if (activeImg) {
clearInterval(checkActive);
toggleOverlay();
}
}, 500);
} else {
alert('No carousel thumbnail found.');
}
} else {
toggleOverlay();
}
}
// Add a "Grid View" button next to the scene title.
function addGridButton() {
const targetSelector = 'h1.Title.PhotosetGallery-PhotosetTitle-Title';
function insertButton() {
const titleElement = document.querySelector(targetSelector);
if (titleElement && !document.getElementById('gridViewButton')) {
const gridButton = document.createElement('button');
gridButton.id = 'gridViewButton';
gridButton.textContent = 'Grid View';
// Style the button to be a bit larger and with a yellow background.
gridButton.style.cssText = `
margin-left: 20px;
padding: 8px 16px;
font-size: 16px;
cursor: pointer;
background-color: yellow;
border: 1px solid #999;
border-radius: 4px;
font-weight: bold;
vertical-align: middle;
`;
gridButton.addEventListener('click', launchGridOverlay);
titleElement.parentElement.insertBefore(gridButton, titleElement.nextSibling);
return true;
}
return false;
}
if (insertButton()) return;
const observer = new MutationObserver((mutations, obs) => {
if (insertButton()) {
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
const pollInterval = setInterval(() => {
if (insertButton()) {
clearInterval(pollInterval);
}
}, 500);
}
if (document.readyState !== 'loading') {
addGridButton();
} else {
document.addEventListener('DOMContentLoaded', addGridButton);
}
})();