Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Carousel Image Preloader
// @namespace    Q2Fyb3VzZWwgSW1hZ2UgUHJlbG9hZGVy
// @version      1.0
// @description  Intelligently preload images in carousels for faster navigation
// @author       smed79
// @license      GPLv3
// @icon         https://i25.servimg.com/u/f25/11/94/21/24/imgloa10.png
// @match        https://*/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  const DEBUG = false; // Set to true to enable console logs
  const PRELOAD_BUFFER = 3; // Number of images to preload ahead/behind
  const PRELOAD_DELAY = 500; // ms delay before preloading adjacent images

  function log(msg) {
    if (DEBUG) console.log('[Carousel Preloader]', msg);
  }

  // Detect if element is likely part of a carousel
  function isCarouselContainer(el) {
    const classes = el.className.toLowerCase();
    const id = el.id.toLowerCase();
    
    // Common carousel indicators
    const carouselPatterns = [
      'carousel', 'slider', 'swiper', 'glide', 'slick',
      'splide', 'owl', 'gallery', 'slideshow', 'lightbox'
    ];
    
    return carouselPatterns.some(pattern => 
      classes.includes(pattern) || id.includes(pattern)
    );
  }

  // Find all images in a container
  function getCarouselImages(container) {
    const images = Array.from(container.querySelectorAll('img'));
    const bgImages = Array.from(container.querySelectorAll('[style*="background-image"]'));
    
    return [...images, ...bgImages].filter(el => {
      // Filter out tiny images (likely icons, buttons)
      if (el.tagName === 'IMG') {
        return el.naturalWidth === 0 || el.naturalWidth > 50;
      }
      return true;
    });
  }

  // Get the currently visible image index
  function getVisibleImageIndex(container, images) {
    for (let i = 0; i < images.length; i++) {
      const img = images[i];
      const style = window.getComputedStyle(img);
      
      // Check various visibility conditions
      if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
        // For parent containers with transform/positioning
        const parent = img.parentElement;
        const parentStyle = window.getComputedStyle(parent);
        if (parentStyle.display !== 'none') {
          return i;
        }
      }
    }
    return 0;
  }

  // Preload image by creating new Image object
  function preloadImage(img) {
    if (img.tagName === 'IMG') {
      const src = img.src || img.dataset.src;
      if (src && !src.includes('data:')) {
        const newImg = new Image();
        newImg.src = src;
        log(`Preloading image: ${src}`);
      }
    } else if (img.style.backgroundImage) {
      const bgImage = img.style.backgroundImage.match(/url\(['"]?([^'")]+)['"]?\)/);
      if (bgImage && bgImage[1]) {
        const newImg = new Image();
        newImg.src = bgImage[1];
        log(`Preloading background image: ${bgImage[1]}`);
      }
    }
  }

  // Preload adjacent images in carousel
  function preloadAdjacentImages(container, images, currentIndex) {
    const start = Math.max(0, currentIndex - PRELOAD_BUFFER);
    const end = Math.min(images.length, currentIndex + PRELOAD_BUFFER + 1);
    
    for (let i = start; i < end; i++) {
      preloadImage(images[i]);
    }
    
    log(`Preloaded images ${start} to ${end-1} (current: ${currentIndex})`);
  }

  // Monitor carousel for navigation and preload accordingly
  function monitorCarousel(container) {
    const images = getCarouselImages(container);
    
    if (images.length < 2) return; // Not a real carousel
    
    log(`Found carousel with ${images.length} images`);
    
    let lastIndex = getVisibleImageIndex(container, images);
    preloadAdjacentImages(container, images, lastIndex);
    
    // Listen for clicks on navigation buttons
    const navButtons = container.querySelectorAll('[class*="arrow"], [class*="next"], [class*="prev"], [class*="button"]');
    navButtons.forEach(btn => {
      btn.addEventListener('click', () => {
        setTimeout(() => {
          const currentIndex = getVisibleImageIndex(container, images);
          if (currentIndex !== lastIndex) {
            log(`Navigation detected: ${lastIndex} -> ${currentIndex}`);
            lastIndex = currentIndex;
            preloadAdjacentImages(container, images, currentIndex);
          }
        }, PRELOAD_DELAY);
      });
    });
    
    // Monitor for swipe/touch events (for mobile)
    let touchStartX = 0;
    container.addEventListener('touchstart', (e) => {
      touchStartX = e.touches[0].clientX;
    }, false);
    
    container.addEventListener('touchend', (e) => {
      const touchEndX = e.changedTouches[0].clientX;
      if (Math.abs(touchEndX - touchStartX) > 50) { // Minimum swipe distance
        setTimeout(() => {
          const currentIndex = getVisibleImageIndex(container, images);
          if (currentIndex !== lastIndex) {
            log(`Swipe detected: ${lastIndex} -> ${currentIndex}`);
            lastIndex = currentIndex;
            preloadAdjacentImages(container, images, currentIndex);
          }
        }, PRELOAD_DELAY);
      }
    }, false);
    
    // Monitor keyboard navigation (arrow keys)
    const handleKeydown = (e) => {
      if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
        setTimeout(() => {
          const currentIndex = getVisibleImageIndex(container, images);
          if (currentIndex !== lastIndex) {
            log(`Keyboard navigation: ${lastIndex} -> ${currentIndex}`);
            lastIndex = currentIndex;
            preloadAdjacentImages(container, images, currentIndex);
          }
        }, PRELOAD_DELAY);
      }
    };
    
    document.addEventListener('keydown', handleKeydown);
    
    // Monitor DOM mutations for dynamically revealed images
    const observer = new MutationObserver(() => {
      const currentIndex = getVisibleImageIndex(container, images);
      if (currentIndex !== lastIndex) {
        lastIndex = currentIndex;
        preloadAdjacentImages(container, images, currentIndex);
      }
    });
    
    observer.observe(container, {
      attributes: true,
      attributeFilter: ['style', 'class'],
      subtree: true
    });
  }

  // Scan page for carousels
  function scanForCarousels() {
    const allContainers = document.querySelectorAll('div, section, article, figure');
    const carousels = [];
    
    allContainers.forEach(container => {
      if (isCarouselContainer(container)) {
        // Check if it's not a child of another carousel
        const isChild = Array.from(carousels).some(carousel => 
          carousel.contains(container)
        );
        
        if (!isChild && getCarouselImages(container).length >= 2) {
          carousels.push(container);
          monitorCarousel(container);
        }
      }
    });
    
    log(`Detected and monitoring ${carousels.length} carousels`);
  }

  // Run on page load
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', scanForCarousels);
  } else {
    scanForCarousels();
  }
  
  // Also scan after a delay to catch dynamically loaded content
  setTimeout(scanForCarousels, 2000);
  
  // Rescan periodically for lazy-loaded pages
  setInterval(scanForCarousels, 5000);

})();