아프리카TV - 사이드바 UI 변경

아프리카TV의 사이드바 UI를 변경합니다.

目前為 2024-01-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name         아프리카TV - 사이드바 UI 변경
// @name:ko         아프리카TV - 사이드바 UI 변경
// @namespace    https://www.afreecatv.com/
// @version      2024-01-16
// @description  아프리카TV의 사이드바 UI를 변경합니다.
// @description:ko  아프리카TV의 사이드바 UI를 변경합니다.
// @author       You
// @match        https://afreecatv.com/
// @match        https://afreecatv.com/?hash=*
// @match        https://www.afreecatv.com/
// @match        https://www.afreecatv.com/?hash=*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=afreecatv.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const css_Darkmode = `
.left_navbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    position: absolute;
    flex-direction: row-reverse;
    top: 0px;
    left: 160px;
}
.left_nav_button {
    font-family: Arial, Helvetica, sans-serif;
    position: relative;
    width: 70px;
    height: 70px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
    z-index: 3001;
    transition: all .2s;
    color: #e5e5e5;
    font-size: 15px;
    font-weight: 600;
}
.left_nav_button.active {
    color: #019BFE;
}
#sidebar {
    width: 240px;
    grid-area: sidebar;
    background-color: #1F1F23;
    color:white;
    margin-right:10px;
    padding-bottom:150px;
}

#sidebar .top-section {
    display: flex;
    align-items: center;
    justify-content: space-around;
    margin: 10px 0px;
}

#sidebar .top-section>span {
    text-transform: uppercase;
    font-weight: 550;
    font-size: 14px;
    margin-top: 6px;
    margin-bottom: 4px;
}

#sidebar .twitch-message-section {
    margin: 0px 10px;
    margin-top: 10px;
    padding: 25px;
    border-radius: 8px;
    background-color: #18181b;
    box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.9);
}

#sidebar .twitch-message-section .title {
    margin: 0px;
    font-size: 1.5rem;
    font-weight: 500;
}

#sidebar .twitch-message-section .title>span {
    color: var(--primary-color);
}

#sidebar .twitch-message-section .description {
    margin: 8px 0px;
    line-height: 1.3rem;
    font-size: 0.9rem;
}

.user {
    display: grid;
    grid-template-areas: "profile-picture username watchers" "profile-picture description blank";
    grid-template-columns: 40px auto auto;
    padding: 6px 10px;
}

.user:hover {
    background-color: #26262c;
    cursor: pointer;
}

.user .profile-picture {
    grid-area: profile-picture;
    width: 32px;
    height: 32px;
    border-radius: 50%;
}

.user .username {
    grid-area: username;
    /*font-size: 0.9rem;*/
    font-size: 15px;
    font-weight: 550;
}

.user .description {
    grid-area: description;
    /*font-size: 0.8rem;*/
    font-size: 13px;
    color: #a1a1a1;
    /* font-weight: 500; */
    letter-spacing: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.user .watchers {
    grid-area: watchers;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    /*font-size: 0.9rem;*/
    font-size: 13px;
    color: #c0c0c0;
    margin-right: 2px;
}

.user .watchers .dot {
    font-size: 7px;
    margin-right: 5px;
}

#listMain #wrap #serviceHeader #afLogo {
    left: 30px;
    height: 72px;
}

.btn_flexible {
    display: none;
}

#innerLnb {
    display: none;
}

#list-container {
    height: 100vh;
    overflow-y: auto;
}

#sidebar {
    height: 100vh;
    overflow-y: auto;
    position: fixed;
}

#sidebar::-webkit-scrollbar {
    display: none;  /* Chrome, Safari, Edge */
}


.tooltip-container {
    width: 320px;
    height: auto;
    position: fixed;
    background-color: #26262C;
}
.tooltip-container img {
    position: relative;
    z-index: 999;
    width: auto;
    height: auto;
    max-height:240px
}
.tooltiptext {
    position: relative;
    z-index: 999;
    width: 320px;
    height: 48px;
    background-color: #26262C;
    color: #fff;
    text-align: center;
    display: flex;
    align-items: center; /* 세로 가운데 정렬 */
    justify-content: center; /* 가로 가운데 정렬 */
    top:-4px;
}

    `;

    const css_Whitemode = `
