YouTube Blocklist

Block YouTube videos, comments, notifications by keywords or channel/user blacklist. Hover to show X button for quick blocking.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Blocklist
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Block YouTube videos, comments, notifications by keywords or channel/user blacklist. Hover to show X button for quick blocking.
// @license      MIT
// @author       Henry Suen
// @match        https://www.youtube.com/
// @match        https://www.youtube.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @icon         https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ======================== Storage ========================
    const STORAGE_KEYS = {
        BLOCKED_CHANNELS: 'yt_blocked_channels',
        BLOCKED_USERS: 'yt_blocked_users',
        BLOCKED_KEYWORDS: 'yt_blocked_keywords',
    };

    function getBlockedChannels() {
        return GM_getValue(STORAGE_KEYS.BLOCKED_CHANNELS, []);
    }
    function setBlockedChannels(list) {
        GM_setValue(STORAGE_KEYS.BLOCKED_CHANNELS, list);
    }
    function getBlockedUsers() {
        return GM_getValue(STORAGE_KEYS.BLOCKED_USERS, []);
    }
    function setBlockedUsers(list) {
        GM_setValue(STORAGE_KEYS.BLOCKED_USERS, list);
    }
    function getBlockedKeywords() {
        return GM_getValue(STORAGE_KEYS.BLOCKED_KEYWORDS, []);
    }
    function setBlockedKeywords(list) {
        GM_setValue(STORAGE_KEYS.BLOCKED_KEYWORDS, list);
    }

    function addBlockedChannel(channel) {
        const list = getBlockedChannels();
        const normalized = channel.trim().toLowerCase();
        if (normalized && !list.includes(normalized)) {
            list.push(normalized);
            setBlockedChannels(list);
        }
    }
    // Block channel name + also block @handle in users (so comments are also hidden)
    function addBlockedChannelWithHandle(displayName, handleHref) {
        addBlockedChannel(displayName);
        if (handleHref) {
            // handleHref is like "/@LinusTechTips" or "/@handle"
            const handle = handleHref.replace(/^\/@?/, '').trim().toLowerCase();
            if (handle) {
                // Add handle to channels list for video filtering
                const chList = getBlockedChannels();
                if (!chList.includes(handle)) {
                    chList.push(handle);
                    setBlockedChannels(chList);
                }
                // Also add @handle to users list so comments from this user are hidden
                const userHandle = '@' + handle;
                const uList = getBlockedUsers();
                const normalizedUser = userHandle.toLowerCase();
                if (!uList.includes(normalizedUser)) {
                    uList.push(normalizedUser);
                    setBlockedUsers(uList);
                }
            }
        }
    }
    function addBlockedUser(user) {
        const list = getBlockedUsers();
        const normalized = user.trim().toLowerCase();
        if (normalized && !list.includes(normalized)) {
            list.push(normalized);
            setBlockedUsers(list);
        }
    }

    // ======================== Inject Styles ========================
    function injectStyles() {
        let style = document.getElementById('ytb-blocklist-styles');
        if (style) return;
        style = document.createElement('style');
        style.id = 'ytb-blocklist-styles';
        style.textContent = `
            /* ===== Block button on video cards ===== */
            .ytb-block-btn {
                position: absolute;
                bottom: 4px;
                right: 4px;
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: rgba(0, 0, 0, 0.75);
                color: #fff;
                font-size: 16px;
                font-weight: bold;
                line-height: 28px;
                text-align: center;
                cursor: pointer;
                z-index: 9999;
                opacity: 0;
                transition: opacity 0.2s;
                border: none;
                user-select: none;
            }
            .ytb-block-btn:hover {
                background: rgba(200, 0, 0, 0.9);
                transform: scale(1.1);
            }
            /* Sidebar horizontal lockup: position button at top-right of whole card */
            .ytLockupViewModelHorizontal .ytb-block-btn {
                bottom: auto;
                top: 4px;
                right: 4px;
            }
            ytd-rich-item-renderer:hover .ytb-block-btn,
            ytd-video-renderer:hover .ytb-block-btn,
            ytd-compact-video-renderer:hover .ytb-block-btn,
            ytd-grid-video-renderer:hover .ytb-block-btn,
            ytd-reel-item-renderer:hover .ytb-block-btn,
            yt-lockup-view-model:hover .ytb-block-btn,
            .ytLockupViewModelHost:hover .ytb-block-btn,
            .ytThumbnailViewModelHost:hover .ytb-block-btn,
            yt-thumbnail-view-model:hover .ytb-block-btn,
            ytd-notification-renderer:hover .ytb-block-btn {
                opacity: 1;
            }

            /* ===== Block button on comments ===== */
            .ytb-block-btn-comment {
                position: absolute;
                bottom: 8px;
                right: 8px;
                width: 24px;
                height: 24px;
                border-radius: 50%;
                background: rgba(0, 0, 0, 0.7);
                color: #fff;
                font-size: 14px;
                font-weight: bold;
                line-height: 24px;
                text-align: center;
                cursor: pointer;
                z-index: 9999;
                opacity: 0;
                transition: opacity 0.2s;
                border: none;
                user-select: none;
            }
            .ytb-block-btn-comment:hover {
                background: rgba(200, 0, 0, 0.9);
                transform: scale(1.1);
            }
            ytd-comment-renderer:hover > .ytb-block-btn-comment,
            ytd-comment-thread-renderer:hover > .ytb-block-btn-comment {
                opacity: 1;
            }

            /* ===== Notification pre-hide 3rd span ===== */
            ytd-notification-renderer:not(.ytb-filtered) yt-formatted-string.message:not(.cbCustomTitle) > span:nth-child(n+3) {
                visibility: hidden !important;
            }
            ytd-notification-renderer.ytb-filtered yt-formatted-string.message > span {
                visibility: visible !important;
            }

            /* ===== Notification bar ===== */
            .ytb-notification-bar {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 10px 16px;
                background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
                border-bottom: 1px solid #0f3460;
                font-size: 13px;
                color: #e0e0e0;
                z-index: 10000;
            }
            .ytb-notification-bar .ytb-bar-info {
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .ytb-notification-bar .ytb-bar-count {
                background: #e53935;
                color: #fff;
                border-radius: 10px;
                padding: 2px 8px;
                font-size: 12px;
                font-weight: bold;
            }
            .ytb-notification-bar .ytb-bar-restore {
                background: rgba(255,255,255,0.1);
                color: #90caf9;
                border: 1px solid #90caf9;
                border-radius: 6px;
                padding: 4px 12px;
                cursor: pointer;
                font-size: 12px;
                transition: all 0.2s;
            }
            .ytb-notification-bar .ytb-bar-restore:hover {
                background: rgba(144,202,249,0.2);
                color: #fff;
            }

            /* ===== Panel overlay ===== */
            .ytb-panel-overlay {
                position: fixed;
                top: 0; left: 0; right: 0; bottom: 0;
                background: rgba(0,0,0,0.7);
                z-index: 99999;
                display: flex;
                align-items: center;
                justify-content: center;
                animation: ytb-fadein 0.2s ease;
            }
            @keyframes ytb-fadein {
                from { opacity: 0; }
                to { opacity: 1; }
            }
            .ytb-panel {
                background: #212121;
                color: #eee;
                border-radius: 16px;
                padding: 0;
                width: 520px;
                max-height: 80vh;
                overflow: hidden;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                box-shadow: 0 16px 48px rgba(0,0,0,0.6);
                display: flex;
                flex-direction: column;
            }
            .ytb-panel-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 20px 24px 16px;
                border-bottom: 1px solid #333;
                background: #1a1a1a;
                border-radius: 16px 16px 0 0;
            }
            .ytb-panel-header h2 {
                margin: 0;
                font-size: 18px;
                font-weight: 600;
                color: #fff;
            }
            .ytb-panel-close {
                width: 32px;
                height: 32px;
                border-radius: 50%;
                background: rgba(255,255,255,0.1);
                border: none;
                color: #aaa;
                font-size: 18px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s;
            }
            .ytb-panel-close:hover {
                background: rgba(255,255,255,0.2);
                color: #fff;
            }
            .ytb-panel-body {
                padding: 20px 24px;
                overflow-y: auto;
                flex: 1;
            }
            .ytb-section {
                margin-bottom: 24px;
            }
            .ytb-section:last-child {
                margin-bottom: 0;
            }
            .ytb-section-title {
                font-size: 13px;
                font-weight: 600;
                color: #90caf9;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                margin-bottom: 10px;
                display: flex;
                align-items: center;
                gap: 6px;
            }
            .ytb-list {
                max-height: 140px;
                overflow-y: auto;
                border: 1px solid #333;
                border-radius: 8px;
                padding: 4px;
                margin-bottom: 10px;
                background: #1a1a1a;
            }
            .ytb-list::-webkit-scrollbar { width: 6px; }
            .ytb-list::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
            .ytb-list-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 6px 10px;
                border-radius: 6px;
                transition: background 0.15s;
            }
            .ytb-list-item:hover { background: #2a2a2a; }
            .ytb-list-item span { font-size: 13px; color: #ddd; }
            .ytb-remove-btn {
                background: transparent;
                color: #e53935;
                border: 1px solid #e53935;
                border-radius: 4px;
                padding: 2px 8px;
                cursor: pointer;
                font-size: 11px;
                transition: all 0.2s;
            }
            .ytb-remove-btn:hover { background: #e53935; color: #fff; }
            .ytb-input-row { display: flex; gap: 8px; }
            .ytb-input-row input {
                flex: 1;
                background: #1a1a1a;
                border: 1px solid #444;
                color: #eee;
                padding: 8px 12px;
                border-radius: 8px;
                font-size: 13px;
                outline: none;
                transition: border-color 0.2s;
            }
            .ytb-input-row input:focus { border-color: #90caf9; }
            .ytb-input-row input::placeholder { color: #666; }
            .ytb-add-btn {
                background: #1e88e5;
                color: #fff;
                border: none;
                border-radius: 8px;
                padding: 8px 16px;
                cursor: pointer;
                font-size: 13px;
                font-weight: 500;
                transition: background 0.2s;
                white-space: nowrap;
            }
            .ytb-add-btn:hover { background: #42a5f5; }
            .ytb-empty {
                color: #555;
                font-size: 12px;
                padding: 8px 10px;
                text-align: center;
                font-style: italic;
            }
            .ytb-toast {
                position: fixed;
                bottom: 30px;
                left: 50%;
                transform: translateX(-50%);
                background: #323232;
                color: #fff;
                padding: 12px 24px;
                border-radius: 8px;
                z-index: 999999;
                font-size: 14px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.4);
                animation: ytb-fadein 0.2s ease;
                transition: opacity 0.3s;
            }

            /* ===== Masthead button (top-left, near YT logo) ===== */
            #ytb-blocklist-btn {
                margin-left: 12px;
                padding: 6px 14px;
                background: transparent;
                border: 1px solid var(--yt-spec-10-percent-layer, #ccc);
                border-radius: 18px;
                font-size: 13px;
                font-weight: 500;
                cursor: pointer;
                transition: background 0.2s;
                white-space: nowrap;
                vertical-align: middle;
                color: inherit;
            }
            #ytb-blocklist-btn:hover {
                background: var(--yt-spec-10-percent-layer, rgba(255,255,255,0.1));
            }
            html:not([dark]) #ytb-blocklist-btn {
                color: #0f0f0f !important;
                border-color: rgba(0, 0, 0, 0.1) !important;
            }
            html[dark] #ytb-blocklist-btn {
                color: #f1f1f1 !important;
                border-color: rgba(255, 255, 255, 0.2) !important;
            }

            /* ===== Channel page block button ===== */
            .ytb-channel-block-btn {
                padding: 4px 12px;
                background: rgba(200, 0, 0, 0.8);
                color: #fff;
                border: none;
                border-radius: 16px;
                font-size: 13px;
                font-weight: 500;
                cursor: pointer;
                transition: background 0.2s;
                white-space: nowrap;
                vertical-align: middle;
            }
            .ytb-channel-block-btn:hover {
                background: rgba(220, 0, 0, 1);
            }
        `;
        document.head.appendChild(style);
    }

    // ======================== Toast ========================
    function showToast(msg, duration = 2000) {
        const toast = document.createElement('div');
        toast.className = 'ytb-toast';
        toast.textContent = msg;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.style.opacity = '0';
            setTimeout(() => toast.remove(), 300);
        }, duration);
    }

    // ======================== Masthead Button (top-left, next to YT logo) ========================
    function addBlocklistButton() {
        if (document.getElementById('ytb-blocklist-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'ytb-blocklist-btn';
        btn.textContent = '🛡 Blocklist';
        btn.addEventListener('click', openPanel);

        // Insert into ytd-masthead #start (same as the reference filter script)
        function insertBtn() {
            const startContainer = document.querySelector('ytd-masthead #start');
            if (startContainer) {
                startContainer.appendChild(btn);
                return true;
            }
            return false;
        }

        if (!insertBtn()) {
            const retryInterval = setInterval(() => {
                if (insertBtn()) clearInterval(retryInterval);
            }, 500);
            // Fallback after 10s: fixed position top-left
            setTimeout(() => {
                clearInterval(retryInterval);
                if (!btn.parentElement) {
                    Object.assign(btn.style, {
                        position: 'fixed', top: '14px', left: '200px',
                        zIndex: '99998', marginLeft: '0',
                    });
                    document.body.appendChild(btn);
                }
            }, 10000);
        }
    }

    // ======================== Management Panel ========================
    // Helper: create element with properties
    function el(tag, props, children) {
        const e = document.createElement(tag);
        if (props) {
            for (const [k, v] of Object.entries(props)) {
                if (k === 'className') e.className = v;
                else if (k === 'textContent') e.textContent = v;
                else if (k === 'id') e.id = v;
                else if (k === 'placeholder') e.placeholder = v;
                else if (k === 'type') e.type = v;
                else if (k === 'title') e.title = v;
                else if (k.startsWith('on')) e[k] = v;
                else e.setAttribute(k, v);
            }
        }
        if (children) {
            for (const child of children) {
                if (typeof child === 'string') e.appendChild(document.createTextNode(child));
                else if (child) e.appendChild(child);
            }
        }
        return e;
    }

    function openPanel() {
        const existing = document.querySelector('.ytb-panel-overlay');
        if (existing) existing.remove();

        const overlay = el('div', { className: 'ytb-panel-overlay' });
        overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });

        const panel = el('div', { className: 'ytb-panel' });

        // Header
        const closeBtn = el('button', { className: 'ytb-panel-close', textContent: '\u00D7' });
        closeBtn.onclick = () => overlay.remove();
        const header = el('div', { className: 'ytb-panel-header' }, [
            el('h2', { textContent: '\u{1F6E1}\uFE0F YouTube Blocklist' }),
            closeBtn
        ]);
        panel.appendChild(header);

        // Body
        const body = el('div', { className: 'ytb-panel-body' });

        // Create sections
        function createSection(emoji, title, listId, inputId, addBtnId, placeholder) {
            const section = el('div', { className: 'ytb-section' });
            const titleRow = el('div', { className: 'ytb-section-title' }, [
                el('span', { textContent: emoji }),
                document.createTextNode(' ' + title)
            ]);
            const list = el('div', { className: 'ytb-list', id: listId });
            const input = el('input', { type: 'text', id: inputId, placeholder: placeholder });
            const addBtn = el('button', { className: 'ytb-add-btn', id: addBtnId, textContent: '+ Add' });
            const inputRow = el('div', { className: 'ytb-input-row' }, [input, addBtn]);
            section.appendChild(titleRow);
            section.appendChild(list);
            section.appendChild(inputRow);
            return section;
        }

        body.appendChild(createSection('\u{1F6AB}', 'Blocked Channels', 'ytb-channel-list', 'ytb-channel-input', 'ytb-channel-add', 'Channel name...'));
        body.appendChild(createSection('\u{1F648}', 'Blocked Users (Comments)', 'ytb-user-list', 'ytb-user-input', 'ytb-user-add', 'Username...'));
        body.appendChild(createSection('\u{1F511}', 'Blocked Keywords', 'ytb-keyword-list', 'ytb-keyword-input', 'ytb-keyword-add', 'Keyword...'));
        panel.appendChild(body);

        overlay.appendChild(panel);
        document.body.appendChild(overlay);

        function renderList(containerId, items, removeCallback) {
            const container = panel.querySelector('#' + containerId);
            while (container.firstChild) container.removeChild(container.firstChild);
            if (items.length === 0) {
                container.appendChild(el('div', { className: 'ytb-empty', textContent: 'No items added yet' }));
                return;
            }
            items.forEach((item, idx) => {
                const span = el('span', { textContent: item });
                const removeBtn = el('button', { className: 'ytb-remove-btn', textContent: 'Remove' });
                removeBtn.onclick = () => { removeCallback(idx); renderAll(); runFilter(); };
                const row = el('div', { className: 'ytb-list-item' }, [span, removeBtn]);
                container.appendChild(row);
            });
        }

        function renderAll() {
            renderList('ytb-channel-list', getBlockedChannels(), (idx) => {
                const list = getBlockedChannels(); list.splice(idx, 1); setBlockedChannels(list);
            });
            renderList('ytb-user-list', getBlockedUsers(), (idx) => {
                const list = getBlockedUsers(); list.splice(idx, 1); setBlockedUsers(list);
            });
            renderList('ytb-keyword-list', getBlockedKeywords(), (idx) => {
                const list = getBlockedKeywords(); list.splice(idx, 1); setBlockedKeywords(list);
            });
        }

        renderAll();

        panel.querySelector('#ytb-channel-add').onclick = () => {
            const input = panel.querySelector('#ytb-channel-input');
            const val = input.value.trim();
            if (val) { addBlockedChannel(val); input.value = ''; renderAll(); runFilter(); }
        };
        panel.querySelector('#ytb-user-add').onclick = () => {
            const input = panel.querySelector('#ytb-user-input');
            const val = input.value.trim();
            if (val) { addBlockedUser(val); input.value = ''; renderAll(); runFilter(); }
        };
        panel.querySelector('#ytb-keyword-add').onclick = () => {
            const input = panel.querySelector('#ytb-keyword-input');
            const val = input.value.trim();
            if (val) {
                const list = getBlockedKeywords();
                const normalized = val.toLowerCase();
                if (!list.includes(normalized)) { list.push(normalized); setBlockedKeywords(list); }
                input.value = '';
                renderAll();
                runFilter();
            }
        };

        // Enter key support
        const inputs = ['ytb-channel-input', 'ytb-user-input', 'ytb-keyword-input'];
        const btns = panel.querySelectorAll('.ytb-add-btn');
        inputs.forEach((id, i) => {
            panel.querySelector('#' + id).addEventListener('keydown', (e) => {
                if (e.key === 'Enter') btns[i].click();
            });
        });
    }

    // Register Tampermonkey menu command as well
    GM_registerMenuCommand('\u{1F4CB} Manage Blocklist', openPanel);

    // ======================== Block Button Injection ========================

    function getChannelNameFromVideoCard(card) {
        // New YouTube layout: channel link href starting with /@
        const channelLink = card.querySelector('a[href^="/@"]');
        if (channelLink && channelLink.textContent.trim()) {
            return channelLink.textContent.trim();
        }
        // Legacy layout selectors
        const selectors = [
            'ytd-channel-name yt-formatted-string a',
            'ytd-channel-name #text a',
            'ytd-channel-name a',
            '#channel-name a',
            '#text.ytd-channel-name a',
            '#byline a',
        ];
        for (const sel of selectors) {
            const el = card.querySelector(sel);
            if (el && el.textContent.trim()) return el.textContent.trim();
        }
        const channelEl = card.querySelector('ytd-channel-name #text, #channel-name #text');
        if (channelEl && channelEl.textContent.trim()) return channelEl.textContent.trim();
        return null;
    }

    function getAuthorFromComment(comment) {
        const selectors = ['#author-text span', '#author-text', 'a#author-text span', 'a#author-text'];
        for (const sel of selectors) {
            const el = comment.querySelector(sel);
            if (el && el.textContent.trim()) return el.textContent.trim();
        }
        return null;
    }

    function injectVideoBlockButton(card) {
        if (card.querySelector('.ytb-block-btn')) return;

        // Find a suitable container for the button
        const thumbnail = card.querySelector('#thumbnail, ytd-thumbnail, .ytd-thumbnail, .ytThumbnailViewModelHost')
            || card.querySelector('yt-thumbnail-view-model');
        const container = thumbnail || card;
        if (container) container.style.position = 'relative';

        const btn = document.createElement('div');
        btn.className = 'ytb-block-btn';
        btn.textContent = '\u2715';
        btn.title = 'Block this channel';
        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const { name, handle } = detectChannel(card);

            if (name) {
                if (confirm('Block channel: "' + name + '"?\n\nAll videos from this channel will be hidden.')) {
                    addBlockedChannelWithHandle(name, handle);
                    showToast('Blocked: ' + name);
                    runFilter();
                }
            } else {
                showToast('Could not detect channel name');
            }
            return false;
        });

        (thumbnail || card).appendChild(btn);
    }

    // Unified channel detection for any video card/lockup
    function detectChannel(card) {
        // 1. Try a[href^="/@"] link (homepage new layout)
        let channelLink = card.querySelector('a[href^="/@"]');
        if (!channelLink && card.parentElement) channelLink = card.parentElement.querySelector('a[href^="/@"]');
        if (!channelLink && card.parentElement?.parentElement) channelLink = card.parentElement.parentElement.querySelector('a[href^="/@"]');

        if (channelLink && channelLink.textContent.trim()) {
            return { name: channelLink.textContent.trim(), handle: channelLink.getAttribute('href') };
        }

        // 2. Sidebar / compact layout: first ytContentMetadataViewModelMetadataRow contains channel name as span text
        const metaRows = card.querySelectorAll('.ytContentMetadataViewModelMetadataRow');
        if (metaRows.length > 0) {
            const firstRowSpan = metaRows[0].querySelector('span.ytAttributedStringHost');
            if (firstRowSpan && firstRowSpan.textContent.trim()) {
                return { name: firstRowSpan.textContent.trim(), handle: null };
            }
        }

        // 3. Legacy selectors
        const legacyName = getChannelNameFromVideoCard(card);
        return { name: legacyName, handle: null };
    }

    function injectCommentBlockButton(comment) {
        if (comment.querySelector('.ytb-block-btn-comment')) return;
        comment.style.position = 'relative';

        const btn = document.createElement('div');
        btn.className = 'ytb-block-btn-comment';
        btn.textContent = '\u2715';
        btn.title = 'Block this user';
        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const author = getAuthorFromComment(comment);
            if (author) {
                if (confirm('Block user: "' + author + '"?\n\nAll comments from this user will be hidden.')) {
                    addBlockedUser(author);
                    showToast('Blocked user: ' + author);
                    runFilter();
                }
            } else {
                showToast('Could not detect username');
            }
        });
        comment.appendChild(btn);
    }

    function injectNotificationBlockButton(notification) {
        if (notification.querySelector('.ytb-block-btn')) return;
        notification.style.position = 'relative';

        const btn = document.createElement('div');
        btn.className = 'ytb-block-btn';
        btn.textContent = '\u2715';
        btn.title = 'Block this channel';
        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const msgEl = notification.querySelector('yt-formatted-string.message:not(.cbCustomTitle)');
            const firstSpan = msgEl ? msgEl.querySelector(':scope > span:first-child') : null;
            const channelName = firstSpan ? firstSpan.textContent.trim() : null;
            // Try to get handle from avatar link
            const avatarLink = notification.querySelector('a[href^="/@"]');
            const handleHref = avatarLink ? avatarLink.getAttribute('href') : null;
            if (channelName) {
                if (confirm('Block channel: "' + channelName + '" from notifications?')) {
                    addBlockedChannelWithHandle(channelName, handleHref);
                    showToast('Blocked: ' + channelName);
                    runFilter();
                }
            } else {
                showToast('Could not detect channel name');
            }
        });
        notification.appendChild(btn);
    }

    // ======================== Filtering Logic ========================

    function matchesKeyword(text) {
        if (!text) return false;
        const keywords = getBlockedKeywords();
        const lowerText = text.toLowerCase();
        return keywords.some(kw => lowerText.includes(kw));
    }

    function isChannelBlocked(channelName) {
        if (!channelName) return false;
        const blocked = getBlockedChannels();
        const lower = channelName.trim().toLowerCase();
        return blocked.some(ch => lower.includes(ch) || ch.includes(lower));
    }

    // Also check by @handle href
    function isChannelBlockedByHandle(handleHref) {
        if (!handleHref) return false;
        const handle = handleHref.replace(/^\/@/, '').trim().toLowerCase();
        if (!handle) return false;
        const blocked = getBlockedChannels();
        return blocked.some(ch => handle.includes(ch) || ch.includes(handle));
    }

    function isUserBlocked(userName) {
        if (!userName) return false;
        const blocked = getBlockedUsers();
        const lower = userName.trim().toLowerCase().replace(/^@/, '');
        return blocked.some(u => lower.includes(u) || u.includes(lower));
    }

    function filterVideoCards() {
        const videoSelectors = [
            'ytd-rich-item-renderer',
            'ytd-video-renderer',
            'ytd-compact-video-renderer',
            'ytd-grid-video-renderer',
            'ytd-reel-item-renderer',
        ];

        videoSelectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(card => {
                injectVideoBlockButton(card);

                const { name: channelName, handle: handleHref } = detectChannel(card);
                const titleEl = card.querySelector('#video-title, #title, .title');
                const title = titleEl ? titleEl.textContent.trim() : '';

                let shouldHide = false;
                if (isChannelBlocked(channelName)) shouldHide = true;
                if (isChannelBlockedByHandle(handleHref)) shouldHide = true;
                if (matchesKeyword(title)) shouldHide = true;
                if (channelName && matchesKeyword(channelName)) shouldHide = true;

                card.style.display = shouldHide ? 'none' : '';
            });
        });

        // yt-lockup-view-model elements (sidebar + other new layouts)
        document.querySelectorAll('yt-lockup-view-model').forEach(lockup => {
            // Skip if inside ytd-rich-item-renderer (already handled above)
            if (lockup.closest('ytd-rich-item-renderer')) return;

            injectVideoBlockButton(lockup);

            const { name: channelName, handle: handleHref } = detectChannel(lockup);

            // Title: from the heading link or span with role="text"
            const titleEl = lockup.querySelector('h3 a span[role="text"], a.ytLockupMetadataViewModelTitle span[role="text"], h3 a');
            const title = titleEl ? titleEl.textContent.trim() : '';

            let shouldHide = false;
            if (isChannelBlocked(channelName)) shouldHide = true;
            if (isChannelBlockedByHandle(handleHref)) shouldHide = true;
            if (matchesKeyword(title)) shouldHide = true;
            if (channelName && matchesKeyword(channelName)) shouldHide = true;

            lockup.style.display = shouldHide ? 'none' : '';
        });
    }

    function filterComments() {
        const commentSelectors = ['ytd-comment-thread-renderer', 'ytd-comment-renderer'];

        commentSelectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(comment => {
                if (selector === 'ytd-comment-thread-renderer' || !comment.closest('ytd-comment-thread-renderer')) {
                    injectCommentBlockButton(comment);
                }

                const author = getAuthorFromComment(comment);
                const contentEl = comment.querySelector('#content-text, .content-text');
                const content = contentEl ? contentEl.textContent.trim() : '';

                let shouldHide = false;
                if (isUserBlocked(author)) shouldHide = true;
                if (matchesKeyword(content)) shouldHide = true;
                if (author && matchesKeyword(author)) shouldHide = true;

                comment.style.display = shouldHide ? 'none' : '';
            });
        });
    }

    // Track if user chose to show blocked notifications (prevent re-hiding by observer)
    let notifShowingBlocked = false;

    function filterNotifications() {
        // If user is viewing blocked notifications, don't re-filter
        if (notifShowingBlocked) return;

        const notifications = document.querySelectorAll('ytd-notification-renderer');
        if (notifications.length === 0) return;

        let hiddenCount = 0;
        const hiddenItems = [];

        notifications.forEach(notification => {
            injectNotificationBlockButton(notification);

            const msgEl = notification.querySelector('yt-formatted-string.message:not(.cbCustomTitle)');
            if (!msgEl) {
                notification.classList.add('ytb-filtered');
                return;
            }

            const spans = msgEl.querySelectorAll(':scope > span');
            const channelName = spans[0] ? spans[0].textContent.trim() : '';
            const contentSpan = spans[2] || null;
            const fullText = msgEl.textContent.trim();

            let shouldHide = false;
            if (isChannelBlocked(channelName)) shouldHide = true;
            if (matchesKeyword(fullText)) shouldHide = true;

            if (shouldHide) {
                notification.style.display = 'none';
                notification.classList.add('ytb-filtered');
                hiddenCount++;
                hiddenItems.push(notification);
            } else {
                notification.style.display = '';
                notification.classList.add('ytb-filtered');
            }
        });

        updateNotificationBar(hiddenCount, hiddenItems);
    }

    function updateNotificationBar(hiddenCount, hiddenItems) {
        const itemsContainer = document.querySelector('ytd-notification-renderer')?.closest('#items');
        if (!itemsContainer) return;

        let bar = itemsContainer.querySelector('.ytb-notification-bar');

        if (hiddenCount === 0) {
            if (bar) bar.remove();
            return;
        }

        if (!bar) {
            bar = document.createElement('div');
            bar.className = 'ytb-notification-bar';
            itemsContainer.insertBefore(bar, itemsContainer.firstChild);
        }

        function renderBarContent(showing) {
            while (bar.firstChild) bar.removeChild(bar.firstChild);

            const info = el('div', { className: 'ytb-bar-info' });
            if (showing) {
                info.appendChild(el('span', { textContent: '\u26A0\uFE0F Showing ' + hiddenCount + ' blocked' }));
            } else {
                info.appendChild(el('span', { textContent: '\u{1F6E1}\uFE0F Blocked:' }));
                info.appendChild(el('span', { className: 'ytb-bar-count', textContent: String(hiddenCount) }));
            }
            bar.appendChild(info);

            const btn = el('button', { className: 'ytb-bar-restore', textContent: showing ? 'Hide again' : 'Show hidden' });
            btn.onclick = () => {
                if (showing) {
                    hiddenItems.forEach(item => {
                        item.style.display = 'none';
                        item.style.opacity = '';
                        item.style.borderLeft = '';
                    });
                    notifShowingBlocked = false;
                    renderBarContent(false);
                } else {
                    hiddenItems.forEach(item => {
                        item.style.display = '';
                        item.style.opacity = '0.6';
                        item.style.borderLeft = '3px solid #e53935';
                    });
                    notifShowingBlocked = true;
                    renderBarContent(true);
                }
            };
            bar.appendChild(btn);
        }

        renderBarContent(false);
    }

    function runFilter() {
        filterVideoCards();
        filterComments();
        filterNotifications();
        injectChannelPageBlockButton();
    }

    // Separate run for non-notification content only
    function runFilterVideosAndComments() {
        filterVideoCards();
        filterComments();
        injectChannelPageBlockButton();
    }

    // ======================== Channel Page Block Button ========================
    function injectChannelPageBlockButton() {
        // Only on channel pages (/@handle or /c/... or /channel/...)
        if (!location.pathname.match(/^\/@|^\/c\/|^\/channel\//)) return;

        dbg('Channel page detected:', location.pathname);

        // Find the channel name heading
        const heading = document.querySelector('yt-dynamic-text-view-model.ytPageHeaderViewModelTitle h1');
        if (!heading) { dbg('No heading found'); return; }
        if (heading.querySelector('.ytb-channel-block-btn')) return;

        // Get channel name from h1's first text span
        const nameSpan = heading.querySelector('span[role="text"]');
        const channelName = nameSpan ? nameSpan.childNodes[0]?.textContent?.trim() : null;
        dbg('Channel name:', channelName);

        // Get @handle
        const handleEl = document.querySelector('.ytPageHeaderViewModelContentMetadata span[style*="font-weight: 500"]');
        const handleText = handleEl ? handleEl.textContent.trim() : null;
        const handleHref = handleText ? '/' + handleText : null;
        dbg('Handle:', handleText);

        if (!channelName) return;

        const btn = document.createElement('button');
        btn.className = 'ytb-channel-block-btn';
        btn.textContent = '\u2715 Block';
        btn.title = 'Block this channel';
        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (confirm('Block channel: "' + channelName + '"?\n\nAll videos from this channel will be hidden.')) {
                addBlockedChannelWithHandle(channelName, handleHref);
                showToast('Blocked: ' + channelName);
            }
        });
        heading.style.display = 'flex';
        heading.style.alignItems = 'center';
        heading.style.gap = '12px';
        heading.appendChild(btn);
    }

    // ======================== Observer ========================
    let filterTimeout = null;
    function debouncedFilter() {
        if (filterTimeout) clearTimeout(filterTimeout);
        filterTimeout = setTimeout(() => {
            // If showing blocked notifications, only filter videos/comments
            if (notifShowingBlocked) {
                runFilterVideosAndComments();
            } else {
                runFilter();
            }
        }, 300);
    }

    function setupObserver() {
        const observer = new MutationObserver((mutations) => {
            let shouldRun = false;
            for (const mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    shouldRun = true;
                    break;
                }
            }
            if (shouldRun) debouncedFilter();
        });

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

    // ======================== URL change handler (YouTube SPA) ========================
    let lastUrl = location.href;
    function setupUrlObserver() {
        const urlObserver = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                // Reset notification showing state on navigation
                notifShowingBlocked = false;
                setTimeout(() => {
                    runFilter();
                    addBlocklistButton();
                }, 1500);
            }
        });
        urlObserver.observe(document.body, { childList: true, subtree: true });
    }

    // ======================== Initialization ========================
    const DEBUG = true;
    function dbg(...args) { if (DEBUG) console.log('[YTB]', ...args); }

    function init() {
        dbg('init() called, URL:', location.href);
        injectStyles();
        dbg('Styles injected');
        addBlocklistButton();
        dbg('Blocklist button added');
        runFilter();
        dbg('Initial filter run complete');
        setupObserver();
        setupUrlObserver();
        dbg('Observers set up. Script ready.');
    }

    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', () => setTimeout(init, 1000));
    }
})();