Twitch Auto Drops Claim

Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置

// ==UserScript==
// @name                Twitch Auto Drops Claim
// @name:zh-TW          Twitch 自動領取掉寶
// @name:zh-CN          Twitch 自动领取掉宝
// @name:en             Twitch Auto Drops Claim
// @name:ja             Twitch 自動ドロップ受け取り
// @name:ko             Twitch 자동 드롭 수령
// @name:ru             Twitch Автоматическое получение дропов
// @version             2025.10.12-Beta
// @author              Canaan HS
// @description         Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
// @description:zh-TW   Twitch 自動領取 (掉寶/Drops) , 窗口標籤顯示進度 , 直播結束時還沒領完 , 會自動尋找任意掉寶直播 , 並開啟後繼續掛機 , 代碼自訂義設置
// @description:zh-CN   Twitch 自动领取 (掉宝/Drops) , 窗口标签显示进度 , 直播结束时还没领完 , 会自动寻找任意掉宝直播 , 并开启后继续挂机 , 代码自定义设置
// @description:en      Automatically claim Twitch Drops, display progress in the tab, and if not finished when the stream ends, it will automatically find another Drops-enabled stream and continue farming. Customizable settings in the code.
// @description:ja      Twitch のドロップを自動的に受け取り、タブに進捗狀況を表示し、ストリーム終了時にまだ受け取っていない場合、自動的に別のドロップ有効なストリームを検索し、収穫を続けます。コードでのカスタマイズ可能な設定
// @description:ko      Twitch 드롭을 자동으로 받아오고 탭에 진행 상황을 표시하며, 스트림이 종료되었을 때 아직 완료되지 않았다면 자동으로 다른 드롭 활성 스트림을 찾아 계속 수집합니다. 코드에서 사용자 정의 설정 가능합니다
// @description:ru      Автоматически получает дропы Twitch, отображает прогресс во вкладке, и если дропы не завершены к концу трансляции, автоматически находит другую трансляцию с активированными дропами и продолжает фарминг. Настраиваемые параметры в коде.

// @match        https://www.twitch.tv/*
// @supportURL   https://github.com/Canaan-HS/MonkeyScript/issues
// @icon         https://cdn-icons-png.flaticon.com/512/8214/8214044.png

// @license      MPL-2.0
// @namespace    https://greasyfork.org/users/989635

// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.close
// @grant        GM_deleteValue
// @grant        window.onurlchange
// @grant        GM_registerMenuCommand

// @run-at       document-body
// ==/UserScript==

