Greasy Fork

Greasy Fork is available in English.

Twitch 聊天室隱藏助手

隐藏聊天输入框与排行榜。内置精简设置面板,支持自定义颜色、位置、感应宽度与秒数。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
})();