.left_navbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    position: absolute;
    flex-direction: row-reverse;
    top: 0px;
    left: 160px; /* 변경된 부분: left 속성으로 수정 */
}
.left_nav_button {
    font-family: Arial, Helvetica, sans-serif;
    /* 나눔고딕 대신 sans-serif 폰트 중 하나를 선택하여 적용 */
    position: relative;
    width: 70px;
    height: 70px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
    z-index: 3001;
    transition: all .2s;
    color: black;
    font-size: 15px;
    font-weight: 600;
}
.left_nav_button.active {
  color: #0545B1;
}
#sidebar {
    width: 240px;
    grid-area: sidebar;
    background-color: #EFEFF1;
    color:black;
    padding-bottom:150px;
}

#sidebar .top-section {
    display: flex;
    align-items: center;
    justify-content: space-around;
    margin: 10px 0px;
}

#sidebar .top-section>span {
    text-transform: uppercase;
    font-weight: 600;
    font-size: 14px;
    margin-top: 6px;
    margin-bottom: 4px;
}

#sidebar .twitch-message-section {
    margin: 0px 10px;
    margin-top: 10px;
    padding: 25px;
    border-radius: 8px;
    background-color: #18181b;
    box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.9);
}

#sidebar .twitch-message-section .title {
    margin: 0px;
    font-size: 1.5rem;
    font-weight: 500;
}

#sidebar .twitch-message-section .title>span {
    color: var(--primary-color);
}

#sidebar .twitch-message-section .description {
    margin: 8px 0px;
    line-height: 1.3rem;
    font-size: 0.9rem;
}

.user {
    display: grid;
    grid-template-areas: "profile-picture username watchers" "profile-picture description blank";
    grid-template-columns: 40px auto auto;
    padding: 6px 10px;
}

.user:hover {
    background-color: #E6E6EA;
    cursor: pointer;
}

.user .profile-picture {
    grid-area: profile-picture;
    width: 32px;
    height: 32px;
    border-radius: 50%;
}

.user .username {
    grid-area: username;
    font-size: 15px;
    font-weight: 600;
}

.user .description {
    grid-area: description;
    font-size: 13px;
    color: #53535F;
    letter-spacing: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.user .watchers {
    grid-area: watchers;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    font-size: 13px;
    color: black;
    margin-right: 2px;
}

.user .watchers .dot {
    font-size: 7px;
    margin-right: 5px;
}

#listMain #wrap #serviceHeader #afLogo {
    left: 30px;
    height: 72px;
}

.btn_flexible {
    display: none;
}

#innerLnb {
    display: none;
}

#list-container {
    height: 100vh;
    overflow-y: auto;
}

#sidebar {
    height: 100vh;
    overflow-y: auto;
    position: fixed;
}

#sidebar::-webkit-scrollbar {
    display: none;  /* Chrome, Safari, Edge */
}

