YouTube: Строгий фильтр по языку (RU/EN)

Добавляет кнопку для строгой фильтрации видео. Режим "Только русский" показывает видео с кириллицей. Режим "Только английский" показывает видео с латиницей, но скрывает, если в названии есть кириллица.

// ==UserScript==
// @name         YouTube: Строгий фильтр по языку (RU/EN)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Добавляет кнопку для строгой фильтрации видео. Режим "Только русский" показывает видео с кириллицей. Режим "Только английский" показывает видео с латиницей, но скрывает, если в названии есть кириллица.
// @author       torch
// @match        *://www.youtube.com/results*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license MIT 
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. Управление состоянием фильтра ---
    // 'none', 'russian', 'english'
    let activeFilterMode = GM_getValue('languageFilterMode', 'none');
    let isFilterEnabled = GM_getValue('isFilterEnabled', false);
    let menuHideTimer;

    // --- 2. Стили ---
    GM_addStyle(`
        #language-filter-container { position: fixed; bottom: 20px; right: 20px; z-index: 10000; }
        #language-filter-toggle { width: 56px; height: 56px; background-color: #303030; color: #FFFFFF; border: 1px solid #505050; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 8px rgba(0,0,0,0.2); transition: background-color 0.3s; }
        #language-filter-toggle:hover { background-color: #4d4d4d; }
        #language-filter-toggle.active { background-color: #007bff; border-color: #0056b3; }
        #language-filter-toggle.active:hover { background-color: #0056b3; }
        #language-filter-toggle svg { width: 24px; height: 24px; fill: #FFFFFF; }
        #language-filter-menu { position: absolute; bottom: 65px; right: 0; background-color: #282828; border: 1px solid #505050; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); padding: 8px; width: 220px; opacity: 0; transform: translateY(10px); transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
        #language-filter-menu.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
        .language-option { display: flex; align-items: center; padding: 8px; cursor: pointer; border-radius: 4px; color: #FFFFFF; }
        .language-option:hover { background-color: #3d3d3d; }
        .language-option label { cursor: pointer; width: 100%; margin-left: 10px; }
        .language-option input[type="radio"] { cursor: pointer; }
        .filter-hidden { display: none !important; }
    `);

    // --- 3. Создание HTML-элементов ---
    const container = document.createElement('div');
    container.id = 'language-filter-container';

    const toggleButton = document.createElement('button');
    toggleButton.id = 'language-filter-toggle';
    toggleButton.innerHTML = `<svg viewBox="0 0 24 24"><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"></path></svg>`;

    const menu = document.createElement('div');
    menu.id = 'language-filter-menu';
    menu.innerHTML = `
        <div class="language-option">
            <input type="radio" id="filter-mode-russian" name="filter-mode" value="russian">
            <label for="filter-mode-russian">Только русский</label>
        </div>
        <div class="language-option">
            <input type="radio" id="filter-mode-english" name="filter-mode" value="english">
            <label for="filter-mode-english">Только английский (без RU)</label>
        </div>
         <div class="language-option">
            <input type="radio" id="filter-mode-none" name="filter-mode" value="none">
            <label for="filter-mode-none">Сбросить (показать все)</label>
        </div>
    `;

    container.appendChild(menu);
    container.appendChild(toggleButton);
    document.body.appendChild(container);

    // --- 4. Логика фильтрации ---
    const regexPatterns = {
        russian: /[а-яА-ЯЁё]/,
        english: /[a-zA-Z]/
    };

    const applyFilter = () => {
        if (!isFilterEnabled || activeFilterMode === 'none') {
            showAllVideos();
            return;
        }

        const videoSelectors = 'ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-shelf-renderer';

        document.querySelectorAll(videoSelectors).forEach(video => {
            const titleElements = Array.from(video.querySelectorAll('#video-title'));
            if (titleElements.length === 0) return;

            // Для полок с видео - проверяем все заголовки
            const titlesText = titleElements.map(el => el.textContent || '').join(' ');

            const hasCyrillic = regexPatterns.russian.test(titlesText);
            const hasLatin = regexPatterns.english.test(titlesText);
            let shouldHide = false;

            if (activeFilterMode === 'russian') {
                if (!hasCyrillic) {
                    shouldHide = true;
                }
            } else if (activeFilterMode === 'english') {
                // Скрываем, если есть русские буквы ИЛИ если нет английских
                if (hasCyrillic || !hasLatin) {
                    shouldHide = true;
                }
            }

            if (shouldHide) {
                video.classList.add('filter-hidden');
            } else {
                video.classList.remove('filter-hidden');
            }
        });
    };

    const showAllVideos = () => {
        document.querySelectorAll('.filter-hidden').forEach(video => {
            video.classList.remove('filter-hidden');
        });
    };

    // --- 5. Обновление UI и сохранение состояния ---
    const updateUIAndSaveState = () => {
        // Кнопка
        if (isFilterEnabled && activeFilterMode !== 'none') {
            toggleButton.classList.add('active');
            toggleButton.title = 'Фильтр активен. Наведите для смены режима.';
        } else {
            toggleButton.classList.remove('active');
            toggleButton.title = 'Включить фильтр по языку';
        }
        // Радиокнопки
        document.querySelector(`#filter-mode-${activeFilterMode}`).checked = true;

        // Сохранение
        GM_setValue('isFilterEnabled', isFilterEnabled);
        GM_setValue('languageFilterMode', activeFilterMode);
    };

    const reapplyFilter = () => {
        if (isFilterEnabled) {
            applyFilter();
        } else {
            showAllVideos();
        }
    };

    // --- 6. Обработчики событий и MutationObserver ---
    container.addEventListener('mouseenter', () => {
        clearTimeout(menuHideTimer);
        menu.classList.add('visible');
    });

    container.addEventListener('mouseleave', () => {
        menuHideTimer = setTimeout(() => { menu.classList.remove('visible'); }, 300);
    });

    toggleButton.addEventListener('click', () => {
        isFilterEnabled = !isFilterEnabled;
        updateUIAndSaveState();
        reapplyFilter();
    });

    menu.addEventListener('change', (event) => {
        if (event.target.name === 'filter-mode') {
            activeFilterMode = event.target.value;
            // Если выбрали режим, а фильтр был выключен - включаем его
            if (activeFilterMode !== 'none' && !isFilterEnabled) {
                isFilterEnabled = true;
            }
            // Если выбрали "Сброс", но оставили главный переключатель включенным
            if(activeFilterMode === 'none' && isFilterEnabled){
                isFilterEnabled = false;
            }
            updateUIAndSaveState();
            reapplyFilter();
        }
    });

    const observer = new MutationObserver(() => {
        if (isFilterEnabled) {
            setTimeout(applyFilter, 300);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // --- 7. Первоначальный запуск ---
    updateUIAndSaveState();
    setTimeout(() => {
        reapplyFilter();
    }, 1000);
})();