非全螢幕時自動展開聊天室
// ==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 });
})();