Twitch聊天室自動展開v2

非全螢幕時自動展開聊天室

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch聊天室自動展開v2
// @version      2.2
// @description  非全螢幕時自動展開聊天室
// @author      BaconEgg
// @match        https://www.twitch.tv/*
// @grant        none
// @namespace https://greasyfork.org/users/735944
// ==/UserScript==

(() => {
    'use strict';

    // 獲取聊天室狀態 按鈕標籤
    const getChatState = () => {
        const label = state.button?.getAttribute('aria-label');
        if (!label) return 'unknown';
        if (label.indexOf('展開') >= 0) return 'collapsed';
        if (label.indexOf('摺疊') >= 0) return 'expanded';
        return 'unknown';
    };

    // 常數定義
    const BUTTON_SELECTOR = '[data-a-target="right-column__toggle-collapse-btn"]';
    const CHAT_CONTAINER_SELECTOR = '[data-a-target="right-column"]';
    const THEATER_MODE_SELECTOR = '.channel-page__video-player--theatre-mode';
    const THROTTLE_DELAY = 500; // 增加到 500ms 進一步降低 CPU 使用率

    // 狀態管理
    const state = {
        button: null,
        chatContainer: null,
        lastCombo: '',
        observer: null,
        isTheaterMode: false, // 快取劇院模式狀態
        pendingRAF: null // 追蹤 requestAnimationFrame
    };

    // 節流工具:確保函數不會高頻呼叫
    const throttle = (fn, delay) => {
        let lastCall = 0;
        let scheduled = null;
        return (...args) => {
            const now = Date.now();
            if (now - lastCall >= delay) {
                lastCall = now;
                fn(...args);
            } else if (!scheduled) {
                scheduled = setTimeout(() => {
                    lastCall = Date.now();
                    scheduled = null;
                    fn(...args);
                }, delay - (now - lastCall));
            }
        };
    };

    // 快取按鈕元素
    const getButton = () => {
        if (state.button?.isConnected) return state.button;
        const chat = getChatContainer();
        if (chat) {
            state.button = chat.querySelector(BUTTON_SELECTOR);
            if (state.button?.isConnected) return state.button;
        }
        state.button = document.querySelector(BUTTON_SELECTOR);
        return state.button;
    };

    // 快取聊天室區塊
    const getChatContainer = () => {
        if (state.chatContainer?.isConnected) return state.chatContainer;
        state.chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
        return state.chatContainer;
    };

    // 檢查是否為劇院模式,並快取結果
    const checkTheaterMode = () => {
        state.isTheaterMode = !!document.querySelector(THEATER_MODE_SELECTOR);
        return state.isTheaterMode;
    };

    // 主要功能:只在狀態改變時才操作 DOM
    const updateChatVisibility = (trigger = '') => {
        try {
            const btn = getButton();
            if (!btn) return;

            const chatState = getChatState();
            const isFS = document.fullscreenElement !== null;
            const theater = state.isTheaterMode; // 使用快取的劇院模式狀態

            const combo = `${isFS}-${theater}-${chatState}`;
            if (combo === state.lastCombo) return;
            state.lastCombo = combo;

            // 取消任何已排程但尚未執行的 RAF
            if (state.pendingRAF) {
                cancelAnimationFrame(state.pendingRAF);
                state.pendingRAF = null;
            }

            if ((!isFS && chatState === 'collapsed') || (isFS && chatState === 'expanded')) {
                state.pendingRAF = requestAnimationFrame(() => {
                    btn.click();
                    state.pendingRAF = null;
                });
            }
        } catch (err) {
            console.error('[Twitch] Error updating chat visibility:', err);
        }
    };

    // 節流版本
    const throttledUpdateChatVisibility = throttle(updateChatVisibility, THROTTLE_DELAY);

    // 觀察器:只在必要時才更新,並用節流避免高頻率
    const startObserver = () => {
        if (state.observer) return;

        // 先檢查劇院模式狀態
        checkTheaterMode();

        const chat = getChatContainer();
        const target = chat || document.body;

        state.observer = new MutationObserver((mutations) => {
            if (!state.button?.isConnected) state.button = null;
            let needsUpdate = false;
            let theaterModeChanged = false;

            for (const mutation of mutations) {
                // 檢查按鈕狀態變化
                if (mutation.type === 'attributes' &&
                    mutation.attributeName === 'aria-label' &&
                    mutation.target.matches &&
                    mutation.target.matches(BUTTON_SELECTOR)) {
                    needsUpdate = true;
                    break;
                }

                // 檢查劇院模式變化
                if (mutation.type === 'attributes' &&
                    mutation.attributeName === 'class' &&
                    mutation.target.classList &&
                    mutation.target.classList.contains('channel-page__video-player')) {
                    theaterModeChanged = true;
                    needsUpdate = true;
                    break;
                }

                // 檢查按鈕是否新增
                if (mutation.type === 'childList' && !state.button) {
                    const addedNodes = Array.from(mutation.addedNodes);
                    if (addedNodes.some(node =>
                        node.nodeType === 1 &&
                        (node.matches?.(BUTTON_SELECTOR) || node.querySelector?.(BUTTON_SELECTOR)))) {
                        needsUpdate = true;
                        break;
                    }
                }
            }

            // 只在需要時更新劇院模式狀態
            if (theaterModeChanged) {
                checkTheaterMode();
            }

            if (needsUpdate) {
                throttledUpdateChatVisibility('Mutation');
            }
        });

        // 優化 MutationObserver 配置
        state.observer.observe(target, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['aria-label', 'class'],
            attributeOldValue: false
        });
    };

    // 初始化優化:只在按鈕存在時才啟動 observer
    const init = () => {
        // 延遲初始化,確保頁面完全載入
        setTimeout(() => {
            const btn = getButton();
            if (btn) {
                checkTheaterMode(); // 初始化時檢查劇院模式
                throttledUpdateChatVisibility('Init');
                startObserver();
            } else {
                // 如果按鈕不存在,設置一個一次性 MutationObserver 來等待按鈕出現
                const tempObserver = new MutationObserver((mutations, observer) => {
                    if (getButton()) {
                        observer.disconnect();
                        init(); // 按鈕出現後再次嘗試初始化
                    }
                });
                tempObserver.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: false
                });
            }
        }, 500); // 延遲 500ms 確保頁面元素已載入
    };

    // 事件監聽
    window.addEventListener('load', () => {
        init();

        // 全螢幕變化事件
        document.addEventListener('fullscreenchange', () => {
            throttledUpdateChatVisibility('Fullscreen');
        }, { passive: true });

        // 頁面可見性變化事件
        document.addEventListener('visibilitychange', () => {
            if (!document.hidden) {
                // 頁面變為可見時,重新檢查劇院模式
                checkTheaterMode();
                throttledUpdateChatVisibility('Visibility');
            }
        }, { passive: true });
    }, { passive: true });
})();