Улучшение пользовательского опыта

Страницы:  1

Ответить
Автор
Сообщение

admin


Всем привет!
Можно улучшить пользовательский опыт с помощью скриптов.
Инструкция:
1. Установить расширение к браузеру для запуска скриптов: Violentmonkey (рекомендуется) или Tampermonkey.
2. Создать в нем новый скрипт, вставить вместо него текст выбранного скрипта и сохранить.
3. Все скрипты рассчитаны на https://tormac.org - HTTP не поддерживается.

TorMac User Menu

Добавляет выпадающее меню со ссылками на сообщения пользователя в теме (и другие ссылки).
Код:
// ==UserScript==
// @name         TorMac User Menu
// @namespace    copyMister
// @version      1.4.1
// @description  Adds a dropdown with quick search links for users in forum topics (e.g., messages by author). Note: Requires JavaScript enabled for tormac.org in Tor Browser (via NoScript or security settings).
// @description:ru  Добавляет выпадающее меню со ссылками на сообщения пользователя в теме (и другие ссылки). Требуется включить JavaScript для tormac.org в Tor Browser (через NoScript или настройки безопасности).
// @author       copyMister
// @license      MIT
// @match        https://tormac.org/viewtopic.php*
// @match        https://tormac.org/torrent/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tormac.org
// @run-at       document-body
// @grant        none
// @homepageURL  https://tormac.org/torrent/uluchshenie-polzovatelskogo-opyta.5301
// ==/UserScript==
(function() {
    'use strict';
    $.holdReady(true);
    document.addEventListener('DOMContentLoaded', function() {
        if (!document.querySelector('#quick-search')) {
            console.error('Quick search element not found');
            $.holdReady(false);
            return;
        }
        // Extract topicId from URL (handles both viewtopic.php and /torrent/*)
        let topicId;
        const url = window.location.href;
        if (url.includes('viewtopic.php')) {
            topicId = new URLSearchParams(window.location.search).get('t');
        } else if (url.includes('/torrent/')) {
            const match = url.match(/\.(\d+)$/);
            topicId = match ? match[1] : null;
        }
        if (!topicId) {
            console.error('Could not extract topicId from URL:', url);
            $.holdReady(false);
            return;
        }
        // Extract forumId (assumes same selector as pornolab.net; adjust if needed)
        const forumOption = document.querySelector('option[value^="search.php?f="]');
        const forumId = forumOption ? forumOption.value.split('=')[1] : null;
        if (!forumId) {
            console.error('Could not extract forumId');
            $.holdReady(false);
            return;
        }
        // Process each post to add user menu
        document.querySelectorAll('#topic_main .poster_btn.td3 > div').forEach(function(div) {
            const userLink = div.firstElementChild.href;
            const userIdMatch = userLink.match(/u=(\d+)/);
            const userId = userIdMatch ? userIdMatch[1] : null;
            if (!userId) return;
            const userName = div.closest('tbody').querySelector('.poster_info > .nick')?.textContent.trim();
            if (!userName) {
                console.error('Could not extract userName for userId:', userId);
                return;
            }
            const userMenu = `usermenu-${userId}`;
            // Add dropdown toggle link
            div.insertAdjacentHTML(
                'beforeend',
                `<a class="txtb menu-root without-caret" href="#${userMenu}">▼</a>`
            );
            // Add dropdown menu if not already present
            if (!document.querySelector(`#${userMenu}`)) {
                document.body.insertAdjacentHTML(
                    'beforeend',
                    `<div id="${userMenu}" class="menu-sub"><div class="menu-a bold nowrap">
                     <table style="width: 100%; border-collapse: collapse; border: 0;">
                         <tr><th style="font-size: 12px; padding: 4px 12px;">${userName}</th></tr>
                     </table>
                     <a class="med" href="search.php?uid=${userId}&search_author=1&only_replies">Все ответы</a>
                     <a class="med" href="search.php?uid=${userId}&t=${topicId}&dm=1">Сообщения в этой теме</a>
                     <a class="med" href="search.php?uid=${userId}&f=${forumId}&dm=1">Сообщения в этом разделе</a>
                     <a class="med" href="search.php?uid=${userId}&search_author=1">Сообщения по всему трекеру</a>
                     <a class="med" href="search.php?uid=${userId}&myt=1">Начатые темы</a>
                     <a class="med" href="tracker.php?rid=${userId}">Раздачи</a></div></div>`
                );
            }
        });
        $.holdReady(false);
    });
})();

TorMac Preloaded Preview

Код:
// ==UserScript==
// @name         TorMac Preloaded Preview
// @version      1.7.0
// @description  Прелоадит и отображает превью изображений под ссылками на tormac.org для страниц трекера, поиска и форумов.
// @author       Ace
// @license      MIT
// @match        *://tormac.org/search*
// @match        *://tormac.org/tracker*
// @match        *://tormac.org/viewforum*
// @match        *://tormac.org/torrent/*
// @match        *://tormac.org/forum/*
// @run-at       document-end
// @require      https://cdnjs.cloudflare.com/ajax/libs/core-js/3.25.1/minified.js
// @grant        none
// @namespace    https://tormac.org/torrent/uluchshenie-polzovatelskogo-opyta.5301
// ==/UserScript==
(async function(){
    const previewSize = {width: "auto", height: "17rem"};
    const inProgressLinks = new Set();
    const failedLinks = new Map();
    const MAX_RETRIES = 3;
    const THROTTLE_INTERVAL = 300;
    const MAX_CONCURRENT_FETCHES = 5;
    let concurrentFetchCount = 0;
    try {
        const style = document.createElement("style");
        style.textContent = `.preview-container{max-width:${previewSize.width};height:${previewSize.height};display:flex;justify-content:center;align-items:center;overflow:hidden}.preview-container img{width:100%;height:100%;object-fit:scale-down}`;
        document.head.appendChild(style);
    } catch (error) {}
    const previewCache = new Map();
    async function fetchWithRetry(url, retries = 0) {
        try {
            const response = await fetch(url, { timeout: 10000 });
            if (!response.ok) {
                if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
                failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
                return [];
            }
            const html = await response.text();
            const doc = new DOMParser().parseFromString(html, "text/html");
            const imgElements = doc.querySelectorAll(".postImg, .postimage, img[title], var.postImg img");
            const imgUrls = Array.from(imgElements).map(img => img.title || img.src || img.dataset.src).filter(Boolean);
            if (imgUrls.length === 0) failedLinks.set(url, "No images found");
            return imgUrls;
        } catch (error) {
            if (retries < MAX_RETRIES) return fetchWithRetry(url, retries + 1);
            failedLinks.set(url, `Failed after ${MAX_RETRIES} retries`);
            return [];
        }
    }
    function normalizeUrl(href) {
        const baseUrl = "https://tormac.org";
        if (!href) return null;
        if (href.startsWith("/")) return baseUrl + href;
        if (href.match(/\/(torrent|forum)\/.+/)) {
            const match = href.match(/\.(\d+)$/);
            const type = href.includes("/torrent") ? "torrent" : "forum";
            if (match) {
                const id = match[1];
                return type === "torrent" ? `${baseUrl}/viewtopic.php?t=${id}` : `${baseUrl}/viewforum.php?f=${id}`;
            }
            return href;
        }
        return href.startsWith(baseUrl) ? href : baseUrl + "/" + href;
    }
    async function getCachedOrFetchPreviewUrls(linkElement) {
        const href = linkElement.getAttribute("href");
        const url = normalizeUrl(href);
        if (!url) return [];
        if (previewCache.has(url)) return Promise.resolve(previewCache.get(url));
        if (inProgressLinks.has(url) || failedLinks.has(url)) return Promise.resolve([]);
        inProgressLinks.add(url);
        const delay = () => new Promise(resolve => setTimeout(resolve, 100));
        while (concurrentFetchCount >= MAX_CONCURRENT_FETCHES) await delay();
        concurrentFetchCount++;
        try {
            const result = await fetchWithRetry(url);
            previewCache.set(url, result);
            return result;
        } finally {
            concurrentFetchCount--;
            inProgressLinks.delete(url);
        }
    }
    function insertPreview(linkElement, imgUrls) {
        try {
            if (!imgUrls || imgUrls.length === 0 || linkElement.hasAttribute("data-preview-processed")) return;
            linkElement.setAttribute("data-preview-processed", "true");
            if (linkElement.nextElementSibling?.classList.contains("preview-container")) return;
            const previewContainer = document.createElement("div");
            previewContainer.className = "preview-container";
            const img = document.createElement("img");
            img.src = imgUrls[0];
            img.loading = "lazy";
            img.addEventListener("error", function() {
                imgUrls.shift();
                if (imgUrls.length > 0) img.src = imgUrls[0];
                else img.remove();
            });
            img.addEventListener("load", function() {
                if (img.naturalHeight < 201) {
                    imgUrls.shift();
                    if (imgUrls.length > 0) img.src = imgUrls[0];
                    else img.remove();
                }
            });
            previewContainer.appendChild(img);
            linkElement.parentNode.insertBefore(previewContainer, linkElement.nextSibling);
            linkElement.parentNode.style.cssText = "padding:1rem;display:flex;justify-content:space-between";
            linkElement.style.cssText = "width:30%";
        } catch (error) {}
    }
    async function preloadPreviews() {
        try {
            const links = document.querySelectorAll(".tLink, .tt-text, .topictitle");
            const tasks = Array.from(links).map(link => async () => {
                const imgUrls = await getCachedOrFetchPreviewUrls(link);
                insertPreview(link, imgUrls);
            });
            await preloadAll(tasks, 5);
        } catch (error) {}
    }
    async function preloadAll(tasks, limit = 5) {
        const results = [];
        try {
            while (tasks.length) {
                const batch = tasks.splice(0, limit).map(task => task());
                results.push(...(await Promise.allSettled(batch)));
            }
        } catch (error) {}
        return results;
    }
    function throttle(func, limit) {
        let lastFunc;
        let lastRan;
        return function(...args) {
            const context = this;
            if (!lastRan) {
                func.apply(context, args);
                lastRan = Date.now();
            } else {
                clearTimeout(lastFunc);
                lastFunc = setTimeout(() => {
                    if (Date.now() - lastRan >= limit) {
                        func.apply(context, args);
                        lastRan = Date.now();
                    }
                }, limit - (Date.now() - lastRan));
            }
        };
    }
    try {
        const throttledObserverCallback = throttle(entries => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const link = entry.target;
                    preloadPreviews().then(() => observer.unobserve(link)).catch(() => observer.observe(link));
                }
            });
        }, THROTTLE_INTERVAL);
        const observer = new IntersectionObserver(throttledObserverCallback);
        const observeLinks = () => {
            document.querySelectorAll(".tLink, .tt-text, .topictitle").forEach(link => {
                if (!link.hasAttribute("data-preview-processed")) observer.observe(link);
            });
        };
        observeLinks();
        const mutationObserver = new MutationObserver(observeLinks);
        mutationObserver.observe(document.body, { childList: true, subtree: true });
    } catch (error) {}
})();