(() => {
    const Backup = GM_getValue("Config", {});
    const Config = {
        Dev: false, // 開發打印

        RestartLive: true, // 使用重啟直播
        EndAutoClose: true, // 全部進度完成後自動關閉
        TryStayActive: true, // 嘗試讓頁面保持活躍
        RestartLiveMute: true, // 重啟的直播靜音
        RestartLowQuality: false, // 重啟直播最低畫質

        UpdateDisplay: true, // 於標題展示更新倒數
        ClearExpiration: true, // 清除過期的掉寶進度
        ProgressDisplay: true, // 於標題展示掉寶進度

        UpdateInterval: 120, // (seconds) 更新進度狀態的間隔
        JudgmentInterval: 6, // (Minute) 經過多長時間進度無增加, 就重啟直播 [設置太短會可能誤檢測]

        FindTag: ["drops", "啟用掉寶", "启用掉宝", "드롭활성화됨"], // 查找直播標籤, 只要有包含該字串即可
        ...Backup
    };
    const supportPage = "https://www.twitch.tv/drops/inventory";
    const supportCheck = (url = location.href) => url === supportPage;
    class Detection {
        constructor() {
            this.progressParse = progress => progress.sort((a, b) => b - a).find(number => number < 100);
            this.getTime = () => {
                const time = this.currentTime;
                const year = time.getFullYear();
                const month = `${time.getMonth() + 1}`.padStart(2, "0");
                const date = `${time.getDate()}`.padStart(2, "0");
                const hour = `${time.getHours()}`.padStart(2, "0");
                const minute = `${time.getMinutes()}`.padStart(2, "0");
                const second = `${time.getSeconds()}`.padStart(2, "0");
                return `${year}-${month}-${date} ${hour}:${minute}:${second}`;
            };
            this.storage = (key, value = null) => value == null ? (value = sessionStorage.getItem(key),
                value != null ? JSON.parse(value) : value) : sessionStorage.setItem(key, JSON.stringify(value));
            this.adapter = {
                _convertPM: time => time.replace(/(\d{1,2}):(\d{2})/, (_, hours, minutes) => `${+hours + 12}:${minutes}`),
                "en-US": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "en-GB": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "es-ES": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "fr-FR": (timeStamp, currentYear) => new Date(`${timeStamp} ${currentYear}`),
                "pt-PT": (timeStamp, currentYear) => {
                    const convert = timeStamp.replace(/(\d{1,2})\/(\d{1,2})/, (_, day, month) => `${month}/${day}`);
                    return new Date(`${convert} ${currentYear}`);
                },
                "pt-BR": (timeStamp, currentYear) => {
                    const ISO = { jan: "Jan", fev: "Feb", mar: "Mar", abr: "Apr", mai: "May", jun: "Jun", jul: "Jul", ago: "Aug", set: "Sep", out: "Oct", nov: "Nov", dez: "Dec", dom: "Sun", seg: "Mon", ter: "Tue", qua: "Wed", qui: "Thu", sex: "Fri", "sáb": "Sat" };
                    const convert = timeStamp.replace(/de/g, "").replace(/(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez|dom|seg|ter|qua|qui|sex|sáb)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "ru-RU": (timeStamp, currentYear) => {
                    const ISO = { "янв": "Jan", "фев": "Feb", "мар": "Mar", "апр": "Apr", "май": "May", "июн": "Jun", "июл": "Jul", "авг": "Aug", "сен": "Sep", "окт": "Oct", "ноя": "Nov", "дек": "Dec", "пн": "Mon", "вт": "Tue", "ср": "Wed", "чт": "Thu", "пт": "Fri", "сб": "Sat", "вс": "Sun" };
                    const convert = timeStamp.replace(/(янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек|пн|вт|ср|чт|пт|сб|вс)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "de-DE": (timeStamp, currentYear) => {
                    const ISO = {
                        jan: "Jan", feb: "Feb", "mär": "Mar", apr: "Apr", mai: "May", jun: "Jun", jul: "Jul", aug: "Aug", sep: "Sep", okt: "Oct", nov: "Nov", dez: "Dec", mo: "Mon", di: "Tue", mi: "Wed", do: "Thu", fr: "Fri", sa: "Sat", so: "Sun"
                    };
                    const convert = timeStamp.replace(/(jan|feb|mär|apr|mai|jun|jul|aug|sep|okt|nov|dez|mo|di|mi|do|fr|sa|so)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "it-IT": (timeStamp, currentYear) => {
                    const ISO = { gen: "Jan", feb: "Feb", mar: "Mar", apr: "Apr", mag: "May", giu: "Jun", lug: "Jul", ago: "Aug", set: "Sep", ott: "Oct", nov: "Nov", dic: "Dec", dom: "Sun", lun: "Mon", mar: "Tue", mer: "Wed", gio: "Thu", ven: "Fri", sab: "Sat" };
                    const convert = timeStamp.replace(/(gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic|dom|lun|mar|mer|gio|ven|sab)/gi, match => ISO[match.toLowerCase()]);
                    return new Date(`${convert} ${currentYear}`);
                },
                "tr-TR": (timeStamp, currentYear) => {
                    const ISO = { oca: "Jan", "şub": "Feb", mar: "Mar", nis: "Apr", may: "May", haz: "Jun", tem: "Jul", "ağu": "Aug", eyl: "Sep", eki: "Oct", kas: "Nov", ara: "Dec", paz: "Sun", pts: "Mon", sal: "Tue", "çar": "Wed", per: "Thu", cum: "Fri", cmt: "Sat" };
                    const convert = timeStamp.replace(/(oca|şub|mar|nis|may|haz|tem|ağu|eyl|eki|kas|ara|paz|pts|sal|çar|per|cum|cmt)/gi, match => ISO[match.toLowerCase()]);
                    const match = convert.match(/(\d{1,2}) ([a-z]+) ([a-z]+) (\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/i);
                    return new Date(`${match[3]} ${match[1]} ${match[2]} ${match[4]} ${match[5]} ${currentYear}`);
                },
                "es-MX": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/^([a-zñáéíóúü]+) (\d{1,2}) de ([a-zñáéíóúü]+), (\d{1,2}:\d{1,2}) (?:[ap]\.m\.) (GMT[+-]\d{1,2})/i);
                    const time = timeStamp.includes("p.m") ? this.adapter._convertPM(match[4]) : match[4];
                    return new Date(`${match[1]}, ${match[2]} ${match[3]}, ${time} ${match[5]} ${currentYear}`);
                },
                "ja-JP": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[3]}:00 ${match[4]}`);
                },
                "ko-KR": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+(\d{1,2}:\d{1,2}) (GMT[+-]\d{1,2})/);
                    const time = timeStamp.includes("오후") ? this.adapter._convertPM(match[3]) : match[3];
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
                },
                "zh-TW": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(\d{1,2}:\d{1,2}) \[(GMT[+-]\d{1,2})\]/);
                    const time = timeStamp.includes("下午") ? this.adapter._convertPM(match[3]) : match[3];
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${time}:00 ${match[4]}`);
                },
                "zh-CN": (timeStamp, currentYear) => {
                    const match = timeStamp.match(/(\d{1,2})\D+(\d{1,2})\D+\D+(GMT[+-]\d{1,2}) (\d{1,2}:\d{1,2})/);
                    return new Date(`${currentYear}-${match[1]}-${match[2]} ${match[4]}:00 ${match[3]}`);
                }
            };
            this.pageRefresh = async (updateDisplay, interval, finishCall) => {
                let timer;
                const start = Date.now();
                const refresh = setInterval(() => {
                    if (!supportCheck()) {
                        clearInterval(refresh);
                        clearTimeout(timer);
                        this.titleObserver?.disconnect();
                        finishCall?.();
                    } else if (updateDisplay) {
                        const elapsed = Math.floor((Date.now() - start) / 1e3);
                        const remaining = interval - elapsed;
                        if (remaining >= 0) {
                            document.title = `【 ${remaining}s 】 ${this.progressStr}`;
                        }
                    }
                }, 1e3);
                timer = setTimeout(() => {
                    clearInterval(refresh);
                    finishCall?.();
                }, (interval + 1) * 1e3);
            };
            this.showProgress = () => {
                this.titleObserver = new MutationObserver(() => {
                    document.title !== this.progressStr && (document.title = this.progressStr);
                });
                this.titleObserver.observe(document.querySelector("title"), {
                    childList: 1,
                    subtree: 0
                });
                document.title = this.progressStr;
            };
            this.expiredCleanup = (element, adapter, timestamp, callback) => {
                const targetTime = adapter?.(timestamp, this.currentTime.getFullYear()) ?? this.currentTime;
                this.currentTime > targetTime ? this.Config.ClearExpiration && element.remove() : callback(element);
            };
            this.progressStr;
            this.titleObserver;
            this.Config = {
                ...Config,
                EndLine: "p a[href='/drops/campaigns']",
                Campaigns: "a[href='/drops/campaigns']",
                Inventory: "a[href='/drops/inventory']",
                allProgress: ".inventory-max-width > div:not(:first-child)",
                ProgressBar: "[role='progressbar'] + div span",
                ActivityTime: ".inventory-campaign-info span:last-child"
            };
        }
        get currentTime() {
            return new Date();
        }
        static async run() {
            regMenu();
            const self = new Detection();
            const config = self.Config;
            const updateDisplay = config.UpdateDisplay;
            let campaigns, inventory, adapter;
            let taskCount, currentProgress, inProgressIndex, progressInfo;
            const initData = () => {
                self.progressStr = "Twitch";
                taskCount = 0, currentProgress = 0, inProgressIndex = 0;
                progressInfo = {};
            };
            initData();
            const process = (token = 10) => {
                campaigns ??= devTrace("Campaigns", document.querySelector(config.Campaigns));
                inventory ??= devTrace("Inventory", document.querySelector(config.Inventory));
                const allProgress = devTrace("AllProgress", document.querySelectorAll(config.allProgress));
                if (allProgress?.length > 0) {
                    let activityTime, progressBar;
                    adapter ??= self.adapter[document.documentElement.lang];
                    allProgress.forEach(data => {
                        activityTime = devTrace("ActivityTime", data.querySelector(config.ActivityTime));
                        self.expiredCleanup(data, adapter, activityTime?.textContent, notExpired => {
                            notExpired.querySelectorAll("button").forEach(draw => {
                                draw.click();
                            });
                            progressBar = devTrace("ProgressBar", notExpired.querySelectorAll(config.ProgressBar));
                            progressInfo[taskCount++] = [...progressBar].map(progress => +progress.textContent);
                        });
                    });
                    const oldTask = self.storage("Task") ?? {};
                    const newTask = Object.fromEntries(Object.entries(progressInfo).map(([key, value]) => [key, self.progressParse(value)]));
                    let taskIndex, newProgress;
                    const taskEntries = Object.entries(newTask);
                    for ([taskIndex, newProgress] of taskEntries) {
                        const oldProgress = oldTask[taskIndex] || newProgress;
                        if (newProgress !== oldProgress) {
                            inProgressIndex = taskIndex;
                            currentProgress = newProgress;
                            break;
                        }
                    }
                    if (typeof inProgressIndex === "number" && taskEntries.length > 1) {
                        [taskIndex, newProgress] = taskEntries.reduce((max, cur) => cur[1] > max[1] ? cur : max);
                        inProgressIndex = taskIndex;
                        currentProgress = newProgress;
                    }
                    self.storage("Task", newTask);
                }
                if (currentProgress > 0) {
                    if (config.ProgressDisplay) {
                        self.progressStr = `${currentProgress}%`;
                        !updateDisplay && self.showProgress();
                    }
                } else if (token > 0 && supportCheck()) {
                    setTimeout(() => {
                        process(token - 1);
                    }, 2e3);
                    return;
                }
                const [record, timestamp] = self.storage("Record") ?? [0, self.getTime()];
                const diffInterval = ~~((self.currentTime - new Date(timestamp)) / (1e3 * 60));
                const notHasToken = token === 0;
                const hasProgress = currentProgress > 0;
                if (diffInterval >= config.JudgmentInterval && hasProgress && currentProgress === record) {
                    config.RestartLive && restartLive.run(inProgressIndex);
                    self.storage("Record", [currentProgress, self.getTime()]);
                } else if (hasProgress && currentProgress !== record) {
                    self.storage("Record", [currentProgress, self.getTime()]);
                } else if (config.EndAutoClose && notHasToken && !hasProgress && record !== 0) {
                    window.open("", "NewWindow", "top=0,left=0,width=1,height=1").close();
                    window.close();
                } else if (notHasToken && supportCheck()) {
                    location.assign(supportPage);
                }
            };
            const waitLoad = (select, interval = 500, timeout = 15e3) => {
                let elapsed = 0;
                return new Promise((resolve, reject) => {
                    const query = () => {
                        if (document.querySelector(select)) resolve(); else {
                            elapsed += interval;
                            elapsed >= timeout ? supportCheck() && location.assign(supportPage) : setTimeout(query, interval);
                        }
                    };
                    setTimeout(query, interval);
                });
            };
            const monitor = () => {
                self.pageRefresh(updateDisplay, config.UpdateInterval, async () => {
                    initData();
                    if (!supportCheck()) {
                        waitSupport();
                        return;
                    }
                    campaigns?.click();
                    await waitLoad(".accordion-header");
                    inventory?.click();
                    await waitLoad(config.EndLine);
                    process();
                    monitor();
                });
            };
            waitEl(document, config.EndLine, () => {
                process();
                monitor();
                config.TryStayActive && stayActive(document);
            }, {
                timeoutResult: true
            });
        }
    }
    class RestartLive {
        constructor() {
            this.liveMute = async _document => {
                waitEl(_document, "video", video => {
                    const silentInterval = setInterval(() => {
                        video.muted = 1;
                    }, 500);
                    setTimeout(() => {
                        clearInterval(silentInterval);
                    }, 15e3);
                });
            };
            this.liveLowQuality = async _document => {
                const dom = _document;
                waitEl(dom, "[data-a-target='player-settings-button']", menu => {
                    menu.click();
                    waitEl(dom, "[data-a-target='player-settings-menu-item-quality']", quality => {
                        quality.click();
                        waitEl(dom, "[data-a-target='player-settings-menu']", settings => {
                            settings.lastElementChild.click();
                            setTimeout(() => menu.click(), 800);
                        });
                    });
                });
            };
            this.waitDocument = async (_window, checkFu) => {
                let _document, animationFrame;
                return new Promise((resolve, reject) => {
                    let observe;
                    _window.onload = () => {
                        cancelAnimationFrame(animationFrame);
                        _document = _window.document;
                        observe = new MutationObserver($throttle(() => {
                            if (checkFu(_document)) {
                                observe.disconnect();
                                resolve(_document);
                            }
                        }, 300));
                        observe.observe(_document, {
                            subtree: 1,
                            childList: 1,
                            characterData: 1
                        });
                    };
                    const query = () => {
                        _document = _window.document;
                        if (_document && checkFu(_document)) {
                            cancelAnimationFrame(animationFrame);
                            observe?.disconnect();
                            resolve(_document);
                        } else {
                            animationFrame = requestAnimationFrame(query);
                        }
                    };
                    animationFrame = requestAnimationFrame(query);
                });
            };
            this.Config = {
                ...Config,
                Offline: ".home-carousel-info strong",
                Online: "[data-a-target='animated-channel-viewers-count']",
                Channel: ".preview-card-channel-link",
                Container: "#directory-game-main-content",
                ContainerHandle: ".scrollable-area",
                ActivityLink1: "[data-test-selector='DropsCampaignInProgressDescription-hint-text-parent']",
                ActivityLink2: "[data-test-selector='DropsCampaignInProgressDescription-no-channels-hint-text']"
            };
        }
        async run(maxIndex) {
            window.open("", "NewWindow", "top=0,left=0,width=1,height=1").close();
            const self = this;
            const config = self.Config;
            let newWindow;
            let channel = document.querySelectorAll(config.ActivityLink2)[maxIndex];
            if (channel) {
                newWindow = window.open(channel.href, "NewWindow");
                dirSearch(newWindow);
            } else {
                channel = document.querySelectorAll(config.ActivityLink1)[maxIndex];
                const openLink = [...channel.querySelectorAll("a")].reverse();
                findLive(0);
                async function findLive(index) {
                    if (openLink.length - 1 < index) return 0;
                    const href = openLink[index].href;
                    newWindow = !newWindow ? window.open(href, "NewWindow") : (newWindow.location.assign(href),
                        newWindow);
                    if (href.includes("directory")) {
                        dirSearch(newWindow);
                    } else {
                        const _document = await self.waitDocument(newWindow, document => document.querySelector(config.Offline) || document.querySelector(config.Online));
                        if (devTrace("Offline", _document.querySelector(config.Offline))) {
                            findLive(index + 1);
                        } else if (devTrace("Online", _document.querySelector(config.Online))) {
                            config.RestartLiveMute && self.liveMute(_document);
                            config.TryStayActive && stayActive(_document);
                            config.RestartLowQuality && self.liveLowQuality(_document);
                        }
                    }
                }
            }
            const pattern = config.FindTag.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
            const tagRegex = new RegExp(pattern, "i");
            async function dirSearch(newWindow) {
                const _document = await self.waitDocument(newWindow, document => document.querySelector(config.Container));
                let scrollHandle;
                const container = devTrace("Container", _document.querySelector(config.Container));
                const startFind = () => {
                    try {
                        scrollHandle ??= devTrace("ContainerHandle", container.closest(config.ContainerHandle));
                        const channel = devTrace("Channel", container.querySelectorAll(`${config.Channel}:not([Drops-Processed])`));
                        const liveLink = [...channel].find(channel => {
                            channel.setAttribute("Drops-Processed", true);
                            const haveDrops = [...channel.nextElementSibling?.querySelectorAll("span")].some(span => tagRegex.test(span.textContent));
                            return haveDrops ? channel : null;
                        });
                        if (liveLink) {
                            liveLink.click();
                            liveLink.click();
                            config.RestartLiveMute && self.liveMute(_document);
                            config.TryStayActive && stayActive(_document);
                            config.RestartLowQuality && self.liveLowQuality(_document);
                        } else if (scrollHandle) {
                            scrollHandle.scrollTo({
                                top: scrollHandle.scrollHeight
                            });
                            setTimeout(startFind, 1500);
                        }
                    } catch {
                        setTimeout(startFind, 1500);
                    }
                };
                startFind();
            }
        }
    }
    async function stayActive(_document) {
        const id = "Stay-Active";
        const head = _document.head;
        if (head.getElementById(id)) return;
        const script = document.createElement("script");
        script.id = id;
        script.textContent = `
            function WorkerCreation(code) {
                const blob = new Blob([code], {type: "application/javascript"});
                return new Worker(URL.createObjectURL(blob));
            }

            const Active = WorkerCreation(\`
                onmessage = function(e) {
                    setTimeout(() => {
                        const { url } = e.data;
                        fetch(url);
                        postMessage({ url });
                    }, 1e4);
                }
            \`);

            Active.postMessage({ url: location.href });
            Active.onmessage = (e) => {
                const { url } = e.data;
                document.querySelector("video")?.play();
                Active.postMessage({ url });
            };

            let emptyAudio = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEA...");
            emptyAudio.loop = true;
            emptyAudio.muted = true;

            // 後台播放 / 前台暫停
            const visHandler = (isHidden) => {
                if (typeof isHidden !== 'boolean') isHidden = document.hidden;
                if (isHidden) {
                    emptyAudio.play().catch(()=>{});
                } else {
                    emptyAudio.pause();
                }
            };

            if (typeof document.hidden !== "undefined") {
                document.addEventListener("visibilitychange", () => visHandler());
            } else {
                window.addEventListener("focus", () => visHandler(false));
                window.addEventListener("blur", () => visHandler(true));
            }

            visHandler();
        `;
        head.append(script);
    }
    function $throttle(func, delay) {
        let lastTime = 0;
        return (...args) => {
            const now = Date.now();
            if (now - lastTime >= delay) {
                lastTime = now;
                func(...args);
            }
        };
    }
    let cleaner = null;
    let traceRecord = {};
    function getCompositeKey(elements) {
        return Array.from(elements).map(el => {
            if (!(el instanceof Element)) return "";
            return el.tagName + (el.id || "id") + (el.className || "class");
        }).join("|");
    }
    function devTrace(tag, element) {
        if (!Config.Dev) return element;
        const record = traceRecord[tag];
        const isNodeList = element instanceof NodeList;
        const recordKey = isNodeList ? getCompositeKey(element) : element;
        if (record && record.has(recordKey)) return element;
        traceRecord[tag] = new Map().set(recordKey, true);
        clearTimeout(cleaner);
        cleaner = setTimeout(() => {
            traceRecord = {};
        }, 1e4);
        const isEmpty = !element || isNodeList && element.length === 0;
        const baseStyle = "padding: 2px 6px; border-radius: 3px; font-weight: bold; margin: 0 2px;";
        const tagStyle = `${baseStyle} background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);`;
        let statusStyle, statusIcon, statusText;
        if (isEmpty) {
            statusStyle = `${baseStyle} background: linear-gradient(45deg, #e74c3c 0%, #c0392b 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);`;
            statusIcon = "❌";
            statusText = "NOT FOUND";
        } else {
            statusStyle = `${baseStyle} background: linear-gradient(45deg, #2ecc71 0%, #27ae60 100%); color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);`;
            statusIcon = "✅";
            statusText = "FOUND";
        }
        console.groupCollapsed(`%c🔍 ${tag} %c${statusIcon} ${statusText}`, tagStyle, statusStyle);
        if (isEmpty) {
            console.log(`%c📭 Element: %c${element === null ? "null" : "empty NodeList"}`, "color: #e74c3c; font-weight: bold;", "color: #c0392b; font-style: italic;");
        } else {
            console.log("%c📦 Element:", "color: #27ae60; font-weight: bold;", element);
        }
        console.trace("🎯 Source");
        console.groupEnd();
        return element;
    }
    async function waitEl(document, selector, found, {
        timeout = 1e4,
        throttle = 200,
        timeoutResult = false
    } = {}) {
        let timer, element;
        const observer = new MutationObserver($throttle(() => {
            element = document.querySelector(selector);
            if (element) {
                observer.disconnect();
                clearTimeout(timer);
                found(element);
            }
        }, throttle));
        observer.observe(document, {
            subtree: 1,
            childList: 1,
            characterData: 1
        });
        timer = setTimeout(() => {
            observer.disconnect();
            timeoutResult && found(element);
        }, timeout);
    }
    function onUrlChange(callback, timeout = 15) {
        let timer = null;
        let cleaned = false;
        let support_urlchange = false;
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;
        const eventHandler = {
            urlchange: () => trigger("urlchange"),
            popstate: () => trigger("popstate"),
            hashchange: () => trigger("hashchange")
        };
        function trigger(type) {
            clearTimeout(timer);
            if (!support_urlchange && type === "urlchange") support_urlchange = true;
            timer = setTimeout(() => {
                if (support_urlchange) off(false, true);
                callback({
                    type: type,
                    url: location.href,
                    domain: location.hostname
                });
            }, Math.max(15, timeout));
        }
        function off(all = true, clean = false) {
            if (clean && cleaned) return;
            clearTimeout(timer);
            history.pushState = originalPushState;
            history.replaceState = originalReplaceState;
            window.removeEventListener("popstate", eventHandler.popstate);
            window.removeEventListener("hashchange", eventHandler.hashchange);
            all && window.removeEventListener("urlchange", eventHandler.urlchange);
            cleaned = true;
        }
        window.addEventListener("urlchange", eventHandler.urlchange);
        window.addEventListener("popstate", eventHandler.popstate);
        window.addEventListener("hashchange", eventHandler.hashchange);
        history.pushState = function () {
            originalPushState.apply(this, arguments);
            trigger("pushState");
        };
        history.replaceState = function () {
            originalReplaceState.apply(this, arguments);
            trigger("replacestate");
        };
        return {
            off: off
        };
    }
    function regMenu() {
        if (Object.keys(Backup).length > 0) {
            GM_registerMenuCommand("🗑️ Clear Config", () => {
                GM_deleteValue("Config");
                location.reload();
            });
        } else {
            const SaveConfig = structuredClone(Config);
            GM_registerMenuCommand("📝 Save Config", () => {
                GM_setValue("Config", SaveConfig);
            });
        }
    }
    function waitSupport() {
        const {
            off
        } = onUrlChange(uri => {
            if (supportCheck(uri.url)) {
                Detection.run();
                off();
            }
        });
    }
    const restartLive = new RestartLive();
    if (supportCheck()) Detection.run(); else waitSupport();
})();