Twitch Auto Click Channel Points Chest and Statistics

Automatically click the Twitch channel points chest, monitor all point increases, and reset the accumulated total when switching channels.

Verze ze dne 19. 04. 2025. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         Twitch Auto Click Channel Points Chest and Statistics
// @name:zh-TW   Twitch 自動點擊忠誠點數寶箱和統計
// @name:zh-CN   Twitch 自动点击忠诚点数宝箱和统计
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Automatically click the Twitch channel points chest, monitor all point increases, and reset the accumulated total when switching channels.
// @description:zh-TW 自動點擊 Twitch 忠誠點數寶箱,並監控所有點數增加,切換直播間累積歸零
// @description:zh-CN 自动点击 Twitch 忠诚点数宝箱,并监控所有点数增加,切换直播间累积归零
// @author       chatgpt
// @match        https://www.twitch.tv/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let totalPoints = 0;
    let lastUrl = location.href;
    const recentPopups = new Set();

    function createPanel() {
        const panel = document.createElement('span');
        panel.id = 'my-loyalty-points-panel';
        panel.style.background = '#18181b';
        panel.style.color = '#FFD600';
        panel.style.padding = '2px 8px';
        panel.style.marginLeft = '8px';
        panel.style.borderRadius = '8px';
        panel.style.fontSize = '14px';
        panel.style.verticalAlign = 'middle';
        panel.style.display = 'inline-block';
        panel.style.zIndex = 9999;
        panel.innerText = `累積領取:${totalPoints} 點`;
        return panel;
    }

    // 插入提示框到主按鈕的最後面
    function insertPanel() {
        const oldPanel = document.getElementById('my-loyalty-points-panel');
        if (oldPanel) oldPanel.remove();

        // 找到主按鈕
        const mainBtn = document.querySelector('button[aria-label="小奇點和點數餘額"]');
        if (mainBtn) {
            if (!mainBtn.querySelector('#my-loyalty-points-panel')) {
                const panel = createPanel();
                mainBtn.appendChild(panel);
            }
        }
    }

    function updatePanel() {
        let panel = document.getElementById('my-loyalty-points-panel');
        if (!panel) {
            insertPanel();
            panel = document.getElementById('my-loyalty-points-panel');
        }
        if (panel) {
            panel.innerText = `累積領取:${totalPoints} 點`;
        }
    }

    function isInDialog(node) {
        while (node) {
            if (
                (node.getAttribute && node.getAttribute('role') === 'dialog') ||
                (node.classList && node.classList.contains('tw-modal'))
            ) {
                return true;
            }
            node = node.parentElement;
        }
        return false;
    }

    function checkAndClickChest() {
        const iconDivs = document.querySelectorAll('.claimable-bonus__icon');
        for (const iconDiv of iconDivs) {
            const btn = iconDiv.closest('button');
            if (
                btn &&
                !btn.disabled &&
                btn.offsetParent !== null &&
                !isInDialog(btn)
            ) {
                btn.click();
                return;
            }
        }
    }

    // 精準抓取浮動分數提示
    function handlePopupNode(node) {
        if (
            node.classList &&
            node.classList.contains('Layout-sc-1xcs6mc-0') &&
            node.classList.contains('bgzAOg')
        ) {
            for (const child of node.childNodes) {
                if (child.nodeType === Node.TEXT_NODE) {
                    const text = child.textContent.trim();
                    const match = text.match(/^\+(\d+)\s*點?$/);
                    if (match) {
                        const key = text + '_' + Date.now();
                        for (let k of recentPopups) {
                            if (k.startsWith(text)) return;
                        }
                        recentPopups.add(key);
                        setTimeout(() => recentPopups.delete(key), 1000);

                        const add = parseInt(match[1], 10);
                        if (!isNaN(add)) {
                            totalPoints += add;
                            updatePanel();
                        }
                    }
                }
            }
        }
    }

    // 監控所有新出現的 .Layout-sc-1xcs6mc-0.bgzAOg
    function observePointPopups() {
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (!(node instanceof HTMLElement)) continue;
                    if (
                        node.classList.contains('Layout-sc-1xcs6mc-0') &&
                        node.classList.contains('bgzAOg')
                    ) {
                        handlePopupNode(node);
                    }
                    // 也檢查所有子孫
                    node.querySelectorAll && node.querySelectorAll('.Layout-sc-1xcs6mc-0.bgzAOg').forEach(handlePopupNode);
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function watchUrlChange() {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            totalPoints = 0;
            updatePanel();
            recentPopups.clear();
        }
    }

    function main() {
        insertPanel();
        checkAndClickChest();
        watchUrlChange();
        observePointPopups();
        // 每秒自動補回提示框
        setInterval(() => {
            let panel = document.getElementById('my-loyalty-points-panel');
            if (!panel) insertPanel();
        }, 1000);
        // 其他定時任務
        setInterval(() => {
            checkAndClickChest();
            watchUrlChange();
        }, 1000);
    }

    window.addEventListener('load', () => setTimeout(main, 1500));
})();