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