Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);

})();