Reddit Search Preview Inline Interactive Gallery Carousel

Interactive Inline Gallery Carousel with full-res Images for Reddit Search Gallery.

  1. // ==UserScript==
  2. // @name Reddit Search Preview Inline Interactive Gallery Carousel
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description Interactive Inline Gallery Carousel with full-res Images for Reddit Search Gallery.
  6. // @author UniverseDev
  7. // @license GPL-3.0-or-later
  8. // @icon https://www.reddit.com/favicon.ico
  9. // @match *://*.reddit.com/search/*type=media*
  10. // @grant GM_addStyle
  11. // @grant GM.xmlHttpRequest
  12. // ==/UserScript==
  13.  
  14. (() => {
  15. 'use strict';
  16.  
  17. GM_addStyle(`
  18. .reddit-carousel {
  19. position: relative;
  20. overflow: hidden;
  21. width: 100%;
  22. height: 100%;
  23. cursor: default;
  24. z-index: 1;
  25. }
  26. .reddit-carousel-slide-container {
  27. display: flex;
  28. transition: transform 150ms ease;
  29. will-change: transform;
  30. z-index: 1;
  31. }
  32. .reddit-carousel-slide {
  33. flex: 0 0 100%;
  34. width: 100%;
  35. text-align: center;
  36. display: flex;
  37. justify-content: center;
  38. align-items: center;
  39. position: relative;
  40. }
  41. .reddit-carousel-slide img {
  42. max-height: 100vw;
  43. height: 100%;
  44. width: 100%;
  45. object-fit: contain;
  46. object-position: center center;
  47. margin: 0 auto;
  48. z-index: 1;
  49. }
  50. .reddit-carousel-loading-overlay {
  51. position: absolute;
  52. top: 0;
  53. left: 0;
  54. width: 100%;
  55. height: 100%;
  56. background-color: #000;
  57. z-index: 10;
  58. }
  59. .reddit-carousel-loading-global {
  60. position: absolute;
  61. top: 0;
  62. left: 0;
  63. width: 100%;
  64. height: 100%;
  65. background-color: #000;
  66. z-index: 11;
  67. }
  68. .reddit-carousel-error {
  69. color: red;
  70. font-size: 14px;
  71. padding: 20px;
  72. z-index: 1;
  73. }
  74. .reddit-carousel-arrow {
  75. position: absolute;
  76. top: 50%;
  77. transform: translateY(-50%);
  78. background: rgba(0,0,0,0.7);
  79. border: none;
  80. width: 30px;
  81. height: 30px;
  82. cursor: pointer;
  83. border-radius: 50%;
  84. display: flex;
  85. align-items: center;
  86. justify-content: center;
  87. z-index: 4;
  88. transition: background 0.2s ease, transform 0.15s ease;
  89. color: #fff;
  90. box-shadow: 0 2px 4px rgba(0,0,0,0.4);
  91. pointer-events: auto;
  92. }
  93. .reddit-carousel-arrow:hover {
  94. background: rgba(0,0,0,0.85);
  95. transform: translateY(-50%) scale(1.1);
  96. box-shadow: 0 4px 8px rgba(0,0,0,0.5);
  97. }
  98. .reddit-carousel-arrow:active {
  99. transform: translateY(-50%) scale(0.95);
  100. }
  101. .reddit-carousel-arrow svg {
  102. width: 16px;
  103. height: 16px;
  104. fill: currentColor;
  105. }
  106. .reddit-carousel-arrow.left { left: 10px; display: none; }
  107. .reddit-carousel-arrow.right { right: 10px; display: flex; }
  108. `);
  109.  
  110. const redditCarousel_animateTransition = (container, start, end, duration, callback) => {
  111. const startTime = performance.now();
  112. const step = now => {
  113. const progress = Math.min((now - startTime) / duration, 1);
  114. const ease = 1 - Math.pow(1 - progress, 3);
  115. container.style.transform = `translate3d(${start + (end - start) * ease}px, 0, 0)`;
  116. if (progress < 1) requestAnimationFrame(step);
  117. else if (callback) callback();
  118. };
  119. requestAnimationFrame(step);
  120. };
  121.  
  122. function adjustSlideHeight(slide) {
  123. const img = slide.querySelector('img');
  124. if (!img) return;
  125. if (!img.complete || img.naturalWidth === 0) {
  126. img.addEventListener('load', () => adjustSlideHeight(slide));
  127. return;
  128. }
  129. const aspectRatio = img.naturalWidth / img.naturalHeight;
  130. const slideWidth = slide.clientWidth;
  131. slide.style.height = (slideWidth / aspectRatio) + 'px';
  132. }
  133.  
  134. function adjustAllSlideHeights() {
  135. document.querySelectorAll('.reddit-carousel-slide').forEach(slide => adjustSlideHeight(slide));
  136. centerImages();
  137. }
  138.  
  139. function centerImages() {
  140. document.querySelectorAll('.reddit-carousel-slide').forEach(slide => {
  141. slide.style.display = 'flex';
  142. slide.style.justifyContent = 'center';
  143. slide.style.alignItems = 'center';
  144. });
  145. }
  146.  
  147. const redditCarousel_createCarousel = (primaryUrls, fallbackUrls, mediaMeta, altText) => {
  148. if (!primaryUrls?.length) return null;
  149. const carousel = document.createElement('div');
  150. carousel.classList.add('reddit-carousel');
  151. carousel.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); });
  152. const slideContainer = document.createElement('div');
  153. slideContainer.classList.add('reddit-carousel-slide-container');
  154. carousel.appendChild(slideContainer);
  155. let imagesLoaded = 0;
  156. const totalImages = primaryUrls.length;
  157. const globalOverlay = document.createElement('div');
  158. globalOverlay.classList.add('reddit-carousel-loading-global');
  159. carousel.appendChild(globalOverlay);
  160. primaryUrls.forEach((url, i) => {
  161. const slide = document.createElement('div');
  162. slide.classList.add('reddit-carousel-slide');
  163. const overlay = document.createElement('div');
  164. overlay.classList.add('reddit-carousel-loading-overlay');
  165. slide.appendChild(overlay);
  166. const img = document.createElement('img');
  167. img.src = url;
  168. img.alt = altText;
  169. if (fallbackUrls && fallbackUrls[i]) img.dataset.fallback = fallbackUrls[i];
  170. img.onerror = function() {
  171. if (this.dataset.fallback && this.src !== this.dataset.fallback) this.src = this.dataset.fallback;
  172. else slide.innerHTML = '<div class="reddit-carousel-error">Image failed to load</div>';
  173. };
  174. img.addEventListener('load', () => {
  175. redditCarousel_recalcDimensions();
  176. redditCarousel_updateArrowVisibility();
  177. updateArrowPositions();
  178. adjustSlideHeight(slide);
  179. const ov = slide.querySelector('.reddit-carousel-loading-overlay');
  180. if (ov) ov.remove();
  181. imagesLoaded++;
  182. if (imagesLoaded === totalImages) {
  183. globalOverlay.remove();
  184. }
  185. });
  186. slide.appendChild(img);
  187. slideContainer.appendChild(slide);
  188. });
  189. const carouselCounterWrapper = document.createElement('div');
  190. carouselCounterWrapper.innerHTML = `<div class="absolute inset-0 overflow-visible flex items-right justify-end">
  191. <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 ">
  192. <span class="flex items-center justify-center">
  193. <span class="carousel-counter flex items-center gap-xs">1/${primaryUrls.length}</span>
  194. </span>
  195. </button>
  196. </div>`;
  197. carousel.appendChild(carouselCounterWrapper);
  198. let redditCarousel_currentIndex = 0, redditCarousel_currentOffset = 0;
  199. const redditCarousel_recalcDimensions = () => {
  200. const containerWidth = slideContainer.clientWidth;
  201. redditCarousel_currentOffset = -redditCarousel_currentIndex * containerWidth;
  202. slideContainer.style.transform = `translate3d(${redditCarousel_currentOffset}px, 0, 0)`;
  203. };
  204. const redditCarousel_updateCounter = () => {
  205. const newCounterText = `${redditCarousel_currentIndex + 1}/${primaryUrls.length}`;
  206. const carouselCounter = carousel.querySelector('.carousel-counter');
  207. if (carouselCounter) carouselCounter.textContent = newCounterText;
  208. };
  209. const redditCarousel_goToSlide = index => {
  210. index = Math.max(0, Math.min(index, primaryUrls.length - 1));
  211. const containerWidth = slideContainer.clientWidth;
  212. const startOffset = redditCarousel_currentOffset;
  213. const endOffset = -index * containerWidth;
  214. redditCarousel_animateTransition(slideContainer, startOffset, endOffset, 150, () => {
  215. redditCarousel_currentOffset = endOffset;
  216. redditCarousel_currentIndex = index;
  217. redditCarousel_updateCounter();
  218. redditCarousel_updateArrowVisibility();
  219. updateArrowPositions();
  220. });
  221. };
  222. let redditCarousel_leftArrow, redditCarousel_rightArrow;
  223. if (primaryUrls.length > 1) {
  224. const redditCarousel_createArrow = dir => {
  225. const btn = document.createElement('button');
  226. btn.classList.add('reddit-carousel-arrow', dir);
  227. btn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); });
  228. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  229. svg.setAttribute("viewBox", "0 0 20 20");
  230. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  231. 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");
  232. svg.appendChild(path);
  233. if (dir === 'right') svg.style.transform = 'scaleX(-1)';
  234. btn.appendChild(svg);
  235. return btn;
  236. };
  237. redditCarousel_leftArrow = redditCarousel_createArrow('left');
  238. redditCarousel_leftArrow.addEventListener('click', () => { redditCarousel_goToSlide(redditCarousel_currentIndex - 1); });
  239. carousel.appendChild(redditCarousel_leftArrow);
  240. redditCarousel_rightArrow = redditCarousel_createArrow('right');
  241. redditCarousel_rightArrow.addEventListener('click', () => { redditCarousel_goToSlide(redditCarousel_currentIndex + 1); });
  242. carousel.appendChild(redditCarousel_rightArrow);
  243. }
  244. const redditCarousel_updateArrowVisibility = () => {
  245. if (primaryUrls.length <= 1) return;
  246. if (redditCarousel_currentIndex === 0) {
  247. redditCarousel_leftArrow.style.display = 'none';
  248. redditCarousel_rightArrow.style.display = 'flex';
  249. } else if (redditCarousel_currentIndex === primaryUrls.length - 1) {
  250. redditCarousel_leftArrow.style.display = 'flex';
  251. redditCarousel_rightArrow.style.display = 'none';
  252. } else {
  253. redditCarousel_leftArrow.style.display = 'flex';
  254. redditCarousel_rightArrow.style.display = 'flex';
  255. }
  256. };
  257. const updateArrowPositions = () => {
  258. requestAnimationFrame(() => {
  259. const rect = carousel.getBoundingClientRect();
  260. const arrowTop = (rect.height - 30) / 2;
  261. if (redditCarousel_leftArrow) redditCarousel_leftArrow.style.top = arrowTop + 'px';
  262. if (redditCarousel_rightArrow) redditCarousel_rightArrow.style.top = arrowTop + 'px';
  263. });
  264. };
  265. updateArrowPositions();
  266. let redditCarousel_resizeTimeout;
  267. window.addEventListener('resize', () => {
  268. clearTimeout(redditCarousel_resizeTimeout);
  269. redditCarousel_resizeTimeout = setTimeout(() => {
  270. redditCarousel_recalcDimensions();
  271. redditCarousel_updateArrowVisibility();
  272. updateArrowPositions();
  273. adjustAllSlideHeights();
  274. }, 100);
  275. });
  276. redditCarousel_updateArrowVisibility();
  277. return carousel;
  278. };
  279.  
  280. const redditCarousel_fetchAndProcessGallery = (postURL, container) => {
  281. GM.xmlHttpRequest({
  282. url: `${postURL}.json`,
  283. method: 'GET',
  284. onload: response => {
  285. if (response.status >= 200 && response.status < 300) {
  286. try {
  287. const jsonData = JSON.parse(response.responseText);
  288. const postData = jsonData[0]?.data?.children[0]?.data;
  289. if (!postData) return;
  290. const altText = postData.title || "Reddit Gallery Image";
  291. let fullResUrls = [];
  292. let previewUrls = [];
  293. let mediaMeta = {};
  294. if (postData.gallery_data && postData.media_metadata) {
  295. mediaMeta = postData.media_metadata;
  296. const { items } = postData.gallery_data;
  297. fullResUrls = items.reduce((acc, item) => {
  298. const meta = mediaMeta[item.media_id];
  299. if (meta && meta.id && meta.m) {
  300. let ext = "jpg";
  301. if (meta.m.includes("png")) ext = "png";
  302. else if (meta.m.includes("webp")) ext = "webp";
  303. acc.push(`https://i.redd.it/${meta.id}.${ext}`);
  304. }
  305. return acc;
  306. }, []);
  307. previewUrls = items.reduce((acc, item) => {
  308. const meta = mediaMeta[item.media_id];
  309. let url;
  310. if (meta && meta.p) {
  311. if (Array.isArray(meta.p) && meta.p.length > 0) {
  312. url = meta.p[meta.p.length - 1].u;
  313. } else if (meta.p.u) {
  314. url = meta.p.u;
  315. }
  316. if (url) {
  317. url = url.replace(/&amp;/g, '&');
  318. acc.push(url);
  319. }
  320. }
  321. return acc;
  322. }, []);
  323. }
  324. let primaryUrls = [];
  325. let fallbackUrls = [];
  326. if (fullResUrls.length > 0) {
  327. primaryUrls = fullResUrls;
  328. fallbackUrls = previewUrls;
  329. } else {
  330. primaryUrls = previewUrls;
  331. }
  332. const carousel = redditCarousel_createCarousel(primaryUrls, fallbackUrls, mediaMeta, altText);
  333. if (carousel) {
  334. const targetElement = container.querySelector('.relative.w-full.h-full');
  335. if (targetElement) {
  336. targetElement.innerHTML = '';
  337. targetElement.appendChild(carousel);
  338. const parentLink = targetElement.closest('a');
  339. if (parentLink) {
  340. parentLink.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); });
  341. }
  342. }
  343. }
  344. } catch (error) {
  345. console.error("JSON parse error:", error);
  346. }
  347. }
  348. },
  349. onerror: err => console.error("Request failed:", err)
  350. });
  351. };
  352.  
  353. const redditCarousel_galleryObserver = new IntersectionObserver((entries, observer) => {
  354. entries.forEach(entry => {
  355. if (entry.isIntersecting) {
  356. const container = entry.target;
  357. if (!container.hasAttribute('data-gallery-intersected')) {
  358. container.setAttribute('data-gallery-intersected', 'true');
  359. const postUnit = container.closest('div[data-id="search-media-post-unit"]');
  360. const postLink = postUnit?.querySelector('a.no-underline');
  361. const target = container || postUnit;
  362. if (postLink?.href && target) redditCarousel_fetchAndProcessGallery(postLink.href, target);
  363. }
  364. observer.unobserve(container);
  365. }
  366. });
  367. }, { threshold: 0.1 });
  368.  
  369. const redditCarousel_processSearchResults = () => {
  370. document.querySelectorAll('div[data-id="search-media-post-unit"]').forEach(post => {
  371. if (post.hasAttribute('data-gallery-checked')) return;
  372. post.setAttribute('data-gallery-checked', 'true');
  373. const indicator = post.querySelector('div.absolute.inset-0.overflow-visible.flex.items-right.justify-end button span');
  374. if (indicator?.textContent.includes('/')) {
  375. const container = post.querySelector('shreddit-aspect-ratio') || post;
  376. if (container) redditCarousel_galleryObserver.observe(container);
  377. }
  378. });
  379. };
  380.  
  381. let redditCarousel_ticking = false;
  382. window.addEventListener('scroll', () => {
  383. if (!redditCarousel_ticking) {
  384. requestAnimationFrame(() => {
  385. redditCarousel_processSearchResults();
  386. redditCarousel_ticking = false;
  387. });
  388. redditCarousel_ticking = true;
  389. }
  390. });
  391. window.addEventListener('load', redditCarousel_processSearchResults);
  392. window.addEventListener('load', adjustAllSlideHeights);
  393. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址