// ==UserScript==
// @name        YouTube Quick Actions (Hide, Not Interested, Donβt Recommend, Save to Playlist)
// @description Adds quick-action buttons like Hide, Save to Playlist, Not Interested, and Donβt Recommend
// @version     2.0.1.6
// @match       https://www.youtube.com/*
// @license     Unlicense
// @icon        https://www.youtube.com/s/desktop/c722ba88/img/logos/favicon_144x144.png
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant       GM_unregisterMenuCommand
// @run-at      document-start
// @compatible  firefox
// @require     https://cdnjs.cloudflare.com/ajax/libs/loglevel/1.9.2/loglevel.min.js
// @namespace   https://greasyfork.org/users/1223791
// @downloadURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions%20%28Hide%2C%20Not%20Interested%2C%20Don%E2%80%99t%20Recommend%2C%20Save%20to%20Playlist%29.user.js
// @updateURL https://update.greasyfork.icu/scripts/533514/YouTube%20Quick%20Actions%20%28Hide%2C%20Not%20Interested%2C%20Don%E2%80%99t%20Recommend%2C%20Save%20to%20Playlist%29.meta.js
// ==/UserScript==
(function ()
{
    "use strict";
    console.log("π«‘ [Youtube Quick Actions] Script initialized");
    const css = String.raw;
    const style = css`
:root {
    --color-primary: rgba(252, 146, 205, 1);
    --color-secondary: rgba(33, 225, 255, 1) ;
}
#quick-actions {
	position: absolute;
	display: none;
	flex-direction: column;
	gap: 0.2rem;
	align-items: flex-start;
}
#quick-actions.location-01 {
	top: 0.5rem;
	left: 0.5rem;
}
#quick-actions.location-02 {
	top: 0.4rem;
	left: 0.4rem;
}
.yt-lockup-view-model--collection-stack-2 ~ #quick-actions.location-02 {
    top: 1.4rem;
}
#content-attachment #quick-actions {
    top: 1.2rem;
    left: 1.2rem;
}
ytd-playlist-video-list-renderer #contents ytd-playlist-video-renderer #quick-actions {
    top: 1.4rem;
    left: 4rem;
}
#quick-actions.location-01 .qa-button {
	width: 3rem;
}
#quick-actions.location-02 .qa-button {
	width: 2.6rem;
}
#quick-actions .qa-button {
	background-color: rgba(0, 0, 0, 0.8);
	z-index: 1000;
	border: 1px solid rgba(255, 255, 255, 0.02);
	height: auto;
	display: flex;
	justify-content: center;
	align-items: center;
	color: white;
	font-size: 1rem;
	font-weight: bold;
	border-radius: 4px;
	cursor: pointer;
	flex-shrink: unset;
	padding: 0.5rem;
	pointer-events: auto !important;
}
#quick-actions .qa-button.circle {
	border-radius: 50%;
}
#quick-actions .qa-button:hover {
	border: 1px solid rgba(255, 255, 255, 0.02);
	background-color: rgba(0, 0, 0, 1);
}
#quick-actions .qa-icon {
	width: 100%;
	height: 100%;
	max-width: 100%;
	max-height: 100%;
}
:is(
    ytd-grid-video-renderer,
    ytd-video-renderer,
    ytd-rich-item-renderer,
    yt-lockup-view-model,
    ytm-shorts-lockup-view-model-v2,
    ytd-compact-video-renderer,
    ytd-rich-grid-media,
    ytm-shorts-lockup-view-model,
    ytd-compact-movie-renderer,
    ytd-playlist-video-renderer):has(.yt-spec-button-shape-next--enable-backdrop-filter-experiment:not([aria-label="Notify me"])) .qa-button.frosted {
        background-color: rgba(0, 0, 0, 0.3) !important;
        backdrop-filter: blur(4px);
        box-shadow: 0px 0px 1px 0px rgba(255, 255, 255, 0.1);
        border: 0px solid #ffffff;
}
:is(
    ytd-grid-video-renderer,
    ytd-video-renderer,
    ytd-rich-item-renderer,
    yt-lockup-view-model,
    ytm-shorts-lockup-view-model-v2,
    ytd-compact-video-renderer,
    ytd-rich-grid-media,
    ytm-shorts-lockup-view-model,
    ytd-compact-movie-renderer,
    ytd-playlist-video-renderer):has(.yt-spec-button-shape-next--enable-backdrop-filter-experiment:not([aria-label="Notify me"])) .qa-button.frosted:hover {
        opacity: 1;
        background: rgba(40, 40, 40, 0.6)!important;
        border: 0px solid #ffffff;
}
:is(ytd-grid-video-renderer,
    ytd-video-renderer,
    ytd-rich-item-renderer,
    yt-lockup-view-model,
    ytm-shorts-lockup-view-model-v2,
    ytd-compact-video-renderer,
    ytd-rich-grid-media,
    ytm-shorts-lockup-view-model,
    ytd-compact-movie-renderer,
    ytd-playlist-video-renderer):hover:has(ytd-menu-renderer, button-view-model, .yt-spec-button-shape-next__icon):not([is-dismissed]):not(:has(ytd-rich-grid-media[is-dismissed])):not(:has(.ytDismissibleItemReplacedContent)) #quick-actions {
        display: flex;
}
:is(yt-lockup-view-model,
    ytd-playlist-video-renderer):hover:has(#quick-actions) {
	position: relative;
}
.fancy {
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-image: linear-gradient(
        45deg,
        var(--color-primary) 17%,
        var(--color-secondary) 100%
    );
    background-size: 400% auto;
    background-position: 0% 50%;
    animation: animate-gradient 12s linear infinite;
    font-weight: bold!important;
    letter-spacing: .1rem;
}
@keyframes animate-gradient {
    0% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
    100% {
        background-position: 0% 50%;
    }
}
`;
    GM_addStyle(style);
    /* -------------------------------------------------------------------------- */
    /*                                  Variables                                 */
    /* -------------------------------------------------------------------------- */
    //Selectors to probe for prop
    const tags = ["ytd-video-renderer",
        "ytd-rich-item-renderer",
        "ytm-shorts-lockup-view-model-v2",
        "yt-lockup-view-model",
        "ytd-playlist-video-renderer",
        "ytd-grid-video-renderer",
        "ytd-compact-movie-renderer",
        "ytd-compact-video-renderer",
        "ytd-rich-grid-media",
        "ytm-shorts-lockup-view-model"];
    //"YTD-MEMBERSHIP-BADGE-RENDERER";
    //Available menu items obj from prop
    const menu_items_from_props = [
        "menu.menuRenderer.items",
        "content.listViewModel.listItems"
    ];
    //Individual menu items from obj
    const menu_items_1 = "listItemViewModel?.title?.content";
    const menu_items_2 = "menuServiceItemRenderer?.text?.runs?.[0]?.text";
    const menu_items_3 = "menuNavigationItemRenderer?.text?.runs?.[0]?.text";
    //Thumbnail size
    const thumbnail_elem_selector = "img.ytCoreImageHost";
    //Action Button selectors
    const action_button_selectors = ["button-view-model", ".shortsLockupViewModelHostOutsideMetadataMenu", "yt-icon-button"];
    //Action Button Label - Aria Label
    const action_menu_text = ['more actions', 'action menu'];
    //Dropdown Menu
    const dropdown_menu_tag_name = "TP-YT-IRON-DROPDOWN";
    //Dropdown Menu Items
    const popup_menu_items_selector = "yt-formatted-string.style-scope.ytd-menu-service-item-renderer, yt-list-item-view-model[role='menuitem'], yt-formatted-string.ytd-menu-navigation-item-renderer";
    // YT Event Names
    const yt_update_action_names = [
        //"ytd-update-grid-state-action",
        //"ytd-rich-item-index-update-action",
        "yt-reload-continuation-items-command"
    ];
    // Icons by Remix Icon (c) Remix Design Licensed under Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0 - https://github.com/Remix-Design/remixicon/blob/master/License
    const icon_not_interested = ``;
    const icon_hide = ``;
    const icon_save = ``;
    const icon_dont_recommend = ``;
    const downloadIcon = ``;
    const icon_trash = ``;
    const icon_edit = ``;
    /* -------------------------------------------------------------------------- */
    /*                                  Functions                                 */
    /* -------------------------------------------------------------------------- */
    /* ----------------------------- Menu Commmands ----------------------------- */
    let is_logging_enabled = GM_getValue("is_logging_enabled", false);
    let use_frosted = GM_getValue("use_frosted", true);
    let use_circle = GM_getValue("use_circle", false);
    const menu_commands = [
        {
            label: () => `Frosted Button: ${use_frosted ? "β
 ON" : "β OFF"}`,
            toggle: () =>
            {
                use_frosted = !use_frosted;
                GM_setValue("use_frosted", use_frosted);
                window.location.reload(true);
            }
        },
        {
            label: () => `Circle Button: ${use_circle ? "β
 ON" : "β OFF"}`,
            toggle: () =>
            {
                use_circle = !use_circle;
                GM_setValue("use_circle", use_circle);
                window.location.reload(true);
            }
        },
        {
            label: () => `Logs: ${is_logging_enabled ? "β
 ON" : "β OFF"}`,
            toggle: () =>
            {
                is_logging_enabled = !is_logging_enabled;
                GM_setValue("is_logging_enabled", is_logging_enabled);
                window.location.reload(true);
            }
        }
    ];
    function registermenu_commands()
    {
        for (const command of menu_commands)
        {
            GM_registerMenuCommand(command.label(), command.toggle);
        }
    }
    registermenu_commands();
    is_logging_enabled ? log.enableAll() : log.disableAll();
    /* ---------------------------- Menu Commands End --------------------------- */
    function append_buttons(element, menulist_items, position)
    {
        const final_menulist_items = [...new Set(menulist_items)];
        const buttons_to_append = [];
        for (const item of final_menulist_items)
        {
            if (!item) continue;
            let class_name;
            let title_text;
            let icon;
            if (item.startsWith("Remove from "))
            {
                class_name = "remove";
                title_text = "Remove from playlist";
                icon = icon_trash;
            } else
            {
                switch (item)
                {
                    case "Not interested":
                        class_name = "not_interested";
                        title_text = "Not interested";
                        icon = icon_not_interested;
                        break;
                    case "Don't recommend channel":
                        class_name = "dont_recommend_channel";
                        title_text = "Don't recommend channel";
                        icon = icon_dont_recommend;
                        break;
                    case "Hide":
                        class_name = "hide";
                        title_text = "Hide video";
                        icon = icon_hide;
                        break;
                    case "Save to playlist":
                        class_name = "save";
                        title_text = "Save to playlist";
                        icon = icon_save;
                        break;
                    case "Delete":
                        class_name = "delete";
                        title_text = "Delete";
                        icon = icon_trash;
                        break;
                    case "Edit":
                        class_name = "edit";
                        title_text = "Edit";
                        icon = icon_edit;
                        break;
                    default:
                        continue;
                }
            }
            const set_frosted = use_frosted ? " frosted" : "";
            const set_circle = use_circle ? " circle" : "";
            buttons_to_append.push(
                ``,
            );
        }
        const buttons_container = document.createElement("div");
        buttons_container.id = "quick-actions";
        buttons_container.classList.add(position);
        buttons_container.innerHTML = buttons_to_append.join("");
        if (!element.querySelector("#quick-actions"))
        {
            element.insertAdjacentElement("beforeend", buttons_container);
        }
    }
    function get_prop(target, path)
    {
        try
        {
            return new Function('object', `return object.${path}`)(target) ?? [];
        } catch
        {
            return [];
        }
    }
    function get_menu_list(target)
    {
        if (!target) return;
        return target.map(item =>
        {
            const paths = [menu_items_1, menu_items_2, menu_items_3];
            for (const path of paths)
            {
                const result = get_prop(item, path);
                if (result.length) return result;
            }
            return null;
        }).filter(Boolean);
    }
    function enable_fallback_tooltip(selector)
    {
        let active_tooltip = null;
        let current_elem = null;
        const create_tooltip = (text) =>
        {
            const tooltip = document.createElement("div");
            tooltip.id = "quick-action-tooltip";
            Object.assign(tooltip.style, {
                position: "fixed",
                zIndex: "999999",
                background: "rgba(0,0,0,0.8)",
                color: "#fff",
                padding: ".6rem 1rem",
                borderRadius: ".8rem",
                fontSize: "1.5rem",
                pointerEvents: "none"
            });
            tooltip.textContent = text;
            document.body.appendChild(tooltip);
            return tooltip;
        };
        document.addEventListener("pointerenter", event =>
        {
            const target_elem = event.target.closest(selector);
            if (!target_elem) return;
            if (current_elem === target_elem) return;
            document.querySelectorAll("#quick-action-tooltip").forEach((element) => element.remove());
            const tooltip_text = target_elem.getAttribute("title") || target_elem.getAttribute("data-text");
            if (!tooltip_text) return;
            if (target_elem.hasAttribute("title"))
            {
                target_elem.setAttribute("data-original-title", tooltip_text);
                target_elem.removeAttribute("title");
            }
            current_elem = target_elem;
            active_tooltip = create_tooltip(tooltip_text);
            const update_position = e =>
            {
                if (!active_tooltip) return;
                active_tooltip.style.left = e.clientX + 8 + "px";
                active_tooltip.style.top = e.clientY + 8 + "px";
            };
            current_elem._tooltip_move = update_position;
            document.addEventListener("pointermove", update_position);
        }, true);
        document.addEventListener("pointerleave", event =>
        {
            const left_elem = event.target.closest(selector);
            if (!left_elem) return;
            if (event.relatedTarget && left_elem.contains(event.relatedTarget))
            {
                return;
            }
            if (active_tooltip)
            {
                document.removeEventListener("pointermove", left_elem._tooltip_move);
                active_tooltip.remove();
                active_tooltip = null;
                delete left_elem._tooltip_move;
            }
            if (left_elem.hasAttribute("data-original-title"))
            {
                left_elem.setAttribute("title", left_elem.getAttribute("data-original-title"));
                left_elem.removeAttribute("data-original-title");
            }
            current_elem = null;
        }, true);
    };
    function get_value_by_path(obj, path)
    {
        return path.split(".").reduce((acc, key) =>
        {
            if (acc && typeof acc === "object" && key in acc)
            {
                return acc[key];
            }
            return undefined;
        }, obj);
    }
    function find_target_path_value(obj)
    {
        if (obj == null || typeof obj !== "object") return null;
        for (const path of menu_items_from_props)
        {
            const value = get_value_by_path(obj, path);
            if (Array.isArray(value))
            {
                log.log(`π Menu Obj Path Used: ${path}`);
                return value;
            }
        }
        for (const key in obj)
        {
            if (obj[key] && typeof obj[key] === "object")
            {
                const found = find_target_path_value(obj[key]);
                if (found !== null) return found;
            }
        }
        return null;
    }
    function find_first_menu_or_list_value(root_element = document)
    {
        const elements = root_element.querySelectorAll("*");
        for (const element of elements)
        {
            if (element.data && typeof element.data === "object")
            {
                const found_value = find_target_path_value(element.data);
                if (found_value !== null)
                {
                    return found_value;
                }
            }
        }
        return null;
    }
    function find_elem_in_parent_dom_tree(origin_elem, tag_names)
    {
        const svg_path_d =
            'M12 16.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5-1.5-.67-1.5-1.5.67-1.5 1.5-1.5zM10.5 12c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5-1.5.67-1.5 1.5zm0-6c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5-1.5.67-1.5 1.5z';
        let node = origin_elem;
        let depth = 0;
        while (node && depth <= 3)
        {
            const found_elems = node.querySelectorAll(tag_names.join(','));
            for (const elem of found_elems)
            {
                // const svg_match = elem.querySelector(`svg path[d='${svg_path_d}']`)
                // if (svg_match) {
                //     return elem
                // }
                const aria_labels = elem.querySelectorAll('[aria-label]');
                const action_button = Array.from(aria_labels).find(element =>
                {
                    if (element.hasAttribute('aria-label'))
                    {
                        const aria_label_text = element.getAttribute('aria-label').toLowerCase();
                        return action_menu_text.some(phrase => aria_label_text.includes(phrase));
                    }
                    return false;
                });
                if (action_button)
                {
                    return elem;
                }
            }
            node = node.parentElement;
            depth++;
        }
        return null;
    }
    async function wait_until(condition_func, { interval = 100, timeout = 3000 } = {})
    {
        const start_time = Date.now();
        while (Date.now() - start_time < timeout)
        {
            const result = condition_func();
            if (result) return result;
            await new Promise((resolve) => setTimeout(resolve, interval));
        }
        throw new Error("β° Timeout: Target element is not visible in time");
    }
    function retry_click(element, { max_attempts = 5, interval = 1000 } = {})
    {
        return new Promise((resolve) =>
        {
            let attempts = 0;
            function try_click()
            {
                if (!element || attempts >= max_attempts)
                {
                    log.log("β οΈ Retry failed or element missing.");
                    return resolve();
                }
                const rect = element.getBoundingClientRect();
                const is_visible = rect.width > 0 && rect.height > 0;
                if (is_visible)
                {
                    element.dispatchEvent(
                        new MouseEvent("click", {
                            view: document.defaultView,
                            bubbles: true,
                            cancelable: true,
                        }),
                    );
                    log.log("π Clicked matching menu item");
                    return resolve();
                } else
                {
                    attempts++;
                    log.log("π Clicking attempt: ", attempts);
                    setTimeout(try_click, interval);
                }
            }
            try_click();
        });
    }
    function get_visible_elem(target_selector, element = document.body)
    {
        const elements = element.querySelectorAll(target_selector);
        if (!elements || elements.length === 0)
        {
            return null;
        }
        for (const element of elements)
        {
            const rect = element.getBoundingClientRect();
            const has_dimensions_and_in_view = rect.width > 0 &&
                rect.height > 0 &&
                rect.bottom > 0 &&
                rect.right > 0 &&
                rect.top < (window.innerHeight || element.documentElement.clientHeight) &&
                rect.left < (window.innerWidth || element.documentElement.clientWidth);
            if (!has_dimensions_and_in_view)
            {
                continue;
            }
            const computed_style = window.getComputedStyle(element);
            const is_visible = computed_style.opacity !== '0' &&
                computed_style.visibility !== 'hidden' &&
                computed_style.display !== 'none';
            if (!is_visible)
            {
                continue;
            }
            log.log("π Found visible element:", element);
            return element;
        }
        log.log("β οΈ No visible menu found.");
        return null;
    }
    /* -------------------------------------------------------------------------- */
    /*                                  Listeners                                 */
    /* -------------------------------------------------------------------------- */
    document.addEventListener("mouseover", (event) =>
    {
        const path = event.composedPath();
        for (let element of path)
        {
            const tag = element?.tagName?.toLowerCase();
            if (tag && !element.querySelector("#quick-actions") && tags.includes(tag))
            {
                log.log("β Video Elem: ", element.tagName, element);
                const data = find_first_menu_or_list_value(element);
                const thumbnail_elem = element.querySelector(thumbnail_elem_selector);
                const thumbnail_size =
                    thumbnail_elem?.getClientRects?.().length > 0
                        ? parseInt(thumbnail_elem.getClientRects()[0].width)
                        : 100;
                log.log("πΌοΈ Thumbnail Size: ", thumbnail_size);
                const container_position = thumbnail_size < 211 ? "location-02" : "location-01";
                if (!data)
                {
                    log.log("β οΈ No props data found.");
                    //NOTE - Observe
                    const liked_playlist = element.querySelectorAll('a[href*="list=LL"]');
                    if (liked_playlist.length > 0)
                    {
                        append_buttons(element, [], container_position);
                    }
                    continue;
                }
                log.log("π₯ Video Menu Props: ", data);
                const menulist_items = get_menu_list(data);
                log.log("π Menu items: ", menulist_items);
                append_buttons(element, menulist_items, container_position);
            } else
            {
                continue;
            }
        }
    }, true);
    document.addEventListener("click", async function (event)
    {
        document.querySelectorAll("#quick-action-tooltip").forEach((element) => element.remove());
        const button = event.target.closest(".qa-button");
        if (!button) return;
        event.stopPropagation();
        event.stopImmediatePropagation();
        event.preventDefault();
        const action_type = button.dataset.icon;
        let response;
        switch (action_type)
        {
            case "not_interested":
                response = "Not interested";
                log.log("π΄ Marking as not interested");
                break;
            case "dont_recommend_channel":
                response = "Don't recommend channel";
                log.log("π« Don't recommend channel");
                break;
            case "hide":
                response = "Hide";
                log.log("ποΈ Hiding video");
                break;
            case "remove":
                response = "Remove from";
                log.log("ποΈ Remove from playlist");
                break;
            case "save":
                response = "Save to playlist";
                log.log("π Saving to playlist");
                break;
            case "edit":
                response = "Edit";
                log.log("βοΈ Edit");
                break;
            case "delete":
                response = "Delete";
                log.log("ποΈ Delete");
                break;
            default:
                log.log("β οΈ Unknown action");
        }
        const menus = find_elem_in_parent_dom_tree(
            button,
            action_button_selectors
        );
        if (!menus)
        {
            log.log("β Menu button not found.");
            return;
        }
        log.log("π― Menu button found:", menus);
        if (menus.id.toLowerCase() === "button")
        {
            menus.dispatchEvent(new MouseEvent("click", { bubbles: false }));
        } else
        {
            menus.querySelector("button").dispatchEvent(new MouseEvent("click", { bubbles: false }));
        }
        log.log("π Button clicked, waiting for menu...");
        try
        {
            const visible_menu = await wait_until(() => get_visible_elem(dropdown_menu_tag_name), {
                interval: 100,
                timeout: 3000,
            });
            if (visible_menu)
            {
                try
                {
                    const target_item = await wait_until(
                        () =>
                        {
                            const items = visible_menu.querySelectorAll(popup_menu_items_selector);
                            return items.length > 0 ? items : null;
                        },
                        {
                            interval: 100,
                            timeout: 5000,
                        },
                    );
                    if (target_item)
                    {
                        log.log("π Target items found:", target_item);
                        for (const item of target_item)
                        {
                            if (
                                item.textContent === response ||
                                (response === "Remove from" && item.textContent.startsWith("Remove from"))
                            )
                            {
                                log.log(`β
 Matched: (${response} = ${item.textContent})`);
                                log.log(`β
`, item);
                                const button = item;
                                await retry_click(button, { max_attempts: 3, interval: 1000 }).finally(() =>
                                {
                                    document.body.click();
                                });
                                break;
                            } else
                            {
                                log.log(`β Not a match: (${response} = ${item.textContent})`);
                            }
                        }
                    }
                } catch (error)
                {
                    log.log("π !", error.message);
                }
            }
        } catch (error)
        {
            log.log("π !!", error.message);
        }
    });
    document.addEventListener("yt-action", (event) =>
    {
        if (yt_update_action_names.includes(event.detail.actionName))
        {
            log.log("π Page updated.");
            document.querySelectorAll("#quick-actions").forEach((element) => element.remove());
        }
    });
    enable_fallback_tooltip(".qa-button");
    /* -------------------------------------------------------------------------- */
    /*         This script is brought to you in support of FIFTY FIFTY π         */
    /* -------------------------------------------------------------------------- */
    if ("π")
    {
        const selectors_to_watch = ['a', 'yt-formatted-string', '.yt-core-attributed-string'];
        const observed_elements = new WeakMap();
        const target_texts = [
            "FIFTY FIFTY Official",
            "FIFTY FIFTY",
            "@WE_FIFTYFIFTY"
        ];
        function has_matching_text(element)
        {
            const text = element.textContent.trim();
            return target_texts.includes(text);
        }
        function observe_text_content_changes(element)
        {
            if (observed_elements.has(element)) return;
            const element_observer = new MutationObserver(() =>
            {
                element.classList.toggle("fancy", has_matching_text(element));
            });
            observed_elements.set(element, element_observer);
            element_observer.observe(element, { characterData: true, childList: true, subtree: true });
        }
        function handle_removed_node(node)
        {
            if (node.nodeType !== 1) return;
            if (observed_elements.has(node))
            {
                observed_elements.get(node).disconnect();
                observed_elements.delete(node);
            }
            node.querySelectorAll(selectors_to_watch.join(',')).forEach(child =>
            {
                if (observed_elements.has(child))
                {
                    observed_elements.get(child).disconnect();
                    observed_elements.delete(child);
                }
            });
        }
        function init()
        {
            document.querySelectorAll(selectors_to_watch.join(',')).forEach(element =>
            {
                if (has_matching_text(element)) element.classList.add("fancy");
                observe_text_content_changes(element);
            });
            const observer = new MutationObserver(mutations =>
            {
                for (const mutation of mutations)
                {
                    for (const node of mutation.addedNodes)
                    {
                        if (node.nodeType !== 1) continue;
                        for (const selector of selectors_to_watch)
                        {
                            const elements = node.matches(selector) ? [node] : node.querySelectorAll(selector);
                            elements.forEach(element =>
                            {
                                if (has_matching_text(element)) element.classList.add("fancy");
                                observe_text_content_changes(element);
                            });
                        }
                    }
                    for (const node of mutation.removedNodes)
                    {
                        handle_removed_node(node);
                    }
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
        if (document.body)
        {
            init();
        } else
        {
            new MutationObserver((_, obs) =>
            {
                if (document.body)
                {
                    obs.disconnect();
                    init();
                }
            }).observe(document.documentElement, { childList: true });
        }
    }
})();