.tooltip-container {
    width: 320px;
    height: auto;
    position: fixed;
    background-color: #E6E6EA;
}
.tooltip-container img {
    position: relative;
    z-index: 999;
    width: auto;
    height: auto;
    max-height:240px
}
.tooltiptext {
    position: relative;
    z-index: 999;
    width: 320px;
    height: 48px;
    background-color: #E6E6EA;
    color: black;
    text-align: center;
    display: flex;
    align-items: center; /* 세로 가운데 정렬 */
    justify-content: center; /* 가로 가운데 정렬 */
    top:-4px;
}
    `;

    function waitForElement(elementSelector, callBack) {
        const element = document.querySelector(elementSelector);

        if (element) {
            callBack(elementSelector, element);
        } else {
            setTimeout(function () {
                waitForElement(elementSelector, callBack);
            }, 1000);
        }
    }

    function desc_order(selector){
        // Get the container element
        const container = document.querySelector(selector);

        // Get all user elements
        const userElements = document.querySelectorAll(`${selector} >.user`);

        // Convert NodeList to Array for easier manipulation
        const userArray = Array.from(userElements);

        // Sort userArray based on the data-watchers attribute
        userArray.sort((a, b) => {
            const watchersA = parseInt(a.getAttribute('data-watchers') || '0');
            const watchersB = parseInt(b.getAttribute('data-watchers') || '0');
            return watchersB - watchersA;
        });

        // Clear container and append sorted elements
        container.innerHTML = '';
        userArray.forEach(user => {
            container.appendChild(user);
        });
    }

    function addNumberSeparator(number) {
        // toLocaleString 메서드를 사용하여 숫자에 구분자 추가
        number = Number(number);
        return number.toLocaleString();
    }

    // 사용자 요소를 생성하는 함수
    function createUserElement(channel) {
        const userElement = document.createElement('div');
        const playerLink = "https://play.afreecatv.com/"+channel.user_id;
        const broad_thumnail = `https://liveimg.afreecatv.com/m/${channel.broad_no}`;
        userElement.classList.add('user');
        userElement.setAttribute('onclick',`window.open('${playerLink}', '_blank')`);
        userElement.setAttribute('data-watchers',`${channel.total_view_cnt}`);
        userElement.setAttribute('broad_thumnail',`${broad_thumnail}`);
        userElement.setAttribute('tooltip',`${channel.broad_title}`);

        const profilePicture = document.createElement('img');
        const pp_webp="https://stimg.afreecatv.com/LOGO/"+channel.user_id.slice(0, 2)+"/"+channel.user_id+"/m/"+channel.user_id+".webp";
        const pp_jpg="https://profile.img.afreecatv.com/LOGO/"+channel.user_id.slice(0, 2)+"/"+channel.user_id+"/m/"+channel.user_id+".jpg";
        profilePicture.src = pp_webp; // 프로필사진
        profilePicture.setAttribute('onerror', `this.onerror=null; this.src='${pp_jpg}'`);
        profilePicture.setAttribute('alt', `${channel.user_id}'`);
        //profilePicture.onerror=`this.onerror=null; this.src='${pp_jpg}'`;
        profilePicture.classList.add('profile-picture');

        const username = document.createElement('span');
        username.classList.add('username');
        username.textContent = channel.user_nick; //스트리머명

        const cat_no = channel.broad_cate_no;

        const categoryList = oMainCategory.category_list;

        const filteredList = categoryList.filter(word => !["전체", "제한"].some(keyword => word.menu_name.includes(keyword)));

        const targetActionContent = cat_no;

        const regexPattern = new RegExp(targetActionContent.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");

        const matchedItem = filteredList.find(item => regexPattern.test(item.action_content));

        // 일치하는 항목이 있다면 해당 항목의 menu_name 리턴, 없으면 null 리턴
        let result = matchedItem ? matchedItem.menu_name : cat_no;

        if(result==="00040121"){
            result = "종합게임";
        }

        const description = document.createElement('span');
        description.classList.add('description');
        description.textContent = result; //카테고리

        const watchers = document.createElement('span');
        watchers.classList.add('watchers');
        watchers.innerHTML = `<span class="dot" role="img" aria-label="Amount of people watching">🔴</span>${addNumberSeparator(channel.total_view_cnt)}</span>`; //시청자수

        userElement.appendChild(profilePicture);
        userElement.appendChild(username);
        userElement.appendChild(description);
        userElement.appendChild(watchers);

        return userElement;
    }

        // 특정 HTML 삽입
        const newHtml = `
            <div id="sidebar">
            </div>
        `;

        // #serviceLnb 하위에 HTML 삽입
        const serviceLnbElement = document.getElementById('serviceLnb');
        if (serviceLnbElement) {
            serviceLnbElement.insertAdjacentHTML('beforeend', newHtml);
        }

    function insertTopChannels(){

        // 특정 HTML 삽입
        const newHtml = `
            <div class="top-section">
                <span>인기 채널</span>
            </div>
            <div class="users-section top">
            </div>
        `;

        // #serviceLnb 하위에 HTML 삽입
        const serviceLnbElement = document.getElementById('sidebar');
        if (serviceLnbElement) {
            serviceLnbElement.insertAdjacentHTML('beforeend', newHtml);
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://live.afreecatv.com/api/main_broad_list_api.php?selectType=action&selectValue=myplus&orderType=view_cnt&pageNo=1&lang=ko_KR',
            headers: {
                'Content-Type': 'application/json',
            },
            onload: function(response) {
                try {
                    // 응답을 JSON으로 파싱
                    const jsonResponse = JSON.parse(response.responseText);

                    // 응답에서 필요한 정보 추출
                    const channels = jsonResponse.broad;

                    // users-section에 동적으로 user 요소 추가
                    const usersSection = document.querySelector('.users-section.top');
                    channels.forEach(channel => {
                        const userElement = createUserElement(channel);
                        usersSection.appendChild(userElement);
                    });
                } catch (error) {
                    console.error('Error parsing JSON:', error);
                }
            },
            onerror: function(error) {
                console.error('Error:', error);
            }
        });
    }

    function insertFavoriteChannels(response){

        // 특정 HTML 삽입
        const newHtml = `
            <div class="top-section">
                <span>즐겨찾기 중인 채널</span>
            </div>
            <div class="users-section follow">
            </div>
        `;

        // #serviceLnb 하위에 HTML 삽입
        const serviceLnbElement = document.getElementById('sidebar');
        if (serviceLnbElement) {
            serviceLnbElement.insertAdjacentHTML('beforeend', newHtml);
        }

        try {
            // 응답에서 필요한 정보 추출
            const jsonData = response;
            // 데이터 배열을 순회하면서 각각의 객체에서 broad_info를 확인합니다.
            jsonData.data.forEach(item => {
                // broad_info가 비어있는지 확인합니다.
                if (item.broad_info.length === 0) { //비방
                    //console.log(`broad_info is empty for user ${item.user_nick}`);
                } else { //방송중
                    // broad_info가 비어있지 않은 경우, 여러가지 작업을 수행할 수 있습니다.
                    //console.log(`broad_info is not empty for user ${item.user_nick}`);
                    // users-section에 동적으로 user 요소 추가
                    const usersSection = document.querySelector('.users-section.follow');
                    const userElement = createUserElement(item.broad_info[0]);
                    usersSection.appendChild(userElement);
                }
            });
        } catch (error) {
            console.error('Error parsing JSON:', error);
        }
    }

    function insertMyplusChannels(){

        // 특정 HTML 삽입
        const newHtml = `
            <div class="top-section">
                <span>MY+ 추천 채널</span>
            </div>
            <div class="users-section myplus">
            </div>
        `;

        // #serviceLnb 하위에 HTML 삽입
        const serviceLnbElement = document.getElementById('sidebar');
        if (serviceLnbElement) {
            serviceLnbElement.insertAdjacentHTML('beforeend', newHtml);
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://live.afreecatv.com/api/myplus/preferbjLiveVodController.php?nInitCnt=6&szRelationType=C',
            headers: {
                'Content-Type': 'application/json',
            },
            onload: function(response) {
                try {
                    // 응답을 JSON으로 파싱
                    const jsonResponse = JSON.parse(response.responseText);

                    // 응답에서 필요한 정보 추출
                    const channels = jsonResponse.DATA.live_list;

                    // users-section에 동적으로 user 요소 추가
                    const usersSection = document.querySelector('.users-section.myplus');
                    channels.forEach(channel => {
                        const userElement = createUserElement(channel);
                        usersSection.appendChild(userElement);
                    });
                } catch (error) {
                    console.error('Error parsing JSON:', error);
                }
            },
            onerror: function(error) {
                console.error('Error:', error);
            }
        });
    }

    // GM_xmlhttpRequest를 사용하여 요청 보내기
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://myapi.afreecatv.com/api/favorite',
        headers: {
            'Content-Type': 'application/json',
        },
        onload: function(response) {
            // 응답 수정
            response = response.responseText;
            response = JSON.parse(response);

            // if 문으로 code 값 확인
            if (response.code === -10000) {
                //console.log('로그인 상태가 아닙니다.');
                insertTopChannels();
            } else {
                //console.log('로그인 상태입니다.');
                insertFavoriteChannels(response);
                insertMyplusChannels();
                insertTopChannels();
                waitForElement('.users-section.follow > .user', function (elementSelector, element) {
                    // 원하는 작업 수행
                    desc_order('.users-section.follow');
                });
                waitForElement('.users-section.myplus  > .user', function (elementSelector, element) {
                    // 원하는 작업 수행
                    desc_order('.users-section.myplus');
                });
            }


        },
        onerror: function(error) {
            console.error('Error:', error);
        }
    });

    // HTML 요소를 가져옵니다.
    const htmlElement = document.querySelector('html');

    // dark 속성의 값을 확인합니다.
    const isDarkMode = htmlElement.getAttribute('dark') === 'true';

    if(isDarkMode){
        GM_addStyle(css_Darkmode);
    } else {
        GM_addStyle(css_Whitemode);
    }

    var listsection = document.querySelector('#list-section');
    // .left_navbar를 찾거나 생성
    var leftNavbar = document.querySelector('.left_navbar');
    if (!leftNavbar) {
        leftNavbar = document.createElement('div');
        leftNavbar.className = 'left_navbar';

        // 페이지의 적절한 위치에 추가
        var targetElement = document.body; // 원하는 위치에 따라 수정
        targetElement.insertBefore(leftNavbar, targetElement.firstChild);
    }

    // 새로운 버튼을 만들기
    var newButton = document.createElement('a');
    newButton.href = 'https://www.afreecatv.com/?hash=all';
    newButton.innerHTML = '<button type="button" class="left_nav_button">전체</button>';
    var newButton2 = document.createElement('a');
    newButton2.href = 'https://www.afreecatv.com/?hash=game';
    newButton2.innerHTML = '<button type="button" class="left_nav_button">게임</button>';
    var newButton3 = document.createElement('a');
    newButton3.href = 'https://www.afreecatv.com/?hash=bora';
    newButton3.innerHTML = '<button type="button" class="left_nav_button">보.라</button>';
    var newButton4 = document.createElement('a');
    newButton4.href = 'https://www.afreecatv.com/?hash=sports';
    newButton4.innerHTML = '<button type="button" class="left_nav_button">스포츠</button>';
    var tooltipContainer = document.createElement('div');
    tooltipContainer.classList.add('tooltip-container');

    // .left_navbar에 버튼 삽입
    listsection.appendChild(tooltipContainer);
    leftNavbar.appendChild(newButton4);
    leftNavbar.appendChild(newButton3);
    leftNavbar.appendChild(newButton2);
    leftNavbar.appendChild(newButton);

    waitForElement('.left_nav_button', function (elementSelector, element) {
        // 원하는 작업 수행
        // Get the current page URL
        const currentPage = window.location.href;

        // Get all navigation links
        const navLinks = document.querySelectorAll('.left_nav_button');

        // Loop through each link and check if it matches the current page
        navLinks.forEach(link => {
            var parentLink = link.parentElement;
            if (parentLink.href === currentPage) {
                link.classList.add('active'); // Add the 'active' class if it matches
            }
        });
    });
    waitForElement('.user', function (elementSelector, element) {

        // HTMLCollection을 가져옴
        const elements = document.getElementsByClassName('user');
        const tooltipcontainer = document.getElementsByClassName('tooltip-container')[0];

        // 각 요소에 대해 반복하면서 이벤트 리스너 추가
        for (const element of elements) {
            element.addEventListener('mouseenter', function() {
                const rect = this.getBoundingClientRect();

                const elementX = rect.left + 240; // 요소의 X 좌표
                const elementY = rect.top; // 요소의 Y 좌표
                console.log(elementX,elementY);
                // 각 툴팁에 대해 위치 설정
                const imgSrc = this.getAttribute('broad_thumnail');
                const broad_title = this.getAttribute('tooltip');

                // 새로운 div 요소를 생성하고 스타일과 내용을 설정
                tooltipcontainer.style.left = `${elementX}px`;
                tooltipcontainer.style.top = `${elementY}px`;

                tooltipcontainer.innerHTML = `<img src="${imgSrc}"><div class="tooltiptext">${broad_title}</div>`;

                tooltipcontainer.style.display = 'block';
            });
            element.addEventListener('mouseleave', function() {
                tooltipcontainer.style.display = 'none';
            });
        }

    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址