您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Interactive Inline Gallery Carousel with full-res Images for Reddit Search Gallery.
当前为
// ==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); })();