Chzzk_L&V: Chatting Plus

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

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

您需要先安裝使用者腳本管理器擴展,如 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.7
// @description  "파트너이며 매니저가 아닌 유저" 또는 지정한 streamer 닉네임의 메시지를 연두색으로 표시, 채팅 닉네임 꾸미기 효과 중 스텔스모드 무력화 및 형광펜 제거, 긴 닉네임 10자 초과 시 생략 처리, 타임머신 기능 안내 및 치트키 구매 팝업 클릭하여 닫기, 스트리머&매니저&방송주인 채팅의 배경색 추가(강조기능)
// @author       DOGJIP
// @match        https://chzzk.naver.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com
// ==/UserScript==

(function() {
    'use strict';

    // ===== 설정: 지정 스트리머 및 켜고끄고 싶은 기능을 수정할 수 있습니다. true/false ====================
    // ===== 사용자 지정 스트리머 리스트 (파트너가 아닌 스트리머의 이름을 아래의 예시처럼 정확히 넣기) =====
    let streamer = ['고수달', '냐 미 Nyami', '새 담', '청 묘', '침착맨', '삼식123','레니아워 RenieHouR'];
    const ENABLE_FIX_UNREADABLE_COLOR    = true; // 닉네임 색상 어두우면 제거
    const ENABLE_REMOVE_BG_COLOR         = true; // 닉네임 백그라운드 제거
    const ENABLE_TRUNCATE_NICKNAME       = true; // 긴 닉네임 자르고 말줄임
    const ENABLE_TOOLTIP_AUTO_CLOSE      = true; // 치트키 툴팁 자동 닫기
    // ===== 사용자 설정 변경 끝 ===========================================================================

    let chatObserver = null;
    let tooltipClosed = false; // 팝업 닫힘 여부 저장

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

    // 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거
    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 = 3) {
        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) {
            console.log('[치트키 팝업] 이미 닫혀 있음, 감시 생략');
            return;
        }

        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    // classList로 직접 탐색
                    const closeButton =
                          node.nodeType === 1 && node.tagName === 'BUTTON' && node.className.includes('cheat_key_tooltip_button_close__')
                              ? node
                              : node.querySelector?.('button[class*="cheat_key_tooltip_button_close__"]');

                    if (closeButton) {
                        console.log('[치트키 팝업] 닫기 버튼 클릭');
                        closeButton.click();
                        tooltipClosed = true; // 닫았음 표시
                        observer.disconnect();
                        return;
                    }
                }
            }
        });

        console.log('[치트키 팝업 감시 시작]');
        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"]') !== null;

        // 매니저·스트리머 검사 (더 안정적인 단일 이미지 선택 방식)
        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, 10);

        // 지정 스트리머인지 확인
        const nameSpan = nicknameElem?.querySelector('.name_text__yQG50');
        const nameText = nameSpan ? nameSpan.textContent.trim() : '';
        const isManualStreamer = streamer.includes(nameText);

        // 연두색 적용 조건:
        // (1) 파트너이며 매니저·스트리머가 아닐 때
        // OR
        // (2) 지정한 닉네임이면서 매니저·스트리머가 아닐 때
        if ((!isManager && !isStreamer) && (isPartner || isManualStreamer)) {
            if (nicknameElem) {
                nicknameElem.style.color = LIGHT_GREEN;
                nicknameElem.style.fontWeight = 'bold';
                nicknameElem.style.textTransform = 'uppercase';
            }
            if (textElem) {
                textElem.style.color = LIGHT_GREEN;
                textElem.style.fontWeight = 'bold';
                textElem.style.textTransform = 'uppercase';
            }
        }
        // 배경색 강조: 방송주인/스트리머/매니저의 채팅 배경을 강조함
        if (isPartner || isStreamer || isManager || isManualStreamer) {
            messageElem.style.backgroundColor = Background_SKYBLUE;
        }
        messageElem.setAttribute('data-partner-processed', 'true');
    }

    // 채팅 MutationObserver 설정
    function setupChatObserver() {
        if (chatObserver) chatObserver.disconnect();
        const chatContainer = document.querySelector('[class*="live_chatting_list_wrapper__"], [class*="vod_chatting_list__"]');
        if (!chatContainer) {
            setTimeout(setupChatObserver, 500);
            return;
        }
        // 기존 채팅 메시지도 스캔하여 처리
        chatContainer.querySelectorAll('[class^="live_chatting_message_chatting_message__"]')
            .forEach(processChatMessage);
        // 새로운 채팅 메시지 감지
        chatObserver = new MutationObserver(mutations => {
            mutations.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; // 페이지 전환 시 다시 감시
                console.log('[SPA] URL 변경 감지, tooltipClosed 초기화');
                setTimeout(() => {
                    setupChatObserver();
                    autoClickTooltipCloseButton(); // 재등록
                }, 1000);
            }
        };
        ['pushState', 'replaceState'].forEach(method => {
            const orig = history[method];
            history[method] = function (...args) {
                orig.apply(this, args);
                onUrlChange();
            };
        });
        window.addEventListener("popstate", onUrlChange);
    }

    // 초기화 함수
    function init() {
        setupChatObserver();
        setupSPADetection();
        if (ENABLE_TOOLTIP_AUTO_CLOSE) {
        autoClickTooltipCloseButton();
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();