// ==UserScript== // @name Chzzk_L&V: Chatting Plus // @namespace Chzzk_Live&VOD: Chatting Plus // @version 2.0.1.2 // @description 파트너·지정 스트리머 채팅 강조 / 닉네임 각종 설정 / 드롭스 접고 펼치기 / 고정댓글, 미션 자동 제어 / 채팅창 접고 펼치기 단축키( ] ) // @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 // @downloadURL https://update.greasyfork.icu/scripts/532068/Chzzk_LV%3A%20Chatting%20Plus.user.js // @updateURL https://update.greasyfork.icu/scripts/532068/Chzzk_LV%3A%20Chatting%20Plus.meta.js // ==/UserScript== (function() { 'use strict'; // 기본 설정 const DEFAULTS = { streamer: ['고수달','냐 미 Nyami','새 담','청 묘','침착맨','삼식123','레니아워 RenieHouR'], exception: ['인챈트 봇','픽셀 봇','스텔라이브 봇'], fixUnreadable: true, removeHighlight: true, truncateName: true, dropsToggle: true, missionHover: true }; // 사용자 설정 불러오기(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_DROPS_TOGGLE = GM_getValue('dropsToggle', DEFAULTS.dropsToggle); const ENABLE_MISSION_HOVER = GM_getValue('missionHover', DEFAULTS.missionHover); let chatObserver = null; let pendingNodes = []; let processScheduled = false; let isChatOpen = true; // 초기 상태: 열림 function scheduleProcess() { if (processScheduled) return; processScheduled = true; window.requestAnimationFrame(() => { pendingNodes.forEach(processChatMessage); pendingNodes = []; processScheduled = false; }); } const LIGHT_GREEN = "rgb(102, 200, 102)"; const Background_SKYBLUE = 'rgba(173, 216, 230, 0.15)'; const colorCache = new Map(); // key: CSS color string, value: 가시성(true=보임, false=지우기) GM_addStyle(` /* 오버레이 */ #cp-settings-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.3); display: flex; align-items: center; justify-content: center; z-index: 9999; overflow: auto; pointer-events: none; } /* 패널: 연회색 배경 */ #cp-settings-panel { background: #b0b0b0; color: #111; padding: 1rem; border-radius: 8px; width: 480px; max-width: 90%; box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-family: sans-serif; pointer-events: auto; } #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; } /* Highlight 클래스 */ .cp-highlight { color: rgb(102, 200, 102) !important; font-weight: bold !important; text-transform: uppercase !important; } /* 설정 체크박스 레이아웃 */ .cp-setting-row { //display: flex; gap: 0.5rem; margin: 0.5rem 0; font-size: 0.8rem; } .cp-setting-label { flex: 1; display: flex; align-items: center; gap: 0.2rem; } /* 백그라운드 색설정 */ .cp-bg { background-color: rgba(173, 216, 230, 0.15) !important; } `); function showCombinedPanel() { if (document.getElementById('cp-settings-overlay')) return; // overlay & panel 기본 구조 재사용 const overlay = document.createElement('div'); overlay.id = 'cp-settings-overlay'; const panel = document.createElement('div'); panel.id = 'cp-settings-panel'; // 현재 저장된 값 불러오기 const curStreamers = GM_getValue('streamer', DEFAULTS.streamer).join(', '); const curExceptions= GM_getValue('exception', DEFAULTS.exception).join(', '); panel.innerHTML = `

강조/제외 닉네임 설정

