YT Comment Filter (Improved)

Automatically hide YouTube comments based on username, specific keywords, and spam text patterns. Features Dual-Mode Observer.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         YT Comment Filter (Improved)
// @namespace    YT Comment Filter (Improved)
// @version      6.2
// @description  Automatically hide YouTube comments based on username, specific keywords, and spam text patterns. Features Dual-Mode Observer.
// @author       Mochamad Adi MR (adimuham.mad)
// @match        *://*.youtube.com/watch*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// @compatible   chrome
// @compatible   firefox
// @compatible   edge
// @compatible   opera
// @compatible   safari
// @supportURL   https://buymeacoffee.com/mochadimr
// @homepageURL  https://gist.github.com/adimuhamad/143a06052413aaecb6ddf1a4e39103c1
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    const CONFIG = {
        STORAGE_KEY_USERS: "yt_filter_custom_words",
        STORAGE_KEY_KEYWORDS: "yt_filter_custom_content_keywords",
        STORAGE_KEY_MODE: "yt_filter_observer_mode",
        THROTTLE_DELAY: 1000,
        SELECTORS: {
            COMMENT_CONTAINER: "ytd-comment-view-model",
            COMMENT_TEXT: "#content-text",
            AUTHOR_TEXT: "#author-text span",
            HEADER_TARGET: "ytd-comments-header-renderer #additional-section",
            THREAD_RENDERER: "ytd-comment-thread-renderer",
            REPLIES_CONTAINER: "#replies",
        },
        CLASSES: {
            HIDDEN: "yt-comment-filter-hidden",
            SHOW_HIDDEN: "yt-filter-showing-hidden",
        },
        DEFAULTS: {
            BLOCKED_USERS: ["vip"],
            BLOCKED_KEYWORDS: ["pulauwin"],
            MODE: "efficient",
        },
        REGEX_NON_LATIN:
            /[^\u0000-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u08A0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u125F\u1280-\u12BF\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1A20-\u1A5F\u1A80-\u1AFF\u1B00-\u1B7F\u1B80-\u1BBF\u1BC0-\u1BFF\u1C00-\u1C4F\u1C50-\u1C7F\u1C90-\u1CBF\u1CC0-\u1CCF\u1CD0-\u1CFF\u1E00-\u1EFF\u1F00-\u1FFF\u2000-\u206F\u2070-\u20CF\u20D0-\u20FF\u2150-\u218F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2DE0-\u2DFF\u2E00-\u2E7F\u2E80-\u2EFF\u2F00-\u2FDF\u2FF0-\u2FFF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31A0-\u31BF\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA6A0-\uA6FF\uA700-\uA71F\uA720-\uA7FF\uA800-\uA82F\uA830-\uA83F\uA840-\uA87F\uA880-\uA8DF\uA8E0-\uA8FF\uA900-\uA92F\uA930-\uA95F\uA960-\uA97F\uA980-\uA9DF\uA9E0-\uA9FF\uAA00-\uAA3F\uAA40-\uAA6F\uAA70-\uAAAB\uAAAC-\uAAAF\uAAB0-\uAABF\uAAC0-\uAADF\uAAE0-\uAAEF\uAAF0-\uAAFF\uAB00-\uAB2F\uAB30-\uAB6F\uAB70-\uABBF\uABC0-\uABFF\uAC00-\uD7AF\uD7B0-\uD7FF\uF900-\uFAFF\uFB00-\uFB4F\uFB50-\uFDFF\uFE00-\uFE0F\uFE10-\uFE1F\uFE20-\uFE2F\uFE30-\uFE4F\uFE50-\uFE6F\uFE70-\uFEFF]/,
    };

    const UI_CONSTANTS = {
        ID: {
            DIALOG_MAIN: "yt-filter-dialog-main",
            DIALOG_CONFIRM: "yt-filter-dialog-confirm",
            OVERLAY: "yt-filter-overlay",
            USER_INPUT: "yt-filter-input-users",
            KEYWORD_INPUT: "yt-filter-input-keywords",
        },
    };

    const Theme = {
        getColors() {
            const isDark =
                document.documentElement.getAttribute("dark") === "true" ||
                (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);

            return {
                background: isDark ? "#212121" : "#ffffff",
                text: isDark ? "#f1f1f1" : "#0f0f0f",
                textSec: isDark ? "#aaaaaa" : "#606060",
                border: isDark ? "#3e3e3e" : "#e5e5e5",
                primary: "#3ea6ff",
                danger: "#ff4e45",
                buttonBg: isDark ? "#303030" : "#f2f2f2",
                inputBg: isDark ? "#121212" : "#f9f9f9",
                overlay: "rgba(0,0,0,0.7)",
                shadow: "rgba(0,0,0,0.5)",
            };
        },
    };

    const Utils = {
        debounce: (func, wait) => {
            let timeout;
            return (...args) => {
                clearTimeout(timeout);
                timeout = setTimeout(() => func(...args), wait);
            };
        },
        throttle: (func, limit) => {
            let inThrottle;
            return function () {
                const args = arguments,
                    context = this;
                if (!inThrottle) {
                    func.apply(context, args);
                    inThrottle = true;
                    setTimeout(() => (inThrottle = false), limit);
                }
            };
        },
        escapeRegExp: (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
        createLeetPattern: (word) => {
            return Utils.escapeRegExp(word)
                .replace(/a/gi, "[a4@]")
                .replace(/i/gi, "[i1l]")
                .replace(/e/gi, "[e3]")
                .replace(/o/gi, "[o0]")
                .replace(/s/gi, "[s5$]")
                .replace(/t/gi, "[t7+]")
                .replace(/g/gi, "[g9]");
        },
        setStyle: (element, styles) => Object.assign(element.style, styles),
    };

    const State = {
        isShowingHidden: false,
        forbiddenUserRegex: null,
        forbiddenContentRegex: null,
        observerMode: "efficient",

        loadSettings: () => {
            State.observerMode = GM_getValue(CONFIG.STORAGE_KEY_MODE, CONFIG.DEFAULTS.MODE);

            const loadRegex = (key, defaults) => {
                const saved = GM_getValue(key, "");
                const custom = saved
                    ? saved
                          .split(",")
                          .map((w) => w.trim())
                          .filter(Boolean)
                    : [];
                const all = [...new Set([...defaults, ...custom])];
                return all.length > 0 ? new RegExp(all.map(Utils.createLeetPattern).join("|"), "i") : null;
            };

            State.forbiddenUserRegex = loadRegex(CONFIG.STORAGE_KEY_USERS, CONFIG.DEFAULTS.BLOCKED_USERS);
            State.forbiddenContentRegex = loadRegex(CONFIG.STORAGE_KEY_KEYWORDS, CONFIG.DEFAULTS.BLOCKED_KEYWORDS);
        },

        saveSettings: (usersStr, keywordsStr) => {
            GM_setValue(CONFIG.STORAGE_KEY_USERS, usersStr);
            GM_setValue(CONFIG.STORAGE_KEY_KEYWORDS, keywordsStr);
            State.loadSettings();
            FilterEngine.processAll();
        },

        applySwitchMode: () => {
            const newMode = State.observerMode === "efficient" ? "fast" : "efficient";
            GM_setValue(CONFIG.STORAGE_KEY_MODE, newMode);
            State.observerMode = newMode;
            ObserverManager.start();
        },
    };

    const SettingsUI = {
        openSettings() {
            if (document.getElementById(UI_CONSTANTS.ID.DIALOG_MAIN)) return;
            const colors = Theme.getColors();
            const userVal = GM_getValue(CONFIG.STORAGE_KEY_USERS, "");
            const keywordVal = GM_getValue(CONFIG.STORAGE_KEY_KEYWORDS, "");

            this._createModal(
                UI_CONSTANTS.ID.DIALOG_MAIN,
                "Filter Configuration",
                colors,
                1000,
                (contentDiv) => {
                    contentDiv.append(this._createLabel("Blocked Usernames", colors));

                    const userArea = this._createTextarea(
                        UI_CONSTANTS.ID.USER_INPUT,
                        userVal,
                        "Example: login, browser, search, google, etc",
                        colors
                    );

                    contentDiv.append(userArea);
                    const spacer = document.createElement("div");
                    Utils.setStyle(spacer, { height: "16px" });
                    contentDiv.append(spacer);
                    contentDiv.append(this._createLabel("Blocked Keywords (Content)", colors));

                    const keyArea = this._createTextarea(
                        UI_CONSTANTS.ID.KEYWORD_INPUT,
                        keywordVal,
                        "Example: winningisland, luckyeagle, gambletoto, etc",
                        colors
                    );

                    contentDiv.append(keyArea);
                    setTimeout(() => userArea.focus(), 100);
                },
                (footerDiv) => {
                    const btnReset = this._createBtn("Reset Filters", "transparent", colors.danger, colors);
                    Utils.setStyle(btnReset, { border: `1px solid ${colors.danger}` });

                    btnReset.onmouseover = () => {
                        btnReset.style.backgroundColor = colors.danger;
                        btnReset.style.color = "#fff";
                    };

                    btnReset.onmouseout = () => {
                        btnReset.style.backgroundColor = "transparent";
                        btnReset.style.color = colors.danger;
                    };

                    btnReset.onclick = () => {
                        this.openConfirm(
                            "Reset All Filters?",
                            "This will delete all custom usernames and keywords.\nAre you sure?",
                            "Yes, Reset",
                            colors.danger,
                            () => {
                                document.getElementById(UI_CONSTANTS.ID.USER_INPUT).value = "";
                                document.getElementById(UI_CONSTANTS.ID.KEYWORD_INPUT).value = "";
                                State.saveSettings("", "");
                            }
                        );
                    };

                    const btnSave = this._createBtn("Save Changes", colors.primary, "#ffffff", colors);

                    btnSave.onclick = () => {
                        const uVal = document.getElementById(UI_CONSTANTS.ID.USER_INPUT).value;
                        const kVal = document.getElementById(UI_CONSTANTS.ID.KEYWORD_INPUT).value;
                        State.saveSettings(uVal, kVal);
                        this.close(UI_CONSTANTS.ID.DIALOG_MAIN);
                    };

                    footerDiv.append(btnReset, btnSave);
                }
            );
        },

        openModeSwitcher() {
            if (document.getElementById(UI_CONSTANTS.ID.DIALOG_CONFIRM)) return;
            const colors = Theme.getColors();
            const currentMode = State.observerMode.toUpperCase();
            const targetMode = currentMode === "EFFICIENT" ? "FAST" : "EFFICIENT";

            const desc =
                targetMode === "FAST"
                    ? "Comments will disappear instantly, but browser CPU usage may increase slightly."
                    : "Saves battery and CPU usage. Comments disappear after scroll stops.";

            this.openConfirm(
                "Switch Observer Mode?",
                `Current Mode: <b>${currentMode}</b>\n\nDo you want to switch to <b>${targetMode}</b> mode?\n${desc}`,
                `Switch to ${targetMode}`,
                colors.primary,
                () => {
                    State.applySwitchMode();
                }
            );
        },

        openConfirm(title, messageHtml, confirmBtnText, confirmBtnColor, onConfirmCallback) {
            const colors = Theme.getColors();

            this._createModal(
                UI_CONSTANTS.ID.DIALOG_CONFIRM,
                title,
                colors,
                2000,
                (contentDiv) => {
                    const p = document.createElement("p");
                    p.innerHTML = messageHtml.replace(/\n/g, "<br>");
                    
                    Utils.setStyle(p, { 
                        lineHeight: "1.5", 
                        fontSize: "14px", 
                        color: colors.textSec 
                    });
                    
                    contentDiv.append(p);
                },
                (footerDiv) => {
                    const btnCancel = this._createBtn("Cancel", "transparent", colors.text, colors);
                    btnCancel.onclick = () => this.close(UI_CONSTANTS.ID.DIALOG_CONFIRM);
                    const btnConfirm = this._createBtn(confirmBtnText, confirmBtnColor, "#ffffff", colors);
                    
                    btnConfirm.onclick = () => {
                        onConfirmCallback();
                        this.close(UI_CONSTANTS.ID.DIALOG_CONFIRM);
                    };

                    footerDiv.append(btnCancel, btnConfirm);
                }
            );
        },

        _createModal(dialogId, titleText, colors, zIndex, contentBuilder, actionBuilder) {
            const overlay = document.createElement("div");
            overlay.id = dialogId + "-overlay";

            Utils.setStyle(overlay, {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100%",
                height: "100%",
                backgroundColor: colors.overlay,
                zIndex: zIndex,
                backdropFilter: "blur(3px)",
            });

            const dialog = document.createElement("div");
            dialog.id = dialogId;

            Utils.setStyle(dialog, {
                position: "fixed",
                top: "50%",
                left: "50%",
                transform: "translate(-50%, -50%)",
                backgroundColor: colors.background,
                color: colors.text,
                borderRadius: "12px",
                padding: "24px",
                width: "450px",
                maxWidth: "90vw",
                maxHeight: "85vh",
                zIndex: zIndex + 1,
                boxShadow: `0 10px 25px ${colors.shadow}`,
                fontFamily: "Roboto, Arial, sans-serif",
                display: "flex",
                flexDirection: "column",
                overflowY: "auto",
            });

            const header = document.createElement("div");

            Utils.setStyle(header, {
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
                marginBottom: "16px",
            });

            const title = document.createElement("h2");
            title.textContent = titleText;

            Utils.setStyle(title, {
                margin: "0", 
                fontSize: "20px", 
                fontWeight: "500"
            });

            const closeBtn = document.createElement("button");
            closeBtn.innerHTML = `<svg viewBox="0 0 24 24" style="width:24px;height:24px;fill:${colors.text}"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>`;

            Utils.setStyle(closeBtn, {
                background: "none", 
                border: "none", 
                cursor: "pointer", 
                padding: "4px"
            });

            closeBtn.onclick = () => this.close(dialogId);
            header.append(title, closeBtn);
            const content = document.createElement("div");
            contentBuilder(content);
            const actions = document.createElement("div");

            Utils.setStyle(actions, {
                display: "flex", 
                justifyContent: "flex-end", 
                gap: "10px", 
                marginTop: "24px"
            });

            actionBuilder(actions);
            dialog.append(header, content, actions);
            document.body.append(overlay, dialog);

            const escHandler = (e) => {
                if (e.key === "Escape") this.close(dialogId);
            };

            dialog.dataset.escHandler = "true";
            document.addEventListener("keydown", escHandler);
            this._escHandlers = this._escHandlers || {};
            this._escHandlers[dialogId] = escHandler;
        },

        _createLabel(text, colors) {
            const lbl = document.createElement("label");
            lbl.textContent = text;

            Utils.setStyle(lbl, {
                display: "block",
                marginBottom: "8px",
                fontSize: "13px",
                fontWeight: "500",
                color: colors.text,
            });

            return lbl;
        },

        _createTextarea(id, val, placeholder, colors) {
            const ta = document.createElement("textarea");
            ta.id = id;
            ta.value = val;
            ta.placeholder = placeholder;

            Utils.setStyle(ta, {
                width: "100%",
                height: "80px",
                backgroundColor: colors.inputBg,
                color: colors.text,
                border: `1px solid ${colors.border}`,
                borderRadius: "8px",
                padding: "10px",
                fontFamily: "monospace",
                fontSize: "12px",
                resize: "vertical",
                boxSizing: "border-box",
                outline: "none",
            });

            ta.onfocus = () => {
                ta.style.borderColor = colors.primary;
            };

            ta.onblur = () => {
                ta.style.borderColor = colors.border;
            };

            return ta;
        },

        _createBtn(text, bg, textColor, colors) {
            const btn = document.createElement("button");
            btn.textContent = text;

            Utils.setStyle(btn, {
                padding: "8px 16px",
                backgroundColor: bg,
                color: textColor,
                border: bg === "transparent" ? `1px solid ${colors.border}` : "none",
                borderRadius: "18px",
                cursor: "pointer",
                fontWeight: "500",
            });

            if (bg === "transparent") {
                btn.onmouseover = () => (btn.style.backgroundColor = colors.buttonBg);
                btn.onmouseout = () => (btn.style.backgroundColor = "transparent");
            }

            return btn;
        },

        close(dialogId) {
            const dialog = document.getElementById(dialogId);
            const overlay = document.getElementById(dialogId + "-overlay");
            if (dialog) dialog.remove();
            if (overlay) overlay.remove();

            if (this._escHandlers && this._escHandlers[dialogId]) {
                document.removeEventListener("keydown", this._escHandlers[dialogId]);
                delete this._escHandlers[dialogId];
            }
        },
    };

    const FilterEngine = {
        isSpam: (commentText, username) => {
            if (CONFIG.REGEX_NON_LATIN.test(commentText)) return true;
            if (username && State.forbiddenUserRegex?.test(username)) return true;
            if (commentText && State.forbiddenContentRegex?.test(commentText)) return true;
            return false;
        },
        processAll: () => {
            const comments = document.querySelectorAll(CONFIG.SELECTORS.COMMENT_CONTAINER);
            let hiddenCount = 0;
            let totalCount = comments.length;

            comments.forEach((container) => {
                const textEl = container.querySelector(CONFIG.SELECTORS.COMMENT_TEXT);
                const userEl = container.querySelector(CONFIG.SELECTORS.AUTHOR_TEXT);
                if (!textEl) return;
                const text = textEl.innerText;
                const user = userEl ? userEl.innerText : "";

                if (FilterEngine.isSpam(text, user)) {
                    FilterEngine.hideComment(container);
                    hiddenCount++;
                } else {
                    FilterEngine.showComment(container);
                }
            });

            UIManager.updateCount(hiddenCount, totalCount);
        },
        hideComment: (container) => {
            if (!container.classList.contains(CONFIG.CLASSES.HIDDEN)) {
                container.style.display = "none";
                container.classList.add(CONFIG.CLASSES.HIDDEN);

                if (container.id === "comment") {
                    const thread = container.closest(CONFIG.SELECTORS.THREAD_RENDERER);
                    const replies = thread?.querySelector(CONFIG.SELECTORS.REPLIES_CONTAINER);
                    if (replies) replies.style.display = "none";
                }
            }
        },
        showComment: (container) => {
            if (container.classList.contains(CONFIG.CLASSES.HIDDEN)) {
                container.style.display = "";
                container.classList.remove(CONFIG.CLASSES.HIDDEN);

                if (container.id === "comment") {
                    const thread = container.closest(CONFIG.SELECTORS.THREAD_RENDERER);
                    const replies = thread?.querySelector(CONFIG.SELECTORS.REPLIES_CONTAINER);
                    if (replies) replies.style.display = "";
                }
            }
        },
    };

    const UIManager = {
        injectStyles: () => {
            GM_addStyle(`
                .${CONFIG.CLASSES.HIDDEN} { display: none; }
                body.${CONFIG.CLASSES.SHOW_HIDDEN} .${CONFIG.CLASSES.HIDDEN} {
                    display: block !important; opacity: 0.6; border: 1px dashed rgba(255, 0, 0, 0.5);
                    border-radius: 8px; margin-bottom: 8px !important;
                }
            `);
        },
        createButton: () => {
            if (document.getElementById("yt-filter-toggle")) return;
            const target = document.querySelector(CONFIG.SELECTORS.HEADER_TARGET);
            if (!target) return;
            const btn = document.createElement("span");
            btn.id = "yt-filter-toggle";

            Utils.setStyle(btn, {
                display: "inline-flex",
                alignItems: "center",
                cursor: "pointer",
                fontFamily: '"Roboto","Arial",sans-serif',
                fontSize: "14px",
                fontWeight: "500",
                color: "var(--yt-spec-text-secondary)",
                marginLeft: "32px",
                position: "relative",
                bottom: "3px",
            });

            const iconStyle = "width:24px;height:24px;fill:currentColor;margin-right:4px";

            const svgs = {
                on: `<svg class="icon-vis-on" style="${iconStyle}" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5C21.27 7.61 17 4.5 12 4.5zm0 13c-3.04 0-5.5-2.46-5.5-5.5S8.96 6.5 12 6.5s5.5 2.46 5.5 5.5-2.46 5.5-5.5 5.5zm0-9c-1.93 0-3.5 1.57-3.5 3.5s1.57 3.5 3.5 3.5 3.5-1.57 3.5-3.5-1.57-3.5-3.5-3.5z"/></svg>`,
                off: `<svg class="icon-vis-off" style="${iconStyle};display:none" viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L21.73 23 23 21.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.93 1.57 3.5 3.5 3.5.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-3.04 0-5.5-2.46-5.5-5.5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.93-1.57-3.5-3.5-3.5l-.16.02z"/></svg>`,
            };

            btn.innerHTML = `${svgs.on}${svgs.off}<span class="yt-filter-label">Hidden</span><span class="yt-filter-status" style="margin: 0 4px;">OFF</span><span class="yt-filter-info" style="display:none">(0/0)</span>`;
            btn.addEventListener("click", UIManager.toggleHidden);
            target.after(btn);
        },
        toggleHidden: () => {
            State.isShowingHidden = !State.isShowingHidden;
            document.body.classList.toggle(CONFIG.CLASSES.SHOW_HIDDEN, State.isShowingHidden);
            UIManager.updateCount();
        },
        updateCount: (passedHidden, passedTotal) => {
            const btn = document.getElementById("yt-filter-toggle");
            if (!btn) return;
            let hiddenCount = passedHidden ?? document.querySelectorAll(`.${CONFIG.CLASSES.HIDDEN}`).length;
            let totalCount = passedTotal ?? document.querySelectorAll(CONFIG.SELECTORS.COMMENT_CONTAINER).length;
            const statusEl = btn.querySelector(".yt-filter-status");
            const infoEl = btn.querySelector(".yt-filter-info");
            const iconOn = btn.querySelector(".icon-vis-on");
            const iconOff = btn.querySelector(".icon-vis-off");

            if (State.isShowingHidden) {
                statusEl.textContent = "OFF";
                infoEl.style.display = "none";
                iconOn.style.display = "none";
                iconOff.style.display = "block";
            } else {
                statusEl.textContent = "ON";
                infoEl.textContent = `(${hiddenCount}/${totalCount})`;
                infoEl.style.display = "inline";
                iconOn.style.display = "block";
                iconOff.style.display = "none";
            }
        },
    };

    const ObserverManager = {
        activeObserver: null,
        start: () => {
            if (ObserverManager.activeObserver) ObserverManager.activeObserver.disconnect();
            if (State.observerMode === "fast") ObserverManager.startFastMode();
            else ObserverManager.startEfficientMode();
        },
        startEfficientMode: () => {
            console.log("[YT Filter] EFFICIENT Mode.");
            const debouncedProcess = Utils.debounce(FilterEngine.processAll, 500);
            ObserverManager.activeObserver = new MutationObserver((mutations) => {
                let shouldProcess = false;

                for (const m of mutations) {
                    if (
                        m.target.nodeName === "YTD-COMMENT-VIEW-MODEL" ||
                        m.target.id === "contents" ||
                        m.target.id === "comments"
                    ) {
                        shouldProcess = true;
                        break;
                    }
                }

                if (shouldProcess || document.readyState === "loading") debouncedProcess();
            });

            ObserverManager.activeObserver.observe(document.body, { childList: true, subtree: true });
        },
        startFastMode: () => {
            console.log("[YT Filter] FAST Mode.");
            const throttledProcess = Utils.throttle(FilterEngine.processAll, CONFIG.THROTTLE_DELAY);
            ObserverManager.activeObserver = new MutationObserver(() => throttledProcess());
            ObserverManager.activeObserver.observe(document.body, { childList: true, subtree: true });
        },
    };

    const init = () => {
        State.loadSettings();
        UIManager.injectStyles();
        GM_registerMenuCommand("⚙ Configure Filter Settings", SettingsUI.openSettings.bind(SettingsUI));
        GM_registerMenuCommand("🔁 Switch Observer Mode", SettingsUI.openModeSwitcher.bind(SettingsUI));
        GM_registerMenuCommand("👁️‍🗨️ Toggle Button Check", UIManager.createButton);

        const btnInterval = setInterval(() => {
            if (document.querySelector(CONFIG.SELECTORS.HEADER_TARGET)) {
                UIManager.createButton();
                clearInterval(btnInterval);
            }
        }, 2000);

        ObserverManager.start();
        setTimeout(FilterEngine.processAll, 2000);
    };

    init();
})();