Greasy Fork

Greasy Fork is available in English.

치지직 편의성 패치

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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