PostImg Gallery Viewer

Добавляет удобный просмотр изображений с перелистыванием и предзагрузкой для PostImg галерей

// ==UserScript==
// @name         PostImg Gallery Viewer
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Добавляет удобный просмотр изображений с перелистыванием и предзагрузкой для PostImg галерей
// @author       NastyaLove
// @license      MIT
// @match        https://postimg.cc/gallery/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=postimg.cc
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Собираем все изображения из галереи
    const images = [];
    document.querySelectorAll('#thumb-list > .thumb-container').forEach(el => {
        const imageKey = el.dataset.image;
        const hotlink = el.dataset.hotlink;
        const name = el.dataset.name;
        const ext = el.dataset.ext;

        const thumbnailUrl = `https://i.postimg.cc/${imageKey}/${name}.${ext}`;
        const fullUrl = `https://i.postimg.cc/${hotlink}/${name}.${ext}`;

        images.push({
            thumbnail: thumbnailUrl,
            full: fullUrl,
            name: `${name}.${ext}`,
            key: imageKey,
            hotlink: hotlink,
            loaded: false,
            preloadedImage: null
        });
    });

    if (images.length === 0) return;

    let currentIndex = 0;
    const PRELOAD_COUNT = 3; // Количество изображений для предзагрузки вперед и назад

    // Создаем стили
    const style = document.createElement('style');
    style.textContent = `
        .gallery-viewer {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.95);
            z-index: 9999;
            display: none;
            flex-direction: column;
        }

        .gallery-viewer.active {
            display: flex;
        }

        .gallery-header {
            padding: 15px 20px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }

        .gallery-info {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }

        .gallery-counter {
            font-size: 16px;
            font-weight: bold;
        }

        .gallery-title {
            font-size: 14px;
            color: #ccc;
            max-width: 600px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .gallery-preload-status {
            font-size: 12px;
            color: #95a5a6;
        }

        .gallery-close {
            background: #e74c3c;
            border: none;
            color: white;
            padding: 8px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
            transition: background 0.2s;
        }

        .gallery-close:hover {
            background: #c0392b;
        }

        .gallery-main {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            overflow: hidden;
            padding: 20px;
        }

        .gallery-image {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
            user-select: none;
            opacity: 0;
            transition: opacity 0.2s;
        }

        .gallery-image.loaded {
            opacity: 1;
        }

        .gallery-nav {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            background: rgba(0, 0, 0, 0.7);
            border: none;
            color: white;
            padding: 20px 15px;
            cursor: pointer;
            font-size: 24px;
            border-radius: 4px;
            transition: background 0.2s;
            z-index: 10;
        }

        .gallery-nav:hover {
            background: rgba(0, 0, 0, 0.9);
        }

        .gallery-nav:disabled {
            opacity: 0.3;
            cursor: not-allowed;
        }

        .gallery-nav-prev {
            left: 20px;
        }

        .gallery-nav-next {
            right: 20px;
        }

        .gallery-thumbnails {
            background: rgba(0, 0, 0, 0.8);
            padding: 15px;
            border-top: 1px solid rgba(255, 255, 255, 0.1);
            overflow-x: auto;
            overflow-y: hidden;
            white-space: nowrap;
            max-height: 150px;
        }

        .gallery-thumbnails::-webkit-scrollbar {
            height: 8px;
        }

        .gallery-thumbnails::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.1);
        }

        .gallery-thumbnails::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.3);
            border-radius: 4px;
        }

        .gallery-thumb {
            display: inline-block;
            width: 100px;
            height: 100px;
            margin-right: 10px;
            cursor: pointer;
            border: 3px solid transparent;
            border-radius: 4px;
            overflow: hidden;
            transition: border-color 0.2s;
            position: relative;
        }

        .gallery-thumb:hover {
            border-color: rgba(255, 255, 255, 0.5);
        }

        .gallery-thumb.active {
            border-color: #3498db;
        }

        .gallery-thumb.preloaded::after {
            content: '✓';
            position: absolute;
            top: 2px;
            right: 2px;
            background: #27ae60;
            color: white;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 10px;
            font-weight: bold;
        }

        .gallery-thumb img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        .gallery-loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 10px;
        }

        .gallery-spinner {
            width: 40px;
            height: 40px;
            border: 4px solid rgba(255, 255, 255, 0.3);
            border-top-color: white;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .open-gallery-btn {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: #3498db;
            color: white;
            border: none;
            padding: 15px 30px;
            border-radius: 50px;
            cursor: pointer;
            font-size: 16px;
            font-weight: bold;
            box-shadow: 0 4px 15px rgba(52, 152, 219, 0.4);
            transition: all 0.3s;
            z-index: 1000;
        }

        .open-gallery-btn:hover {
            background: #2980b9;
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(52, 152, 219, 0.6);
        }
    `;
    document.head.appendChild(style);

    // Создаем HTML структуру галереи
    const viewer = document.createElement('div');
    viewer.className = 'gallery-viewer';
    viewer.innerHTML = `
        <div class="gallery-header">
            <div class="gallery-info">
                <div class="gallery-counter">
                    <span class="current">1</span> / <span class="total">${images.length}</span>
                </div>
                <div class="gallery-title"></div>
                <div class="gallery-preload-status"></div>
            </div>
            <button class="gallery-close">✕ Закрыть</button>
        </div>
        <div class="gallery-main">
            <button class="gallery-nav gallery-nav-prev">‹</button>
            <img class="gallery-image" src="" alt="">
            <div class="gallery-loading">
                <div class="gallery-spinner"></div>
                <div>Загрузка...</div>
            </div>
            <button class="gallery-nav gallery-nav-next">›</button>
        </div>
        <div class="gallery-thumbnails"></div>
    `;
    document.body.appendChild(viewer);

    // Кнопка открытия галереи
    const openBtn = document.createElement('button');
    openBtn.className = 'open-gallery-btn';
    openBtn.textContent = `📷 Просмотр (${images.length})`;
    document.body.appendChild(openBtn);

    // Элементы
    const mainImage = viewer.querySelector('.gallery-image');
    const loading = viewer.querySelector('.gallery-loading');
    const counterCurrent = viewer.querySelector('.current');
    const counterTotal = viewer.querySelector('.total');
    const titleEl = viewer.querySelector('.gallery-title');
    const preloadStatus = viewer.querySelector('.gallery-preload-status');
    const prevBtn = viewer.querySelector('.gallery-nav-prev');
    const nextBtn = viewer.querySelector('.gallery-nav-next');
    const closeBtn = viewer.querySelector('.gallery-close');
    const thumbnailsContainer = viewer.querySelector('.gallery-thumbnails');

    // Создаем миниатюры
    const thumbElements = [];
    images.forEach((img, index) => {
        const thumb = document.createElement('div');
        thumb.className = 'gallery-thumb';
        thumb.innerHTML = `<img src="${img.thumbnail}" alt="${img.name}">`;
        thumb.addEventListener('click', () => showImage(index));
        thumbnailsContainer.appendChild(thumb);
        thumbElements.push(thumb);
    });

    // Функция предзагрузки изображения
    function preloadImage(index) {
        if (index < 0 || index >= images.length) return Promise.resolve();
        if (images[index].loaded) return Promise.resolve();

        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                images[index].loaded = true;
                images[index].preloadedImage = img;
                thumbElements[index].classList.add('preloaded');
                updatePreloadStatus();
                resolve();
            };
            img.onerror = () => {
                console.warn(`Ошибка предзагрузки изображения ${index}`);
                reject();
            };
            img.src = images[index].full;
        });
    }

    // Предзагрузка окружающих изображений
    function preloadSurroundingImages(centerIndex) {
        const promises = [];

        // Предзагружаем текущее изображение с наивысшим приоритетом
        if (!images[centerIndex].loaded) {
            promises.push(preloadImage(centerIndex));
        }

        // Предзагружаем следующие изображения
        for (let i = 1; i <= PRELOAD_COUNT; i++) {
            const nextIndex = centerIndex + i;
            if (nextIndex < images.length && !images[nextIndex].loaded) {
                promises.push(preloadImage(nextIndex));
            }
        }

        // Предзагружаем предыдущие изображения
        for (let i = 1; i <= PRELOAD_COUNT; i++) {
            const prevIndex = centerIndex - i;
            if (prevIndex >= 0 && !images[prevIndex].loaded) {
                promises.push(preloadImage(prevIndex));
            }
        }

        return Promise.all(promises);
    }

    // Обновление статуса предзагрузки
    function updatePreloadStatus() {
        const loadedCount = images.filter(img => img.loaded).length;
        preloadStatus.textContent = `Предзагружено: ${loadedCount}/${images.length}`;
    }

    // Показать изображение
    function showImage(index) {
        if (index < 0 || index >= images.length) return;

        currentIndex = index;
        const img = images[index];

        // Обновляем счетчик
        counterCurrent.textContent = index + 1;
        titleEl.textContent = img.name;

        // Показываем загрузку
        loading.style.display = 'flex';
        mainImage.classList.remove('loaded');

        // Если изображение уже предзагружено, показываем его сразу
        if (img.loaded && img.preloadedImage) {
            mainImage.src = img.preloadedImage.src;
            mainImage.classList.add('loaded');
            loading.style.display = 'none';
        } else {
            // Загружаем изображение
            const tempImg = new Image();
            tempImg.onload = () => {
                mainImage.src = img.full;
                mainImage.classList.add('loaded');
                loading.style.display = 'none';
                img.loaded = true;
                img.preloadedImage = tempImg;
                thumbElements[index].classList.add('preloaded');
                updatePreloadStatus();
            };
            tempImg.onerror = () => {
                loading.innerHTML = '<div>Ошибка загрузки</div>';
            };
            tempImg.src = img.full;
        }

        // Обновляем активную миниатюру
        thumbElements.forEach((thumb, i) => {
            thumb.classList.toggle('active', i === index);
        });

        // Прокручиваем к активной миниатюре
        const activeThumb = thumbnailsContainer.children[index];
        if (activeThumb) {
            activeThumb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
        }

        // Обновляем кнопки навигации
        prevBtn.disabled = index === 0;
        nextBtn.disabled = index === images.length - 1;

        // Запускаем предзагрузку окружающих изображений
        preloadSurroundingImages(index);
    }

    // Навигация
    prevBtn.addEventListener('click', () => showImage(currentIndex - 1));
    nextBtn.addEventListener('click', () => showImage(currentIndex + 1));

    // Клавиатурная навигация
    document.addEventListener('keydown', (e) => {
        if (!viewer.classList.contains('active')) return;

        if (e.key === 'ArrowLeft') showImage(currentIndex - 1);
        if (e.key === 'ArrowRight') showImage(currentIndex + 1);
        if (e.key === 'Escape') closeViewer();
    });

    // Открыть/закрыть галерею
    function openViewer() {
        viewer.classList.add('active');
        showImage(0);
        document.body.style.overflow = 'hidden';
    }

    function closeViewer() {
        viewer.classList.remove('active');
        document.body.style.overflow = '';
    }

    openBtn.addEventListener('click', openViewer);
    closeBtn.addEventListener('click', closeViewer);

    // Закрытие по клику на фон
    viewer.addEventListener('click', (e) => {
        if (e.target === viewer) closeViewer();
    });

    // Инициализация - предзагружаем первые несколько изображений
    updatePreloadStatus();
    preloadSurroundingImages(0).then(() => {
        console.log(`PostImg Gallery Viewer: Найдено ${images.length} изображений, предзагружено первые ${Math.min(PRELOAD_COUNT + 1, images.length)}`);
    });
})();