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