YouTube Keyword Filter

Фильтр видео (белый список)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Keyword Filter
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Фильтр видео (белый список)
// @author       torch
// @match        *://www.youtube.com/@*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY_WORDS = 'yt_filter_keywords';
    const STORAGE_KEY_ACTIVE = 'yt_filter_active';

    let keywords = (localStorage.getItem(STORAGE_KEY_WORDS) || '').toLowerCase().split(',').map(k => k.trim()).filter(k => k);
    let isActive = localStorage.getItem(STORAGE_KEY_ACTIVE) === 'true';

    // --- Стили ---
    const styles = `
        #yt-safe-btn {
            position: fixed;
            bottom: 30px;
            right: 80px; /* Чуть левее чата */
            width: 50px;
            height: 50px;
            background: #065fd4;
            border: 2px solid #fff;
            border-radius: 50%;
            color: white;
            font-size: 24px;
            cursor: pointer;
            z-index: 2147483647;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            transition: transform 0.2s;
            user-select: none;
        }
        #yt-safe-btn:hover { transform: scale(1.1); }
        #yt-safe-panel {
            position: fixed;
            bottom: 90px;
            right: 80px;
            width: 300px;
            background: #212121;
            border: 1px solid #444;
            padding: 15px;
            border-radius: 10px;
            z-index: 2147483647;
            box-shadow: 0 10px 30px rgba(0,0,0,0.7);
            display: none;
            color: #fff;
            font-family: Roboto, Arial, sans-serif;
        }
        #yt-safe-title { margin: 0 0 10px 0; font-size: 16px; font-weight: bold; }
        #yt-safe-textarea {
            width: 100%;
            height: 80px;
            background: #121212;
            color: #fff;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 5px;
            box-sizing: border-box;
            margin-bottom: 10px;
            resize: vertical;
        }
        .yt-safe-row { display: flex; justify-content: space-between; gap: 10px; }
        .yt-safe-btn-ui {
            flex: 1;
            padding: 8px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            color: #fff;
        }
        #yt-btn-toggle { background: #cc0000; }
        #yt-btn-toggle.active { background: #2ba640; }
        #yt-btn-save { background: #3ea6ff; color: #000; }
        .yt-safe-desc { font-size: 11px; color: #aaa; margin-top: 8px; line-height: 1.3; }
    `;

    // Добавляем стили безопасным методом
    const styleEl = document.createElement('style');
    styleEl.textContent = styles;
    document.head.appendChild(styleEl);

    // --- Создание интерфейса через DOM API (без innerHTML) ---
    function createSafeInterface() {
        if (document.getElementById('yt-safe-btn')) return;

        // 1. Кнопка
        const btn = document.createElement('div');
        btn.id = 'yt-safe-btn';
        btn.textContent = '🛡️';
        btn.title = 'Настроить фильтр';
        btn.onclick = (e) => {
            e.stopPropagation();
            const panel = document.getElementById('yt-safe-panel');
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        };
        document.body.appendChild(btn);

        // 2. Панель
        const panel = document.createElement('div');
        panel.id = 'yt-safe-panel';

        // Заголовок
        const title = document.createElement('div');
        title.id = 'yt-safe-title';
        title.textContent = 'Фильтр (Белый список)';
        panel.appendChild(title);

        // Текстовое поле
        const textarea = document.createElement('textarea');
        textarea.id = 'yt-safe-textarea';
        textarea.value = localStorage.getItem(STORAGE_KEY_WORDS) || '';
        textarea.placeholder = 'Слова через запятую (пример: майнкрафт, asmr)';
        panel.appendChild(textarea);

        // Кнопки
        const btnRow = document.createElement('div');
        btnRow.className = 'yt-safe-row';

        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'yt-btn-toggle';
        toggleBtn.className = 'yt-safe-btn-ui';
        toggleBtn.textContent = isActive ? 'ВКЛЮЧЕН' : 'ВЫКЛЮЧЕН';
        if (isActive) toggleBtn.classList.add('active');

        toggleBtn.onclick = () => {
            isActive = !isActive;
            localStorage.setItem(STORAGE_KEY_ACTIVE, isActive);
            toggleBtn.textContent = isActive ? 'ВКЛЮЧЕН' : 'ВЫКЛЮЧЕН';
            toggleBtn.classList.toggle('active', isActive);
            console.log('[Фильтр] Статус:', isActive);
            runFilter();
        };

        const saveBtn = document.createElement('button');
        saveBtn.id = 'yt-btn-save';
        saveBtn.className = 'yt-safe-btn-ui';
        saveBtn.textContent = 'Применить';

        saveBtn.onclick = () => {
            const text = textarea.value;
            localStorage.setItem(STORAGE_KEY_WORDS, text);
            keywords = text.toLowerCase().split(',').map(k => k.trim()).filter(k => k);
            console.log('[Фильтр] Новые слова:', keywords);
            runFilter();
            saveBtn.textContent = 'OK!';
            setTimeout(() => saveBtn.textContent = 'Применить', 1000);
        };

        btnRow.appendChild(toggleBtn);
        btnRow.appendChild(saveBtn);
        panel.appendChild(btnRow);

        // Описание
        const desc = document.createElement('div');
        desc.className = 'yt-safe-desc';
        desc.textContent = 'Оставляет только видео, содержащие эти слова. Пустое поле = показывает всё.';
        panel.appendChild(desc);

        document.body.appendChild(panel);

        // Скрытие при клике вне
        document.addEventListener('click', (e) => {
            if (!panel.contains(e.target) && e.target !== btn) {
                panel.style.display = 'none';
            }
        });
    }

    // --- Логика фильтрации ---
    function runFilter() {
        // Селекторы для видео на главной, в поиске, в плейлистах и шортс
        const selectors = [
            'ytd-rich-item-renderer',
            'ytd-video-renderer',
            'ytd-grid-video-renderer',
            'ytd-compact-video-renderer',
            'ytd-reel-item-renderer',
            'ytd-playlist-video-renderer'
        ];

        const videos = document.querySelectorAll(selectors.join(','));

        videos.forEach(video => {
            // Если выключено или список пуст - сбрасываем скрытие
            if (!isActive || keywords.length === 0) {
                video.style.display = '';
                return;
            }

            // Ищем элементы с текстом заголовка
            const titleEl = video.querySelector('#video-title, #video-title-link');
            if (!titleEl) return;

            // Получаем текст (и aria-label, т.к. там часто полное название)
            const text = (titleEl.innerText + ' ' + (titleEl.getAttribute('aria-label') || '')).toLowerCase();

            // Проверяем совпадение
            const match = keywords.some(word => text.includes(word));

            if (match) {
                video.style.display = ''; // Показать
            } else {
                video.style.display = 'none'; // Скрыть
            }
        });
    }

    // --- Запуск ---
    const observer = new MutationObserver(() => {
        // Гарантируем наличие кнопки
        if (!document.getElementById('yt-safe-btn')) {
            createSafeInterface();
        }
        // Запускаем фильтр (с задержкой для производительности)
        runFilter();
    });

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

    // Первый запуск
    setTimeout(() => {
        createSafeInterface();
        runFilter();
    }, 1000);

    console.log('[Фильтр] Скрипт v4.0 загружен (Trusted Types Fix)');

})();