Chzzk_L&V: Chatting Plus

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

目前為 2025-05-04 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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