Enter ↵: 저장 Esc : 취소 (저장시 새로고침 및 적용)
`; overlay.appendChild(panel); document.body.appendChild(overlay); panel.setAttribute('tabindex', '0'); panel.focus(); panel.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); panel.querySelector('#cp-save-btn').click(); } else if (e.key === 'Escape') { e.preventDefault(); panel.querySelector('#cp-cancel-btn').click(); } }); panel.querySelector('#cp-save-btn').addEventListener('click', () => { const s = panel.querySelector('#cp-streamer-input').value; const e = panel.querySelector('#cp-exception-input').value; const fixUnread = panel.querySelector('#cp-fix-unread').checked; const removeHl = panel.querySelector('#cp-remove-hl').checked; const truncateName = panel.querySelector('#cp-truncate').checked; const dropsToggleVal = panel.querySelector('#cp-drops-toggle').checked; GM_setValue('streamer', Array.from(new Set(s.split(',').map(x=>x.trim()).filter(x=>x))) ); GM_setValue('exception', Array.from(new Set(e.split(',').map(x=>x.trim()).filter(x=>x))) ); GM_setValue('fixUnreadable', fixUnread); GM_setValue('removeHighlight', removeHl); GM_setValue('truncateName', truncateName); GM_setValue('dropsToggle', dropsToggleVal); GM_setValue('missionHover', document.querySelector('#cp-mission-hover').checked); document.body.removeChild(overlay); location.reload(); }); panel.querySelector('#cp-cancel-btn').addEventListener('click', () => { document.body.removeChild(overlay); }); } // 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거 function fixUnreadableNicknameColor(nicknameElem) { if (!nicknameElem) return; // 하이라이트 색상은 검사 제외 const cssColor = window.getComputedStyle(nicknameElem).color; if (cssColor === LIGHT_GREEN) return; // 캐시 검사 (이미 검사한 값 제외)미 if (colorCache.has(cssColor)) { if (colorCache.get(cssColor) === false) { nicknameElem.style.color = ''; } return; } // 밝기 계산 로직 const rgbaMatch = cssColor.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 = ''; colorCache.set(cssColor, visibility >= 50); } // 유틸: 닉네임 배경 제거 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 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 && nicknameElem.classList.add('cp-highlight'); textElem && textElem.classList.add('cp-highlight'); } // 배경 강조 if ((isPartner || isStreamer || isManager || isManualStreamer) && !exception.includes(nameText)) { messageElem.classList.add('cp-bg'); } 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(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.className.includes('live_chatting_message_chatting_message__')) { pendingNodes.push(node); } else { node.querySelectorAll('[class^="live_chatting_message_chatting_message__"]') .forEach(n => pendingNodes.push(n)); } }); }); scheduleProcess(); }); chatObserver.observe(chatContainer, { childList: true, subtree: false }); } // 미션창 + 고정 채팅 자동 접고 펼치기 (마우스 호버링) function setupMissionHover(retry = 0) { // 1) 미션창 wrapper const fixedWrapper = document.querySelector('.live_chatting_list_fixed__Wy3TT'); if (!fixedWrapper) { if (retry < 10) { return setTimeout(() => setupMissionHover(retry + 1), 500); } return; } // 2) 토글 버튼을 찾아주는 유틸 const getButtons = () => { const missionBtn = fixedWrapper.querySelector('.live_chatting_fixed_mission_folded_button__bBWS2'); const fixedChat = document.querySelector('.live_chatting_fixed_container__2tQz6'); const chatBtn = fixedChat ?.querySelector('.live_chatting_fixed_control__FCHpN button:not([aria-haspopup])'); return { missionBtn, chatBtn }; }; // 3) 모두 펼치기 const openAll = () => { const { missionBtn, chatBtn } = getButtons(); if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'false') { missionBtn.click(); } if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'false') { chatBtn.click(); } }; // 4) 모두 접기 const closeAll = () => { const { missionBtn, chatBtn } = getButtons(); if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'true') { missionBtn.click(); } if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'true') { chatBtn.click(); } }; // 5) 초기에는 무조건 펼친 상태로 openAll(); // 6) 한 번만 바인딩 if (fixedWrapper._missionHoverBound) return; fixedWrapper._missionHoverBound = true; // 7) 마우스 들어오면 펼치기 fixedWrapper.addEventListener('pointerenter', () => { openAll(); }); // 8) 마우스 나가면 접기 fixedWrapper.addEventListener('pointerleave', () => { closeAll(); }); // 9) 사용자가 직접 클릭(수동 토글)이력 저장 const { missionBtn } = getButtons(); missionBtn?.addEventListener('click', () => { fixedWrapper.dataset.userExpanded = missionBtn.getAttribute('aria-expanded') === 'true' ? 'true' : ''; }); } // ▽ 드롭스 토글용 CSS GM_addStyle(` #drops_info.drops-collapsed .live_information_drops_wrapper__gQBUq, #drops_info.drops-collapsed .live_information_drops_text__xRtWS, #drops_info.drops-collapsed .live_information_drops_default__jwWot, #drops_info.drops-collapsed .live_information_drops_area__7VJJr { display: none !important; } .live_information_drops_icon_drops__2YXie { transition: transform .2s; } #drops_info.drops-collapsed .live_information_drops_icon_drops__2YXie { transform: rotate(-90deg); } .live_information_drops_toggle_icon { margin-left: 10px; font-size: 18px; cursor: pointer; display: inline-block; } `); // === 키입력 ] 을 통해 채팅 접고 펼치기 === function closeChat() { const btn = document.querySelector('.live_chatting_header_button__t2pa1'); if (btn) { btn.click(); } else { console.warn('채팅 접기 버튼을 찾을 수 없습니다.'); } } function openChat() { const btn = document .querySelector('svg[viewBox="0 0 38 34"]') ?.closest('button'); if (btn) { btn.click(); } else { console.warn('기본 채팅 토글 버튼을 찾을 수 없습니다.'); } } function onKeydown(e) { const tag = e.target.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return; if (e.key === ']') { if (isChatOpen) { closeChat(); isChatOpen = false; } else { openChat(); isChatOpen = true; } } } window.addEventListener('keydown', onKeydown); function initDropsToggle() { const container = document.getElementById('drops_info'); if (!container || container.classList.contains('drops-init')) return; const header = container.querySelector('.live_information_drops_header__920BX'); if (!header) return; // 마크 표시 및 초기 숨김 상태 const toggleIcon = document.createElement('span'); toggleIcon.classList.add('live_information_drops_toggle_icon'); toggleIcon.textContent = '▼'; header.appendChild(toggleIcon); header.style.cursor = 'pointer'; container.classList.add('drops-collapsed'); container.classList.add('drops-init'); header.addEventListener('click', () => { const collapsed = container.classList.toggle('drops-collapsed'); toggleIcon.textContent = collapsed ? '▼' : '▲'; }); } function setupDropsToggleObserver() { initDropsToggle(); const obs = new MutationObserver(() => { initDropsToggle(); }); obs.observe(document.body, { childList: true, subtree: true }); } function setupSPADetection() { let lastUrl = location.href; const onUrlChange = () => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(() => { setupChatObserver(); if (ENABLE_MISSION_HOVER) setupMissionHover(); if (ENABLE_DROPS_TOGGLE) setupDropsToggleObserver(); }, 500); } }; ['pushState', 'replaceState'].forEach(method => { const orig = history[method]; history[method] = function(...args) { orig.apply(this, args); onUrlChange(); }; }); window.addEventListener('popstate', onUrlChange); } // 설정 메뉴 추가 GM_registerMenuCommand("⚙️ Chzzk: Chatting Plus 설정 변경", showCombinedPanel); // 초기화 function init() { setupChatObserver(); setupSPADetection(); if (ENABLE_MISSION_HOVER) setupMissionHover(); if (ENABLE_DROPS_TOGGLE) setupDropsToggleObserver(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();