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