Greasy Fork is available in English.

치지직 편의성 패치

치지직에서 아직 지원하지 않는 기능을 임시로 지원합니다.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         치지직 편의성 패치
// @namespace    https://yoonu.io/
// @version      0.4.2
// @description  치지직에서 아직 지원하지 않는 기능을 임시로 지원합니다.
// @author       Yoonu
// @match        https://chzzk.naver.com/*
// @icon         https://ssl.pstatic.net/static/nng/glive/icon/favicon.png
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

// 기능 설정 - 사용하고 싫으면 true / 사용하기 싫으면 false
const COLORIZE_CHAT = true;             // 채팅 닉네임 색상화
const REMOVE_CHEEZE_MESSAGE = true;     // 치즈 메시지 제거하기
const VIEW_NAV_INFO = false;            // 사이드바 정보 보기
const DISABLE_SCROLL_ANIMATION = true;  // 창이 비활성화된 상태에서 채팅이 쌓이면 보이는 스크롤 애니메이션 비활성화
const DISABLE_CLICK_EVENT = true;       // 클릭으로 영상 멈추기 비활성화

// 닉네임 색상 목록
const COLOR_LIST = [
    "rgb(255, 0, 0)",
    "rgb(0, 0, 255)",
    "rgb(0, 128, 0)",
    "rgb(178, 34, 34)",
    "rgb(255, 127, 80)",
    "rgb(154, 205, 50)",
    "rgb(255, 69, 0)",
    "rgb(46, 139, 87)",
    "rgb(218, 165, 32)",
    "rgb(210, 105, 30)",
    "rgb(95, 158, 160)",
    "rgb(30, 144, 255)",
    "rgb(255, 105, 180)",
    "rgb(138, 43, 226)",
    "rgb(0, 255, 127)"
];

const VIEWER_FONT_SIZE = "13px";
const CATEGORY_FONT_SIZE = "9px";

const addStyle = () => {
    // 채팅창 애니메이션
    style = "";
    if(DISABLE_SCROLL_ANIMATION) {
        style += `
            div[class^='live_chatting_list_item'] {
                overflow-anchor: none;
            }
        `;
    }
    GM_addStyle(style);
}

function toggleFullscreen() {
    let elem = document.getElementById("live_player_layout");
    document.fullscreenElement ? document.exitFullscreen() : elem.requestFullscreen();
}

const getRandomNumber = (max, seed) => {
    if(seed.length === 0)
        return 0;

    let val = 0;
    for (let i = 0; i < seed.length; i++)
        val += seed.charCodeAt(i);

    return val % max;
}

const fetchApi = async (url) => {
    const options = {
        "credentials": "include"
    };

    const response = await fetch(url, options);
    return await response.json();
}

// 채팅 옵저버
const setChatObserver = (chatNode) => {
    const callback = (mutationList, observer) => {
        for (let mutation of mutationList) {
            if (mutation.type !== "childList")
                continue;

            for(let addedNode of mutation.addedNodes) {
                // 치즈 메시지 제거
                if(REMOVE_CHEEZE_MESSAGE && addedNode.className?.includes("live_chatting_list_donation")) {
                    addedNode.classList.add("blind");
                    continue;
                }

                // 닉네임 색상 입히기
                const nameTextNode = addedNode.querySelector("span[class^='name_text']");
                if (nameTextNode) {
                    const nickname = nameTextNode.innerText;
                    const num = getRandomNumber(COLOR_LIST
                    .length, nickname);
                    nameTextNode.style.color = COLOR_LIST
                [num];
                }
            }
        }
    }

    const observer = new MutationObserver(callback);
    observer.observe(chatNode, { attributes: false, childList: true, subtree: true });
}

// 사이드바 라이브 현황 읽어오기
const fetchLiveStatus = async (listNode, fixOrder) => {
    /*
    const followListUrl = "https://api.chzzk.naver.com/service/v1/channels/followings/live";
    const recommendListUrl = "https://api.chzzk.naver.com/service/v1/home/recommendation-channels";

    const data = await fetchApi(followListUrl);
    const followingList = data.content.followingList;
    */

    if(!navigation.className.includes("navigator_is_expanded"))
        return;

    const links = listNode.querySelectorAll("a[href^='/live/']");

    for(let link of links) {
        const streamerCode = link.pathname.split("/")[2];
        const apiUrl = `https://api.chzzk.naver.com/polling/v1/channels/${streamerCode}/live-status`;
        const data = await fetchApi(apiUrl);
        const viewerCountClass = "navigator_viewer_count";
        const categoryClass = "navigator_category";

        let viewerCount = link.querySelector("." + viewerCountClass);
        if(!viewerCount) {
            viewerCount = document.createElement("div");
            viewerCount.style.fontSize = VIEWER_FONT_SIZE;
            viewerCount.className = viewerCountClass;
            link.appendChild(viewerCount);
        }

        const strongWrapper = link.querySelector("strong[class^='navigator_name']");
        let category = strongWrapper.querySelector("." + categoryClass);
        if(!category) {
            category = document.createElement("div");
            category.appendChild(strongWrapper.childNodes[0].cloneNode(true))
            category.style.fontSize = CATEGORY_FONT_SIZE;
            category.className = categoryClass;
            strongWrapper.appendChild(category);
        }

        viewerCount.innerHTML = data.content.concurrentUserCount;

        category.classList.toggle("blind", data.content.liveCategoryValue.trim() === "");
        category.querySelector("span[class^='name_text'").innerHTML = data.content.liveCategoryValue;

        // 네비게이션바 전체를 접었다가 펼치면 순서가 바뀌는걸 수정
        if(fixOrder)
            link.appendChild(viewerCount);
    };
}

// 주기적으로 API 읽기
const fetchInterval = (listNode) => {
    fetchLiveStatus(listNode);
    fetchTimer = setTimeout(fetchInterval, 60 * 1000, listNode);
}

let layoutBody, navigation, fetchTimer;

(function() {
    'use strict';

    const layoutCallback = () => {
        const chatWindow = layoutBody.querySelector("section > aside");
        if(chatWindow)
            setChatObserver(chatWindow)
    }

    const navigationCallback = (mutationList, observer) => {
        for(let mutation of mutationList) {
            if(mutation.type === "childList"){
                mutation.addedNodes.forEach(addedNode => fetchInterval(addedNode));
            } else if(mutation.type === "attributes") {
                mutation.target.childNodes.forEach(childNode => fetchLiveStatus(childNode, true));
            } else {
                console.log(mutation);
            }
        }

        // if(navigation.childNodes.length >= 2)
        //     navigationObserver.disconnect();
    }

    // 메인섹션(방송, 채팅)
    if(COLORIZE_CHAT) {
        layoutBody = document.getElementById("layout-body");
        const layoutBodyObserver = new MutationObserver(layoutCallback);
        layoutBodyObserver.observe(layoutBody, { attributes: false, childList: true, subtree: false });
    }

    // 네비게이션(사이드바)
    if(VIEW_NAV_INFO) {
        navigation = document.getElementById("navigation");
        const navigationObserver = new MutationObserver(navigationCallback);
        navigationObserver.observe(navigation, { attributes: true, childList: true, subtree: false });
    }

    Element.prototype._addEventListener = Element.prototype.addEventListener;
    Element.prototype.addEventListener = function (type, event, options) {
        if (type === "click" && this.className === "pzp-pc__video" && DISABLE_CLICK_EVENT)
            this._addEventListener("dblclick", toggleFullscreen);
        else
            this._addEventListener(type, event, options);
    };
})();