您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
아프리카TV의 사이드바 UI를 변경합니다.
当前为
// ==UserScript== // @name 아프리카TV - 사이드바 UI 변경 // @name:ko 아프리카TV - 사이드바 UI 변경 // @namespace https://www.afreecatv.com/ // @version 2024-01-14 // @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 속성으로 수정 */ } .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: #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 { position: relative; display: inline-block; border-bottom: 1px dotted black; } .tooltip .tooltiptext { visibility: hidden; width: 120px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; /* Position the tooltip */ position: absolute; z-index: 1; top: -5px; left: 105%; } .tooltip:hover .tooltiptext { visibility: visible; } .tooltip-container { position: relative; } .tooltip { position: absolute; z-index: 999; width: 240px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; visibility: hidden; top: 45px; left: 0; } .tooltip-container:hover .tooltip { visibility: visible; } .tooltip-container .tooltip:hover { visibility: hidden; } `; 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 { position: relative; } .tooltip { position: absolute; z-index: 999; width: 240px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; visibility: hidden; top: 45px; left: 0; } .tooltip-container:hover .tooltip { visibility: visible; } .tooltip-container .tooltip:hover { visibility: hidden; } `; 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.tooltip-container`); // 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; userElement.classList.add('user'); userElement.classList.add('tooltip-container'); userElement.setAttribute('onclick',`window.open('${playerLink}', '_blank')`); userElement.setAttribute('data-watchers',`${channel.total_view_cnt}`); const tooltip = document.createElement('div'); tooltip.classList.add('tooltip'); tooltip.textContent = 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(tooltip); 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); } // .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>'; // .left_navbar에 버튼 삽입 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 } }); }); })();