Chzzk_L&V: Chatting Plus

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

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

// ==UserScript==
// @name         Chzzk_L&V: Chatting Plus
// @namespace    Chzzk_Live&VOD: Chatting Plus
// @version      1.5
// @description  "파트너이며 매니저가 아닌 스트리머"의 닉네임과 메시지를 연두색으로 표시, 채팅 닉네임 꾸미기 효과 중 스텔스모드 무력화 및 형광펜 제거, 긴 닉네임 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';

    const LIGHT_GREEN = "rgb(102,200,102)";
    let chatObserver = null;

    // 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거
    // (예: color가 rgba(0, 0, 0, 0)인 경우 brightness가 0이므로 해당 스타일을 초기화)
    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() {
        const closeButton = document.querySelector('.cheat_key_tooltip_button_close__QrFhG');

        // 버튼이 있으면 클릭
        if (closeButton) {
            closeButton.click(); // 툴팁 닫기 버튼 클릭
        }
    }

    function processChatMessage(messageElem) {
        if (messageElem.getAttribute('data-partner-processed') === 'true') return;

        // (기존 파트너 판별 조건은 그대로 유지)
        const isPartner = messageElem.querySelector('[class*="name_icon__zdbVH"]') !== null;

        // 배지 검사 (매니저, 스트리머 여부)
        const badgeImgs = messageElem.querySelectorAll('.badge_container__a64XB img');
        let isManager = false;
        let isStreamer = false;
        badgeImgs.forEach(img => {
            if (img.src.includes("manager.png")) isManager = true;
            if (img.src.includes("streamer.png")) isStreamer = true;
        });

        const nicknameElem = messageElem.querySelector('.live_chatting_username_nickname__dDbbj');
        const textElem = messageElem.querySelector('.live_chatting_message_text__DyleH');

        // 공통 처리: 가독성 개선, 백그라운드 제거, 긴 닉네임 자르기
        fixUnreadableNicknameColor(nicknameElem);
        removeBackgroundColor(nicknameElem);
        truncateNickname(nicknameElem, 10);

        // 조건: 파트너 스트리머이며, 매니저와 스트리머 본인이 아닐 때만 연두색 적용
        if (isPartner && !isManager && !isStreamer) {
            if (nicknameElem) nicknameElem.style.color = LIGHT_GREEN;
            if (textElem) textElem.style.color = LIGHT_GREEN;
        }

        messageElem.setAttribute('data-partner-processed', 'true');
    }

        // 툴팁 닫기 기능을 위한 MutationObserver 설정
    function setupTooltipObserver() {
        const tooltipObserver = new MutationObserver(() => {
            // 툴팁이 생성될 때마다 자동으로 닫기 버튼 클릭
            autoClickTooltipCloseButton();
        });

        // 툴팁이 포함된 요소가 있을 때만 감지
        const tooltipContainer = document.querySelector('.tooltip');
        if (tooltipContainer) {
            tooltipObserver.observe(tooltipContainer, { childList: true, subtree: true });
        }
    }

    // 채팅 감지 및 처리
    function setupChatObserver() {
        if (chatObserver) chatObserver.disconnect();
        // live와 vod 모두 지원하도록 채팅 컨테이너 선택자 수정
        const chatContainer = document.querySelector('[class*="live_chatting_list_wrapper__"], [class*="vod_chatting_list__"]');
        if (!chatContainer) {
            setTimeout(setupChatObserver, 500);
            return;
        }

        // 기존 메시지 처리
        const existingMessages = chatContainer.querySelectorAll('[class^="live_chatting_message_chatting_message__"]');
        existingMessages.forEach(msg => processChatMessage(msg));

        // 신규 메시지 감지 (MutationObserver)
        chatObserver = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.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 });
    }

    function setupSPADetection() {
        let lastUrl = location.href;
        const onUrlChange = () => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                setTimeout(setupChatObserver, 1000);
            }
        };
        ['pushState','replaceState'].forEach(method => {
            const original = history[method];
            history[method] = function (...args) {
                original.apply(this, args);
                onUrlChange();
            };
        });
        window.addEventListener("popstate", onUrlChange);
    }

    function init() {
        setupChatObserver();
        setupSPADetection();
        setupTooltipObserver();
    }

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