// ==UserScript==
// @name Fortnite Image Replacer
// @version 1.11
// @description Manual image replacement for Fortnite.gg with enhanced error handling
// @match https://fortnite.gg/*
// @grant none
// @namespace https://greasyfork.org/users/789838
// ==/UserScript==
(function() {
'use strict'; // Использование строгого режима JavaScript
// Прозрачный пиксель в формате base64, используется как fallback
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
// Функция для проверки доступности изображения по URL
function checkImage(url, callback) {
const img = new Image();
img.onload = () => callback(true); // Изображение загружено успешно
img.onerror = () => callback(false); // Ошибка загрузки изображения
img.src = url + '?t=' + Date.now(); // Добавление timestamp для избежания кеширования
}
// Функция для захвата кадра из видео
function captureVideoFrame(videoUrl, callback) {
const video = document.createElement('video');
video.crossOrigin = 'anonymous'; // Разрешение кросс-доменных запросов
let attempts = 0; // Счетчик попыток захвата кадра
// Функция для очистки ресурсов
const cleanup = () => {
video.removeEventListener('loadeddata', onLoaded);
video.removeEventListener('seeked', onSeeked);
video.removeEventListener('error', onError);
video.remove();
};
// Обработчик события загрузки метаданных видео
const onLoaded = () => {
if (video.duration === Infinity || video.duration < 2) {
video.currentTime = Math.min(0.01, video.duration || 0.01);
return;
}
video.currentTime = 2; // Перемотка на 2 секунду для захвата кадра
};
// Обработчик события завершения перемотки видео
const onSeeked = () => {
try {
if (video.readyState < 2) return;
// Создание canvas для рендеринга кадра видео
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 800;
canvas.height = video.videoHeight || 450;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
callback(canvas.toDataURL()); // Возврат кадра в формате data URL
cleanup();
} catch (e) {
handleVideoError(e);
}
};
// Обработчик ошибок при работе с видео
const handleVideoError = (e) => {
if (attempts++ < 2) {
video.currentTime = 0.01; // Повторная попытка с другим временем
return;
}
console.error('Video capture failed:', e);
callback(null); // Возврат null при неудаче
cleanup();
};
const onError = (e) => {
handleVideoError(e);
};
// Подписка на события видео
video.addEventListener('loadeddata', onLoaded);
video.addEventListener('seeked', onSeeked);
video.addEventListener('error', onError);
video.src = videoUrl;
}
// Основная функция замены изображений
function replaceImages() {
// Поиск всех изображений с классами или атрибутами, содержащими путь к изображениям предметов
const images = document.querySelectorAll('.img, img[src*="/img/items/"]');
images.forEach((image) => {
const originalSrc = image.src || image.getAttribute('data-src');
if (!originalSrc) return;
// Обработка изображений из раздела survey
const surveyMatch = originalSrc.match(/https:\/\/fortnite\.gg\/img\/items-survey\/(\d+)\.jpg(\?.+)?/);
if (surveyMatch) {
// Замена на изображение большего размера
image.src = `https://fortnite.gg/img/items-survey/big/${surveyMatch[1]}.jpg${surveyMatch[2] || ''}`;
return;
}
// Обработка иконок предметов
const iconMatch = originalSrc.match(/\/img\/items\/(\d+)\/icon\.(png|jpg)(\?.+)?/);
if (iconMatch) {
const itemId = iconMatch[1];
const featuredUrl = `https://fortnite.gg/img/items/${itemId}/featured.png${iconMatch[3] || ''}`;
// Проверка существования featured-изображения
checkImage(featuredUrl, (exists) => {
if (!exists) {
// Если featured-изображения нет, пытаемся получить кадр из видео
const videoUrl = `https://fnggcdn.com/items/${itemId}/video.mp4?t=${Date.now()}`;
captureVideoFrame(videoUrl, (frameUrl) => {
image.src = frameUrl || TRANSPARENT_PIXEL; // Fallback на прозрачный пиксель
image.style.display = 'block';
if (!frameUrl) image.style.opacity = '0'; // Скрытие при неудаче
});
return;
}
// Использование featured-изображения, если оно доступно
image.onerror = null;
image.src = featuredUrl;
});
}
});
}
// Функция добавления панели управления на страницу
function addControlPanel() {
// Стили для панели управления
const panelStyle = [
'position: fixed',
'top: 20px',
'right: 20px',
'background: rgba(0,0,0,0.8)',
'padding: 15px',
'border-radius: 8px',
'z-index: 9999',
'box-shadow: 0 2px 10px rgba(0,0,0,0.5)'
].join(';');
// Стили для кнопок
const buttonStyle = [
'display: block',
'width: 100%',
'margin: 5px 0',
'padding: 10px',
'background: #5865F2',
'color: white',
'border: none',
'border-radius: 4px',
'cursor: pointer',
'font-family: inherit',
'transition: background 0.2s'
].join(';');
// Создание элементов панели управления
const panel = document.createElement('div');
panel.style.cssText = panelStyle;
// Кнопка замены изображений
const replaceBtn = document.createElement('button');
replaceBtn.textContent = '🖼️ Replace Images';
replaceBtn.style.cssText = buttonStyle;
replaceBtn.onclick = replaceImages;
replaceBtn.onmouseover = () => replaceBtn.style.background = '#4752C4';
replaceBtn.onmouseout = () => replaceBtn.style.background = '#5865F2';
// Кнопка очистки кеша
const clearBtn = document.createElement('button');
clearBtn.textContent = '🧹 Clear Cache';
clearBtn.style.cssText = buttonStyle;
clearBtn.onclick = () => {
if (confirm('Clear all cached data and reload?')) {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
}
};
clearBtn.onmouseover = () => clearBtn.style.background = '#DA3636';
clearBtn.onmouseout = () => clearBtn.style.background = '#5865F2';
// Добавление кнопок на панель и панели на страницу
panel.appendChild(replaceBtn);
document.body.appendChild(panel);
}
// Добавление панели управления после полной загрузки страницы
window.addEventListener('load', addControlPanel);
})();