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

})();