Chzzk_L&V: Chatting Plus

파트너·지정 스트리머 채팅 강조 / 닉네임 각종 설정 / 드롭스 접고 펼치기 / 고정댓글, 미션 자동 제어 / 채팅창 접고 펼치기 단축키( ] )

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

您需要先安裝使用者腳本管理器擴展,如 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      2.0.1.1
// @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
// ==/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 = `
      <h3>강조/제외 닉네임 설정</h3>
      <label>연두색으로 강조할 닉네임 (콤마로 구분 //파트너 기본 지원):</label>
      <textarea id="cp-streamer-input">${curStreamers}</textarea>
      <label>배경색 강조 제외할 닉네임 (콤마로 구분 //매니저 봇등):</label>
      <textarea id="cp-exception-input">${curExceptions}</textarea>

      <label><h4>유틸 기능 (온/오프)------------------------------------------------------</h4></label>
      <div class="cp-setting-row">
          <label class="cp-setting-label">
                <input type="checkbox" id="cp-fix-unread" ${ENABLE_FIX_UNREADABLE_COLOR ? 'checked' : ''}> 투명 닉네임 제거</label>
          <label class="cp-setting-label">
                <input type="checkbox" id="cp-remove-hl" ${ENABLE_REMOVE_BG_COLOR ? 'checked' : ''}> 형광펜 제거)</label>
          <label class="cp-setting-label">
                <input type="checkbox" id="cp-truncate" ${ENABLE_TRUNCATE_NICKNAME ? 'checked' : ''}> 길이 제한 (최대:10자)</label>
      </div>

      <div class="cp-setting-row">
          <label class="cp-setting-label">
                <input type="checkbox" id="cp-drops-toggle" ${ENABLE_DROPS_TOGGLE ? 'checked' : ''}> 드롭스 토글 기능</label>
          <label class="cp-setting-label">
                <input type="checkbox" id="cp-mission-hover" ${ENABLE_MISSION_HOVER ? 'checked' : ''}> 고정 댓글, 미션 자동 펼치고 접기 <br>(처음 펼침, 마우스 지나가면 접힘)</label>
      </div>
            <label><h4>-----------------------------------------------------------------------------</h4></label>
      <div>
        <button id="cp-save-btn">저장</button>
        <button id="cp-cancel-btn">취소</button>
      </div>
      <div style="font-size:0.75rem; color:#666; text-align:right; margin-top:0.5rem;">
         Enter ↵: 저장 Esc : 취소 (저장시 새로고침 및 적용)
      </div>
    `;
        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) 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');

    header.addEventListener('click', () => {
      const collapsed = container.classList.toggle('drops-collapsed');
     toggleIcon.textContent = collapsed ? '▼' : '▲';
    });
  }

      // SPA
  function setupDropsToggleObserver() {
    initDropsToggle();
    new MutationObserver((muts, obs) => {
      if (document.getElementById('drops_info')) {
        initDropsToggle();
        obs.disconnect();
      }
    }).observe(document.body, { childList: true, subtree: true });
  }


    // SPA 탐지
    function setupSPADetection() {
        let lastUrl = location.href;
        const onUrlChange = () => {
            if (location.href !== lastUrl) {
                setTimeout(() => {
                    setupChatObserver();
                    if (ENABLE_MISSION_HOVER) setupMissionHover();
                }, 1000);
            }
        };
        ['pushState','replaceState'].forEach(m => {
            const orig = history[m];
            history[m] = 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();
})();