// ==UserScript== // @name 아프리카TV - 사이드바 UI 변경 // @name:ko 아프리카TV - 사이드바 UI 변경 // @namespace https://www.afreecatv.com/ // @version 2024-01-19 // @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 // @downloadURL none // ==/UserScript== (function() { 'use strict'; const css_Darkmode = ` .users-section.myplus > .user.show-more { display: none; } .users-section.follow > .user.show-more { display: none; } #toggleButton, #toggleButton2 { padding:12px; color:#e5e5e5; } .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 { z-index: 999; 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 = ` .users-section.myplus > .user.show-more { display: none; } .users-section.follow > .user.show-more { display: none; } #toggleButton, #toggleButton2 { padding:12px; color:black; } .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 { z-index: 999; 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}`); userElement.setAttribute('user_id',`${channel.user_id}`); 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 = `🔴${addNumberSeparator(channel.total_view_cnt)}`; //시청자수 userElement.appendChild(profilePicture); userElement.appendChild(username); userElement.appendChild(description); userElement.appendChild(watchers); return userElement; } // 특정 HTML 삽입 const newHtml = ` `; // #serviceLnb 하위에 HTML 삽입 const serviceLnbElement = document.getElementById('serviceLnb'); if (serviceLnbElement) { serviceLnbElement.insertAdjacentHTML('beforeend', newHtml); } function insertTopChannels(){ // 특정 HTML 삽입 const newHtml = `
인기 채널
`; // #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 = `
즐겨찾기 중인 채널
`; // #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 = `
MY+ 추천 채널
`; // #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'); removeDuplicates(); showMore_follow(); showMore_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 = ''; var newButton2 = document.createElement('a'); newButton2.href = 'https://www.afreecatv.com/?hash=game'; newButton2.innerHTML = ''; var newButton3 = document.createElement('a'); newButton3.href = 'https://www.afreecatv.com/?hash=bora'; newButton3.innerHTML = ''; var newButton4 = document.createElement('a'); newButton4.href = 'https://www.afreecatv.com/?hash=sports'; newButton4.innerHTML = ''; 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 = `
${broad_title}
`; tooltipcontainer.style.display = 'block'; }); element.addEventListener('mouseleave', function() { tooltipcontainer.style.display = 'none'; }); } }); function showMore_myplus(){ const userContainer = document.querySelector('.users-section.myplus'); const users = userContainer.querySelectorAll('.user'); users.forEach((user, index) => { if (index >= 6) { user.classList.add('show-more'); } }); // 동적으로 버튼 생성 및 삽입 const toggleButton = document.createElement('button'); toggleButton.textContent = `더 보기 (${users.length - 6})`; toggleButton.id = 'toggleButton'; userContainer.appendChild(toggleButton); const toggle_button = document.getElementById('toggleButton'); toggle_button.addEventListener('click', function () { const users = userContainer.querySelectorAll('.user'); const hiddenUsers = users.length - 6; // 숨겨진 요소의 개수 계산 const lastUser = users[users.length - 1]; if (lastUser.classList.contains('show-more')) { // 더 보기를 눌렀을 때 toggleButton.textContent = `접기`; users.forEach((user, index) => { if (index >= 6) { user.classList.remove('show-more'); } }); } else { // 접기를 눌렀을 때 toggleButton.textContent = `더 보기 (${hiddenUsers})`; users.forEach((user, index) => { if (index >= 6) { user.classList.add('show-more'); } }); } }); } function showMore_follow(){ const userContainer = document.querySelector('.users-section.follow'); const users = userContainer.querySelectorAll('.user'); users.forEach((user, index) => { if (index >= 6) { user.classList.add('show-more'); } }); // 동적으로 버튼 생성 및 삽입 const toggleButton = document.createElement('button'); toggleButton.textContent = `더 보기 (${users.length - 6})`; toggleButton.id = 'toggleButton2'; userContainer.appendChild(toggleButton); const toggle_button = document.getElementById('toggleButton2'); toggle_button.addEventListener('click', function () { const users = userContainer.querySelectorAll('.user'); const hiddenUsers = users.length - 6; // 숨겨진 요소의 개수 계산 const lastUser = users[users.length - 1]; if (lastUser.classList.contains('show-more')) { // 더 보기를 눌렀을 때 toggleButton.textContent = `접기`; users.forEach((user, index) => { if (index >= 6) { user.classList.remove('show-more'); } }); } else { // 접기를 눌렀을 때 toggleButton.textContent = `더 보기 (${hiddenUsers})`; users.forEach((user, index) => { if (index >= 6) { user.classList.add('show-more'); } }); } }); } function removeDuplicates(){ // .users-section.follow > .user 모든 요소 반복 document.querySelectorAll('.users-section.follow > .user').forEach(followUser => { const followUserId = followUser.getAttribute('user_id'); // .users-section.myplus > .user 모든 요소 반복 document.querySelectorAll('.users-section.myplus > .user').forEach(myplusUser => { const myplusUserId = myplusUser.getAttribute('user_id'); // user_id 일치 여부 확인 if (followUserId === myplusUserId) { // 일치할 경우 .user 요소 제거 myplusUser.remove(); } }); }); } })();