// ==UserScript==
// @name Reddit Search Preview Inline Interactive Gallery Carousel
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Interactive Inline Gallery Carousel with full-res Images for Reddit Search Gallery.
// @author UniverseDev
// @license GPL-3.0-or-later
// @icon https://www.reddit.com/favicon.ico
// @match *://*.reddit.com/search/*type=media*
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// ==/UserScript==
(() => {
'use strict';
if (window.__redditCarouselLoaded) return;
window.__redditCarouselLoaded = true;
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
let currentAnimationFrame = null;
const redditCarousel_animateTransition = (container, start, end, duration, callback) => {
const startTime = performance.now();
const step = now => {
const progress = Math.min((now - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
container.style.transform = `translateX(${start + (end - start) * ease}px)`;
if (progress < 1) {
currentAnimationFrame = requestAnimationFrame(step);
} else if (callback) {
callback();
}
};
currentAnimationFrame = requestAnimationFrame(step);
};
window.addEventListener('beforeunload', () => {
if (currentAnimationFrame) {
cancelAnimationFrame(currentAnimationFrame);
}
});
GM_addStyle(`
.reddit-carousel {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
margin-bottom: 10px;
border: 1px solid #ddd;
background-color: black;
cursor: default;
z-index: 1;
padding: 0;
}
.reddit-carousel-slide-container {
display: flex;
height: 100%;
transition: transform 125ms ease;
z-index: 1;
}
.reddit-carousel-slide {
flex: 0 0 100%;
width: 100%;
height: 100%;
text-align: center;
background-color: black;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.reddit-carousel-slide img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
margin: 0 auto;
z-index: 1;
}
.reddit-carousel-error {
color: red;
font-size: 14px;
padding: 20px;
z-index: 1;
}
.reddit-carousel-arrow {
position: absolute;
background: rgba(0,0,0,0.7);
border: none;
width: 30px;
height: 30px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
transition: background 0.2s ease;
color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
box-sizing: border-box;
margin: 0;
}
.reddit-carousel-arrow:hover {
background: rgba(0,0,0,0.85);
}
.reddit-carousel-arrow:active {
transform: scale(0.95);
}
.reddit-carousel-arrow.left {
left: 10px;
top: 50%;
transform: translateY(-50%);
}
.reddit-carousel-arrow.right {
right: 10px;
top: 50%;
transform: translateY(-50%) scaleX(-1);
}
.reddit-carousel-arrow svg {
width: 16px;
height: 16px;
fill: currentColor;
}
@media (max-width: 400px) {
.reddit-carousel-arrow {
width: 25px;
height: 25px;
}
}
`);
const redditCarousel_createCarousel = (items, mediaMeta, altText) => {
if (!items?.length) return null;
const carousel = document.createElement('div');
carousel.classList.add('reddit-carousel');
carousel.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
});
const slideContainer = document.createElement('div');
slideContainer.classList.add('reddit-carousel-slide-container');
carousel.appendChild(slideContainer);
items.forEach(item => {
const slide = document.createElement('div');
slide.classList.add('reddit-carousel-slide');
const img = document.createElement('img');
if (typeof item === "object") {
img.src = item.src;
if (item.srcset) img.srcset = item.srcset;
if (item.sizes) img.sizes = item.sizes;
} else {
img.src = item;
}
img.setAttribute("loading", "lazy");
img.onerror = function() {
slide.innerHTML = '<div class="reddit-carousel-error">Image failed to load</div>';
};
img.addEventListener('load', () => {
redditCarousel_recalcDimensions();
redditCarousel_updateArrowVisibility();
if (img.naturalWidth <= slide.clientWidth && img.naturalHeight <= slide.clientHeight) {
slide.style.justifyContent = 'flex-start';
img.style.margin = '0';
} else {
slide.style.justifyContent = 'center';
slide.style.alignItems = 'center';
}
});
slide.appendChild(img);
slideContainer.appendChild(slide);
});
const counterWrapper = document.createElement('div');
counterWrapper.innerHTML = `<div class="absolute inset-0 overflow-visible flex items-right justify-end">
<button rpl="" class="pointer-events-none m-xs leading-4 pl-2xs pr-2xs py-0 text-sm h-fit button-small px-[var(--rem10)] button-media items-center justify-center button inline-flex ">
<span class="flex items-center justify-center">
<span class="counter-text flex items-center gap-xs">1/${items.length}</span>
</span>
</button>
</div>`;
carousel.appendChild(counterWrapper);
let redditCarousel_currentIndex = 0, redditCarousel_currentOffset = 0;
const redditCarousel_recalcDimensions = () => {
const containerWidth = slideContainer.clientWidth;
redditCarousel_currentOffset = -redditCarousel_currentIndex * containerWidth;
slideContainer.style.transform = `translateX(${redditCarousel_currentOffset}px)`;
};
const redditCarousel_updateCounter = () => {
const newCounterText = `${redditCarousel_currentIndex + 1}/${items.length}`;
const counterText = carousel.querySelector('.counter-text');
if (counterText) counterText.textContent = newCounterText;
};
const redditCarousel_goToSlide = index => {
index = Math.max(0, Math.min(index, items.length - 1));
const containerWidth = slideContainer.clientWidth;
const startOffset = redditCarousel_currentOffset;
const endOffset = -index * containerWidth;
redditCarousel_animateTransition(slideContainer, startOffset, endOffset, 125, () => {
redditCarousel_currentOffset = endOffset;
redditCarousel_currentIndex = index;
redditCarousel_updateCounter();
redditCarousel_updateArrowVisibility();
});
};
let redditCarousel_leftArrow, redditCarousel_rightArrow;
if (items.length > 1) {
const redditCarousel_createArrow = dir => {
const btn = document.createElement('button');
btn.classList.add('reddit-carousel-arrow', dir);
btn.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
});
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 20 20");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M12.793 19.707l-9-9a1 1 0 0 1 0-1.414l9-9 1.414 1.414L5.914 10l8.293 8.293-1.414 1.414Z");
svg.appendChild(path);
btn.appendChild(svg);
return btn;
};
redditCarousel_leftArrow = redditCarousel_createArrow('left');
redditCarousel_leftArrow.addEventListener('click', () => {
redditCarousel_goToSlide(redditCarousel_currentIndex - 1);
});
carousel.appendChild(redditCarousel_leftArrow);
redditCarousel_rightArrow = redditCarousel_createArrow('right');
redditCarousel_rightArrow.addEventListener('click', () => {
redditCarousel_goToSlide(redditCarousel_currentIndex + 1);
});
carousel.appendChild(redditCarousel_rightArrow);
}
const redditCarousel_updateArrowVisibility = () => {
if (items.length <= 1) return;
if (redditCarousel_currentIndex === 0) {
redditCarousel_leftArrow.style.display = 'none';
redditCarousel_rightArrow.style.display = 'flex';
} else if (redditCarousel_currentIndex === items.length - 1) {
redditCarousel_leftArrow.style.display = 'flex';
redditCarousel_rightArrow.style.display = 'none';
} else {
redditCarousel_leftArrow.style.display = 'flex';
redditCarousel_rightArrow.style.display = 'flex';
}
};
window.addEventListener('resize', debounce(() => {
redditCarousel_recalcDimensions();
redditCarousel_updateArrowVisibility();
}, 100));
redditCarousel_updateArrowVisibility();
return carousel;
};
const redditCarousel_fetchAndProcessGallery = (postURL, container) => {
GM.xmlHttpRequest({
url: `${postURL}.json`,
method: 'GET',
onload: response => {
if (response.status >= 200 && response.status < 300) {
try {
const jsonData = JSON.parse(response.responseText);
const postData = jsonData[0]?.data?.children[0]?.data;
if (!postData) return;
const altText = postData.title || "";
let fullResItems = [];
let previewItems = [];
let mediaMeta = {};
if (postData?.gallery_data && postData?.media_metadata) {
const { items } = postData.gallery_data;
mediaMeta = postData.media_metadata;
fullResItems = items.reduce((acc, item) => {
const meta = mediaMeta[item.media_id];
if (meta && meta.id && meta.m) {
let ext = "jpg";
if (meta.m.includes("png")) ext = "png";
else if (meta.m.includes("webp")) ext = "webp";
acc.push({ src: `https://i.redd.it/${meta.id}.${ext}` });
}
return acc;
}, []);
}
if (fullResItems.length === 0) {
console.log("Full-res extraction failed, falling back to previews...");
if (postData.preview?.images?.length > 0) {
previewItems = postData.preview.images.map(image => {
let sourceUrl = image.source?.url;
if (!sourceUrl) return null;
sourceUrl = sourceUrl.replace(/&/g, '&');
let srcset = image.resolutions?.map(res => {
let url = res.url.replace(/&/g, '&');
return `${url} ${res.width}w`;
}).join(', ');
return {
src: sourceUrl,
srcset,
sizes: "(min-width: 1415px) 750px, (min-width: 768px) 50vw, 100vw"
};
}).filter(x => x);
} else if (postData.thumbnail && postData.thumbnail.startsWith('http')) {
previewItems = [{ src: postData.thumbnail }];
}
}
const itemsToUse = fullResItems.length > 0 ? fullResItems : previewItems;
const carousel = redditCarousel_createCarousel(itemsToUse, mediaMeta, altText);
if (carousel) {
container.innerHTML = '';
container.appendChild(carousel);
const counterSpan = container.querySelector('div.absolute.inset-0.overflow-visible.flex.items-right.justify-end button span');
if (counterSpan) {
counterSpan.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
});
}
}
} catch (error) {
console.error("JSON parse error:", error);
}
}
},
onerror: err => console.error("Request failed:", err)
});
};
const redditCarousel_galleryObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const container = entry.target;
container.setAttribute('data-gallery-intersected', 'true');
const postUnit = container.closest('div[data-id="search-media-post-unit"]');
const postLink = postUnit?.querySelector('a.no-underline');
if (postLink?.href) {
redditCarousel_fetchAndProcessGallery(postLink.href, container);
}
observer.unobserve(container);
}
});
}, { threshold: 0.1 });
const redditCarousel_processSearchResults = () => {
document.querySelectorAll('div[data-id="search-media-post-unit"]').forEach(post => {
if (post.hasAttribute('data-gallery-checked')) return;
post.setAttribute('data-gallery-checked', 'true');
const indicator = post.querySelector('div.absolute.inset-0.overflow-visible.flex.items-right.justify-end button span');
if (indicator?.textContent.includes('/')) {
const container = post.querySelector('shreddit-aspect-ratio');
if (container) {
redditCarousel_galleryObserver.observe(container);
}
}
});
};
let redditCarousel_ticking = false;
window.addEventListener('scroll', () => {
if (!redditCarousel_ticking) {
requestAnimationFrame(() => {
redditCarousel_processSearchResults();
redditCarousel_ticking = false;
});
redditCarousel_ticking = true;
}
});
window.addEventListener('load', redditCarousel_processSearchResults);
})();