Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);

})();