TorMac Preview (только Violentmonkey&#41;

Код:
// ==UserScript==
// @name         TorMac Preview
// @namespace    http://tampermonkey.net/
// @version      1.1.2
// @description  Предварительный просмотр скриншотов на tormac.org
// @author       С
// @match        https://tormac.org/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==
(function() {
    'use strict';
    //====================================
    // НАСТРОЙКИ
    //====================================
    const defaultSettings = {
        previewThumbnailSize: 100,
        lightboxThumbnailSize: 800,
        previewMaxWidth: 500,
        previewMaxHeight: 500,
        previewGridColumns: 3,
        maxThumbnailsBeforeSpoiler: 12,
        previewPosition: 'bottomLeft',
        colorTheme: 'light',
        hoverEffectTime: 0.3,
        previewHideDelay: 300,
        enableAutoPreview: true,
        hidePreviewIfEmpty: true,
        neverUseSpoilers: false,
        siteSettings: {
            tormac: {
                enabled: true,
                useFullSizeInLightbox: true,
                clickBehavior: 'lightbox',
                supportedHostings: ['umpic', 'fastpic', 'imagebam', 'imgbox', 'imageban', 'imgur', 'postimage']
            }
        },
        navButtonsSize: 60,
        navButtonsVisibility: 'hover',
        keyboardShortcuts: {
            close: 'Escape',
            prev: 'ArrowLeft',
            next: 'ArrowRight',
            reset: 'Home',
            fullscreen: 'F'
        },
        cacheTTL: 3600,
        maxCacheSize: 50,
        enableLogging: false
    };
    function loadSettings() {
        const savedSettings = GM_getValue('tormacPreviewSettings');
        let settings = Object.assign({}, defaultSettings);
        if (savedSettings) {
            try {
                const parsed = JSON.parse(savedSettings);
                settings = mergeDeep(settings, parsed);
            } catch (e) {
                console.error('Ошибка при загрузке настроек:', e);
            }
        }
        return settings;
    }
    function saveSettings(settings) {
        GM_setValue('tormacPreviewSettings', JSON.stringify(settings));
    }
    function mergeDeep(target, source) {
        const isObject = obj => obj && typeof obj === 'object' && !Array.isArray(obj);
        if (!isObject(target) || !isObject(source)) {
            return source;
        }
        Object.keys(source).forEach(key => {
            const targetValue = target[key];
            const sourceValue = source[key];
            if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
                target[key] = sourceValue;
            } else if (isObject(targetValue) && isObject(sourceValue)) {
                target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
            } else {
                target[key] = sourceValue;
            }
        });
        return target;
    }
    const settings = loadSettings();
    function log(...args) {
        if (settings.enableLogging) {
            const timestamp = new Date().toLocaleTimeString();
            console.log(`[${timestamp}] tormac Preview:`, ...args);
        }
    }
    //====================================
    // ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
    //====================================
    function createElement(tag, properties = {}, styles = {}) {
        const element = document.createElement(tag);
        Object.assign(element, properties);
        Object.assign(element.style, styles);
        return element;
    }
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }
    function addHoverEffect(element, imgElement) {
        imgElement.style.transition = `transform ${settings.hoverEffectTime}s ease`;
        element.addEventListener('mouseenter', () => {
            imgElement.style.transform = 'scale(1.05)';
        });
        element.addEventListener('mouseleave', () => {
            imgElement.style.transform = 'scale(1)';
        });
    }
    function createThumbnail(imgData, openImageFunc, siteName) {
        const aElement = createElement('a', { href: imgData.fullUrl });
        const clickBehavior = settings.siteSettings[siteName].clickBehavior;
        aElement.addEventListener('click', function(e) {
            e.preventDefault();
            if (clickBehavior === 'lightbox') {
                openImageFunc(imgData.thumbUrl, imgData.fullUrl);
            } else {
                window.open(imgData.fullUrl, '_blank');
            }
        });
        const imgElement = createElement('img', {
            src: imgData.thumbUrl,
            alt: 'Thumbnail'
        }, {
            maxWidth: '100%',
            maxHeight: `${settings.previewThumbnailSize}px`,
            objectFit: 'cover',
            borderRadius: '4px'
        });
        addHoverEffect(aElement, imgElement);
        aElement.appendChild(imgElement);
        return aElement;
    }
    function addImagesToContainer(container, imageLinks, openImageFunc, siteName, startIndex = 0, endIndex = imageLinks.length) {
        const links = imageLinks.slice(startIndex, endIndex);
        links.forEach(imgData => {
            const thumbnail = createThumbnail(imgData, openImageFunc, siteName);
            container.appendChild(thumbnail);
        });
    }
    //====================================
    // ОКНО НАСТРОЕК
    //====================================
    const settingsDialogHTML = `
<div id="tormac-preview-settings-backdrop" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 10000; display: flex; justify-content: center; align-items: center;">
    <div id="tormac-preview-settings-dialog" style="background-color: var(--bg-color); border-radius: 8px; padding: 20px; max-width: 800px; width: 90%; max-height: 90vh; overflow-y: auto; position: relative; color: var(--text-color);">
        <style>
            :root {
                --bg-color: ${settings.colorTheme === 'dark' ? '#222' : 'white'};
                --text-color: ${settings.colorTheme === 'dark' ? '#eee' : 'black'};
                --border-color: ${settings.colorTheme === 'dark' ? '#444' : '#ccc'};
            }
            #tormac-preview-settings-dialog input, #tormac-preview-settings-dialog select, #tormac-preview-settings-dialog button {
                background-color: var(--bg-color);
                color: var(--text-color);
                border: 1px solid var(--border-color);
                border-radius: 4px;
                padding: 5px;
            }
            #tormac-preview-settings-dialog button:hover {
                background-color: var(--border-color);
            }
        </style>
        <h2 style="margin-top: 0; border-bottom: 1px solid var(--border-color); padding-bottom: 10px;">Настройки tormac Preview</h2>
        <div style="position: absolute; top: 20px; right: 20px; cursor: pointer; font-size: 24px; font-weight: bold;" id="tormac-preview-settings-close">×</div>
        <div style="display: flex; flex-wrap: wrap; gap: 20px;">
            <div style="flex: 1; min-width: 300px;">
                <h3>Размеры и внешний вид</h3>
                <div style="margin-bottom: 15px;">
                    <label for="previewThumbnailSize">Размер миниатюр в окне предпросмотра (px):</label>
                    <input type="range" id="previewThumbnailSize" min="50" max="500" step="10" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>50</span>
                        <span id="previewThumbnailSizeValue">100</span>
                        <span>500</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="lightboxThumbnailSize">Размер изображений в лайтбоксе (px):</label>
                    <input type="range" id="lightboxThumbnailSize" min="400" max="1500" step="100" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>400</span>
                        <span id="lightboxThumbnailSizeValue">800</span>
                        <span>1500</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="previewMaxWidth">Максимальная ширина окна предпросмотра (px):</label>
                    <input type="range" id="previewMaxWidth" min="200" max="1000" step="50" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>200</span>
                        <span id="previewMaxWidthValue">500</span>
                        <span>1000</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="previewMaxHeight">Максимальная высота окна предпросмотра (px):</label>
                    <input type="range" id="previewMaxHeight" min="200" max="1000" step="50" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>200</span>
                        <span id="previewMaxHeightValue">500</span>
                        <span>1000</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="previewGridColumns">Количество столбцов в сетке миниатюр:</label>
                    <input type="range" id="previewGridColumns" min="1" max="8" step="1" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>1</span>
                        <span id="previewGridColumnsValue">3</span>
                        <span>8</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="maxThumbnailsBeforeSpoiler">Макс. количество миниатюр до спойлера:</label>
                    <input type="range" id="maxThumbnailsBeforeSpoiler" min="3" max="50" step="1" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>3</span>
                        <span id="maxThumbnailsBeforeSpoilerValue">12</span>
                        <span>50</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="previewPosition">Положение окна предпросмотра:</label>
                    <select id="previewPosition" style="width: 100%;">
                        <option value="bottomRight">Снизу справа</option>
                        <option value="bottomLeft">Снизу слева</option>
                        <option value="topRight">Сверху справа</option>
                        <option value="topLeft">Сверху слева</option>
                    </select>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="colorTheme">Цветовая схема:</label>
                    <select id="colorTheme" style="width: 100%;">
                        <option value="light">Светлая</option>
                        <option value="dark">Темная</option>
                        <option value="system">Системная</option>
                    </select>
                </div>
            </div>
            <div style="flex: 1; min-width: 300px;">
                <h3>Поведение</h3>
                <div style="margin-bottom: 15px;">
                    <label for="previewHideDelay">Задержка перед скрытием окна предпросмотра (мс):</label>
                    <input type="range" id="previewHideDelay" min="100" max="2000" step="100" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>100</span>
                        <span id="previewHideDelayValue">300</span>
                        <span>2000</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="hoverEffectTime">Время анимации эффекта наведения (сек):</label>
                    <input type="range" id="hoverEffectTime" min="0.1" max="1.0" step="0.1" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>0.1</span>
                        <span id="hoverEffectTimeValue">0.3</span>
                        <span>1.0</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label>
                        <input type="checkbox" id="enableAutoPreview">
                        Включить окно предпросмотра
                    </label>
                </div>
                <div style="margin-bottom: 15px;">
                    <label>
                        <input type="checkbox" id="hidePreviewIfEmpty">
                        Не показывать окно предпросмотра, если нет скриншотов
                    </label>
                </div>
                <div style="margin-bottom: 15px;">
                    <label>
                        <input type="checkbox" id="neverUseSpoilers">
                        Никогда не скрывать изображения под спойлер
                    </label>
                </div>
                <h3>Настройки сайта</h3>
                <div style="margin-bottom: 15px;">
                    <label>
                        <input type="checkbox" id="tormacEnabled">
                        Включить для tormac
                    </label>
                </div>
                <div style="margin-bottom: 15px; margin-left: 20px;">
                    <label>
                        <input type="checkbox" id="tormacUseFullSize">
                        Полные изображения в лайтбоксе
                    </label>
                </div>
                <div style="margin-bottom: 15px; margin-left: 20px;">
                    <label for="tormacClickBehavior">Поведение при клике на изображение:</label>
                    <select id="tormacClickBehavior" style="width: 100%;">
                        <option value="lightbox">Открывать в лайтбоксе</option>
                        <option value="newTab">Открывать в новой вкладке</option>
                    </select>
                </div>
                <div style="margin-bottom: 15px; margin-left: 20px;">
                    <label>Поддерживаемые хостинги:</label>
                    <div style="display: flex; flex-wrap: wrap; gap: 10px;">
                        ${['umpic', 'fastpic', 'imagebam', 'imgbox', 'imageban', 'imgur', 'postimage'].map(hosting => `
                            <label style="flex: 1 1 45%;">
                                <input type="checkbox" name="supportedHostings" value="${hosting}" ${settings.siteSettings.tormac.supportedHostings.includes(hosting) ? 'checked' : ''}>
                                ${hosting.charAt(0).toUpperCase() + hosting.slice(1)}
                            </label>
                        `).join('')}
                    </div>
                </div>
            </div>
            <div style="flex: 1; min-width: 300px;">
                <h3>Кнопки навигации</h3>
                <div style="margin-bottom: 15px;">
                    <label for="navButtonsSize">Размер кнопок навигации (px):</label>
                    <input type="range" id="navButtonsSize" min="30" max="100" step="5" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>30</span>
                        <span id="navButtonsSizeValue">60</span>
                        <span>100</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="navButtonsVisibility">Видимость кнопок навигации:</label>
                    <select id="navButtonsVisibility" style="width: 100%;">
                        <option value="always">Всегда видимы</option>
                        <option value="hover">Видимы при наведении</option>
                        <option value="never">Всегда скрыты</option>
                    </select>
                </div>
                <h3>Горячие клавиши</h3>
                <div style="margin-bottom: 15px;">
                    <label for="closeKey">Закрытие лайтбокса:</label>
                    <div style="display: flex; gap: 5px;">
                        <input type="text" id="closeKey" style="width: 100%;" readonly>
                        <button id="changeCloseKey">Изменить</button>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="prevKey">Предыдущее изображение:</label>
                    <div style="display: flex; gap: 5px;">
                        <input type="text" id="prevKey" style="width: 100%;" readonly>
                        <button id="changePrevKey">Изменить</button>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="nextKey">Следующее изображение:</label>
                    <div style="display: flex; gap: 5px;">
                        <input type="text" id="nextKey" style="width: 100%;" readonly>
                        <button id="changeNextKey">Изменить</button>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="resetKey">Сброс позиции и масштаба:</label>
                    <div style="display: flex; gap: 5px;">
                        <input type="text" id="resetKey" style="width: 100%;" readonly>
                        <button id="changeResetKey">Изменить</button>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="fullscreenKey">Полноэкранный режим:</label>
                    <div style="display: flex; gap: 5px;">
                        <input type="text" id="fullscreenKey" style="width: 100%;" readonly>
                        <button id="changeFullscreenKey">Изменить</button>
                    </div>
                </div>
                <h3>Кэширование</h3>
                <div style="margin-bottom: 15px;">
                    <label for="cacheTTL">Время жизни кэша (сек):</label>
                    <input type="range" id="cacheTTL" min="300" max="7200" step="300" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>300</span>
                        <span id="cacheTTLValue">3600</span>
                        <span>7200</span>
                    </div>
                </div>
                <div style="margin-bottom: 15px;">
                    <label for="maxCacheSize">Максимальный размер кэша:</label>
                    <input type="range" id="maxCacheSize" min="10" max="100" step="10" style="width: 100%;">
                    <div style="display: flex; justify-content: space-between;">
                        <span>10</span>
                        <span id="maxCacheSizeValue">50</span>
                        <span>100</span>
                    </div>
                </div>
                <h3>Отладка</h3>
                <div style="margin-bottom: 15px;">
                    <label>
                        <input type="checkbox" id="enableLogging">
                        Включить логирование
                    </label>
                </div>
                <div style="margin-top: 30px; display: flex; gap: 10px; justify-content: space-between;">
                    <button id="saveSettings" style="padding: 8px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer;">Сохранить настройки</button>
                    <button id="resetSettings" style="padding: 8px 15px; background-color: #f44336; color: white; border: none; cursor: pointer;">Сбросить настройки</button>
                </div>
            </div>
        </div>
    </div>
</div>
`;
    function openSettingsDialog() {
        if (document.getElementById('tormac-preview-settings-backdrop')) return;
        const dialogContainer = createElement('div', { innerHTML: settingsDialogHTML });
        document.body.appendChild(dialogContainer);
        const elements = {
            previewThumbnailSize: document.getElementById('previewThumbnailSize'),
            previewThumbnailSizeValue: document.getElementById('previewThumbnailSizeValue'),
            lightboxThumbnailSize: document.getElementById('lightboxThumbnailSize'),
            lightboxThumbnailSizeValue: document.getElementById('lightboxThumbnailSizeValue'),
            previewMaxWidth: document.getElementById('previewMaxWidth'),
            previewMaxWidthValue: document.getElementById('previewMaxWidthValue'),
            previewMaxHeight: document.getElementById('previewMaxHeight'),
            previewMaxHeightValue: document.getElementById('previewMaxHeightValue'),
            previewGridColumns: document.getElementById('previewGridColumns'),
            previewGridColumnsValue: document.getElementById('previewGridColumnsValue'),
            maxThumbnailsBeforeSpoiler: document.getElementById('maxThumbnailsBeforeSpoiler'),
            maxThumbnailsBeforeSpoilerValue: document.getElementById('maxThumbnailsBeforeSpoilerValue'),
            previewPosition: document.getElementById('previewPosition'),
            colorTheme: document.getElementById('colorTheme'),
            previewHideDelay: document.getElementById('previewHideDelay'),
            previewHideDelayValue: document.getElementById('previewHideDelayValue'),
            hoverEffectTime: document.getElementById('hoverEffectTime'),
            hoverEffectTimeValue: document.getElementById('hoverEffectTimeValue'),
            enableAutoPreview: document.getElementById('enableAutoPreview'),
            hidePreviewIfEmpty: document.getElementById('hidePreviewIfEmpty'),
            neverUseSpoilers: document.getElementById('neverUseSpoilers'),
            tormacEnabled: document.getElementById('tormacEnabled'),
            tormacUseFullSize: document.getElementById('tormacUseFullSize'),
            tormacClickBehavior: document.getElementById('tormacClickBehavior'),
            supportedHostings: document.querySelectorAll('input[name="supportedHostings"]'),
            navButtonsSize: document.getElementById('navButtonsSize'),
            navButtonsSizeValue: document.getElementById('navButtonsSizeValue'),
            navButtonsVisibility: document.getElementById('navButtonsVisibility'),
            closeKey: document.getElementById('closeKey'),
            prevKey: document.getElementById('prevKey'),
            nextKey: document.getElementById('nextKey'),
            resetKey: document.getElementById('resetKey'),
            fullscreenKey: document.getElementById('fullscreenKey'),
            changeCloseKey: document.getElementById('changeCloseKey'),
            changePrevKey: document.getElementById('changePrevKey'),
            changeNextKey: document.getElementById('changeNextKey'),
            changeResetKey: document.getElementById('changeResetKey'),
            changeFullscreenKey: document.getElementById('changeFullscreenKey'),
            cacheTTL: document.getElementById('cacheTTL'),
            cacheTTLValue: document.getElementById('cacheTTLValue'),
            maxCacheSize: document.getElementById('maxCacheSize'),
            maxCacheSizeValue: document.getElementById('maxCacheSizeValue'),
            enableLogging: document.getElementById('enableLogging'),
            saveSettings: document.getElementById('saveSettings'),
            resetSettings: document.getElementById('resetSettings'),
            closeButton: document.getElementById('tormac-preview-settings-close')
        };
        for (const [key, element] of Object.entries(elements)) {
            if (!element && key !== 'supportedHostings') {
                log(`Ошибка: Элемент с ID ${key} не найден в DOM`);
                alert(`Ошибка: Элемент с ID ${key} не найден. Проверьте HTML-разметку окна настроек.`);
                closeSettingsDialog();
                return;
            }
        }
        elements.previewThumbnailSize.value = settings.previewThumbnailSize;
        elements.previewThumbnailSizeValue.textContent = settings.previewThumbnailSize;
        elements.lightboxThumbnailSize.value = settings.lightboxThumbnailSize;
        elements.lightboxThumbnailSizeValue.textContent = settings.lightboxThumbnailSize;
        elements.previewMaxWidth.value = settings.previewMaxWidth;
        elements.previewMaxWidthValue.textContent = settings.previewMaxWidth;
        elements.previewMaxHeight.value = settings.previewMaxHeight;
        elements.previewMaxHeightValue.textContent = settings.previewMaxHeight;
        elements.previewGridColumns.value = settings.previewGridColumns;
        elements.previewGridColumnsValue.textContent = settings.previewGridColumns;
        elements.maxThumbnailsBeforeSpoiler.value = settings.maxThumbnailsBeforeSpoiler;
        elements.maxThumbnailsBeforeSpoilerValue.textContent = settings.maxThumbnailsBeforeSpoiler;
        elements.previewPosition.value = settings.previewPosition;
        elements.colorTheme.value = settings.colorTheme;
        elements.previewHideDelay.value = settings.previewHideDelay;
        elements.previewHideDelayValue.textContent = settings.previewHideDelay;
        elements.hoverEffectTime.value = settings.hoverEffectTime;
        elements.hoverEffectTimeValue.textContent = settings.hoverEffectTime;
        elements.enableAutoPreview.checked = settings.enableAutoPreview;
        elements.hidePreviewIfEmpty.checked = settings.hidePreviewIfEmpty;
        elements.neverUseSpoilers.checked = settings.neverUseSpoilers;
        elements.tormacEnabled.checked = settings.siteSettings.tormac.enabled;
        elements.tormacUseFullSize.checked = settings.siteSettings.tormac.useFullSizeInLightbox;
        elements.tormacClickBehavior.value = settings.siteSettings.tormac.clickBehavior;
        elements.navButtonsSize.value = settings.navButtonsSize;
        elements.navButtonsSizeValue.textContent = settings.navButtonsSize;
        elements.navButtonsVisibility.value = settings.navButtonsVisibility;
        elements.closeKey.value = settings.keyboardShortcuts.close;
        elements.prevKey.value = settings.keyboardShortcuts.prev;
        elements.nextKey.value = settings.keyboardShortcuts.next;
        elements.resetKey.value = settings.keyboardShortcuts.reset;
        elements.fullscreenKey.value = settings.keyboardShortcuts.fullscreen;
        elements.cacheTTL.value = settings.cacheTTL;
        elements.cacheTTLValue.textContent = settings.cacheTTL;
        elements.maxCacheSize.value = settings.maxCacheSize;
        elements.maxCacheSizeValue.textContent = settings.maxCacheSize;
        elements.enableLogging.checked = settings.enableLogging;
        elements.previewThumbnailSize.addEventListener('input', () => {
            elements.previewThumbnailSizeValue.textContent = elements.previewThumbnailSize.value;
        });
        elements.lightboxThumbnailSize.addEventListener('input', () => {
            elements.lightboxThumbnailSizeValue.textContent = elements.lightboxThumbnailSize.value;
        });
        elements.previewMaxWidth.addEventListener('input', () => {
            elements.previewMaxWidthValue.textContent = elements.previewMaxWidth.value;
        });
        elements.previewMaxHeight.addEventListener('input', () => {
            elements.previewMaxHeightValue.textContent = elements.previewMaxHeight.value;
        });
        elements.previewGridColumns.addEventListener('input', () => {
            elements.previewGridColumnsValue.textContent = elements.previewGridColumns.value;
        });
        elements.maxThumbnailsBeforeSpoiler.addEventListener('input', () => {
            elements.maxThumbnailsBeforeSpoilerValue.textContent = elements.maxThumbnailsBeforeSpoiler.value;
        });
        elements.previewHideDelay.addEventListener('input', () => {
            elements.previewHideDelayValue.textContent = elements.previewHideDelay.value;
        });
        elements.hoverEffectTime.addEventListener('input', () => {
            elements.hoverEffectTimeValue.textContent = elements.hoverEffectTime.value;
        });
        elements.navButtonsSize.addEventListener('input', () => {
            elements.navButtonsSizeValue.textContent = elements.navButtonsSize.value;
        });
        elements.cacheTTL.addEventListener('input', () => {
            elements.cacheTTLValue.textContent = elements.cacheTTL.value;
        });
        elements.maxCacheSize.addEventListener('input', () => {
            elements.maxCacheSizeValue.textContent = elements.maxCacheSize.value;
        });
        function setupKeyChangeHandler(keyField, changeButton) {
            changeButton.addEventListener('click', () => {
                const originalText = changeButton.textContent;
                keyField.value = 'Нажмите клавишу...';
                changeButton.textContent = 'Отмена';
                const keyHandler = (e) => {
                    e.preventDefault();
                    keyField.value = e.key;
                    document.removeEventListener('keydown', keyHandler);
                    changeButton.textContent = originalText;
                };
                document.addEventListener('keydown', keyHandler);
                changeButton.addEventListener('click', () => {
                    document.removeEventListener('keydown', keyHandler);
                    changeButton.textContent = originalText;
                    keyField.value = settings.keyboardShortcuts[keyField.id.replace('Key', '')];
                }, { once: true });
            });
        }
        setupKeyChangeHandler(elements.closeKey, elements.changeCloseKey);
        setupKeyChangeHandler(elements.prevKey, elements.changePrevKey);
        setupKeyChangeHandler(elements.nextKey, elements.changeNextKey);
        setupKeyChangeHandler(elements.resetKey, elements.changeResetKey);
        setupKeyChangeHandler(elements.fullscreenKey, elements.changeFullscreenKey);
        elements.saveSettings.addEventListener('click', () => {
            const newSettings = {
                previewThumbnailSize: parseInt(elements.previewThumbnailSize.value),
                lightboxThumbnailSize: parseInt(elements.lightboxThumbnailSize.value),
                previewMaxWidth: parseInt(elements.previewMaxWidth.value),
                previewMaxHeight: parseInt(elements.previewMaxHeight.value),
                previewGridColumns: parseInt(elements.previewGridColumns.value),
                maxThumbnailsBeforeSpoiler: parseInt(elements.maxThumbnailsBeforeSpoiler.value),
                previewPosition: elements.previewPosition.value,
                colorTheme: elements.colorTheme.value,
                previewHideDelay: parseInt(elements.previewHideDelay.value),
                hoverEffectTime: parseFloat(elements.hoverEffectTime.value),
                enableAutoPreview: elements.enableAutoPreview.checked,
                hidePreviewIfEmpty: elements.hidePreviewIfEmpty.checked,
                neverUseSpoilers: elements.neverUseSpoilers.checked,
                siteSettings: {
                    tormac: {
                        enabled: elements.tormacEnabled.checked,
                        useFullSizeInLightbox: elements.tormacUseFullSize.checked,
                        clickBehavior: elements.tormacClickBehavior.value,
                        supportedHostings: Array.from(elements.supportedHostings)
                            .filter(input => input.checked)
                            .map(input => input.value)
                    }
                },
                navButtonsSize: parseInt(elements.navButtonsSize.value),
                navButtonsVisibility: elements.navButtonsVisibility.value,
                keyboardShortcuts: {
                    close: elements.closeKey.value,
                    prev: elements.prevKey.value,
                    next: elements.nextKey.value,
                    reset: elements.resetKey.value,
                    fullscreen: elements.fullscreenKey.value
                },
                cacheTTL: parseInt(elements.cacheTTL.value),
                maxCacheSize: parseInt(elements.maxCacheSize.value),
                enableLogging: elements.enableLogging.checked
            };
            saveSettings(newSettings);
            Object.assign(settings, newSettings);
            closeSettingsDialog();
        });
        elements.resetSettings.addEventListener('click', () => {
            if (confirm('Вы уверены, что хотите сбросить все настройки на значения по умолчанию?')) {
                saveSettings(defaultSettings);
                Object.assign(settings, defaultSettings);
                closeSettingsDialog();
            }
        });
        elements.closeButton.addEventListener('click', closeSettingsDialog);
        const backdrop = document.getElementById('tormac-preview-settings-backdrop');
        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) {
                closeSettingsDialog();
            }
        });
    }
    function closeSettingsDialog() {
        const dialog = document.getElementById('tormac-preview-settings-backdrop');
        if (dialog) dialog.remove();
    }
    //====================================
    // САЙТОЗАВИСИМЫЕ НАСТРОЙКИ
    //====================================
    const siteSpecificFunctions = {
        tormac: {
            getScreenshotLinks: function(spoilerElement) {
                const links = [];
                const aElements = spoilerElement.querySelectorAll('a[href]');
                aElements.forEach(link => {
                    const imgElement = link.querySelector('img');
                    const varElement = link.querySelector('var.postImg');
                    if (imgElement && imgElement.src) {
                        const fullUrl = varElement && varElement.getAttribute('title') ?
                            varElement.getAttribute('title').split('?')[0] :
                            link.href.split('?')[0];
                        const thumbUrl = imgElement.src.split('?')[0];
                        if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i)) {
                            links.push({ fullUrl, thumbUrl });
                        }
                    }
                });
                return links;
            },
            getScreenshotsFromPost: function(postElement) {
    const links = [];
    const processedUrls = new Set(); // Множество для отслеживания обработанных ссылок
    // Сбор ссылок из спойлеров
    const spWrapSpoilers = postElement.querySelectorAll('div.sp-wrap, div.spoiler, div.sp-body');
    spWrapSpoilers.forEach(spoiler => {
        const aElements = spoiler.querySelectorAll('a[href]');
        aElements.forEach(link => {
            const imgElement = link.querySelector('img');
            const varElement = link.querySelector('var.postImg');
            if (imgElement && imgElement.src) {
                const fullUrl = varElement && varElement.getAttribute('title') ?
                    varElement.getAttribute('title').split('?')[0] :
                    link.href.split('?')[0];
                const thumbUrl = imgElement.src.split('?')[0];
                if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i) && !processedUrls.has(fullUrl)) {
                    links.push({ fullUrl, thumbUrl });
                    processedUrls.add(fullUrl); // Добавляем в множество обработанных
                }
            }
        });
    });
                if (links.length === 0) {
                    log('Скриншоты не найдены в спойлерах, ищем по всему посту');
                    const aElements = postElement.querySelectorAll('a.zoom, a.postLink, a[href]');
                    aElements.forEach(link => {
                        if (!link.closest('div[style*="float"]') && !link.querySelector('img[style*="float"]')) {
                            const imgElement = link.querySelector('img');
                            const varElement = link.querySelector('var.postImg');
                            if (imgElement && imgElement.src) {
                                const fullUrl = varElement && varElement.getAttribute('title') ?
                                    varElement.getAttribute('title').split('?')[0] :
                                    link.href.split('?')[0];
                                const thumbUrl = imgElement.src.split('?')[0];
                                if (fullUrl.match(/\.(jpg|jpeg|png|webp|avif)$/i)) {
                                    links.push({ fullUrl, thumbUrl });
                                }
                            }
                        }
                    });
                }
                return links;
            },
            getCover: function(postElement) {
                const varElement = postElement.querySelector('var.postImg.postImgAligned.img-right[title]');
                if (varElement && varElement.getAttribute('title')) {
                    return varElement.getAttribute('title').split('?')[0];
                }
                const alignedImg = postElement.querySelector('var.postImg.postImgAligned.img-right img');
                if (alignedImg && alignedImg.src) {
                    return alignedImg.src.split('?')[0];
                }
                return null;
            },
            openImage: function(imageUrl, fullImageUrl = null) {
                const thumbnails = [];
                const fullSizeUrls = [];
                collectImagesFromPreview(thumbnails, fullSizeUrls);
                let currentIndex = thumbnails.indexOf(imageUrl);
                if (currentIndex === -1) {
                    thumbnails.push(imageUrl);
                    fullSizeUrls.push(fullImageUrl || imageUrl);
                    currentIndex = thumbnails.length - 1;
                }
                const useFullSize = settings.siteSettings.tormac.useFullSizeInLightbox;
                if (useFullSize) {
                    processImageUrls(fullSizeUrls, function(processedUrls) {
                        showImageLightbox(imageUrl, thumbnails, processedUrls, currentIndex, true);
                    });
                } else {
                    showImageLightbox(imageUrl, thumbnails, fullSizeUrls, currentIndex, useFullSize);
                }
            }
        }
    };
    const sitesConfig = {
        tormac: {
            matchUrl: 'https://tormac.org/',
            topicLinkSelector: 'a[href*="/torrent/"], a[href*="viewtopic.php?t="]',
            firstPostSelector: 'div.post_body',
            spoilerSelector: '.sp-wrap, .spoiler, .sp-body, .hide.spoiler-wrap',
            getScreenshots: siteSpecificFunctions.tormac.getScreenshotLinks,
            getScreenshotsFromPost: siteSpecificFunctions.tormac.getScreenshotsFromPost,
            getCover: siteSpecificFunctions.tormac.getCover,
            openImage: siteSpecificFunctions.tormac.openImage
        }
    };
    //====================================
    // ОБРАБОТКА ИЗОБРАЖЕНИЙ
    //====================================
    let cachedRequests = {};
    function cleanupCache() {
        const now = Date.now() / 1000;
        Object.keys(cachedRequests).forEach(url => {
            if (now - cachedRequests[url].timestamp > settings.cacheTTL) {
                delete cachedRequests[url];
            }
        });
        const cacheKeys = Object.keys(cachedRequests);
        if (cacheKeys.length > settings.maxCacheSize) {
            const keysToRemove = cacheKeys.slice(0, cacheKeys.length - settings.maxCacheSize);
            keysToRemove.forEach(key => delete cachedRequests[key]);
        }
    }
    function processImageUrls(fullSizeUrls, callback) {
        let pendingUrls = 0;
        const processedUrls = [...fullSizeUrls];
        if (fullSizeUrls.length === 0) {
            return callback(processedUrls);
        }
        for (let i = 0; i < fullSizeUrls.length; i++) {
            const url = fullSizeUrls[i];
            const supportedHostings = settings.siteSettings.tormac.supportedHostings;
            const isUmpic = supportedHostings.includes('umpic') && url.match(/umpic\.ru\//);
            const isFastPic = supportedHostings.includes('fastpic') && url.match(/fastpic\.(org|ru)\/(view|big)\//);
            const isImageBam = supportedHostings.includes('imagebam') && url.match(/imagebam\.com\/view\//);
            const isImgBox = supportedHostings.includes('imgbox') && url.match(/imgbox\.com\//);
            const isImageBan = supportedHostings.includes('imageban') && url.match(/imageban\.ru\/show\//);
            const isImgur = supportedHostings.includes('imgur') && url.match(/imgur\.com\/[a-zA-Z0-9]+/);
            const isPostImage = supportedHostings.includes('postimage') && url.match(/postimg\.cc\/[a-zA-Z0-9]+/);
            if (isUmpic || isFastPic || isImageBam || isImgBox || isImageBan || isImgur || isPostImage) {
                pendingUrls++;
                let requestUrl = url;
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: requestUrl,
                    onload: function(response) {
                        const html = response.responseText;
                        let directUrl = null;
                        if (isUmpic) {
                            const imgMatch = html.match(/<img[^>]+src=["'](https?:\/\/s\d+\.umpic\.ru\/big\/[^"']+)["']/i);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isFastPic) {
                            const imgMatch = html.match(/<img src="(https?:\/\/i\d+\.fastpic\.org\/big\/[^"]+)"[^>]*class="image/);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isImageBam) {
                            const imgMatch = html.match(/<div class="view-image">.*?src="(https?:\/\/images\d+\.imagebam\.com\/[^"]+)".*?<\/div>/s);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isImgBox) {
                            const imgMatch = html.match(/<div class="image-container">.*?src="(https?:\/\/images\d+\.imgbox\.com\/[^"]+)".*?<\/div>/s);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isImageBan) {
                            const imgMatch = html.match(/<div class="docs-pictures clearfix">.*?src="(https?:\/\/i\d+\.imageban\.ru\/out\/[^"]+)".*?<\/div>/s);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isImgur) {
                            const imgMatch = html.match(/<img[^>]*src="(https?:\/\/i\.imgur\.com\/[^"]+\.(jpg|png|gif))"/);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        } else if (isPostImage) {
                            const imgMatch = html.match(/<img[^>]*src="(https?:\/\/i\.postimg\.cc\/[^"]+\.(jpg|png|gif))"/);
                            if (imgMatch && imgMatch[1]) directUrl = imgMatch[1];
                        }
                        if (directUrl) {
                            processedUrls[i] = directUrl;
                            log('Получена прямая ссылка:', directUrl);
                        }
                        pendingUrls--;
                        if (pendingUrls === 0) {
                            callback(processedUrls);
                        }
                    },
                    onerror: function(error) {
                        log('Ошибка получения URL:', error);
                        pendingUrls--;
                        if (pendingUrls === 0) {
                            callback(processedUrls);
                        }
                    }
                });
            }
        }
        if (pendingUrls === 0) {
            callback(processedUrls);
        }
    }
    function collectImagesFromPreview(thumbnails, fullSizeUrls) {
        const previewContainer = document.getElementById('torrent-preview');
        if (previewContainer) {
            const imageContainers = previewContainer.querySelectorAll('a[href]');
            imageContainers.forEach(link => {
                const img = link.querySelector('img');
                if (img && img.src) {
                    thumbnails.push(img.src);
                    fullSizeUrls.push(link.href);
                }
            });
        }
    }
    //====================================
    // ЛАЙТБОКС
    //====================================
    let isLightboxOpen = false;
    function createControlButton(content, title, onClick, fontSize = '28px') {
        const button = createElement('div', {
            innerHTML: content,
            title: title
        }, {
            color: 'white',
            fontSize: fontSize,
            cursor: 'pointer',
            opacity: '0.7',
            backgroundColor: 'rgba(0, 0, 0, 0.3)',
            borderRadius: '50%',
            width: `${settings.navButtonsSize}px`,
            height: `${settings.navButtonsSize}px`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        });
        button.addEventListener('mouseenter', () => button.style.opacity = '1');
        button.addEventListener('mouseleave', () => button.style.opacity = '0.7');
        if (onClick) button.addEventListener('click', onClick);
        return button;
    }
    function showImageLightbox(imageUrl, thumbnails = [], fullSizeUrls = [], currentIndex = -1, useFullSizeForDisplay = false) {
        isLightboxOpen = true;
        const existingLightbox = document.getElementById('tormac-preview-lightbox');
        if (existingLightbox) existingLightbox.remove();
        const originalOverflow = document.body.style.overflow;
        const isDarkTheme = settings.colorTheme === 'dark' ||
            (settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
        const lightbox = createElement('div', { id: 'tormac-preview-lightbox' }, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            backgroundColor: isDarkTheme ? 'rgba(0, 0, 0, 0.9)' : 'rgba(0, 0, 0, 0.8)',
            zIndex: '10000',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            cursor: 'pointer'
        });
        const imgContainer = createElement('div', {}, {
            position: 'relative',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            maxWidth: '95vw',
            maxHeight: '95vh',
            overflow: 'visible'
        });
        const contentContainer = createElement('div', {}, {
            position: 'relative',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            overflow: 'visible'
        });
        const loadingIndicator = createElement('div', {
            textContent: 'Загрузка...'
        }, {
            color: 'white',
            fontSize: '20px',
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            zIndex: '10001'
        });
        contentContainer.appendChild(loadingIndicator);
        const img = createElement('img', {}, {
            maxWidth: `${settings.lightboxThumbnailSize}px`,
            maxHeight: `${settings.lightboxThumbnailSize}px`,
            border: `2px solid ${isDarkTheme ? '#444' : 'white'}`,
            boxShadow: '0 0 20px rgba(0, 0, 0, 0.5)',
            cursor: 'move',
            display: 'none',
            zIndex: '10002'
        });
        img.onload = () => {
            loadingIndicator.style.display = 'none';
            img.style.display = 'block';
        };
        img.onerror = () => {
            loadingIndicator.textContent = 'Ошибка загрузки изображения';
        };
        img.src = useFullSizeForDisplay && currentIndex >= 0 && fullSizeUrls[currentIndex] ?
            fullSizeUrls[currentIndex] : imageUrl;
        contentContainer.appendChild(img);
        const closeLightbox = () => {
            document.body.style.overflow = originalOverflow;
            lightbox.remove();
            document.removeEventListener('keydown', keyHandler);
            isLightboxOpen = false;
        };
        let isDragging = false;
        let startX, startY, translateX = 0, translateY = 0, lastTranslateX = 0, lastTranslateY = 0;
        let scale = 1;
        const minScale = 0.5, maxScale = 3, scaleStep = 0.1;
        const prevImage = () => {
            if (thumbnails.length > 1 && currentIndex > 0) {
                currentIndex--;
                loadingIndicator.style.display = 'block';
                img.style.display = 'none';
                img.src = useFullSizeForDisplay ? fullSizeUrls[currentIndex] : thumbnails[currentIndex];
                updateNavButtons();
            }
        };
        const nextImage = () => {
            if (thumbnails.length > 1 && currentIndex < thumbnails.length - 1) {
                currentIndex++;
                loadingIndicator.style.display = 'block';
                img.style.display = 'none';
                img.src = useFullSizeForDisplay ? fullSizeUrls[currentIndex] : thumbnails[currentIndex];
                updateNavButtons();
            }
        };
        const resetImage = () => {
            scale = 1;
            translateX = 0;
            translateY = 0;
            lastTranslateX = 0;
            lastTranslateY = 0;
            contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
        };
        const toggleFullscreen = () => {
            if (!document.fullscreenElement) {
                lightbox.requestFullscreen().catch(err => log('Ошибка включения полноэкранного режима:', err));
            } else {
                document.exitFullscreen().catch(err => log('Ошибка выхода из полноэкранного режима:', err));
            }
        };
        const startDrag = e => {
            if (e.button === 0) {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                lastTranslateX = translateX;
                lastTranslateY = translateY;
                contentContainer.style.cursor = 'grabbing';
                e.preventDefault();
                e.stopPropagation();
            }
        };
        const moveDrag = e => {
            if (!isDragging) return;
            translateX = lastTranslateX + (e.clientX - startX);
            translateY = lastTranslateY + (e.clientY - startY);
            contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
            e.preventDefault();
            e.stopPropagation();
        };
        const endDrag = e => {
            if (isDragging) {
                isDragging = false;
                contentContainer.style.cursor = 'auto';
                img.style.cursor = 'move';
                if (e) e.stopPropagation();
            }
        };
        let mouseStartedOnImage = false;
        contentContainer.addEventListener('mousedown', () => mouseStartedOnImage = true);
        const keyHandler = e => {
            if (e.key === settings.keyboardShortcuts.close) {
                closeLightbox();
            } else if (e.key === settings.keyboardShortcuts.prev) {
                prevImage();
            } else if (e.key === settings.keyboardShortcuts.next) {
                nextImage();
            } else if (e.key === settings.keyboardShortcuts.reset) {
                resetImage();
            } else if (e.key === settings.keyboardShortcuts.fullscreen) {
                toggleFullscreen();
            }
        };
        document.addEventListener('keydown', keyHandler);
        let prevButton = null, nextButton = null;
        const updateNavButtons = () => {
            if (prevButton && nextButton) {
                prevButton.style.visibility = currentIndex > 0 ? 'visible' : 'hidden';
                nextButton.style.visibility = currentIndex < thumbnails.length - 1 ? 'visible' : 'hidden';
            }
        };
        contentContainer.addEventListener('mouseenter', () => {
            if (prevButton && nextButton && settings.navButtonsVisibility === 'hover') {
                if (currentIndex > 0) prevButton.style.display = 'flex';
                if (currentIndex < thumbnails.length - 1) nextButton.style.display = 'flex';
            }
        });
        contentContainer.addEventListener('mouseleave', () => {
            if (prevButton && nextButton && settings.navButtonsVisibility === 'hover') {
                prevButton.style.display = 'none';
                nextButton.style.display = 'none';
            }
        });
        if (thumbnails.length > 1 && currentIndex !== -1) {
            const buttonSize = settings.navButtonsSize;
            const navButtonStyles = {
                position: 'absolute',
                top: '50%',
                transform: 'translateY(-50%)',
                zIndex: '10005',
                userSelect: 'none',
                display: settings.navButtonsVisibility === 'always' ? 'flex' : 'none'
            };
            prevButton = createControlButton('‹', 'Предыдущее изображение', e => {
                e.stopPropagation();
                prevImage();
            }, `${buttonSize}px`);
            Object.assign(prevButton.style, navButtonStyles, { left: '-40px' });
            nextButton = createControlButton('›', 'Следующее изображение', e => {
                e.stopPropagation();
                nextImage();
            }, `${buttonSize}px`);
            Object.assign(nextButton.style, navButtonStyles, { right: '-40px' });
            if (settings.navButtonsVisibility === 'never') {
                prevButton.style.display = 'none';
                nextButton.style.display = 'none';
            }
            contentContainer.appendChild(prevButton);
            contentContainer.appendChild(nextButton);
            updateNavButtons();
        }
        contentContainer.addEventListener('mousedown', startDrag);
        document.addEventListener('mousemove', moveDrag);
        document.addEventListener('mouseup', endDrag);
        contentContainer.addEventListener('wheel', e => {
            e.preventDefault();
            e.stopPropagation();
            const delta = e.deltaY > 0 ? -scaleStep : scaleStep;
            scale = Math.min(maxScale, Math.max(minScale, scale + delta));
            contentContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
        }, { passive: false });
        lightbox.addEventListener('click', e => {
            if (!mouseStartedOnImage) {
                closeLightbox();
            }
            mouseStartedOnImage = false;
        });
        imgContainer.appendChild(contentContainer);
        lightbox.appendChild(imgContainer);
        document.body.appendChild(lightbox);
        document.body.style.overflow = 'hidden';
    }
    //====================================
    // ПРЕДПРОСМОТР
    //====================================
    let previewWindow = null;
    let currentPreviewLink = null;
    let hoverPreviewLink = null;
    let removeTimeout = null;
    let currentRequest = null;
    let requestInProgress = false;
    const eventHandlers = [];
    function removePreviewWindow() {
        if (previewWindow) {
            previewWindow.remove();
            previewWindow = null;
            currentPreviewLink = null;
            hoverPreviewLink = null;
        }
        eventHandlers.forEach(({ element, type, handler }) => {
            element.removeEventListener(type, handler);
        });
        eventHandlers.length = 0;
    }
    function handleSpoilersDynamic() {
        const spoilerHeaders = document.querySelectorAll('.sp-head.clickable, .sp-wrap > div[onclick]');
        spoilerHeaders.forEach(header => {
            const clickHandler = () => {
                setTimeout(() => {
                    const link = currentPreviewLink || hoverPreviewLink;
                    if (link) {
                        createPreviewWindow({ target: link }, sitesConfig.tormac);
                    }
                }, 100);
            };
            header.addEventListener('click', clickHandler);
            eventHandlers.push({ element: header, type: 'click', handler: clickHandler });
        });
    }
    function processResponseData(response, requestLink, siteConfig) {
        if (!previewWindow || (currentPreviewLink !== requestLink && hoverPreviewLink !== requestLink)) return;
        const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
        const firstPost = doc.querySelector(siteConfig.firstPostSelector);
        if (!firstPost) {
            log('Ошибка: Не удалось найти первый пост с селектором', siteConfig.firstPostSelector);
            previewWindow.innerHTML = 'Не удалось найти первый пост';
            return;
        }
        const siteName = 'tormac';
        const coverUrl = siteConfig.getCover(firstPost);
        const coverContainer = createElement('div', {}, {
            float: 'right',
            marginLeft: '10px',
            marginBottom: '10px',
            maxWidth: '150px'
        });
        if (coverUrl) {
            const coverLink = createElement('a', { href: coverUrl });
            const coverImage = createElement('img', {
                src: coverUrl,
                alt: 'Обложка'
            }, {
                maxWidth: '100%',
                height: 'auto',
                borderRadius: '6px'
            });
            addHoverEffect(coverLink, coverImage);
            coverLink.addEventListener('click', e => {
                e.preventDefault();
                const clickBehavior = settings.siteSettings[siteName].clickBehavior;
                if (clickBehavior === 'lightbox') {
                    siteConfig.openImage(coverUrl, coverUrl);
                } else {
                    window.open(coverUrl, '_blank');
                }
            });
            coverLink.appendChild(coverImage);
            coverContainer.appendChild(coverLink);
        }
        let screenshotLinks = siteConfig.getScreenshotsFromPost(firstPost);
        if (!previewWindow || (currentPreviewLink !== requestLink && hoverPreviewLink !== requestLink)) return;
        if (settings.hidePreviewIfEmpty && !coverUrl && screenshotLinks.length === 0) {
            removePreviewWindow();
            return;
        }
        const isDarkTheme = settings.colorTheme === 'dark' ||
            (settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
        previewWindow.innerHTML = '';
        if (coverUrl) {
            previewWindow.appendChild(coverContainer);
        }
        const infoElement = createElement('div', {
            textContent: `Скриншоты: ${screenshotLinks.length ? screenshotLinks.length : 'Не найдены'}`
        }, {
            marginBottom: '10px'
        });
        previewWindow.appendChild(infoElement);
        if (screenshotLinks.length > 0) {
            const imagesContainer = createElement('div', {}, {
                display: 'grid',
                gridTemplateColumns: `repeat(${settings.previewGridColumns}, 1fr)`,
                gap: '5px',
                justifyItems: 'center'
            });
            const maxVisible = settings.neverUseSpoilers ? screenshotLinks.length : settings.maxThumbnailsBeforeSpoiler;
            addImagesToContainer(imagesContainer, screenshotLinks, siteConfig.openImage, siteName, 0, maxVisible);
            previewWindow.appendChild(imagesContainer);
            if (!settings.neverUseSpoilers && screenshotLinks.length > settings.maxThumbnailsBeforeSpoiler) {
                const spoilerContainer = createElement('div', {}, {
                    marginTop: '10px'
                });
                const spoilerButton = createElement('button', {
                    textContent: 'Показать остальные скриншоты'
                }, {
                    background: isDarkTheme ? '#333' : '#f0f0f0',
                    border: `1px solid ${isDarkTheme ? '#555' : '#ccc'}`,
                    color: isDarkTheme ? '#eee' : 'black',
                    padding: '5px 10px',
                    cursor: 'pointer',
                    width: '100%',
                    borderRadius: '4px'
                });
                const hiddenImagesContainer = createElement('div', {}, {
                    display: 'none',
                    gridTemplateColumns: `repeat(${settings.previewGridColumns}, 1fr)`,
                    gap: '5px',
                    justifyItems: 'center',
                    marginTop: '10px'
                });
                addImagesToContainer(hiddenImagesContainer, screenshotLinks, siteConfig.openImage, siteName, settings.maxThumbnailsBeforeSpoiler);
                const buttonClickHandler = () => {
                    hiddenImagesContainer.style.display = hiddenImagesContainer.style.display === 'none' ? 'grid' : 'none';
                    spoilerButton.textContent = hiddenImagesContainer.style.display === 'none' ?
                        'Показать остальные скриншоты' : 'Скрыть скриншоты';
                };
                spoilerButton.addEventListener('click', buttonClickHandler);
                eventHandlers.push({ element: spoilerButton, type: 'click', handler: buttonClickHandler });
                spoilerContainer.appendChild(spoilerButton);
                spoilerContainer.appendChild(hiddenImagesContainer);
                previewWindow.appendChild(spoilerContainer);
            }
        }
        if (previewWindow) {
            const mouseEnterHandler = () => {
                clearTimeout(removeTimeout);
                removeTimeout = null;
            };
            const mouseLeaveHandler = () => {
                clearTimeout(removeTimeout);
                if (!isLightboxOpen) {
                    removeTimeout = setTimeout(() => removePreviewWindow(), settings.previewHideDelay);
                }
            };
            previewWindow.addEventListener('mouseenter', mouseEnterHandler);
            previewWindow.addEventListener('mouseleave', mouseLeaveHandler);
            requestLink.addEventListener('mouseleave', mouseLeaveHandler);
            eventHandlers.push(
                { element: previewWindow, type: 'mouseenter', handler: mouseEnterHandler },
                { element: previewWindow, type: 'mouseleave', handler: mouseLeaveHandler },
                { element: requestLink, type: 'mouseleave', handler: mouseLeaveHandler }
            );
        }
    }
    function createPreviewWindow(event, siteConfig) {
        if (!settings.enableAutoPreview || !settings.siteSettings.tormac.enabled) return;
        const link = event.target.closest(siteConfig.topicLinkSelector);
        if (!link || link.closest('h1.maintitle')) return;
        hoverPreviewLink = link;
        if (removeTimeout) {
            clearTimeout(removeTimeout);
            removeTimeout = null;
        }
        if (previewWindow && currentPreviewLink === link) return;
        if (currentRequest && requestInProgress) {
            try {
                currentRequest.abort();
            } catch (e) {
                log('Ошибка при отмене запроса:', e);
            }
            currentRequest = null;
            requestInProgress = false;
        }
        removePreviewWindow();
        currentPreviewLink = link;
        const isDarkTheme = settings.colorTheme === 'dark' ||
            (settings.colorTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
        previewWindow = createElement('div', {
            id: 'torrent-preview',
            innerHTML: 'Загрузка...'
        }, {
            position: 'absolute',
            backgroundColor: isDarkTheme ? '#222' : 'white',
            color: isDarkTheme ? '#eee' : 'black',
            border: `1px solid ${isDarkTheme ? '#444' : '#ccc'}`,
            padding: '10px',
            boxShadow: '0 0 10px rgba(0,0,0,0.5)',
            zIndex: '1000',
            maxWidth: `${settings.previewMaxWidth}px`,
            maxHeight: `${settings.previewMaxHeight}px`,
            overflowY: 'auto',
            borderRadius: '8px'
        });
        document.body.appendChild(previewWindow);
        const updatePosition = () => {
            if (!previewWindow) return;
            const rect = link.getBoundingClientRect();
            const scrollX = window.scrollX, scrollY = window.scrollY;
            switch (settings.previewPosition) {
                case 'topLeft':
                    previewWindow.style.top = `${rect.top + scrollY - previewWindow.offsetHeight - 5}px`;
                    previewWindow.style.left = `${rect.left + scrollX}px`;
                    break;
                case 'topRight':
                    previewWindow.style.top = `${rect.top + scrollY - previewWindow.offsetHeight - 5}px`;
                    previewWindow.style.left = `${rect.right + scrollX - previewWindow.offsetWidth}px`;
                    break;
                case 'bottomLeft':
                    previewWindow.style.top = `${rect.bottom + scrollY + 5}px`;
                    previewWindow.style.left = `${rect.left + scrollX}px`;
                    break;
                case 'bottomRight':
                default:
                    previewWindow.style.top = `${rect.bottom + scrollY + 5}px`;
                    previewWindow.style.left = `${rect.right + scrollX - previewWindow.offsetWidth}px`;
                    break;
            }
        };
        updatePosition();
        const scrollHandler = () => updatePosition();
        window.addEventListener('scroll', scrollHandler);
        eventHandlers.push({ element: window, type: 'scroll', handler: scrollHandler });
        if (window.location.href.includes('viewtopic.php?t=')) {
            const firstPost = document.querySelector(siteConfig.firstPostSelector);
            if (firstPost) {
                processResponseData({ responseText: document.documentElement.outerHTML }, link, siteConfig);
                handleSpoilersDynamic();
                return;
            }
        }
        let requestUrl = link.href;
        if (requestUrl.includes('/torrent/')) {
            const topicId = requestUrl.match(/\.(\d+)$/);
            if (!topicId) {
                log('Ошибка: Не удалось извлечь topicId из URL:', requestUrl);
                previewWindow.innerHTML = 'Ошибка обработки URL';
                return;
            }
            requestUrl = `https://tormac.org/viewtopic.php?t=${topicId[1]}`;
        }
        if (!requestUrl.startsWith('https://tormac.org/')) {
            log('Ошибка: Недопустимый URL:', requestUrl);
            previewWindow.innerHTML = 'Недопустимый URL';
            return;
        }
        const cached = cachedRequests[requestUrl];
        if (cached && (Date.now() / 1000 - cached.timestamp) < settings.cacheTTL) {
            log('Используем кэшированный ответ для:', requestUrl);
            processResponseData(cached, link, siteConfig);
            handleSpoilersDynamic();
            return;
        }
        requestInProgress = true;
        currentRequest = GM_xmlhttpRequest({
            method: 'GET',
            url: requestUrl,
            onload: response => {
                requestInProgress = false;
                cachedRequests[requestUrl] = { responseText: response.responseText, timestamp: Date.now() / 1000 };
                cleanupCache();
                if (hoverPreviewLink !== link && currentPreviewLink !== link) {
                    log('Мышь перешла на другую ссылку, игнорируем ответ для:', requestUrl);
                    return;
                }
                if (!previewWindow) return;
                processResponseData(response, link, siteConfig);
                handleSpoilersDynamic();
                currentRequest = null;
            },
            onerror: error => {
                requestInProgress = false;
                currentRequest = null;
                if (previewWindow) previewWindow.innerHTML = 'Ошибка загрузки данных';
                log('Ошибка AJAX-запроса:', error);
            }
        });
    }
    const handleMouseEnter = debounce(function(e) {
        if (isLightboxOpen) return;
        const siteConfig = sitesConfig.tormac;
        if (siteConfig && window.location.href.startsWith(siteConfig.matchUrl)) {
            createPreviewWindow(e, siteConfig);
        }
    }, 100);
    const handleMouseOver = e => {
        if (isLightboxOpen) return;
        const link = e.target.closest(sitesConfig.tormac.topicLinkSelector);
        if (link && link !== hoverPreviewLink) {
            hoverPreviewLink = link;
        }
    };
    //====================================
    // ИНИЦИАЛИЗАЦИЯ
    //====================================
    document.addEventListener('DOMContentLoaded', () => {
        handleSpoilersDynamic();
        const observer = new MutationObserver(() => {
            const link = currentPreviewLink || hoverPreviewLink;
            if (link) {
                createPreviewWindow({ target: link }, sitesConfig.tormac);
            }
        });
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
    GM_registerMenuCommand('⚙️ Настройки tormac Preview', openSettingsDialog);
    document.addEventListener('mouseenter', handleMouseEnter, true);
    document.addEventListener('mouseover', handleMouseOver);
})();
 

Yello


admin, удобные штуки
 
logo
Error