F95Zone Enhanced Page Loader

Раскрывает спойлеры, загружает изображения, нажимает "загрузить еще", проходит страницы-маски и ОЧИЩАЕТ ЗАГОЛОВОК, добавляя в него инфо-список

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         F95Zone Enhanced Page Loader
// @version      0.1
// @description  Раскрывает спойлеры, загружает изображения, нажимает "загрузить еще", проходит страницы-маски и ОЧИЩАЕТ ЗАГОЛОВОК, добавляя в него инфо-список
// @match        https://f95zone.to/*
// @grant        none
// @run-at       document-end
// @namespace https://greasyfork.org/users/789838
// ==/UserScript==

(function() {
    'use strict'; // Включаем "строгий режим" JavaScript.

    /**
     * Вспомогательная функция для вывода сообщений в консоль разработчика (F12).
     * @param {string} message - Сообщение для вывода.
     */
    const log = (message) => console.log(`[F95Zone Loader] ${message}`);

    // --- Глобальные переменные для управления заголовком ---
    // Переменная для хранения оригинального "чистого" заголовка
    let originalBaseTitle = null;
    // Переменная для хранения последнего заголовка, который МЫ установили
    let lastSetTitle = null;

    // --- ФУНКЦИЯ ДЛЯ СТРАНИЦЫ-МАСКИ ---
    /**
     * Ищет и нажимает на ссылку "Продолжить" (a.host_link) на страницах-масках.
     * @returns {boolean} - true, если ссылка была найдена и нажата, иначе false.
     */
    function clickContinueLink() {
        // Ищем ссылку по классу 'host_link'
        const continueLink = document.querySelector('a.host_link');

        if (continueLink) {
            log('Страница-маска обнаружена. Нажимаем "Продолжить"...');
            // Немедленно нажимаем на ссылку
            continueLink.click();
            return true;
        }
        return false;
    }

    // --- ГЛАВНАЯ ФУНКЦИЯ-ОРКЕСТРАТОР (ДЛЯ СТРАНИЦ ТЕМ) ---
    /**
     * Эта функция является центральной точкой управления. Она вызывает все остальные
     * функции в правильном порядке для полной обработки страницы.
     */
    function processPageElements() {
        log('Запуск комплексной обработки элементов...');
        expandSpoilers();      // Сначала раскрываем спойлеры, чтобы сделать изображения видимыми.
        loadLazyImages();      // Затем загружаем все "ленивые" изображения.
        clickLoadMoreButton(); // Ищем кнопку "загрузить еще".
        cleanPageTitle();      // Обновляем заголовок (на случай, если теги загрузились динамически).
    }

    // --- ФУНКЦИЯ ДЛЯ РАСКРЫТИЯ СПОЙЛЕРОВ ---
    /**
     * Находит на странице все скрытые спойлеры и программно нажимает на них,
     * чтобы показать их содержимое.
     */
    function expandSpoilers() {
        // Ищем все элементы с классом '.bbCodeSpoiler-button', которые еще не были нами обработаны.
        const spoilerButtons = document.querySelectorAll('.bbCodeSpoiler-button:not([data-processed="true"])');

        if (spoilerButtons.length > 0) {
            log(`Найдено ${spoilerButtons.length} новых спойлеров для раскрытия.`);
            spoilerButtons.forEach(button => {
                button.click(); // Имитируем клик мышью по кнопке спойлера.
                button.setAttribute('data-processed', 'true');
            });
        }
    }

    // --- ФУНКЦИЯ ДЛЯ ЗАГРУЗКИ "ЛЕНИВЫХ" ИЗОБРАЖЕНИЙ ---
    /**
     * Находит все изображения, которые используют механизм "ленивой загрузки" (lazy loading),
     * и заставляет их загрузиться немедленно.
     */
    function loadLazyImages() {
        // Ищем все теги <img> с классом 'lazyload', которые еще не были нами обработаны.
        const lazyImages = document.querySelectorAll('img.lazyload:not([data-processed="true"])');

        if (lazyImages.length > 0) {
            log(`Найдено ${lazyImages.length} "ленивых" изображений для загрузки.`);
            lazyImages.forEach(img => {
                const dataSrc = img.getAttribute('data-src');

                // Проверяем, что 'data-src' существует и что он еще не присвоен в 'src'.
                if (dataSrc && img.src !== dataSrc) {
                    img.src = dataSrc;
                    img.classList.remove('lazyload');
                }
                img.setAttribute('data-processed', 'true');
            });
        }
    }

    // --- ФУНКЦИЯ ДЛЯ НАЖАТИЯ КНОПКИ "ЗАГРУЗИТЬ ЕЩЕ" ---
    /**
     * Ищет и нажимает на ссылки/кнопки, которые динамически подгружают
     * дополнительный контент на страницу (например, старые комментарии).
     */
    function clickLoadMoreButton() {
        // Ищем ссылку с классом 'js-loadMore'.
        const loadMoreLink = document.querySelector('a.js-loadMore:not([data-processed="true"])');

        if (loadMoreLink) {
            log('Найдена кнопка "загрузить еще", нажимаем...');
            loadMoreLink.click();
            loadMoreLink.setAttribute('data-processed', 'true');
        }
    }

    /**
     * ФУНКЦИЯ: Собирает текст из элементов списка 'listInline--bullet' в одну строку.
     * @returns {string} - Отформатированная строка (например, "Item 1, Item 2") или пустая строка.
     */
    function getListItemsAsText() {
        // Ищем НЕ <li>, а сразу <a class="tagItem"> (ссылки тегов)
        // которые находятся внутри списков с нужными классами.
        // Это предотвращает попадание "Автор темы" и "Дата начала" в список.
        const listItems = document.querySelectorAll('.listInline.listInline--bullet a.tagItem');

        if (listItems.length > 0) {
            const texts = Array.from(listItems).map(item => {
                // .textContent.trim() - получаем текст и убираем лишние пробелы по краям
                return item.textContent.trim();
            });
            // Соединяем все найденные теги в одну строку через запятую и пробел.
            return texts.join(', ');
        }
        return ""; // Возвращаем пустую строку, если ничего не найдено.
    }


    /**
     * ФУНКЦИЯ: Очищает заголовок, добавляя в него информацию из списков.
     * Предотвращает бесконечный цикл, используя 'originalBaseTitle'.
     * @returns {boolean} - true, если заголовок был изменен, иначе false.
     */
    function cleanPageTitle() {
        const titleElement = document.querySelector('title');
        if (!titleElement) return false;

        const currentTitle = titleElement.textContent;

        // --- 1. Находим "чистый" базовый заголовок (только один раз) ---
        // Если мы еще не сохраняли базовый заголовок, делаем это.
        if (originalBaseTitle === null) {
            originalBaseTitle = currentTitle; // По умолчанию, базовый - это текущий

            // Ищем разделитель в *текущем* (который = оригинальному) заголовке
            let separatorIndex = currentTitle.indexOf(" | F95zone");
            if (separatorIndex === -1) separatorIndex = currentTitle.indexOf("| F95zone");
            if (separatorIndex === -1) separatorIndex = currentTitle.indexOf(" _ F95zone"); // с неразрывным пробелом
            if (separatorIndex === -1) separatorIndex = currentTitle.indexOf(" _ F95zone"); // с обычным пробелом

            // Если нашли разделитель, отсекаем суффикс и сохраняем.
            if (separatorIndex !== -1) {
                originalBaseTitle = currentTitle.substring(0, separatorIndex);
            }
        }

        // --- 2. Получаем текст из списков ---
        const listItemsText = getListItemsAsText();

        // --- 3. Собираем новый заголовок ---
        // Всегда строим заголовок на основе *оригинального* базового
        let newTitle = originalBaseTitle;
        if (listItemsText) {
            // Если текст из списков найден, добавляем его БЕЗ ДЕФИСА
            newTitle = `${originalBaseTitle} ${listItemsText}`;
        }

        // --- 4. Обновляем заголовок, если он отличается от текущего ---
        if (newTitle !== currentTitle) {
            document.title = newTitle; // Установка document.title изменит titleElement.textContent
            lastSetTitle = newTitle; // Сохраняем то, что мы установили
            log(`Заголовок страницы обновлен: "${newTitle}"`);
            return true;
        }

        // Если заголовок уже правильный, просто обновляем lastSetTitle
        lastSetTitle = newTitle;
        return false;
    }

    /**
     * Наблюдатель за основным контентом (новые посты и т.д.).
     */
    const contentObserver = new MutationObserver((mutationsList) => {
        // Эта функция будет вызываться каждый раз, когда на странице что-то меняется.
        for (const mutation of mutationsList) {
            // Нас интересуют только те изменения, где были добавлены новые элементы (узлы).
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                // "Debounce": используем задержку, чтобы не запускать функцию 100 раз в секунду
                // во время активной подгрузки.
                clearTimeout(contentObserver.debounce);
                contentObserver.debounce = setTimeout(() => {
                    log('Обнаружен новый контент на странице, запускаем повторную обработку...');
                    processPageElements();
                }, 500);
                return; // Выходим из цикла, т.к. мы уже установили таймер.
            }
        }
    });

    /**
     * Наблюдатель специально для заголовка <title>.
     * Предотвращает бесконечный цикл, сравнивая с 'lastSetTitle'.
     */
    const titleObserver = new MutationObserver(() => {
        const currentTitle = document.title;

        // ЕСЛИ (мы уже устанавливали заголовок) И (текущий заголовок НЕ равен тому, что мы установили)
        // Это значит, что-то *другое* (другой скрипт) изменило заголовок.
        if (lastSetTitle && currentTitle !== lastSetTitle) {
            log('Обнаружено стороннее изменение заголовка. Восстанавливаем...');
            // Перезапускаем нашу функцию, чтобы она все исправила.
            cleanPageTitle();
        }
    });

    // --- ЗАПУСК СКРИПTA ---
    log('Скрипт F95Zone Enhanced Loader v3.2 инициализирован.');

    /**
     * Функция для первоначального сканирования страницы после ее полной загрузки.
     */
    const runInitialScan = () => {
        // 1. Сначала пытаемся нажать "Продолжить" (для страниц-масок)
        const clickedContinue = clickContinueLink();

        // 2. Если это НЕ была страница-маска, проверяем, не страница ли это темы
        if (!clickedContinue && window.location.href.includes('/threads/')) {
            log('Страница темы обнаружена. Запускаем полный набор функций.');

            // Запускаем первую проверку элементов
            processPageElements();

            // Активируем наблюдателя за контентом
            contentObserver.observe(document.body, {
                childList: true,
                subtree: true
            });

            // Активируем наблюдателя за заголовком
            const titleElement = document.querySelector('title');
            if (titleElement) {
                titleObserver.observe(titleElement, {
                    childList: true
                });
            }
        } else if (!clickedContinue) {
            log('Это не страница темы и не страница-маска. Скрипт в режиме ожидания.');
        }
    };

    // Ждем, пока вся страница (включая скрипты) полностью загрузится.
    if (document.readyState === 'complete') {
        runInitialScan();
    } else {
        window.addEventListener('load', runInitialScan, { once: true });
    }

})();