Greasy Fork is available in English.
隐藏聊天输入框与排行榜。内置精简设置面板,支持自定义颜色、位置、感应宽度与秒数。
// ==UserScript==
// @name Twitch Chat Stealth
// @name:zh-TW Twitch 聊天室隱藏助手
// @name:zh-CN Twitch 聊天室隱藏助手
// @version 1.3
// @license MIT
// @description Stealthily hide the chat input and leaderboard. Features a slim dashboard for custom colors and positions.
// @description:zh-TW 隱藏聊天輸入框與排行榜。內建精簡設定面板,支援自定義顏色、位置、感應寬度與秒數。
// @description:zh-CN 隐藏聊天输入框与排行榜。内置精简设置面板,支持自定义颜色、位置、感应宽度与秒数。
// @author Scott
// @match https://www.twitch.tv/popout/*/chat*
// @match https://m.twitch.tv/*/chat*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @namespace http://greasyfork.icu/users/1284613
// ==/UserScript==
(function() {
'use strict';
const store = {
get: (key, def) => GM_getValue(key, def),
set: (key, val) => { GM_setValue(key, val); applyDynamicStyles(); }
};
const lang = (navigator.language || navigator.userLanguage).includes('zh') ? 'zh' : 'en';
const ui = {
title: lang === 'zh' ? '聊天室設定' : 'Chat Config',
hideRank: lang === 'zh' ? '屏蔽社群精華與排行榜' : 'Hide Community Highlights & Rank',
pos: lang === 'zh' ? '感應條位置' : 'Bar Position',
width: lang === 'zh' ? '感應條長度' : 'Bar Width',
color: lang === 'zh' ? '感應條顏色' : 'Bar Color',
delay: lang === 'zh' ? '自動收合秒數' : 'Auto-hide (sec)',
close: lang === 'zh' ? '點擊空白處關閉設定' : 'Click outside to close',
left: lang === 'zh' ? '靠左' : 'Left',
right: lang === 'zh' ? '靠右' : 'Right',
center: lang === 'zh' ? '置中' : 'Mid',
full: lang === 'zh' ? '全寬' : 'Full',
always: lang === 'zh' ? '永久顯示' : 'Always',
custom: lang === 'zh' ? '自定義' : 'Custom'
};
GM_addStyle(`
.chat-input, [data-a-target="chat-input-container"] {
transition: all 0.2s ease-out !important;
overflow: hidden !important;
}
.stealth-mode { height: 0px !important; min-height: 0px !important; opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; }
.hide-rank-mode .eOHCrm.Layout-sc-1xcs6mc-0,
.hide-rank-mode [data-a-target="community-highlight-stack__scroll-area"],
.hide-rank-mode .chat-room__header { display: none !important; }
#stealth-toggle { position: fixed; bottom: 0 !important; z-index: 999998; transition: opacity 0.2s; -webkit-tap-highlight-color: transparent; cursor: pointer; }
#stealth-dash {
position: fixed; top: 10%; left: -220px; width: 200px;
background: rgba(24, 24, 27, 0.98); backdrop-filter: blur(10px);
color: #efeff1; z-index: 1000000; border: 1px solid #333;
border-radius: 0 8px 8px 0; box-shadow: 4px 0 20px rgba(0,0,0,0.6);
transition: left 0.3s ease; padding: 15px; display: flex; flex-direction: column; gap: 15px; font-family: sans-serif;
}
#stealth-dash.open { left: 0; }
#stealth-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; display: none; }
#stealth-overlay.open { display: block; }
.dash-item { display: flex; flex-direction: column; gap: 8px; }
.dash-label { font-size: 13px; font-weight: bold; color: #efeff1; display: flex; justify-content: space-between; align-items: center; }
/* 強制按鈕內容置中 */
.dash-btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.dash-btn {
background: #3a3a3d; border: none; color: white; padding: 8px; cursor: pointer; border-radius: 4px; font-size: 12px;
display: flex; align-items: center; justify-content: center; text-align: center;
}
.dash-btn.active { background: #9147ff; }
.switch-container { display: flex; align-items: center; justify-content: space-between; cursor: pointer; width: 100%; }
.switch-bg { position: relative; width: 36px; height: 18px; background: #3a3a3d; border-radius: 20px; transition: 0.3s; display: flex; align-items: center; padding: 0 2px; }
.switch-dot { width: 14px; height: 14px; background: white; border-radius: 50%; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
.active-bg { background: #9147ff; }
.active-dot { transform: translateX(18px); }
/* 自定義輸入框置中校準 */
.custom-container { display: flex; gap: 2px; background: #3a3a3d; border-radius: 4px; padding: 4px; align-items: center; justify-content: center; }
.custom-input { background: #18181b; border: 1px solid #444; color: white; border-radius: 4px; padding: 2px 0; width: 35px; font-size: 11px; text-align: center; }
input[type=range] { accent-color: #9147ff; cursor: pointer; }
input[type=color] { background: none; border: 1px solid #444; width: 100%; height: 26px; cursor: pointer; border-radius: 4px; padding: 0; }
`);
function applyDynamicStyles() {
const pos = store.get('pos', 'right');
const width = store.get('width', 30);
const color = store.get('color', '#9147ff');
const hideRank = store.get('hideRank', false);
const el = document.getElementById('stealth-toggle');
if (!el) return;
let style = `opacity: 0.1; height: 5px; background: ${color}; `;
if (pos === 'left') style += `left: 0; width: ${width}%; right: auto;`;
else if (pos === 'center') style += `left: ${50 - width/2}%; width: ${width}%; right: auto;`;
else if (pos === 'full') style += `left: 0; width: 100%; right: auto;`;
else style += `right: 0; width: ${width}%; left: auto;`;
el.style.cssText = style;
document.body.classList.toggle('hide-rank-mode', hideRank);
}
function createDashboard() {
const overlay = document.createElement('div');
overlay.id = 'stealth-overlay';
const dash = document.createElement('div');
dash.id = 'stealth-dash';
const currentDelay = store.get('delay', 5000);
const isAlways = currentDelay === 999999;
const isPreset = [3000, 5000].includes(currentDelay);
dash.innerHTML = `
<div style="font-size: 15px; font-weight: bold; color: #9147ff; text-align: center; border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 5px;">${ui.title}</div>
<div class="dash-item">
<div class="switch-container" id="sw-rank">
<span class="dash-label">${ui.hideRank}</span>
<div class="switch-bg ${store.get('hideRank') ? 'active-bg' : ''}">
<div class="switch-dot ${store.get('hideRank') ? 'active-dot' : ''}"></div>
</div>
</div>
</div>
<div class="dash-item">
<span class="dash-label">${ui.pos}</span>
<div class="dash-btn-group">
<button class="dash-btn pos-btn ${store.get('pos')==='left'?'active':''}" data-val="left">${ui.left}</button>
<button class="dash-btn pos-btn ${store.get('pos')==='right'?'active':''}" data-val="right">${ui.right}</button>
<button class="dash-btn pos-btn ${store.get('pos')==='center'?'active':''}" data-val="center">${ui.center}</button>
<button class="dash-btn pos-btn ${store.get('pos')==='full'?'active':''}" data-val="full">${ui.full}</button>
</div>
</div>
<div class="dash-item">
<span class="dash-label">${ui.delay}</span>
<div class="dash-btn-group" style="grid-template-columns: 1fr 1fr 2fr;">
<button class="dash-btn delay-btn ${currentDelay===3000?'active':''}" data-ms="3000">3s</button>
<button class="dash-btn delay-btn ${currentDelay===5000?'active':''}" data-ms="5000">5s</button>
<div class="custom-container">
<span style="font-size:10px;">${ui.custom}</span>
<input type="number" class="custom-input" id="custom-ms" value="${!isAlways && !isPreset ? currentDelay/1000 : ''}" placeholder="?">
</div>
</div>
<button class="dash-btn delay-btn ${isAlways?'active':''}" data-ms="999999">${ui.always}</button>
</div>
<div class="dash-item">
<span class="dash-label">${ui.width} <span>${store.get('width', 30)}%</span></span>
<input type="range" id="range-width" min="10" max="100" value="${store.get('width', 30)}">
</div>
<div class="dash-item">
<span class="dash-label">${ui.color}</span>
<input type="color" id="color-picker" value="${store.get('color', '#9147ff')}">
</div>
<div style="font-size: 11px; color: #888; text-align: center; margin-top: 5px;">${ui.close}</div>
`;
document.body.appendChild(overlay);
document.body.appendChild(dash);
overlay.onclick = () => { dash.classList.remove('open'); overlay.classList.remove('open'); };
dash.querySelector('#sw-rank').onclick = function() {
const now = !store.get('hideRank', false);
this.querySelector('.switch-bg').classList.toggle('active-bg', now);
this.querySelector('.switch-dot').classList.toggle('active-dot', now);
store.set('hideRank', now);
};
dash.querySelectorAll('.pos-btn').forEach(btn => {
btn.onclick = () => {
dash.querySelectorAll('.pos-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
store.set('pos', btn.dataset.val);
};
});
const delayBtns = dash.querySelectorAll('.delay-btn');
const customInp = dash.querySelector('#custom-ms');
delayBtns.forEach(btn => {
btn.onclick = () => {
delayBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
customInp.value = '';
store.set('delay', parseInt(btn.dataset.ms));
};
});
customInp.onchange = () => {
const val = parseFloat(customInp.value);
if (val > 0) {
delayBtns.forEach(b => b.classList.remove('active'));
store.set('delay', val * 1000);
}
};
dash.querySelector('#range-width').oninput = (e) => {
e.target.previousElementSibling.querySelector('span').innerText = e.target.value + '%';
store.set('width', e.target.value);
};
dash.querySelector('#color-picker').oninput = (e) => store.set('color', e.target.value);
}
let isExpanded = false;
let idleTimer = null;
function toggle(forceExpand = null) {
const container = document.querySelector('.chat-input') || document.querySelector('[data-a-target="chat-input-container"]');
if (!container) return;
isExpanded = (forceExpand !== null) ? forceExpand : !isExpanded;
if (isExpanded) {
container.classList.remove('stealth-mode');
setTimeout(() => { (container.querySelector('[data-slate-editor="true"]') || container.querySelector('textarea'))?.focus(); }, 100);
resetIdleTimer();
} else { container.classList.add('stealth-mode'); clearTimeout(idleTimer); }
}
function resetIdleTimer() {
clearTimeout(idleTimer);
const delay = store.get('delay', 5000);
if (isExpanded && delay < 999999) {
idleTimer = setTimeout(() => toggle(false), delay);
}
}
function init() {
const container = document.querySelector('.chat-input') || document.querySelector('[data-a-target="chat-input-container"]');
if (!container) { setTimeout(init, 1000); return; }
createDashboard();
const btn = document.createElement('div');
btn.id = 'stealth-toggle';
btn.onclick = (e) => { e.stopPropagation(); toggle(); };
document.body.appendChild(btn);
applyDynamicStyles();
toggle(false);
GM_registerMenuCommand("⚙️ " + ui.title, () => {
document.getElementById('stealth-dash').classList.add('open');
document.getElementById('stealth-overlay').classList.add('open');
});
document.addEventListener('click', (e) => {
const delay = store.get('delay', 5000);
if (isExpanded && delay < 999999 && !container.contains(e.target) && e.target.id !== 'stealth-toggle') {
toggle(false);
}
});
}
init();
})();