Greasy Fork

Greasy Fork is available in English.

Chzzk_L&V: Chatting Plus

"파트너이며 매니저가 아닌 유저" 또는 지정한 streamer 닉네임의 메시지를 연두색으로 표시, 채팅 닉네임 꾸미기 효과 중 스텔스모드 무력화 및 형광펜 제거, 긴 닉네임 10자 초과 시 생략 처리, 타임머신 기능 안내 및 치트키 구매 팝업 클릭하여 닫기

目前为 2025-05-04 提交的版本。查看 最新版本

// ==UserScript==
// @name         Chzzk_L&V: Chatting Plus
// @namespace    Chzzk_Live&VOD: Chatting Plus
// @version      1.9.1
// @description  "파트너이며 매니저가 아닌 유저" 또는 지정한 streamer 닉네임의 메시지를 연두색으로 표시, 채팅 닉네임 꾸미기 효과 중 스텔스모드 무력화 및 형광펜 제거, 긴 닉네임 10자 초과 시 생략 처리, 타임머신 기능 안내 및 치트키 구매 팝업 클릭하여 닫기
// @author       DOGJIP
// @match        https://chzzk.naver.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com
// ==/UserScript==

(function() {
    'use strict';

    // 현재 스크립트 버전
    const CURRENT_VERSION = '1.9.1';

    // 기본 설정
    const DEFAULTS = {
        streamer: ['고수달','냐 미 Nyami','새 담','청 묘','침착맨','삼식123','레니아워 RenieHouR'],
        exception: ['인챈트 봇','픽셀 봇','스텔라이브 봇','뚜팔봇'],
        fixUnreadable: true,
        removeHighlight: true,
        truncateName: true,
        autoCloseTooltip: true
    };

    // 사용자 설정값이 없을 때만 기본값으로 초기화
    if (!GM_getValue('scriptVersion')) {
        GM_setValue('streamer', Array.from(new Set(DEFAULTS.streamer)));
        GM_setValue('exception', Array.from(new Set(DEFAULTS.exception)));
        GM_setValue('fixUnreadable', DEFAULTS.fixUnreadable);
        GM_setValue('removeHighlight', DEFAULTS.removeHighlight);
        GM_setValue('truncateName', DEFAULTS.truncateName);
        GM_setValue('autoCloseTooltip', DEFAULTS.autoCloseTooltip);
    }
    GM_setValue('scriptVersion', CURRENT_VERSION);

    // 사용자 설정 불러오기(GM_getValue)
    let streamer       = GM_getValue('streamer', DEFAULTS.streamer);
    let exception      = GM_getValue('exception', DEFAULTS.exception);
    const ENABLE_FIX_UNREADABLE_COLOR = GM_getValue('fixUnreadable', DEFAULTS.fixUnreadable);
    const ENABLE_REMOVE_BG_COLOR      = GM_getValue('removeHighlight', DEFAULTS.removeHighlight);
    const ENABLE_TRUNCATE_NICKNAME    = GM_getValue('truncateName', DEFAULTS.truncateName);
    const ENABLE_TOOLTIP_AUTO_CLOSE   = GM_getValue('autoCloseTooltip', DEFAULTS.autoCloseTooltip);

    let chatObserver = null;
    let tooltipClosed = false;

    const LIGHT_GREEN = "rgb(102, 200, 102)";
    const Background_SKYBLUE = 'rgba(173, 216, 230, 0.15)';

        GM_addStyle(`
  /* 오버레이 */
  #cp-settings-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0, 0, 0, 0.7);
    display: flex; align-items: center; justify-content: center;
    z-index: 9999;
  }

  /* 패널: 연회색 배경 */
  #cp-settings-panel {
    background: #b0b0b0;
    color: #111;
    padding: 1rem;
    border-radius: 8px;
    width: 320px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    font-family: sans-serif;
  }
  #cp-settings-panel h3 {
    margin-top: 0;
    color: #111;
  }

  /* 입력창 */
  #cp-settings-panel textarea {
    width: 100%;
    height: 80px;
    margin-bottom: 0.75rem;
    background: #fff;
    color: #111;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 0.5rem;
    resize: vertical;
  }

  /* 버튼 컨테이너: flex layout */
  #cp-settings-panel > div {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
  }

  /* 버튼 공통 */
  #cp-settings-panel button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    font-size: 0.9rem;
    cursor: pointer;
  }

  /* 저장 버튼 */
  #cp-settings-panel button#cp-save-btn,
  #cp-settings-panel button#cp-exc-save-btn {
    background: #007bff;
    color: #fff;
  }

  /* 취소 버튼 */
  #cp-settings-panel button#cp-cancel-btn,
  #cp-settings-panel button#cp-exc-cancel-btn {
    background: #ddd;
    color: #111;
    margin-left: auto;
  }

  /* 버튼 호버 시 약간 어두워지기 */
  #cp-settings-panel button:hover {
    opacity: 0.9;
  }
`);
    function createPanel(title, key, textareaId, saveBtnId, cancelBtnId) {
        if (document.getElementById('cp-settings-overlay')) return;
        const overlay = document.createElement('div'); overlay.id = 'cp-settings-overlay';
        const panel = document.createElement('div'); panel.id = 'cp-settings-panel';
        const current = GM_getValue(key, DEFAULTS[key]).join(', ');
        panel.innerHTML = `
      <h3>${title}</h3>
      <textarea id="${textareaId}">${current}</textarea>
      <div>
        <button id="${saveBtnId}">저장</button>
        <button id="${cancelBtnId}">취소</button>
      </div>
    `;
        overlay.appendChild(panel); document.body.appendChild(overlay);
        panel.querySelector(`#${saveBtnId}`).addEventListener('click', () => {
            const val = panel.querySelector(`#${textareaId}`).value;
            const arr = Array.from(new Set(val.split(',').map(s => s.trim()).filter(s => s)));
            GM_setValue(key, arr);
            document.body.removeChild(overlay);
            location.reload();
        });
        panel.querySelector(`#${cancelBtnId}`).addEventListener('click', () => {
            document.body.removeChild(overlay);
        });
    }

    function showSettingsPanel() { createPanel('스트리머 목록 편집', 'streamer', 'cp-streamer-input', 'cp-save-btn', 'cp-cancel-btn'); }
    function showExceptionPanel() { createPanel('제외 대상 닉네임 편집', 'exception', 'cp-exception-input', 'cp-exc-save-btn', 'cp-exc-cancel-btn'); }


    // 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거
    function fixUnreadableNicknameColor(nicknameElem) {
        if (!nicknameElem) return;
        const computedColor = window.getComputedStyle(nicknameElem).color;
        const rgbaMatch = computedColor.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?([0-9.]+))?\)/);
        if (!rgbaMatch) return;
        const r = parseInt(rgbaMatch[1], 10);
        const g = parseInt(rgbaMatch[2], 10);
        const b = parseInt(rgbaMatch[3], 10);
        const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;
        const brightness = (r * 299 + g * 587 + b * 114) / 1000;
        const visibility = brightness * a;
        if (visibility < 50) nicknameElem.style.color = '';
    }

    // 유틸: 닉네임 배경 제거
    function removeBackgroundColor(nicknameElem) {
        if (!nicknameElem) return;
        const bgTarget = nicknameElem.querySelector('[style*="background-color"]');
        if (bgTarget) bgTarget.style.removeProperty('background-color');
    }

    // 유틸: 닉네임 자르기
    function truncateNickname(nicknameElem, maxLen = 10) {
        if (!nicknameElem) return;
        const textSpan = nicknameElem.querySelector('.name_text__yQG50');
        if (!textSpan) return;
        const fullText = textSpan.textContent;
        if (fullText.length > maxLen) textSpan.textContent = fullText.slice(0, maxLen) + '...';
    }

    // 유틸: 치트키 팝업 자동 닫기
    function autoClickTooltipCloseButton() {
        if (tooltipClosed) return;
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;
                    const closeBtn = node.tagName === 'BUTTON' && node.className.includes('cheat_key_tooltip_button_close__')
                        ? node : node.querySelector('button[class*="cheat_key_tooltip_button_close__"]');
                    if (closeBtn) {
                        closeBtn.click(); tooltipClosed = true; observer.disconnect(); return;
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: false });
    }

    // 채팅 메시지 처리
    function processChatMessage(messageElem) {
        if (messageElem.getAttribute('data-partner-processed') === 'true') return;
        const isPartner = !!messageElem.querySelector('[class*="name_icon__zdbVH"]');
        const badgeImg = messageElem.querySelector('.badge_container__a64XB img[src*="manager.png"], .badge_container__a64XB img[src*="streamer.png"]');
        const isManager = badgeImg?.src.includes('manager.png');
        const isStreamer = badgeImg?.src.includes('streamer.png');
        const nicknameElem = messageElem.querySelector('.live_chatting_username_nickname__dDbbj');
        const textElem = messageElem.querySelector('.live_chatting_message_text__DyleH');

        if (ENABLE_FIX_UNREADABLE_COLOR) fixUnreadableNicknameColor(nicknameElem);
        if (ENABLE_REMOVE_BG_COLOR)    removeBackgroundColor(nicknameElem);
        if (ENABLE_TRUNCATE_NICKNAME)  truncateNickname(nicknameElem);

        const nameText = nicknameElem?.querySelector('.name_text__yQG50')?.textContent.trim() || '';
        const isManualStreamer = streamer.includes(nameText);

        // 연두색 스타일
        if ((!isManager && !isStreamer) && (isPartner || isManualStreamer)) {
            nicknameElem && Object.assign(nicknameElem.style, { color: LIGHT_GREEN, fontWeight: 'bold', textTransform: 'uppercase' });
            textElem && Object.assign(textElem.style, { color: LIGHT_GREEN, fontWeight: 'bold', textTransform: 'uppercase' });
        }
        // 배경 강조
        if ((isPartner || isStreamer || isManager || isManualStreamer) && !exception.includes(nameText)) {
            messageElem.style.backgroundColor = Background_SKYBLUE;
        }
        messageElem.setAttribute('data-partner-processed', 'true');
    }

    // 채팅 옵저버 설정
    function setupChatObserver() {
        if (chatObserver) chatObserver.disconnect();
        const chatContainer = document.querySelector('[class*="live_chatting_list_wrapper__"], [class*="vod_chatting_list__"]');
        if (!chatContainer) return setTimeout(setupChatObserver, 500);
        chatContainer.querySelectorAll('[class^="live_chatting_message_chatting_message__"]').forEach(processChatMessage);
        chatObserver = new MutationObserver(muts => muts.forEach(m => m.addedNodes.forEach(node => {
            if (node.nodeType !== 1) return;
            if (node.className.includes('live_chatting_message_chatting_message__')) processChatMessage(node);
            else node.querySelectorAll('[class^="live_chatting_message_chatting_message__"]').forEach(processChatMessage);
        })));
        chatObserver.observe(chatContainer, { childList: true, subtree: true });
    }

    // SPA 탐지
    function setupSPADetection() {
        let lastUrl = location.href;
        const onUrlChange = () => {
            if (location.href !== lastUrl) {
                lastUrl = location.href; tooltipClosed = false;
                setTimeout(() => { setupChatObserver(); autoClickTooltipCloseButton(); }, 1000);
            }
        };
        ['pushState','replaceState'].forEach(m => {
            const orig = history[m];
            history[m] = function(...args) { orig.apply(this,args); onUrlChange(); };
        });
        window.addEventListener('popstate', onUrlChange);
    }

    // 설정 메뉴 추가
    GM_registerMenuCommand("🤖 강조할 닉네임 설정(파트너아닌경우))", showSettingsPanel);

    GM_registerMenuCommand("🛡️ 배경 강조 제외 닉네임 설정(매니저 봇등)", showExceptionPanel);

    GM_registerMenuCommand(`🛠️ 닉네임 은신 제거(2티어): ${ENABLE_FIX_UNREADABLE_COLOR?'✔️':'❌'}`, () => {
        GM_setValue('fixUnreadable', !ENABLE_FIX_UNREADABLE_COLOR);
        location.reload();
    });
    GM_registerMenuCommand(`🖍️ 형광펜 제거(2티어): ${ENABLE_REMOVE_BG_COLOR?'✔️':'❌'}`, () => {
        GM_setValue('removeHighlight', !ENABLE_REMOVE_BG_COLOR);
        location.reload();
    });
    GM_registerMenuCommand(`✂️ 닉네임 자르기(Max:10): ${ENABLE_TRUNCATE_NICKNAME?'✔️':'❌'}`, () => {
        GM_setValue('truncateName', !ENABLE_TRUNCATE_NICKNAME);
        location.reload();
    });
    GM_registerMenuCommand(`❎ 툴팁 자동 닫기(타임머신관련): ${ENABLE_TOOLTIP_AUTO_CLOSE?'✔️':'❌'}`, () => {
        GM_setValue('autoCloseTooltip', !ENABLE_TOOLTIP_AUTO_CLOSE);
        location.reload();
    });

    // 초기화
    function init() {
        setupChatObserver();
        setupSPADetection();
        if (ENABLE_TOOLTIP_AUTO_CLOSE) autoClickTooltipCloseButton();
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();