// ==UserScript==
// @name SOOP (숲) - 사이드바 UI 변경
// @name:ko SOOP (숲) - 사이드바 UI 변경
// @namespace https://greasyfork.org/ko/scripts/484713
// @version 20250621
// @description SOOP (숲)의 사이드바 UI를 변경합니다.
// @description:ko SOOP (숲)의 사이드바 UI를 변경합니다.
// @author You
// @match https://www.sooplive.co.kr/*
// @match https://play.sooplive.co.kr/*
// @match https://vod.sooplive.co.kr/player/*
// @icon https://res.sooplive.co.kr/afreeca.ico
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @connect sooplive.co.kr
// @connect naver.com
// @run-at document-end
// @license
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
const NEW_UPDATE_DATE = 20250610;
const CURRENT_URL = window.location.href;
const IS_DARK_MODE = document.documentElement.getAttribute('dark') === 'true';
const HIDDEN_BJ_LIST = [];
let allFollowUserIds = GM_getValue('allFollowUserIds', []);
let STATION_FEED_DATA;
let menuIds = {};
let categoryMenuIds = {};
let wordMenuIds = {};
let displayFollow = GM_getValue("displayFollow", 6);
let displayMyplus = GM_getValue("displayMyplus", 6);
let displayMyplusvod = GM_getValue("displayMyplusvod", 4);
let displayTop = GM_getValue("displayTop", 6);
let myplusPosition = GM_getValue("myplusPosition", 1);
let myplusOrder = GM_getValue("myplusOrder", 1);
let blockedUsers = GM_getValue('blockedUsers', []);
let blockedCategories = GM_getValue('blockedCategories', []);
let blockedWords = GM_getValue('blockedWords', []); // 방송 목록 차단 단어
let registeredWords = GM_getValue("registeredWords",""); // 채팅창 차단 단어
let selectedUsers = GM_getValue("selectedUsers",""); // 유저 채팅 모아보기 아이디
let nicknameWidth = GM_getValue("nicknameWidth",126);
let isOpenNewtabEnabled = GM_getValue("isOpenNewtabEnabled", 0);
let isSidebarMinimized = GM_getValue("isSidebarMinimized", 0);
let showSidebarOnScreenMode = GM_getValue("showSidebarOnScreenMode", 1);
let showSidebarOnScreenModeAlways = GM_getValue("showSidebarOnScreenModeAlways", 0);
let savedCategory = GM_getValue("szBroadCategory",0);
let isAutoChangeMuteEnabled = GM_getValue("isAutoChangeMuteEnabled", 0);
let isAutoChangeQualityEnabled = GM_getValue("isAutoChangeQualityEnabled", 0);
let isNo1440pEnabled = GM_getValue("isNo1440pEnabled", 0);
let isDuplicateRemovalEnabled = GM_getValue("isDuplicateRemovalEnabled", 1);
let isRemainingBufferTimeEnabled = GM_getValue("isRemainingBufferTimeEnabled", 1);
let isPinnedStreamWithNotificationEnabled = GM_getValue("isPinnedStreamWithNotificationEnabled", 0);
let isPinnedStreamWithPinEnabled = GM_getValue("isPinnedStreamWithPinEnabled", 0);
let isBottomChatEnabled = GM_getValue("isBottomChatEnabled", 0);
let isMakePauseButtonEnabled = GM_getValue("isMakePauseButtonEnabled", 1);
let isCaptureButtonEnabled = GM_getValue("isCaptureButtonEnabled", 1);
let isStreamDownloadEnabled = GM_getValue("isStreamDownloadEnabled", 0);
let isMakeSharpModeShortcutEnabled = GM_getValue("isMakeSharpModeShortcutEnabled", 1);
let isMakeLowLatencyShortcutEnabled = GM_getValue("isMakeLowLatencyShortcutEnabled", 1);
let isSendLoadBroadEnabled = GM_getValue("isSendLoadBroadEnabled", 1);
let isSelectBestQualityEnabled = GM_getValue("isSelectBestQualityEnabled", 1);
let isHideSupporterBadgeEnabled = GM_getValue("isHideSupporterBadgeEnabled",0);
let isHideFanBadgeEnabled = GM_getValue("isHideFanBadgeEnabled",0);
let isHideSubBadgeEnabled = GM_getValue("isHideSubBadgeEnabled",0);
let isHideVIPBadgeEnabled = GM_getValue("isHideVIPBadgeEnabled",0);
let isHideManagerBadgeEnabled = GM_getValue("isHideManagerBadgeEnabled",0);
let isHideStreamerBadgeEnabled = GM_getValue("isHideStreamerBadgeEnabled",0);
let isBlockWordsEnabled = GM_getValue("isBlockWordsEnabled",0);
let isAutoClaimGemEnabled = GM_getValue("isAutoClaimGemEnabled",0);
let isVideoSkipHandlerEnabled = GM_getValue("isVideoSkipHandlerEnabled",0);
let isSmallUserLayoutEnabled = GM_getValue("isSmallUserLayoutEnabled",0);
let isChannelFeedEnabled = GM_getValue("isChannelFeedEnabled",1);
let isChangeFontEnabled = GM_getValue("isChangeFontEnabled", 0);
let isCustomSidebarEnabled = GM_getValue("isCustomSidebarEnabled", 1);
let isRemoveCarouselEnabled = GM_getValue("isRemoveCarouselEnabled", 0);
let isDocumentTitleUpdateEnabled = GM_getValue("isDocumentTitleUpdateEnabled", 1);
let isRemoveRedistributionTagEnabled = GM_getValue("isRemoveRedistributionTagEnabled", 1);
let isRemoveWatchLaterButtonEnabled = GM_getValue("isRemoveWatchLaterButtonEnabled", 1);
let isRemoveBroadStartTimeTagEnabled = GM_getValue("isRemoveBroadStartTimeTagEnabled", 0);
let isBroadTitleTextEllipsisEnabled = GM_getValue("isBroadTitleTextEllipsisEnabled", 0);
let isUnlockCopyPasteEnabled = GM_getValue("isUnlockCopyPasteEnabled", 0);
let isAlignNicknameRightEnabled = GM_getValue("isAlignNicknameRightEnabled", 0);
//let isPreviewModalEnabled = GM_getValue("isPreviewModalEnabled", 1);
let isReplaceEmptyThumbnailEnabled = GM_getValue("isReplaceEmptyThumbnailEnabled", 1);
let isAutoScreenModeEnabled = GM_getValue("isAutoScreenModeEnabled", 0);
let isAdjustDelayNoGridEnabled = GM_getValue("isAdjustDelayNoGridEnabled", 0);
let ishideButtonsAboveChatInputEnabled = GM_getValue("ishideButtonsAboveChatInputEnabled", 0);
let isExpandVODChatAreaEnabled = GM_getValue("isExpandVODChatAreaEnabled", 1);
let isExpandLiveChatAreaEnabled = GM_getValue("isExpandLiveChatAreaEnabled", 1);
let isOpenExternalPlayerEnabled = GM_getValue("isOpenExternalPlayerEnabled", 0);
let isOpenExternalPlayerFromSidebarEnabled = GM_getValue("isOpenExternalPlayerFromSidebarEnabled", 0);
let isRemoveShadowsFromCatchEnabled = GM_getValue("isRemoveShadowsFromCatchEnabled", 0);
let isChzzkTopChannelsEnabled = GM_getValue("isChzzkTopChannelsEnabled", 0);
let isChzzkFollowChannelsEnabled = GM_getValue("isChzzkFollowChannelsEnabled", 0);
let isAdaptiveSpeedControlEnabled = GM_getValue("isAdaptiveSpeedControlEnabled", 0);
let isShowSelectedMessagesEnabled = GM_getValue("isShowSelectedMessagesEnabled", 0);
let isShowDeletedMessagesEnabled = GM_getValue("isShowDeletedMessagesEnabled", 0);
let isNoAutoVODEnabled = GM_getValue("isNoAutoVODEnabled", 1);
let isHideEsportsInfoEnabled = GM_getValue("isHideEsportsInfoEnabled",0);
let isBlockedCategorySortingEnabled = GM_getValue("isBlockedCategorySortingEnabled",0);
let isChatCounterEnabled = GM_getValue("isChatCounterEnabled",1);
let isRandomSortEnabled = GM_getValue("isRandomSortEnabled",0);
let isPinnedOnlineOnlyEnabled = GM_getValue("isPinnedOnlineOnlyEnabled",0);
const WEB_PLAYER_SCROLL_LEFT = isSidebarMinimized ? 52 : 240;
function loadHlsScript() {
// hls.js 동적 로드
const hlsScript = document.createElement('script');
hlsScript.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
hlsScript.onload = function() {
//console.log('hls.js가 성공적으로 로드되었습니다.');
};
hlsScript.onerror = function() {
//console.error('hls.js 로드 중 오류가 발생했습니다.');
};
document.head.appendChild(hlsScript);
}
function applyFontStyles() {
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
* {
font-family: 'Inter' !important;
}
`;
document.head.appendChild(style);
}
const getHiddenbjList = async () => {
const url = "https://live.sooplive.co.kr/api/hiddenbj/hiddenbjController.php";
const response = await fetchBroadList(url, 3000);
if (response?.RESULT === 1) {
return response.DATA || [];
} else {
return [];
}
};
const getStationFeed = async () => {
// 채널 피드가 비활성화된 경우 빈 배열을 반환합니다.
if (!isChannelFeedEnabled) {
return [];
}
const feedUrl = "https://myapi.sooplive.co.kr/api/feed?index_reg_date=0&user_id=&is_bj_write=1&feed_type=&page=1";
const response = await fetchBroadList(feedUrl, 5000);
return response?.data || [];
};
function loadCategoryData() {
// 현재 시간 기록
const currentTime = new Date().getTime();
// 이전 실행 시간 불러오기
const lastExecutionTime = GM_getValue("lastExecutionTime", 0);
// 마지막 실행 시간으로부터 15분 이상 경과했는지 확인
if (currentTime - lastExecutionTime >= 900000) {
// URL에 현재 시간을 쿼리 스트링으로 추가해서 캐시 방지
const url = "https://live.sooplive.co.kr/script/locale/ko_KR/broad_category.js?" + currentTime;
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Content-Type": "text/plain; charset=utf-8"
},
onload: function(response) {
if (response.status === 200) {
// 성공적으로 데이터를 받았을 때 처리할 코드 작성
let szBroadCategory = response.responseText;
//console.log(szBroadCategory);
// 이후 처리할 작업 추가
szBroadCategory = JSON.parse(szBroadCategory.split('var szBroadCategory = ')[1].slice(0, -1));
if (szBroadCategory.CHANNEL.RESULT === "1") {
// 데이터 저장
GM_setValue("szBroadCategory", szBroadCategory);
// 현재 시간을 마지막 실행 시간으로 업데이트
GM_setValue("lastExecutionTime", currentTime);
}
} else {
console.error("Failed to load data:", response.statusText);
}
},
onerror: function(error) {
console.error("Error occurred while loading data:", error);
}
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
if (isChangeFontEnabled) applyFontStyles();
loadCategoryData();
});
} else {
if (isChangeFontEnabled) applyFontStyles();
loadCategoryData();
}
const CommonStyles = `
#blockWordsInput::placeholder, #selectedUsersInput::placeholder {
font-size: 14px;
}
/* Expand 토글용 li 스타일 */
.expand-toggle-li {
width: 32px;
height: 32px;
cursor: pointer;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
list-style: none;
background-size: 20px;
/* 채팅 확장 아이콘 */
background-image: url('data:image/svg+xml,%3Csvg%20fill%3D%22%23757B8A%22%20height%3D%2264%22%20width%3D%2264%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%20xml%3Aspace%3D%22preserve%22%20stroke%3D%22%23757B8A%22%3E%3Cg%20stroke-width%3D%220%22%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M335.085%20207.085%20469.333%2072.837V128c0%2011.782%209.551%2021.333%2021.333%2021.333S512%20139.782%20512%20128V21.335q-.001-1.055-.106-2.107c-.031-.316-.09-.622-.135-.933-.054-.377-.098-.755-.172-1.13-.071-.358-.169-.705-.258-1.056-.081-.323-.152-.648-.249-.968-.104-.345-.234-.678-.355-1.015-.115-.319-.22-.641-.35-.956s-.284-.616-.428-.923c-.153-.324-.297-.651-.467-.969-.158-.294-.337-.574-.508-.86-.186-.311-.362-.626-.565-.93-.211-.316-.447-.613-.674-.917-.19-.253-.366-.513-.568-.76a22%2022%200%200%200-1.402-1.551l-.011-.012-.011-.01a22%2022%200%200%200-1.552-1.403c-.247-.203-.507-.379-.761-.569-.303-.227-.6-.462-.916-.673-.304-.203-.619-.379-.931-.565-.286-.171-.565-.35-.859-.508-.318-.17-.644-.314-.969-.467-.307-.145-.609-.298-.923-.429-.315-.13-.637-.236-.957-.35-.337-.121-.669-.25-1.013-.354-.32-.097-.646-.168-.969-.249-.351-.089-.698-.187-1.055-.258-.375-.074-.753-.119-1.13-.173-.311-.044-.617-.104-.933-.135A22%2022%200%200%200%20490.667%200H384c-11.782%200-21.333%209.551-21.333%2021.333S372.218%2042.666%20384%2042.666h55.163L304.915%20176.915c-8.331%208.331-8.331%2021.839%200%2030.17s21.839%208.331%2030.17%200zm-158.17%2097.83L42.667%20439.163V384c0-11.782-9.551-21.333-21.333-21.333C9.551%20362.667%200%20372.218%200%20384v106.667q.001%201.055.106%202.105c.031.315.09.621.135.933.054.377.098.756.173%201.13.071.358.169.704.258%201.055.081.324.152.649.249.969.104.344.233.677.354%201.013.115.32.22.642.35.957s.284.616.429.923c.153.324.297.651.467.969.158.294.337.573.508.859.186.311.362.627.565.931.211.316.446.612.673.916.19.254.366.514.569.761q.664.811%201.403%201.552l.01.011.012.011q.741.738%201.551%201.402c.247.203.507.379.76.568.304.227.601.463.917.674.303.203.618.379.93.565.286.171.565.35.86.508.318.17.645.314.969.467.307.145.609.298.923.428s.636.235.956.35c.337.121.67.25%201.015.355.32.097.645.168.968.249.351.089.698.187%201.056.258.375.074.753.118%201.13.172.311.044.618.104.933.135q1.05.105%202.104.106H128c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333H72.837l134.248-134.248c8.331-8.331%208.331-21.839%200-30.17s-21.839-8.331-30.17%200zm330.821%20198.51c.226-.302.461-.598.671-.913.204-.304.38-.62.566-.932.17-.285.349-.564.506-.857.17-.318.315-.646.468-.971.145-.306.297-.607.428-.921.13-.315.236-.637.35-.957.121-.337.25-.669.354-1.013.097-.32.168-.646.249-.969.089-.351.187-.698.258-1.055.074-.375.118-.753.173-1.13.044-.311.104-.617.135-.933a22%2022%200%200%200%20.106-2.107V384c0-11.782-9.551-21.333-21.333-21.333s-21.333%209.551-21.333%2021.333v55.163L335.085%20304.915c-8.331-8.331-21.839-8.331-30.17%200s-8.331%2021.839%200%2030.17l134.248%20134.248H384c-11.782%200-21.333%209.551-21.333%2021.333S372.218%20512%20384%20512h106.667q1.055-.001%202.105-.106c.315-.031.621-.09.933-.135.377-.054.756-.098%201.13-.173.358-.071.704-.169%201.055-.258.324-.081.649-.152.969-.249.344-.104.677-.233%201.013-.354.32-.115.642-.22.957-.35s.615-.283.921-.428c.325-.153.653-.297.971-.468.293-.157.572-.336.857-.506.312-.186.628-.363.932-.566.315-.211.611-.445.913-.671.255-.191.516-.368.764-.571q.804-.659%201.54-1.392l.023-.021.021-.023q.732-.736%201.392-1.54c.205-.248.382-.509.573-.764zM72.837%2042.667H128c11.782%200%2021.333-9.551%2021.333-21.333C149.333%209.551%20139.782%200%20128%200H21.332q-1.054.001-2.104.106c-.316.031-.622.09-.933.135-.377.054-.755.098-1.13.172-.358.071-.705.169-1.056.258-.323.081-.648.152-.968.249-.345.104-.678.234-1.015.355-.319.115-.641.22-.956.35-.315.131-.618.284-.925.43-.323.152-.65.296-.967.466-.295.158-.575.338-.862.509-.31.185-.625.36-.928.563-.317.212-.615.448-.92.676-.252.189-.511.364-.756.566a21.5%2021.5%200%200%200-2.977%202.977c-.202.245-.377.504-.566.757-.228.305-.464.603-.676.92-.203.303-.378.617-.564.928-.171.286-.351.567-.509.862-.17.317-.313.643-.466.967-.145.307-.299.61-.43.925-.13.315-.235.636-.35.956-.121.337-.25.67-.355%201.015-.097.32-.168.645-.249.968-.089.351-.187.698-.258%201.056-.074.375-.118.753-.172%201.13-.044.311-.104.618-.135.933A22%2022%200%200%200%200%2021.333V128c0%2011.782%209.551%2021.333%2021.333%2021.333S42.666%20139.782%2042.666%20128V72.837l134.248%20134.248c8.331%208.331%2021.839%208.331%2030.17%200s8.331-21.839%200-30.17z%22%2F%3E%3C%2Fsvg%3E');
}
.expandVODChat .expand-toggle-li,
.expandLiveChat .expand-toggle-li {
background-image: url('data:image/svg+xml,%3Csvg%20fill%3D%22%23757B8A%22%20height%3D%2264%22%20width%3D%2264%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%20xml%3Aspace%3D%22preserve%22%20stroke%3D%22%23757B8A%22%3E%3Cg%20stroke-width%3D%220%22%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M320.106%20172.772c.031.316.09.622.135.933.054.377.098.755.172%201.13.071.358.169.705.258%201.056.081.323.152.648.249.968.104.345.234.678.355%201.015.115.319.22.641.35.956.131.315.284.618.43.925.152.323.296.65.466.967.158.294.337.574.508.86.186.311.362.626.565.93.211.316.447.613.674.917.19.253.365.513.568.759a21.4%2021.4%200%200%200%202.977%202.977c.246.202.506.378.759.567.304.228.601.463.918.675.303.203.618.379.929.565.286.171.566.351.861.509.317.17.644.314.968.466.307.145.609.298.924.429.315.13.637.236.957.35.337.121.669.25%201.013.354.32.097.646.168.969.249.351.089.698.187%201.055.258.375.074.753.119%201.13.173.311.044.617.104.932.135q1.051.105%202.105.106H448c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333h-55.163L505.752%2036.418c8.331-8.331%208.331-21.839%200-30.17s-21.839-8.331-30.17%200L362.667%20119.163V64c0-11.782-9.551-21.333-21.333-21.333C329.551%2042.667%20320%2052.218%20320%2064v106.668q.001%201.053.106%202.104zM170.667%2042.667c-11.782%200-21.333%209.551-21.333%2021.333v55.163L36.418%206.248c-8.331-8.331-21.839-8.331-30.17%200s-8.331%2021.839%200%2030.17l112.915%20112.915H64c-11.782%200-21.333%209.551-21.333%2021.333C42.667%20182.449%2052.218%20192%2064%20192h106.667q1.055-.001%202.105-.106c.316-.031.622-.09.933-.135.377-.054.755-.098%201.13-.172.358-.071.705-.169%201.056-.258.323-.081.648-.152.968-.249.345-.104.678-.234%201.015-.355.319-.115.641-.22.956-.35.315-.131.618-.284.925-.43.323-.152.65-.296.967-.466.295-.158.575-.338.862-.509.311-.185.625-.361.928-.564.317-.212.615-.448.92-.676.252-.189.511-.364.757-.566a21.5%2021.5%200%200%200%202.977-2.977c.202-.246.377-.505.566-.757.228-.305.464-.603.676-.92.203-.303.378-.617.564-.928.171-.286.351-.567.509-.862.17-.317.313-.643.466-.967.145-.307.299-.61.43-.925.13-.315.235-.636.35-.956.121-.337.25-.67.355-1.015.097-.32.168-.645.249-.968.089-.351.187-.698.258-1.056.074-.375.118-.753.172-1.13.044-.311.104-.618.135-.933q.105-1.05.106-2.104V64c-.002-11.782-9.553-21.333-21.335-21.333zm21.227%20296.561c-.031-.316-.09-.622-.135-.933-.054-.377-.098-.755-.172-1.13-.071-.358-.169-.705-.258-1.056-.081-.323-.152-.648-.249-.968-.104-.345-.234-.678-.355-1.015-.115-.319-.22-.641-.35-.956-.131-.315-.284-.618-.43-.925-.152-.323-.296-.65-.466-.967-.158-.295-.338-.575-.509-.862-.185-.311-.361-.625-.564-.928-.212-.317-.448-.615-.676-.92-.189-.252-.364-.511-.566-.757a21.5%2021.5%200%200%200-2.977-2.977c-.246-.202-.505-.377-.757-.566-.305-.228-.603-.464-.92-.676-.303-.203-.617-.378-.928-.564-.286-.171-.567-.351-.862-.509-.317-.17-.643-.313-.967-.466-.307-.145-.61-.299-.925-.43-.315-.13-.636-.235-.956-.35-.337-.121-.67-.25-1.015-.355-.32-.097-.645-.168-.968-.249-.351-.089-.698-.187-1.056-.258-.375-.074-.753-.118-1.13-.172-.311-.044-.618-.104-.933-.135q-1.051-.105-2.105-.106H64c-11.782%200-21.333%209.551-21.333%2021.333S52.218%20362.664%2064%20362.664h55.163L6.248%20475.582c-8.331%208.331-8.331%2021.839%200%2030.17s21.839%208.331%2030.17%200l112.915-112.915V448c0%2011.782%209.551%2021.333%2021.333%2021.333s21.333-9.551%2021.333-21.333V341.332a21%2021%200%200%200-.105-2.104zm200.943%2023.439H448c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333H341.333q-1.055.001-2.105.106c-.315.031-.621.09-.932.135-.378.054-.756.098-1.13.173-.358.071-.704.169-1.055.258-.324.081-.649.152-.969.249-.344.104-.677.233-1.013.354-.32.115-.642.22-.957.35-.315.131-.617.284-.924.429-.324.153-.65.296-.968.466-.295.158-.575.338-.861.509-.311.186-.626.362-.929.565-.316.212-.614.447-.918.675-.253.19-.512.365-.759.567a21.4%2021.4%200%200%200-2.977%202.977c-.202.246-.378.506-.568.759-.227.304-.463.601-.674.917-.203.304-.379.619-.565.93-.171.286-.351.566-.508.86-.17.317-.313.643-.466.967-.145.307-.299.61-.43.925-.13.315-.235.636-.35.956-.121.337-.25.67-.355%201.015-.097.32-.168.645-.249.968-.089.351-.187.698-.258%201.056-.074.374-.118.753-.172%201.13-.044.311-.104.618-.135.933q-.105%201.05-.106%202.104V448c0%2011.782%209.551%2021.333%2021.333%2021.333s21.333-9.551%2021.333-21.333v-55.163l112.915%20112.915c8.331%208.331%2021.839%208.331%2030.17%200s8.331-21.839%200-30.17z%22%2F%3E%3C%2Fsvg%3E');
}
html[dark="true"] .expand-toggle-li {
background-image: url('data:image/svg+xml,%3Csvg%20fill%3D%22%23ACB0B9%22%20height%3D%2264%22%20width%3D%2264%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%20xml%3Aspace%3D%22preserve%22%3E%3Cg%20stroke-width%3D%220%22%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M335.085%20207.085%20469.333%2072.837V128c0%2011.782%209.551%2021.333%2021.333%2021.333S512%20139.782%20512%20128V21.335q-.001-1.055-.106-2.107c-.031-.316-.09-.622-.135-.933-.054-.377-.098-.755-.172-1.13-.071-.358-.169-.705-.258-1.056-.081-.323-.152-.648-.249-.968-.104-.345-.234-.678-.355-1.015-.115-.319-.22-.641-.35-.956s-.284-.616-.428-.923c-.153-.324-.297-.651-.467-.969-.158-.294-.337-.574-.508-.86-.186-.311-.362-.626-.565-.93-.211-.316-.447-.613-.674-.917-.19-.253-.366-.513-.568-.76a22%2022%200%200%200-1.402-1.551l-.011-.012-.011-.01a22%2022%200%200%200-1.552-1.403c-.247-.203-.507-.379-.761-.569-.303-.227-.6-.462-.916-.673-.304-.203-.619-.379-.931-.565-.286-.171-.565-.35-.859-.508-.318-.17-.644-.314-.969-.467-.307-.145-.609-.298-.923-.429-.315-.13-.637-.236-.957-.35-.337-.121-.669-.25-1.013-.354-.32-.097-.646-.168-.969-.249-.351-.089-.698-.187-1.055-.258-.375-.074-.753-.119-1.13-.173-.311-.044-.617-.104-.933-.135A22%2022%200%200%200%20490.667%200H384c-11.782%200-21.333%209.551-21.333%2021.333S372.218%2042.666%20384%2042.666h55.163L304.915%20176.915c-8.331%208.331-8.331%2021.839%200%2030.17s21.839%208.331%2030.17%200m-158.17%2097.83L42.667%20439.163V384c0-11.782-9.551-21.333-21.333-21.333C9.551%20362.667%200%20372.218%200%20384v106.667q.001%201.055.106%202.105c.031.315.09.621.135.933.054.377.098.756.173%201.13.071.358.169.704.258%201.055.081.324.152.649.249.969.104.344.233.677.354%201.013.115.32.22.642.35.957s.284.616.429.923c.153.324.297.651.467.969.158.294.337.573.508.859.186.311.362.627.565.931.211.316.446.612.673.916.19.254.366.514.569.761q.664.811%201.403%201.552l.01.011.012.011q.741.738%201.551%201.402c.247.203.507.379.76.568.304.227.601.463.917.674.303.203.618.379.93.565.286.171.565.35.86.508.318.17.645.314.969.467.307.145.609.298.923.428s.636.235.956.35c.337.121.67.25%201.015.355.32.097.645.168.968.249.351.089.698.187%201.056.258.375.074.753.118%201.13.172.311.044.618.104.933.135q1.05.105%202.104.106H128c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333H72.837l134.248-134.248c8.331-8.331%208.331-21.839%200-30.17s-21.839-8.331-30.17%200m330.821%20198.51c.226-.302.461-.598.671-.913.204-.304.38-.62.566-.932.17-.285.349-.564.506-.857.17-.318.315-.646.468-.971.145-.306.297-.607.428-.921.13-.315.236-.637.35-.957.121-.337.25-.669.354-1.013.097-.32.168-.646.249-.969.089-.351.187-.698.258-1.055.074-.375.118-.753.173-1.13.044-.311.104-.617.135-.933a22%2022%200%200%200%20.106-2.107V384c0-11.782-9.551-21.333-21.333-21.333s-21.333%209.551-21.333%2021.333v55.163L335.085%20304.915c-8.331-8.331-21.839-8.331-30.17%200s-8.331%2021.839%200%2030.17l134.248%20134.248H384c-11.782%200-21.333%209.551-21.333%2021.333S372.218%20512%20384%20512h106.667q1.055-.001%202.105-.106c.315-.031.621-.09.933-.135.377-.054.756-.098%201.13-.173.358-.071.704-.169%201.055-.258.324-.081.649-.152.969-.249.344-.104.677-.233%201.013-.354.32-.115.642-.22.957-.35s.615-.283.921-.428c.325-.153.653-.297.971-.468.293-.157.572-.336.857-.506.312-.186.628-.363.932-.566.315-.211.611-.445.913-.671.255-.191.516-.368.764-.571q.804-.659%201.54-1.392l.023-.021.021-.023q.732-.736%201.392-1.54c.205-.248.382-.509.573-.764M72.837%2042.667H128c11.782%200%2021.333-9.551%2021.333-21.333C149.333%209.551%20139.782%200%20128%200H21.332q-1.054.001-2.104.106c-.316.031-.622.09-.933.135-.377.054-.755.098-1.13.172-.358.071-.705.169-1.056.258-.323.081-.648.152-.968.249-.345.104-.678.234-1.015.355-.319.115-.641.22-.956.35-.315.131-.618.284-.925.43-.323.152-.65.296-.967.466-.295.158-.575.338-.862.509-.31.185-.625.36-.928.563-.317.212-.615.448-.92.676-.252.189-.511.364-.756.566a21.5%2021.5%200%200%200-2.977%202.977c-.202.245-.377.504-.566.757-.228.305-.464.603-.676.92-.203.303-.378.617-.564.928-.171.286-.351.567-.509.862-.17.317-.313.643-.466.967-.145.307-.299.61-.43.925-.13.315-.235.636-.35.956-.121.337-.25.67-.355%201.015-.097.32-.168.645-.249.968-.089.351-.187.698-.258%201.056-.074.375-.118.753-.172%201.13-.044.311-.104.618-.135.933A22%2022%200%200%200%200%2021.333V128c0%2011.782%209.551%2021.333%2021.333%2021.333S42.666%20139.782%2042.666%20128V72.837l134.248%20134.248c8.331%208.331%2021.839%208.331%2030.17%200s8.331-21.839%200-30.17z%22%2F%3E%3C%2Fsvg%3E') !important;
}
html[dark="true"] .expandVODChat .expand-toggle-li,
html[dark="true"] .expandLiveChat .expand-toggle-li {
background-image: url('data:image/svg+xml,%3Csvg%20fill%3D%22%23ACB0B9%22%20height%3D%2264%22%20width%3D%2264%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%20xml%3Aspace%3D%22preserve%22%20stroke%3D%22%23ACB0B9%22%3E%3Cg%20stroke-width%3D%220%22%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke%3D%22%23CCC%22%20stroke-width%3D%222.048%22%2F%3E%3Cpath%20d%3D%22M320.106%20172.772c.031.316.09.622.135.933.054.377.098.755.172%201.13.071.358.169.705.258%201.056.081.323.152.648.249.968.104.345.234.678.355%201.015.115.319.22.641.35.956.131.315.284.618.43.925.152.323.296.65.466.967.158.294.337.574.508.86.186.311.362.626.565.93.211.316.447.613.674.917.19.253.365.513.568.759a21.4%2021.4%200%200%200%202.977%202.977c.246.202.506.378.759.567.304.228.601.463.918.675.303.203.618.379.929.565.286.171.566.351.861.509.317.17.644.314.968.466.307.145.609.298.924.429.315.13.637.236.957.35.337.121.669.25%201.013.354.32.097.646.168.969.249.351.089.698.187%201.055.258.375.074.753.119%201.13.173.311.044.617.104.932.135q1.051.105%202.105.106H448c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333h-55.163L505.752%2036.418c8.331-8.331%208.331-21.839%200-30.17s-21.839-8.331-30.17%200L362.667%20119.163V64c0-11.782-9.551-21.333-21.333-21.333C329.551%2042.667%20320%2052.218%20320%2064v106.668q.001%201.053.106%202.104zM170.667%2042.667c-11.782%200-21.333%209.551-21.333%2021.333v55.163L36.418%206.248c-8.331-8.331-21.839-8.331-30.17%200s-8.331%2021.839%200%2030.17l112.915%20112.915H64c-11.782%200-21.333%209.551-21.333%2021.333C42.667%20182.449%2052.218%20192%2064%20192h106.667q1.055-.001%202.105-.106c.316-.031.622-.09.933-.135.377-.054.755-.098%201.13-.172.358-.071.705-.169%201.056-.258.323-.081.648-.152.968-.249.345-.104.678-.234%201.015-.355.319-.115.641-.22.956-.35.315-.131.618-.284.925-.43.323-.152.65-.296.967-.466.295-.158.575-.338.862-.509.311-.185.625-.361.928-.564.317-.212.615-.448.92-.676.252-.189.511-.364.757-.566a21.5%2021.5%200%200%200%202.977-2.977c.202-.246.377-.505.566-.757.228-.305.464-.603.676-.92.203-.303.378-.617.564-.928.171-.286.351-.567.509-.862.17-.317.313-.643.466-.967.145-.307.299-.61.43-.925.13-.315.235-.636.35-.956.121-.337.25-.67.355-1.015.097-.32.168-.645.249-.968.089-.351.187-.698.258-1.056.074-.375.118-.753.172-1.13.044-.311.104-.618.135-.933q.105-1.05.106-2.104V64c-.002-11.782-9.553-21.333-21.335-21.333zm21.227%20296.561c-.031-.316-.09-.622-.135-.933-.054-.377-.098-.755-.172-1.13-.071-.358-.169-.705-.258-1.056-.081-.323-.152-.648-.249-.968-.104-.345-.234-.678-.355-1.015-.115-.319-.22-.641-.35-.956-.131-.315-.284-.618-.43-.925-.152-.323-.296-.65-.466-.967-.158-.295-.338-.575-.509-.862-.185-.311-.361-.625-.564-.928-.212-.317-.448-.615-.676-.92-.189-.252-.364-.511-.566-.757a21.5%2021.5%200%200%200-2.977-2.977c-.246-.202-.505-.377-.757-.566-.305-.228-.603-.464-.92-.676-.303-.203-.617-.378-.928-.564-.286-.171-.567-.351-.862-.509-.317-.17-.643-.313-.967-.466-.307-.145-.61-.299-.925-.43-.315-.13-.636-.235-.956-.35-.337-.121-.67-.25-1.015-.355-.32-.097-.645-.168-.968-.249-.351-.089-.698-.187-1.056-.258-.375-.074-.753-.118-1.13-.172-.311-.044-.618-.104-.933-.135q-1.051-.105-2.105-.106H64c-11.782%200-21.333%209.551-21.333%2021.333S52.218%20362.664%2064%20362.664h55.163L6.248%20475.582c-8.331%208.331-8.331%2021.839%200%2030.17s21.839%208.331%2030.17%200l112.915-112.915V448c0%2011.782%209.551%2021.333%2021.333%2021.333s21.333-9.551%2021.333-21.333V341.332a21%2021%200%200%200-.105-2.104zm200.943%2023.439H448c11.782%200%2021.333-9.551%2021.333-21.333s-9.551-21.333-21.333-21.333H341.333q-1.055.001-2.105.106c-.315.031-.621.09-.932.135-.378.054-.756.098-1.13.173-.358.071-.704.169-1.055.258-.324.081-.649.152-.969.249-.344.104-.677.233-1.013.354-.32.115-.642.22-.957.35-.315.131-.617.284-.924.429-.324.153-.65.296-.968.466-.295.158-.575.338-.861.509-.311.186-.626.362-.929.565-.316.212-.614.447-.918.675-.253.19-.512.365-.759.567a21.4%2021.4%200%200%200-2.977%202.977c-.202.246-.378.506-.568.759-.227.304-.463.601-.674.917-.203.304-.379.619-.565.93-.171.286-.351.566-.508.86-.17.317-.313.643-.466.967-.145.307-.299.61-.43.925-.13.315-.235.636-.35.956-.121.337-.25.67-.355%201.015-.097.32-.168.645-.249.968-.089.351-.187.698-.258%201.056-.074.374-.118.753-.172%201.13-.044.311-.104.618-.135.933q-.105%201.05-.106%202.104V448c0%2011.782%209.551%2021.333%2021.333%2021.333s21.333-9.551%2021.333-21.333v-55.163l112.915%20112.915c8.331%208.331%2021.839%208.331%2030.17%200s8.331-21.839%200-30.17z%22%2F%3E%3C%2Fsvg%3E') !important;
}
.screen_mode .expand-toggle-li,
.fullScreen_mode .expand-toggle-li {
display: none !important;
}
.customSidebar #serviceLnb {
display: none !important;
}
.left_navbar {
display: flex;
align-items: center;
justify-content: flex-end;
position: fixed;
flex-direction: row-reverse;
top: 0px;
left: 128px;
z-index: 9999;
background-color: white;
}
html[dark="true"] .left_navbar {
background-color: #0c0d0e;
}
html[dark="true"] .left_nav_button {
color: #e5e5e5;
}
html:not([dark="true"]) .left_nav_button {
color: #1F1F23;
}
html[dark="true"] .left_nav_button {
color: #e5e5e5;
}
html:not([dark="true"]) .left_nav_button {
color: #1F1F23;
}
.left_navbar button.left_nav_button {
position: relative;
width: 68px;
height: 64px;
padding: 0;
border: 0;
cursor: pointer;
z-index: 3001;
font-size: 1.25em !important;
font-weight: 600;
}
@media (max-width: 1280px) {
#serviceHeader .left_navbar {
left: 124px !important;
}
#serviceHeader .left_nav_button {
width: 58px !important;
font-size: 1.2em !important;
}
}
@media (max-width: 1100px) {
#serviceHeader .left_navbar {
left: 120px !important;
}
#serviceHeader .left_nav_button {
width: 46px !important;
font-size: 1.1em !important;
}
}
#sidebar {
top: 64px;
display: flex !important;
flex-direction: column !important;
}
#sidebar .top-section.follow {
order: 1;
}
#sidebar .users-section.follow {
order: 2;
}
#sidebar .top-section.myplus {
order: 3;
}
#sidebar .users-section.myplus {
order: 4;
}
#sidebar .top-section.myplusvod {
order: 5;
}
#sidebar .users-section.myplusvod {
order: 6;
}
#sidebar .top-section.top {
order: 7;
}
#sidebar .users-section.top {
order: 8;
}
.starting-line .chatting-list-item .message-container .username {
width: ${nicknameWidth}px !important;
}
.duration-overlay {
position: absolute;
top: 235px;
right: 4px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 5px;
font-size: 15px;
border-radius: 3px;
z-index:9999;
line-height: 17px;
}
#studioPlayKorPlayer,
#studioPlayKor,
#studioPlay,
.btn-broadcast {
display: none;
}
#myModal.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
color: black;
}
#myModal .modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
border-radius: 10px;
width: clamp(400px, 80%, 550px);
}
#myModal .myModalClose {
color: #aaa;
float: right;
font-size: 36px;
font-weight: bold;
margin-top: -12px;
}
#myModal .myModalClose:hover,
#myModal .myModalClose:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
#myModal .option {
margin-bottom: 10px;
display: flex;
align-items: center;
}
#myModal .option label {
margin-right: 10px;
font-size: 15px;
}
#myModal .switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
transform: scale(0.9); /* 축소 */
}
#myModal .switch input {
display: none;
}
#myModal .slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
#myModal .slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
#myModal .slider.round {
border-radius: 34px;
min-width: 60px;
}
#myModal .slider.round:before {
border-radius: 50%;
}
#myModal input:checked + .slider {
background-color: #2196F3;
}
#myModal input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
#myModal input:checked + .slider:before {
transform: translateX(26px);
}
#myModal #range {
width: 100%;
}
#myModal #rangeValue {
display: inline-block;
margin-left: 10px;
}
#myModal .divider {
width: 100%; /* 가로 폭 설정 */
height: 1px; /* 세로 높이 설정 */
background-color: #000; /* 배경색 설정 */
margin: 20px 0; /* 위아래 여백 설정 */
}
#openModalBtn {
box-sizing: border-box;
font-size: 12px;
line-height: 1.2 !important;
font-family: "NG";
list-style: none;
position: relative;
margin-left: 12px;
width: 40px;
height: 40px;
}
#topInnerHeader #openModalBtn {
margin-right: 12px;
}
#openModalBtn > button.btn-settings-ui {
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none'%3e%3cpath stroke='%23757B8A' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4' d='M8.269 2.061c.44-1.815 3.022-1.815 3.462 0a1.782 1.782 0 0 0 2.658 1.101c1.595-.971 3.42.854 2.449 2.449a1.781 1.781 0 0 0 1.1 2.658c1.816.44 1.816 3.022 0 3.462a1.781 1.781 0 0 0-1.1 2.659c.971 1.595-.854 3.42-2.449 2.448a1.781 1.781 0 0 0-2.658 1.101c-.44 1.815-3.022 1.815-3.462 0a1.781 1.781 0 0 0-2.658-1.101c-1.595.972-3.42-.854-2.449-2.448a1.782 1.782 0 0 0-1.1-2.659c-1.816-.44-1.816-3.021 0-3.462a1.782 1.782 0 0 0 1.1-2.658c-.972-1.595.854-3.42 2.449-2.449a1.781 1.781 0 0 0 2.658-1.1Z'/%3e%3cpath stroke='%23757B8A' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4' d='M13.1 10a3.1 3.1 0 1 1-6.2 0 3.1 3.1 0 0 1 6.2 0Z'/%3e%3c/svg%3e") 50% 50% no-repeat !important;
background-size: 18px !important;
}
html[dark="true"] #openModalBtn > button.btn-settings-ui {
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none'%3e%3cpath stroke='%23ACB0B9' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4' d='M8.269 2.061c.44-1.815 3.022-1.815 3.462 0a1.782 1.782 0 0 0 2.658 1.101c1.595-.971 3.42.854 2.449 2.449a1.781 1.781 0 0 0 1.1 2.658c1.816.44 1.816 3.022 0 3.462a1.781 1.781 0 0 0-1.1 2.659c.971 1.595-.854 3.42-2.449 2.448a1.781 1.781 0 0 0-2.658 1.101c-.44 1.815-3.022 1.815-3.462 0a1.781 1.781 0 0 0-2.658-1.101c-1.595.972-3.42-.854-2.449-2.448a1.782 1.782 0 0 0-1.1-2.659c-1.816-.44-1.816-3.021 0-3.462a1.782 1.782 0 0 0 1.1-2.658c-.972-1.595.854-3.42 2.449-2.449a1.781 1.781 0 0 0 2.658-1.1Z'/%3e%3cpath stroke='%23ACB0B9' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4' d='M13.1 10a3.1 3.1 0 1 1-6.2 0 3.1 3.1 0 0 1 6.2 0Z'/%3e%3c/svg%3e") 50% 50% no-repeat !important;
background-size: 18px !important;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* .red-dot이 있을 때만 회전 */
#openModalBtn:has(.red-dot) .btn-settings-ui {
animation: rotate 4s linear infinite;
animation-duration: 4s; /* 4초에 한 번 회전 */
animation-iteration-count: 10; /* 10번 반복 */
}
#sidebar.max {
width: 240px;
}
#sidebar.min {
width: 52px;
}
#sidebar.min .users-section a.user span {
display: none;
}
#sidebar.min .users-section button {
font-size:12px;
padding: 4px;
}
#sidebar.max .button-fold-sidebar {
background-size: 7px 11px;
background-repeat: no-repeat;
width: 26px;
height: 26px;
background-position: center;
position: absolute;
top: 13px;
left: 200px;
}
#sidebar.max .button-unfold-sidebar {
display:none;
}
#sidebar.min .button-fold-sidebar {
display:none;
}
#sidebar.min .button-unfold-sidebar {
background-size: 7px 11px;
background-repeat: no-repeat;
width: 26px;
height: 26px;
background-position: center;
position: relative;
top: 8px;
left: 12px;
padding-top:16px;
padding-bottom:12px;
}
#sidebar.min .top-section span.max{
display:none;
}
#sidebar.max .top-section span.min{
display:none;
}
#toggleButton, #toggleButton2, #toggleButton3, #toggleButton4 {
padding: 7px 0px;
width: 100%;
text-align: center;
font-size: 14px;
}
html[dark="true"] #toggleButton,
html[dark="true"] #toggleButton2,
html[dark="true"] #toggleButton3,
html[dark="true"] #toggleButton4 {
color:#A1A1A1;
}
html:not([dark="true"]) #toggleButton,
html:not([dark="true"]) #toggleButton2,
html:not([dark="true"]) #toggleButton3,
html:not([dark="true"]) #toggleButton4 {
color: #53535F;
}
#sidebar {
grid-area: sidebar;
padding-bottom: 360px;
height: 100vh;
overflow-y: auto;
position: fixed;
scrollbar-width: none; /* 파이어폭스 */
transition: all 0.1s ease-in-out; /* 부드러운 전환 효과 */
}
#sidebar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
#sidebar .top-section {
display: flex;
align-items: center;
justify-content: space-around;
margin: 12px 0px 6px 0px;
line-height: 17px;
}
#sidebar .top-section > span {
text-transform: uppercase;
font-weight: 550;
font-size: 14px;
margin-top: 6px;
margin-bottom: 2px;
}
.users-section .user.show-more {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
pointer-events: none;
}
.users-section .user {
display: grid;
grid-template-areas: "profile-picture username watchers" "profile-picture description blank";
grid-template-columns: 40px auto auto;
padding: 5px 10px;
max-height: 50px;
opacity: 1;
overflow: hidden;
transition: opacity 0.7s ease;
}
.users-section .user:hover {
cursor: pointer;
}
.users-section .user .profile-picture {
grid-area: profile-picture;
width: 30px;
height: 30px;
border-radius: 50%;
line-height: 20px;
}
.users-section .user .username {
grid-area: username;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.6px;
margin-left:1px;
line-height: 17px;
}
.users-section .user .description {
grid-area: description;
font-size: 13px;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left:1px;
line-height: 16px;
}
.users-section .user .watchers {
grid-area: watchers;
display: flex;
align-items: center;
justify-content: flex-end;
font-weight: 400;
font-size: 14px;
margin-right: 2px;
line-height: 17px;
}
.users-section .user .watchers .dot {
font-size: 10px;
margin-right: 5px;
color: #ff2424;
}
.users-section .user .watchers .dot.greendot {
color: #34c76b !important;
}
.tooltip-container {
z-index: 999;
width: 460px;
height: auto;
position: fixed;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 10px;
box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.1s ease-in-out;
pointer-events: none;
}
.tooltip-container.visible {
opacity: 1;
pointer-events: auto;
}
.tooltip-container img {
z-index: 999;
width: 100%; /* 컨테이너의 너비에 맞게 확장 */
height: 260px; /* 고정 높이 */
object-fit: cover; /* 비율 유지하며 공간에 맞게 잘리기 */
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.tooltiptext {
position: relative;
z-index: 999;
width: 100%;
max-width: 460px;
height: auto;
text-align: center;
box-sizing: border-box;
padding: 14px 20px;
font-size: 17px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
line-height: 22px;
overflow-wrap: break-word;
}
.tooltiptext .dot {
font-size: 11px;
margin-right: 2px;
vertical-align: middle;
line-height: 22px;
display: inline-block;
}
.profile-grayscale {
filter: grayscale(100%) contrast(85%);
opacity: .8;
}
#sidebar.max .small-user-layout.show-more {
max-height: 0;
opacity: 0;
padding: 0 !important;
pointer-events: none;
}
#sidebar.max .small-user-layout {
grid-template-areas: "profile-picture username description watchers" !important;
grid-template-columns: 24px auto 1fr auto !important;
padding: 4px 10px !important;
gap: 8px !important;
max-height: 32px;
opacity: 1;
overflow: hidden;
transition: opacity 0.4s ease;
}
#sidebar.max .small-user-layout .profile-picture {
width: 24px !important;
height: 24px !important;
border-radius: 20% !important;
}
#sidebar.max .small-user-layout .username {
max-width: 80px !important;
font-size: 14px !important;
line-height: 24px !important;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#sidebar.max .small-user-layout .description {
font-size: 12px !important;
line-height: 24px !important;
}
#sidebar.max .small-user-layout .watchers {
font-size: 14px !important;
line-height: 24px !important;
}
#sidebar.max .small-user-layout .watchers .dot {
font-size: 8px !important;
margin-right: 4px !important;
}
.customSidebar #serviceHeader .a_d_banner {
display: none !important;
}
.customSidebar #serviceHeader .btn_flexible+.logo_wrap {
left: 24px !important;
}
.customSidebar #serviceHeader .logo_wrap {
left: 24px !important;
}
html[dark="true"] .users-section .user.user-offline span {
filter: grayscale(1) brightness(0.8); /* 다크모드: 완전 흑백과 약간 어둡게 */
}
html:not([dark="true"]) .users-section .user.user-offline span {
opacity: 0.7; /* 밝은 모드: 투명하게 */
}
/* darkMode Sidebar Styles */
html[dark="true"] #sidebar.max .button-fold-sidebar {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none slice' viewBox='0 0 7 11'%3e%3cpath fill='%23f9f9f9' d='M5.87 11.01L.01 5.51 5.87.01l1.08 1.01-4.74 4.45L7 9.96 5.87 11z'/%3e%3c/svg%3e");
}
html[dark="true"] #sidebar.min .button-unfold-sidebar {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none slice' viewBox='0 0 7 11'%3e%3cpath fill='%23f9f9f9' d='M1.13 11.01l5.86-5.5L1.13.01.05 1.02l4.74 4.45L0 9.96 1.13 11z'/%3e%3c/svg%3e");
}
html[dark="true"] #sidebar {
color: white;
background-color: #1F1F23;
}
html[dark="true"] #sidebar .top-section > span {
color:#DEDEE3;
}
html[dark="true"] #sidebar .top-section > span > a {
color:#DEDEE3;
}
html[dark="true"] .users-section .user:hover {
background-color: #26262c;
}
html[dark="true"] .users-section .user .username {
color:#DEDEE3;
}
html[dark="true"] .users-section .user .description {
color: #a1a1a1;
}
html[dark="true"] .users-section .user .watchers {
color: #c0c0c0;
}
html[dark="true"] .tooltip-container {
background-color: #26262C;
}
html[dark="true"] .tooltiptext {
color: #fff;
background-color: #26262C;
}
/* whiteMode Sidebar Styles */
html:not([dark="true"]) #sidebar.max .button-fold-sidebar {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none slice' viewBox='0 0 7 11'%3e%3cpath fill='%23888' d='M5.87 11.01L.01 5.51 5.87.01l1.08 1.01-4.74 4.45L7 9.96 5.87 11z'/%3e%3c/svg%3e");
}
html:not([dark="true"]) #sidebar.min .button-unfold-sidebar {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none slice' viewBox='0 0 7 11'%3e%3cpath fill='%23888' d='M1.13 11.01l5.86-5.5L1.13.01.05 1.02l4.74 4.45L0 9.96 1.13 11z'/%3e%3c/svg%3e");
}
html:not([dark="true"]) #sidebar {
color: white;
background-color: #EFEFF1;
}
html:not([dark="true"]) #sidebar .top-section > span {
color:#0E0E10;
}
html:not([dark="true"]) #sidebar .top-section > span > a {
color:#0E0E10;
}
html:not([dark="true"]) .users-section .user:hover {
background-color: #E6E6EA;
}
html:not([dark="true"]) .users-section .user .username {
color:#1F1F23;
}
html:not([dark="true"]) .users-section .user .description {
color: #53535F;
}
html:not([dark="true"]) .users-section .user .watchers {
color: black;
}
html:not([dark="true"]) .tooltip-container {
background-color: #E6E6EA;
}
html:not([dark="true"]) .tooltiptext {
color: black;
background-color: #E6E6EA;
}
`;
const mainPageCommonStyles = `
._moreDot_layer button {
text-align: left;
}
/*----- preview-modal 시작 -----*/
.preview-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(5px);
}
.preview-modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0;
width: 80%;
max-width: 800px;
max-height: 800px;
border-radius: 10px;
border: 1px solid #cccccc52;
overflow: hidden;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
pointer-events: auto;
}
.preview-modal .preview-close {
position: absolute;
top: 10px;
right: 15px;
color: #fff;
font-size: 30px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
z-index: 10;
}
.preview-modal .preview-close:hover,
.preview-modal .preview-close:focus {
color: #e50914;
}
.preview-modal .thumbnail-container {
position: relative;
width: 100%;
height: 450px;
background-color: black;
display: flex;
justify-content: center;
align-items: center;
}
.preview-modal .thumbnail-container img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.preview-modal .preview-modal-content video {
width: clamp(100%, 50vw, 800px);
height: 449px;
display: none;
}
.preview-modal .info {
color: white;
text-align: left;
padding: 28px;
background-color: rgba(0, 0, 0, 0.65);
}
.preview-modal .streamer-name {
font-size: 50px;
font-weight: bold;
letter-spacing: -2px;
}
.preview-modal .video-title {
font-size: 20px;
margin: 20px 0 30px 0;
}
.preview-modal .tags {
display: flex;
justify-content: left;
flex-wrap: wrap;
flex-direction: row;
margin-left: -3px;
}
.preview-modal .tags a {
margin: 5px;
color: white;
text-decoration: none;
border: 1px solid #fff;
padding: 5px 10px;
border-radius: 5px;
transition: background-color 0.3s;
}
.preview-modal .tags a:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.preview-modal .start-button {
background-color: #2d6bffba;
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
font-size: 22px;
cursor: pointer;
display: inline-block; /* inline-block으로 변경 */
width: auto; /* 너비는 자동으로 */
text-align: center;
text-decoration: none;
transition: background-color 0.3s;
}
.preview-modal .start-button:hover {
background-color: #2d6bff8f;
}
/*----- preview-modal 끝 -----*/
.customSidebar .btn_flexible {
display: none;
}
#sidebar {
z-index: 1401;
}
body.customSidebar main {
padding-left: 238px !important;
}
body.customSidebar .catch_webplayer_wrap {
margin-left: 24px !important;
}
`;
const playerCommonStyles = `
.screen_mode .left_navbar,
.fullScreen_mode .left_navbar {
display: none;
}
.customSidebar .btn_flexible {
display: none;
}
/* 스크롤바 스타일링 */
html {
overflow: auto; /* 스크롤 기능 유지 */
}
/* Firefox 전용 스크롤바 감추기 */
html::-webkit-scrollbar {
display: none; /* 크롬 및 사파리에서 */
}
/* Firefox에서는 아래와 같이 처리 */
html {
scrollbar-width: none; /* Firefox에서 스크롤바 감추기 */
-ms-overflow-style: none; /* Internet Explorer 및 Edge */
}
.customSidebar #player,
.customSidebar #webplayer #webplayer_contents #player_area .float_box,
.customSidebar #webplayer #webplayer_contents #player_area
{
min-width: 180px !important;
}
.customSidebar.screen_mode #webplayer,
.customSidebar.screen_mode #sidebar
{
transition: all 0.25s ease-in-out !important;
}
@media screen and (max-width: 892px) {
.screen_mode.bottomChat #webplayer #player .view_ctrl,
.screen_mode.bottomChat #webplayer .wrapping.side {
display: block !important;
}
}
.customSidebar #webplayer_contents {
width: calc(100vw - ${WEB_PLAYER_SCROLL_LEFT}px) !important;
gap:0 !important;
padding: 0 !important;
margin: 64px 0 0 !important;
left: ${WEB_PLAYER_SCROLL_LEFT}px !important;
}
.customSidebar.top_hide #webplayer_contents,
.customSidebar.top_hide #sidebar {
top: 0 !important;
margin-top: 0 !important;
min-height: 100vh !important;
}
/* sidebar가 .max 클래스를 가질 때, body에 .screen_mode가 없을 경우 */
body:not(.screen_mode):not(.fullScreen_mode):has(#sidebar.max) #webplayer_contents {
width: calc(100vw - 240px) !important;
left: 240px !important;
}
/* sidebar가 .min 클래스를 가질 때, body에 .screen_mode가 없을 경우 */
body:not(.screen_mode):not(.fullScreen_mode):has(#sidebar.min) #webplayer_contents {
width: calc(100vw - 52px) !important;
left: 52px !important;
}
.customSidebar.screen_mode #webplayer #webplayer_contents,
.customSidebar.fullScreen_mode #webplayer #webplayer_contents {
top: 0 !important;
left: 0 !important;
width: 100vw;
height: 100vh !important;
margin: 0 !important;
}
.customSidebar.screen_mode #sidebar{
display: none !important;
top: 0 !important;
}
.customSidebar.screen_mode #sidebar .button-fold-sidebar,
.customSidebar.screen_mode #sidebar .button-unfold-sidebar
{
display: none !important;
}
.customSidebar.screen_mode.showSidebar #sidebar{
display: flex !important;
}
.customSidebar.screen_mode #webplayer_contents,
.customSidebar.fullScreen_mode #webplayer_contents{
width: 100vw !important
}
.customSidebar.screen_mode.showSidebar:has(#sidebar.min) #webplayer_contents {
width: calc(100vw - 52px) !important
}
.customSidebar.screen_mode.showSidebar:has(#sidebar.max) #webplayer_contents {
width: calc(100vw - 240px) !important
}
.screen_mode.bottomChat #webplayer #webplayer_contents {
top: 0 !important;
margin: 0 !important;
}
.screen_mode.bottomChat #player {
min-height: auto !important;
}
.screen_mode.bottomChat #webplayer #webplayer_contents {
position: relative;
box-sizing: border-box;
flex: auto;
display: flex;
flex-direction: column !important;
justify-content:flex-start !important;
}
.screen_mode.bottomChat #webplayer #webplayer_contents .wrapping.side {
width: 100% !important;
max-height: calc(100vh - (100vw * 9 / 16)) !important;
}
.screen_mode.bottomChat.showSidebar:has(#sidebar.min) #webplayer #webplayer_contents .wrapping.side {
width: 100% !important;
max-height: calc(100vh - ((100vw - 52px) * 9 / 16)) !important;
}
.screen_mode.bottomChat.showSidebar:has(#sidebar.max) #webplayer #webplayer_contents .wrapping.side {
width: 100% !important;
max-height: calc(100vh - ((100vw - 240px) * 9 / 16)) !important;
}
.screen_mode.bottomChat #webplayer #webplayer_contents .wrapping.side section.box.chatting_box {
height: 100% !important;
}
.screen_mode.bottomChat #webplayer #webplayer_contents .wrapping.side section.box.chatting_box #chatting_area {
height: 100% !important;
min-height: 10vh !important;
}
.screen_mode.bottomChat #webplayer #webplayer_contents #player_area .htmlplayer_wrap,
.screen_mode.bottomChat #webplayer #webplayer_contents #player_area .htmlplayer_content,
.screen_mode.bottomChat #webplayer #webplayer_contents #player_area .float_box,
.screen_mode.bottomChat #webplayer #webplayer_contents #player_area #player {
height: auto !important;
max-height: max-content;
}
.customSidebar #player {
max-height: 100vh !important;
}
`;
//======================================공용 함수======================================//
const checkIfTimeover = (timestamp) => {
const now = Date.now();
const inputTime = timestamp * 1000; // 초 단위 타임스탬프를 밀리초로 변환
// 24시간(1일) = 86400000 밀리초
return (now - inputTime) > 86400000;
};
const timeSince = (serverTimeStr) => {
// 입력 문자열 → ISO 8601 + KST 오프셋으로 변환
const toKSTDate = (str) => {
const iso = str.replace(' ', 'T') + '+09:00';
return new Date(iso);
};
const postTime = toKSTDate(serverTimeStr).getTime(); // 게시물 작성 시각 (KST)
const now = Date.now(); // 현재 시각 (밀리초 기준, UTC)
const seconds = Math.floor((now - postTime) / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 365) return `${Math.floor(days / 365)}년 전`;
if (days > 30) return `${Math.floor(days / 30)}개월 전`;
if (days > 0) return `${days}일 전`;
if (hours > 0) return `${hours}시간 전`;
if (minutes > 0) return `${minutes}분 전`;
return `${seconds}초 전`;
};
const waitForElement = (elementSelector, callBack, maxAttempts = 200, interval = 200) => {
let attempts = 0;
const checkElement = () => {
const element = document.body.querySelector(elementSelector);
if (element) {
callBack(elementSelector, element);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(checkElement, interval); // 반복 검사
} else {
console.warn(`Reached maximum attempts. ${elementSelector} not found.`);
}
};
checkElement(); // 첫 번째 검사 호출
};
const waitForElementAsync = (elementSelector, maxAttempts = 200, attemptInterval = 200) => {
return new Promise((resolve, reject) => {
let attempts = 0;
const checkElement = () => {
const element = document.body.querySelector(elementSelector);
if (element) {
resolve(element); // 요소를 찾으면 resolve
} else if (attempts < maxAttempts) {
attempts += 1; // attempts 증가
setTimeout(checkElement, attemptInterval); // 반복 검사
} else {
reject(`Reached maximum attempts. ${elementSelector} not found.`); // 최대 시도 횟수 초과
}
};
checkElement(); // 첫 번째 검사 호출
});
};
const updateElementWithContent = (targetElement, newContent) => {
// DocumentFragment 생성
const createFragment = (content) => {
const fragment = document.createDocumentFragment();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
// tempDiv의 자식 요소를 fragment에 추가
while (tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
return fragment;
};
// 기존 내용을 지우고 DocumentFragment를 적용
const applyFragment = (fragment) => {
targetElement.innerHTML = ''; // 기존 내용을 모두 지움
targetElement.appendChild(fragment); // 새로운 내용 추가
};
// DocumentFragment 생성 후 적용
applyFragment(createFragment(newContent));
};
const manageRedDot = () => {
const RED_DOT_CLASS = 'red-dot';
const style = document.createElement('style');
style.textContent = `
.${RED_DOT_CLASS} {
position: absolute;
top: 8px;
right: 8px;
width: 4px;
height: 4px;
background-color: red;
border-radius: 50%;
}
`;
document.head.appendChild(style);
const lastUpdateDate = GM_getValue('lastUpdateDate', 0);
const btn = document.querySelector('#openModalBtn > button');
// 빨간 점 추가 함수
const showRedDot = () => {
if (!btn || document.querySelector(`#openModalBtn .${RED_DOT_CLASS}`)) return;
const redDot = document.createElement('div');
redDot.classList.add(RED_DOT_CLASS);
btn.parentElement.appendChild(redDot);
};
// 빨간 점 제거 함수
const hideRedDot = () => {
const redDot = document.querySelector(`#openModalBtn .${RED_DOT_CLASS}`);
if (redDot) redDot.remove();
};
// 날짜를 비교하여 빨간 점 표시
if (NEW_UPDATE_DATE > lastUpdateDate) {
showRedDot();
} else {
hideRedDot();
}
// 버튼 클릭 시 이벤트 핸들러 추가
btn?.addEventListener('click', () => {
GM_setValue('lastUpdateDate', NEW_UPDATE_DATE);
hideRedDot();
});
};
const addNumberSeparator = (number) => {
number = Number(number);
// 숫자가 10,000 이상일 때
if (number >= 10000) {
const displayNumber = (number / 10000).toFixed(1);
return displayNumber.endsWith('.0') ?
displayNumber.slice(0, -2) + '만' : displayNumber + '만';
}
return number.toLocaleString();
};
const addNumberSeparatorAll = (number) => {
number = Number(number);
// 숫자가 10,000 이상일 때
if (number >= 10000) {
const displayNumber = (number / 10000).toFixed(1);
return displayNumber.endsWith('.0') ?
displayNumber.slice(0, -2) + '만' : displayNumber + '만';
}
// 숫자가 1,000 이상일 때
else if (number >= 1000) {
const displayNumber = (number / 1000).toFixed(1);
return displayNumber.endsWith('.0') ?
displayNumber.slice(0, -2) + '천' : displayNumber + '천';
}
// 기본적으로 쉼표 추가
return number.toLocaleString();
};
const getCategoryName = (targetCateNo) => {
const searchCategory = (categories) => {
for (const category of categories) {
if (category.cate_no === targetCateNo) {
return category.cate_name;
}
if (category.child?.length) {
const result = searchCategory(category.child);
if (result) return result;
}
}
return targetCateNo === "ADULT_BROAD_CATE" ? "연령제한" : null;
};
return searchCategory(savedCategory.CHANNEL.BROAD_CATEGORY);
};
const getCategoryNo = (targetCateName) => {
const searchCategory = (categories) => {
for (const category of categories) {
if (category.cate_name === targetCateName) {
return category.cate_no;
}
if (category.child?.length) {
const result = searchCategory(category.child);
if (result) return result;
}
}
return targetCateName === "연령제한" ? "ADULT_BROAD_CATE" : null;
};
return searchCategory(savedCategory.CHANNEL.BROAD_CATEGORY);
};
// 차단 목록을 저장합니다.
function saveBlockedUsers() {
GM_setValue('blockedUsers', blockedUsers);
}
// 사용자를 차단 목록에 추가합니다.
function blockUser(userName, userId) {
// 이미 차단된 사용자인지 확인
if (!isUserBlocked(userId)) {
blockedUsers.push({ userName, userId });
saveBlockedUsers();
alert(`사용자 ${userName}(${userId})를 차단했습니다.\n차단 해제 메뉴는 템퍼몽키 아이콘을 누르면 있습니다.`);
registerUnblockMenu({ userName, userId });
} else {
alert(`사용자 ${userName}(${userId})는 이미 차단되어 있습니다.`);
}
}
// 함수: 사용자 차단 해제
function unblockUser(userId) {
// 차단된 사용자 목록에서 해당 사용자 찾기
let unblockedUser = blockedUsers.find(user => user.userId === userId);
// 사용자를 찾았을 때만 차단 해제 및 메뉴 삭제 수행
if (unblockedUser) {
// 차단된 사용자 목록에서 해당 사용자 제거
blockedUsers = blockedUsers.filter(user => user.userId !== userId);
// 변경된 목록을 저장
GM_setValue('blockedUsers', blockedUsers);
alert(`사용자 ${userId}의 차단이 해제되었습니다.`);
unregisterUnblockMenu(unblockedUser.userName);
}
}
// 사용자가 이미 차단되어 있는지 확인합니다.
function isUserBlocked(userId) {
return blockedUsers.some(user => user.userId === userId);
}
// 함수: 동적으로 메뉴 등록
function registerUnblockMenu(user) {
// GM_registerMenuCommand로 메뉴를 등록하고 메뉴 ID를 기록
let menuId = GM_registerMenuCommand(`💔 차단 해제 - ${user.userName}`, function() {
unblockUser(user.userId);
});
// 메뉴 ID를 기록
menuIds[user.userName] = menuId;
}
// 함수: 동적으로 메뉴 삭제
function unregisterUnblockMenu(userName) {
// userName을 기반으로 저장된 메뉴 ID를 가져와서 삭제
let menuId = menuIds[userName];
if (menuId) {
GM_unregisterMenuCommand(menuId);
delete menuIds[userName]; // 삭제된 메뉴 ID를 객체에서도 제거
}
}
// 카테고리 목록을 저장합니다.
function saveBlockedCategories() {
GM_setValue('blockedCategories', blockedCategories);
}
// 카테고리를 차단 목록에 추가합니다.
function blockCategory(categoryName, categoryId) {
// 이미 차단된 카테고리인지 확인
if (!isCategoryBlocked(categoryId)) {
blockedCategories.push({ categoryName, categoryId });
saveBlockedCategories();
alert(`카테고리 ${categoryName}(${categoryId})를 차단했습니다.\n차단 해제 메뉴는 템퍼몽키 아이콘을 누르면 있습니다.`);
registerCategoryUnblockMenu({ categoryName, categoryId });
} else {
alert(`카테고리 ${categoryName}(${categoryId})는 이미 차단되어 있습니다.`);
}
}
// 함수: 카테고리 차단 해제
function unblockCategory(categoryId) {
// 차단된 카테고리 목록에서 해당 카테고리 찾기
let unblockedCategory = blockedCategories.find(category => category.categoryId === categoryId);
// 카테고리를 찾았을 때만 차단 해제 및 메뉴 삭제 수행
if (unblockedCategory) {
// 차단된 카테고리 목록에서 해당 카테고리 제거
blockedCategories = blockedCategories.filter(category => category.categoryId !== categoryId);
// 변경된 목록을 저장
GM_setValue('blockedCategories', blockedCategories);
alert(`카테고리 ${categoryId}의 차단이 해제되었습니다.`);
unregisterCategoryUnblockMenu(unblockedCategory.categoryName);
}
}
// 카테고리가 이미 차단되어 있는지 확인합니다.
function isCategoryBlocked(categoryId) {
return blockedCategories.some(category => category.categoryId === categoryId);
}
// 함수: 동적으로 카테고리 메뉴 등록
function registerCategoryUnblockMenu(category) {
// GM_registerMenuCommand로 카테고리 메뉴를 등록하고 메뉴 ID를 기록
let menuId = GM_registerMenuCommand(`💔 카테고리 차단 해제 - ${category.categoryName}`, function() {
unblockCategory(category.categoryId);
});
// 메뉴 ID를 기록
categoryMenuIds[category.categoryName] = menuId;
}
// 함수: 동적으로 카테고리 메뉴 삭제
function unregisterCategoryUnblockMenu(categoryName) {
// categoryName을 기반으로 저장된 메뉴 ID를 가져와서 삭제
let menuId = categoryMenuIds[categoryName];
if (menuId) {
GM_unregisterMenuCommand(menuId);
delete categoryMenuIds[categoryName]; // 삭제된 메뉴 ID를 객체에서도 제거
}
}
// 단어 목록을 저장합니다.
function saveBlockedWords() {
GM_setValue('blockedWords', blockedWords);
}
// 단어를 차단 목록에 추가합니다.
function blockWord(word) {
// 단어의 양쪽 공백 제거
word = word.trim();
// 단어가 두 글자 이상인지 확인
if (word.length < 2) {
alert("단어는 두 글자 이상이어야 합니다.");
return;
}
// 이미 차단된 단어인지 확인
if (!isWordBlocked(word)) {
blockedWords.push(word);
saveBlockedWords();
alert(`단어 "${word}"를 차단했습니다.`);
registerWordUnblockMenu(word);
} else {
alert(`단어 "${word}"는 이미 차단되어 있습니다.`);
}
}
// 함수: 단어 차단 해제
function unblockWord(word) {
// 차단된 단어 목록에서 해당 단어 찾기
let unblockedWord = blockedWords.find(blockedWord => blockedWord === word);
// 단어를 찾았을 때만 차단 해제 및 메뉴 삭제 수행
if (unblockedWord) {
// 차단된 단어 목록에서 해당 단어 제거
blockedWords = blockedWords.filter(blockedWord => blockedWord !== word);
// 변경된 목록을 저장
saveBlockedWords();
alert(`단어 "${word}"의 차단이 해제되었습니다.`);
unregisterWordUnblockMenu(word);
}
}
// 단어가 이미 차단되어 있는지 확인합니다.
function isWordBlocked(word) {
const lowerCaseWord = word.toLowerCase();
return blockedWords.map(word => word.toLowerCase()).includes(lowerCaseWord);
}
// 함수: 동적으로 단어 차단 해제 메뉴 등록
function registerWordUnblockMenu(word) {
// GM_registerMenuCommand로 단어 차단 해제 메뉴를 등록하고 메뉴 ID를 기록
let menuId = GM_registerMenuCommand(`💔 단어 차단 해제 - ${word}`, function() {
unblockWord(word);
});
// 메뉴 ID를 기록
wordMenuIds[word] = menuId;
}
// 함수: 동적으로 단어 차단 해제 메뉴 삭제
function unregisterWordUnblockMenu(word) {
// word를 기반으로 저장된 메뉴 ID를 가져와서 삭제
let menuId = wordMenuIds[word];
if (menuId) {
GM_unregisterMenuCommand(menuId);
delete wordMenuIds[word]; // 삭제된 메뉴 ID를 객체에서도 제거
}
}
function registerMenuBlockingWord() {
// GM 메뉴에 단어 차단 등록 메뉴를 추가합니다.
GM_registerMenuCommand('단어 등록 | 방제에 포함시 차단', function() {
// 사용자에게 차단할 단어 입력을 요청
let word = prompt('차단할 단어 (2자 이상): ');
// 입력한 단어가 있을 때만 처리
if (word) {
blockWord(word);
}
});
}
const desc_order = (selector) => {
const container = document.body.querySelector(selector);
const userElements = container.children;
const categories = [[], [], [], [], [], []];
for (let i = 0; i < userElements.length; i++) {
const user = userElements[i];
const isPin = user.getAttribute('is_pin') === 'Y';
const hasBroadThumbnail = user.hasAttribute('broad_thumbnail');
const isMobilePush = user.getAttribute('is_mobile_push') === 'Y';
const isOffline = user.hasAttribute('is_offline');
const broad_cate_no = user.getAttribute('broad_cate_no');
const isBlocked = blockedCategories.some(b => b.categoryId === broad_cate_no);
if (isPin && hasBroadThumbnail) {
categories[0].push(user); // 1. 고정 + 생방
} else if (isPin) {
categories[1].push(user); // 2. 고정 + 오프라인
} else if (isMobilePush && !isOffline) {
categories[2].push(user); // 3. 알림 켜짐 + 생방
} else if (isBlocked && isBlockedCategorySortingEnabled) {
categories[4].push(user); // 5. 차단된 카테고리 (고정 제외, 알림 켜짐 제외)
} else if (!isMobilePush && !isOffline) {
categories[3].push(user); // 4. 알림 꺼짐 + 생방
} else {
categories[5].push(user); // 6. 그 외
}
}
categories.forEach((category, index) => {
if (index === 5 || selector !== '.users-section.follow') { // 방송국 글이거나 즐찾 채널이 아닌 경우 시청자 많은 순
category.sort(compareWatchers);
} else {
category.sort(isRandomSortEnabled ? stableRandomOrder : compareWatchers)
}
});
container.innerHTML = '';
const fragment = document.createDocumentFragment();
categories.forEach(category => {
category.forEach(user => fragment.appendChild(user));
});
container.appendChild(fragment);
};
const compareWatchers = (a, b) => {
// Get watchers data only once for each element
const watchersA = a.dataset.watchers ? +a.dataset.watchers : 0; // Use dataset for better performance
const watchersB = b.dataset.watchers ? +b.dataset.watchers : 0; // Use dataset for better performance
return watchersB - watchersA; // Sort by watchers
}
const stableRandomOrder = (() => {
// 한 번에 여러 개를 정렬할 때 일관된 랜덤성을 유지하려면, 미리 섞어주는 방식이 좋습니다.
// 이 함수는 내부적으로 shuffle된 index 맵을 사용해서 안정적인 무작위 정렬을 구현합니다.
let randomMap = new WeakMap();
return (a, b) => {
if (!randomMap.has(a)) randomMap.set(a, Math.random());
if (!randomMap.has(b)) randomMap.set(b, Math.random());
return randomMap.get(a) - randomMap.get(b);
};
})();
const makeTopNavbarAndSidebar = (page) => {
// .left_navbar를 찾거나 생성
let leftNavbar = document.body.querySelector('.left_navbar');
if (!leftNavbar) {
leftNavbar = document.createElement('div');
leftNavbar.className = 'left_navbar';
// 페이지의 적절한 위치에 추가
waitForElement('#serviceHeader', function (elementSelector, element) {
element.prepend(leftNavbar);
});
}
const buttonData = [
{ href: 'https://www.sooplive.co.kr/live/all', text: 'LIVE', onClickTarget: '#live > a' },
{ href: 'https://www.sooplive.co.kr/my/favorite', text: 'MY', onClickTarget: '#my > a' },
{ href: 'https://www.sooplive.co.kr/directory/category', text: '탐색', onClickTarget: '#cate > a' },
{ href: 'https://vod.sooplive.co.kr/player/catch', text: '캐치', onClickTarget: '#catch > a' }
];
// 버튼을 미리 만들어 DocumentFragment에 추가
const buttonFragment = document.createDocumentFragment();
buttonData.reverse().forEach(data => {
const newButton = document.createElement('a');
newButton.innerHTML = `${data.text} `;
const isTargetUrl = CURRENT_URL.startsWith("https://www.sooplive.co.kr");
// 이벤트 리스너 함수 정의
const triggerClick = (event) => {
event.preventDefault();
const targetElement = isTargetUrl && data.onClickTarget ? document.querySelector(data.onClickTarget) : null;
if (targetElement) {
targetElement.click(); // 타겟 요소 클릭
} else {
console.warn("타겟 요소를 찾을 수 없음:", data.onClickTarget);
}
};
// MutationObserver 설정: 타겟 요소가 로드될 때까지 기다림
if (isTargetUrl && data.onClickTarget) {
const observer = new MutationObserver((mutations, observer) => {
const targetElement = document.querySelector(data.onClickTarget);
if (targetElement) {
observer.disconnect(); // 요소가 확인되면 Observer 중지
newButton.addEventListener('click', triggerClick);
}
});
observer.observe(document.body, { childList: true, subtree: true });
} else {
// 기본 링크 설정
newButton.href = data.href;
newButton.target = isOpenNewtabEnabled ? "_blank" : "_self";
}
buttonFragment.appendChild(newButton);
});
leftNavbar.appendChild(buttonFragment); // 한 번에 추가
const tooltipContainer = document.createElement('div');
tooltipContainer.classList.add('tooltip-container');
const sidebarClass = isSidebarMinimized ? "min" : "max";
if (page === "main") {
const newHtml = `
`;
const serviceLnbElement = document.getElementById('soop-gnb');
if (serviceLnbElement) {
serviceLnbElement.insertAdjacentHTML('afterend', newHtml);
}
document.body.appendChild(tooltipContainer);
}
if (page === "player") {
const sidebarHtml = `
`;
document.body.insertAdjacentHTML('beforeend', sidebarHtml);
document.body.appendChild(tooltipContainer);
}
}
const createUserElementChzzk = (channel, is_mobile_push) => {
const {
liveTitle: liveTitle,
liveImageUrl: liveImageUrl,
concurrentUserCount: concurrentUserCount,
openDate: openDate,
liveCategoryValue: liveCategoryValue,
liveCategory: liveCategory,
channel: channelInfo,
liveInfo: liveInfo
} = channel;
const userId = channelInfo.channelId;
const playerLink = `https://chzzk.naver.com/live/${channelInfo.channelId}`;
const broadThumbnail = liveImageUrl ? liveImageUrl.split('{type}').join('360')
: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQIHWNgAAAAAgABz8g15QAAAABJRU5ErkJggg==";
const profileImg = channelInfo?.channelImageUrl;
const channelPage = 'https://chzzk.naver.com/'+userId;
const channelName = channelInfo?.channelName;
const emptyimage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQIHWNgAAAAAgABz8g15QAAAABJRU5ErkJggg==';
const userElement = document.createElement('a');
userElement.classList.add('user');
if (isSmallUserLayoutEnabled) userElement.classList.add('small-user-layout');
userElement.setAttribute('href', playerLink);
if (isOpenNewtabEnabled) {
userElement.setAttribute('target', '_blank');
} else {
userElement.setAttribute('target', '_self');
}
userElement.setAttribute('data-watchers', concurrentUserCount ?? liveInfo.concurrentUserCount);
userElement.setAttribute('broad_thumbnail', broadThumbnail);
userElement.setAttribute('tooltip', liveTitle ?? liveInfo.liveTitle);
userElement.setAttribute('user_id', userId);
userElement.setAttribute('broad_start', openDate ?? 'NotAvailable');
userElement.setAttribute('is_mobile_push', is_mobile_push === "Y" ? 'Y' : 'N');
userElement.setAttribute('is_pin', 'N');
const profilePicture = document.createElement('img');
profilePicture.src = profileImg || emptyimage;
profilePicture.setAttribute('loading', 'lazy');
const profileClickHandler =
`
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth === 52) {
if(event.ctrlKey) {
window.open('${playerLink}', '_blank');
return;
}
location.href = '${playerLink}';
} else {
window.open('${channelPage}', '_blank');
}
`;
// 프로필 클릭 & 새 탭 열기: 최소화 시 생방송, 최대화 시 방송국
const profileClickHandlerForNewtab = `
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth === 52) {
window.open('${playerLink}', '_blank');
} else {
window.open('${channelPage}', '_blank');
}
`
profilePicture.setAttribute('onclick', isOpenNewtabEnabled === 1 ?
profileClickHandlerForNewtab :
profileClickHandler
);
profilePicture.setAttribute('onmousedown', `
if (event.button === 1) {
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth !== 52) {
window.open('${channelPage}', '_blank');
}
}
`);
profilePicture.classList.add('profile-picture');
const username = document.createElement('span');
username.classList.add('username');
username.textContent = (is_mobile_push === "Y") ? `🖈${channelName}` : channelName;
username.setAttribute('title', is_mobile_push === "Y" ? '고정됨(알림 받기 켜짐)' : '');
username.title = username.textContent;
const description = document.createElement('span');
description.classList.add('description');
description.textContent = liveCategoryValue ?? liveInfo.liveCategoryValue;
description.title = description.textContent;
userElement.setAttribute('broad_cate_no', liveCategory ?? '');
const watchers = document.createElement('span');
watchers.classList.add('watchers');
const dot = document.createElement('span');
dot.classList.add('dot', 'greendot');
dot.setAttribute('role', 'img');
dot.textContent = '●';
const userCount = addNumberSeparator(concurrentUserCount ?? liveInfo.concurrentUserCount);
const countText = document.createTextNode(userCount);
watchers.append(dot, countText);
userElement.append(profilePicture, username, description, watchers);
return userElement;
}
const createUserElement = (channel, is_mobile_push, is_pin) => {
const {
user_id: userId,
broad_no: broadNo,
total_view_cnt: totalViewCnt,
broad_title: broadTitle,
user_nick: userNick,
broad_start: broadStart,
broad_cate_no: catNo,
category_name: categoryName,
subscription_only: subscriptionOnly
} = channel;
const isSubOnly = Number(subscriptionOnly || 0) > 0;
const playerLink = `https://play.sooplive.co.kr/${userId}/${broadNo}`;
const broadThumbnail = `https://liveimg.sooplive.co.kr/m/${broadNo}`;
const userElement = document.createElement('a');
userElement.classList.add('user');
if (isSmallUserLayoutEnabled) userElement.classList.add('small-user-layout');
userElement.setAttribute('href', playerLink);
if (isOpenNewtabEnabled) {
userElement.setAttribute('target', '_blank');
} else if (isSendLoadBroadEnabled) {
userElement.setAttribute('onclick', `
if(event.ctrlKey || (window.location.href.indexOf('play.sooplive.co.kr') === -1) ) return;
event.preventDefault();
event.stopPropagation();
if (document.body.querySelector('div.loading') && getComputedStyle(document.body.querySelector('div.loading')).display === 'none') {
liveView.playerController.sendLoadBroad('${userId}', ${broadNo});
typeof resetChatData === 'function' && resetChatData();
} else {
location.href = '${playerLink}';
}
`);
} else {
userElement.setAttribute('target', '_self');
}
userElement.setAttribute('data-watchers', totalViewCnt);
userElement.setAttribute('broad_thumbnail', broadThumbnail);
userElement.setAttribute('tooltip', broadTitle);
userElement.setAttribute('user_id', userId);
userElement.setAttribute('broad_start', broadStart);
if (isOpenExternalPlayerFromSidebarEnabled) {
userElement.setAttribute('oncontextmenu', `
event.preventDefault();
event.stopPropagation();
(async () => {
const aid = await getBroadAid2('${userId}', ${broadNo});
if (aid) openHlsStream('${userNick}', 'https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=' + aid);
})();
`);
}
if (is_mobile_push) {
userElement.setAttribute('is_mobile_push', is_mobile_push);
userElement.setAttribute('is_pin', is_pin ? 'Y' : 'N');
}
const profilePicture = document.createElement('img');
const pp_webp = `https://stimg.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.webp`;
const pp_jpg = `https://profile.img.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.jpg`;
profilePicture.src = pp_webp;
profilePicture.setAttribute('loading', 'lazy');
profilePicture.setAttribute('onerror', `this.onerror=null; this.src='${pp_jpg}'`);
const profileClickHandler = isSendLoadBroadEnabled ?
// 프로필 클릭 & 현재 탭 & 빠른 전환
`
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth === 52) {
if (event.ctrlKey) {
window.open('${playerLink}', '_blank');
return;
}
if (document.body.querySelector('div.loading') && getComputedStyle(document.body.querySelector('div.loading')).display === 'none') {
liveView.playerController.sendLoadBroad('${userId}', ${broadNo});
typeof resetChatData === 'function' && resetChatData();
} else {
location.href = '${playerLink}';
}
} else {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
`
:
// 프로필 클릭 & 현재 탭 & 새로고침
`
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth === 52) {
if(event.ctrlKey) {
window.open('${playerLink}', '_blank');
return;
}
location.href = '${playerLink}';
} else {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
`;
// 프로필 클릭 & 새 탭 열기: 최소화 시 생방송, 최대화 시 방송국
const profileClickHandlerForNewtab = `
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth === 52) {
window.open('${playerLink}', '_blank');
} else {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
`
profilePicture.setAttribute('onclick', isOpenNewtabEnabled === 1 ?
profileClickHandlerForNewtab :
profileClickHandler
);
profilePicture.setAttribute('onmousedown', `
if (event.button === 1) {
event.preventDefault();
event.stopPropagation();
if (document.getElementById('sidebar').offsetWidth !== 52) {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
}
`);
profilePicture.classList.add('profile-picture');
const username = document.createElement('span');
username.classList.add('username');
username.textContent = (is_pin || is_mobile_push === "Y") ? `🖈${userNick}` : userNick;
username.setAttribute('title', is_pin ? '고정됨(상단 고정 켜짐)' : is_mobile_push === "Y" ? '고정됨(알림 받기 켜짐)' : '');
username.title = username.textContent;
const description = document.createElement('span');
description.classList.add('description');
description.textContent = categoryName || getCategoryName(catNo);
description.title = description.textContent;
userElement.setAttribute('broad_cate_no', catNo);
const watchers = document.createElement('span');
watchers.classList.add('watchers');
// ●
const dot = document.createElement('span');
dot.classList.add('dot');
dot.setAttribute('role', 'img');
dot.textContent = isSubOnly ? '★' : '●';
dot.title = isSubOnly ? '구독자 전용' : '';
const viewCountText = document.createTextNode(addNumberSeparator(totalViewCnt));
watchers.append(dot, viewCountText);
userElement.append(profilePicture, username, description, watchers);
return userElement;
};
const createUserElement_vod = (channel) => {
const {
user_id: userId,
title_no: broadNo,
view_cnt: totalViewCnt,
title: broadTitle,
user_nick: userNick,
vod_duration: vodDuration,
reg_date: regDate,
thumbnail,
} = channel;
const playerLink = `https://vod.sooplive.co.kr/player/${broadNo}`;
const broadThumbnail = thumbnail.replace("http://", "https://");
const userElement = document.createElement('a');
userElement.classList.add('user');
if (isSmallUserLayoutEnabled) userElement.classList.add('small-user-layout');
userElement.setAttribute('href', playerLink);
if (isOpenNewtabEnabled) {
userElement.setAttribute('target', '_blank');
}
userElement.setAttribute('data-watchers', totalViewCnt);
userElement.setAttribute('broad_thumbnail', broadThumbnail);
userElement.setAttribute('tooltip', broadTitle);
userElement.setAttribute('user_id', userId);
userElement.setAttribute('vod_duration', vodDuration);
const profilePicture = document.createElement('img');
const pp_webp = `https://stimg.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.webp`;
const pp_jpg = `https://profile.img.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.jpg`;
profilePicture.src = pp_webp;
profilePicture.setAttribute('loading', 'lazy');
profilePicture.setAttribute('onerror', `this.onerror=null; this.src='${pp_jpg}'`);
const profileClickHandler = `
event.preventDefault();
event.stopPropagation();
const sidebarWidth = document.getElementById('sidebar').offsetWidth;
if (sidebarWidth === 52) {
if (event.ctrlKey) {
window.open('${playerLink}', '_blank');
return;
}
location.href = '${playerLink}';
} else {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
`;
profilePicture.setAttribute('onclick', isOpenNewtabEnabled ?
`event.preventDefault(); event.stopPropagation(); window.open('${playerLink}', '_blank');` :
profileClickHandler
);
profilePicture.setAttribute('onmousedown', `
if (event.button === 1) {
if (document.getElementById('sidebar').offsetWidth !== 52) {
event.preventDefault();
event.stopPropagation();
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
}
`);
profilePicture.classList.add('profile-picture', 'profile-grayscale');
const username = document.createElement('span');
username.classList.add('username');
username.textContent = userNick;
username.title = username.textContent;
const description = document.createElement('span');
description.classList.add('description');
description.textContent = vodDuration;
description.title = vodDuration;
const watchers = document.createElement('span');
watchers.classList.add('watchers');
watchers.textContent = timeSince(regDate);
userElement.append(profilePicture, username, description, watchers);
return userElement;
};
const createUserElement_offline = (channel, isFeeditem) => {
const {
user_id: userId,
total_view_cnt: totalViewCnt,
user_nick: userNick,
is_mobile_push: isMobilePush,
is_pin: isPin,
reg_date_human: reg_date_human,
} = channel;
const playerLink = isFeeditem ? isFeeditem.url : `https://ch.sooplive.co.kr/${userId}`;
const isOffline = "Y";
const feedTimestamp = isFeeditem ? isFeeditem.reg_timestamp : false;
const feedRegDate = isFeeditem ? isFeeditem.reg_date : false;
const userElement = document.createElement('a');
userElement.classList.add('user');
userElement.classList.add('user-offline');
if (isSmallUserLayoutEnabled) userElement.classList.add('small-user-layout');
userElement.setAttribute('href', playerLink);
userElement.setAttribute('target', '_blank');
userElement.setAttribute('broad_start', feedRegDate || '');
userElement.setAttribute('data-watchers', isFeeditem ? feedTimestamp : totalViewCnt);
userElement.setAttribute('user_id', userId);
if (isFeeditem) {
if (isFeeditem.photo_cnt) {
userElement.setAttribute('broad_thumbnail', `https:${isFeeditem.photos[0].url}`);
} else {
userElement.setAttribute('data-tooltip-listener', 'false');
}
userElement.setAttribute('tooltip', isFeeditem.title_name);
} else {
userElement.setAttribute('data-tooltip-listener', 'false');
}
if (isMobilePush) {
userElement.setAttribute('is_mobile_push', isMobilePush);
userElement.setAttribute('is_pin', isPin ? 'Y' : 'N');
}
userElement.setAttribute('is_offline', isOffline);
const profilePicture = document.createElement('img');
const pp_webp = `https://stimg.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.webp`;
const pp_jpg = `https://profile.img.sooplive.co.kr/LOGO/${userId.slice(0, 2)}/${userId}/m/${userId}.jpg`;
profilePicture.src = pp_webp;
profilePicture.setAttribute('loading', 'lazy');
profilePicture.setAttribute('onerror', `this.onerror=null; this.src='${pp_jpg}'`);
const profileClickHandler = `
event.preventDefault();
event.stopPropagation();
const sidebarWidth = document.getElementById('sidebar').offsetWidth;
if (sidebarWidth === 52) {
window.open('${playerLink}', '_blank');
} else {
window.open('https://ch.sooplive.co.kr/${userId}', '_blank');
}
`;
profilePicture.setAttribute('onclick', profileClickHandler);
profilePicture.classList.add('profile-picture', 'profile-grayscale');
const username = document.createElement('span');
username.classList.add('username');
username.textContent = isPin ? `🖈${userNick}` : userNick;
username.title = username.textContent;
if (isPin) username.setAttribute('title', '고정됨(상단 고정 켜짐)');
const description = document.createElement('span');
description.classList.add('description');
description.textContent = isFeeditem ? isFeeditem.title_name : '';
description.title = isFeeditem ? isFeeditem.title_name : '';
const watchers = document.createElement('span');
watchers.classList.add('watchers');
if (isFeeditem) {
// 피드 아이템이 있으면 시간 표시
watchers.textContent = isFeeditem.reg_date_human;
} else {
// 오프라인 상태일 경우 ● + "오프라인"
const dot = document.createElement('span');
dot.classList.add('dot', 'profile-grayscale');
dot.setAttribute('role', 'img');
dot.textContent = '●';
const offlineText = document.createTextNode('오프라인');
watchers.append(dot, offlineText);
}
userElement.append(profilePicture, username, description, watchers);
return userElement;
};
const isUserInFollowSection = (userid) => {
const followUsers = document.body.querySelectorAll('.users-section.follow .user');
// 유저가 포함되어 있는지 확인
return Array.from(followUsers).some(user => user.getAttribute('user_id') === userid);
}
const insertFoldButton = () => {
const foldButton = `
`;
const webplayer_scroll = document.getElementById('webplayer_scroll') || document.getElementById('list-container');
const serviceLnbElement = document.getElementById('sidebar');
if (serviceLnbElement) {
serviceLnbElement.insertAdjacentHTML('beforeend', foldButton);
// 클릭 이벤트 리스너를 정의
const toggleSidebar = () => {
isSidebarMinimized = !isSidebarMinimized;
// max 클래스가 있으면 제거하고 min 클래스 추가
if (serviceLnbElement.classList.toggle('max')) {
serviceLnbElement.classList.remove('min');
webplayer_scroll.style.left = '240px';
} else {
serviceLnbElement.classList.remove('max');
serviceLnbElement.classList.add('min');
webplayer_scroll.style.left = '52px';
}
// isSidebarMinimized 값을 저장
GM_setValue("isSidebarMinimized", isSidebarMinimized ? 1 : 0);
};
// 버튼에 클릭 이벤트 리스너 추가
const buttons = serviceLnbElement.querySelectorAll('.button-fold-sidebar, .button-unfold-sidebar');
for (const button of buttons) {
button.addEventListener('click', toggleSidebar);
}
}
};
const fetchBroadList = async (url, timeout) => {
const CACHE_EXPIRY_MS = 45 * 1000; // 45초
const cacheKey = `fetchCache_${encodeURIComponent(url)}`;
// 1. LocalStorage 확인
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const { timestamp, data } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_EXPIRY_MS) {
return data;
}
} catch (e) {
console.warn(url, 'Cache parse error in LocalStorage, ignoring.');
}
}
// 2. GM 저장소 확인
const gmCached = await GM_getValue(cacheKey, null);
if (gmCached) {
try {
const { timestamp, data } = JSON.parse(gmCached);
if (Date.now() - timestamp < CACHE_EXPIRY_MS) {
// LocalStorage에도 저장해두기 (빠른 재사용을 위해)
localStorage.setItem(cacheKey, gmCached);
return data;
}
} catch (e) {
console.warn(url, 'Cache parse error in GM storage, ignoring.');
}
}
// 3. 요청 수행
return new Promise((resolve) => {
let timeoutId;
if (timeout) {
timeoutId = setTimeout(() => {
console.error(url, `Request timed out after ${timeout} ms`);
resolve([]);
}, timeout);
}
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Content-Type': 'application/json'
},
onload: async (response) => {
if (timeoutId) clearTimeout(timeoutId);
try {
if (response.status >= 200 && response.status < 300) {
const jsonResponse = JSON.parse(response.responseText);
// 에러 응답 처리
if (jsonResponse?.RESULT === -1 || (jsonResponse?.code && jsonResponse.code < 0)) {
console.error(url, `API Error (Login Required or other): ${jsonResponse.MSG || jsonResponse.message}`);
localStorage.removeItem(cacheKey);
await GM_setValue(cacheKey, ""); // GM 저장소도 삭제 (빈 문자열)
resolve([]);
} else {
const cacheData = JSON.stringify({
timestamp: Date.now(),
data: jsonResponse
});
// LocalStorage + GM 저장소에 저장
localStorage.setItem(cacheKey, cacheData);
await GM_setValue(cacheKey, cacheData);
resolve(jsonResponse);
}
} else if (response.status === 401) {
console.error(url, "Unauthorized: 401 error - possibly invalid credentials");
resolve([]);
} else {
console.error(url, `Error: ${response.status}`);
resolve([]);
}
} catch (error) {
console.error(url, "Parsing error: ", error);
resolve([]);
}
},
onerror: (error) => {
if (timeoutId) clearTimeout(timeoutId);
console.error(url, "Request error: " + error.message);
resolve([]);
}
});
});
};
const insertTopChannels = async (update) => {
const topIcon = IS_DARK_MODE ?
` `
: ` `;
const newHtml = `
`;
const serviceLnbElement = document.getElementById('sidebar');
if (serviceLnbElement && !update) {
serviceLnbElement.insertAdjacentHTML('beforeend', newHtml);
}
const openList = document.body.querySelectorAll('.users-section.top .user:not(.show-more)').length;
try {
const [hiddenBjList, broadListResponse] = await Promise.all([getHiddenbjList(), fetchBroadList('https://live.sooplive.co.kr/api/main_broad_list_api.php?selectType=action&orderType=view_cnt&pageNo=1&lang=ko_KR')]);
HIDDEN_BJ_LIST.length = 0;
HIDDEN_BJ_LIST.push(...hiddenBjList);
const channels = broadListResponse.broad;
const usersSection = document.querySelector('.users-section.top');
let temp_html = '';
channels.forEach(channel => {
const isBlocked = blockedWords.some(word => channel.broad_title.toLowerCase().includes(word.toLowerCase())) ||
HIDDEN_BJ_LIST.includes(channel.user_id) || isCategoryBlocked(channel.broad_cate_no) || isUserBlocked(channel.user_id);
if (!isBlocked) {
const userElement = createUserElement(channel, 0, 0);
temp_html += userElement.outerHTML;
}
});
if (isChzzkTopChannelsEnabled) {
const chzzkTopChannelsData = await fetchBroadList('https://api.chzzk.naver.com/service/v1/lives?size=50&sortType=POPULAR');
const chzzkChannels = chzzkTopChannelsData.content.data;
chzzkChannels.forEach(channel => {
const userElement = createUserElementChzzk(channel, 0);
temp_html += userElement.outerHTML;
});
}
if (update) {
updateElementWithContent(usersSection, temp_html);
} else {
usersSection.insertAdjacentHTML('beforeend', temp_html);
}
desc_order('.users-section.top');
showMore('.users-section.top', 'toggleButton3', update ? openList : displayTop, displayTop);
makeThumbnailTooltip();
} catch (error) {
console.error("Error:", error);
}
};
const extractFollowUserIds = (response) => {
allFollowUserIds = response.data.map(item => item.user_id); // 모든 user_id를 추출하여 전역 배열에 저장
GM_setValue("allFollowUserIds", allFollowUserIds);
};
const insertFavoriteChannels = async (update) => {
let followingListSoop;
let followingListChzzk;
if (isChzzkFollowChannelsEnabled) {
[followingListSoop, followingListChzzk] = await Promise.all([fetchBroadList('https://myapi.sooplive.co.kr/api/favorite'), fetchBroadList('https://api.chzzk.naver.com/service/v1/channels/followings/live',3000)]);
} else {
followingListSoop = await fetchBroadList('https://myapi.sooplive.co.kr/api/favorite');
}
const isSooploggedIn = followingListSoop?.data;
const isChzzkloggedIn = followingListChzzk?.code === 200;
if (!isSooploggedIn && !isChzzkloggedIn) {
return;
}
if (isSooploggedIn){
extractFollowUserIds(followingListSoop);
}
const followIcon = IS_DARK_MODE ?
` `
: ` `;
if (!update) {
const newHtml = `
`;
const serviceLnbElement = document.getElementById('sidebar');
serviceLnbElement?.insertAdjacentHTML('beforeend', newHtml);
}
const openList = document.body.querySelectorAll('.users-section.follow .user:not(.show-more)').length;
try {
let tempHtmlArray = [];
const usersSection = document.querySelector('.users-section.follow');
if (isSooploggedIn){
const feedData = await getStationFeed(); // 피드 데이터를 비동기적으로 가져옴
const feedUserIdSet = new Set(feedData.map(feedItem => feedItem.station_user_id));
tempHtmlArray = followingListSoop.data.reduce((acc, item) => {
const { is_live, user_id, broad_info } = item;
const is_mobile_push = isPinnedStreamWithNotificationEnabled === 1 ? item.is_mobile_push : "N";
const is_pin = isPinnedStreamWithPinEnabled === 1 ? item.is_pin : false;
if (is_live) { // 생방송 중
broad_info.forEach(channel => {
const userElement = createUserElement(channel, is_mobile_push, is_pin);
acc.push(userElement.outerHTML);
});
} else if (feedUserIdSet.has(user_id)) { // 비방 + 방송국 새 글
const feedItems = feedData.filter(feedItem => feedItem.station_user_id === user_id);
feedItems.forEach(feedItem => {
if (feedItem?.reg_timestamp && checkIfTimeover(feedItem.reg_timestamp)) {
return; // 타임오버된 경우 넘어감
}
const userElement = createUserElement_offline(item, feedItem);
acc.push(userElement.outerHTML);
});
} else if (is_pin && !isPinnedOnlineOnlyEnabled ) { // 비방 + 상단 고정 + 설정값
const userElement = createUserElement_offline(item, null);
acc.push(userElement.outerHTML);
}
return acc;
}, []);
}
if (isChzzkloggedIn){
// 기존의 tempHtmlArray에 추가하도록 변경
followingListChzzk?.content?.followingList.forEach(item => {
const is_mobile_push = isPinnedStreamWithNotificationEnabled === 1 ? (item?.channel?.personalData?.following?.notification === true ? "Y" : "N") : "N";
const userElement = createUserElementChzzk(item, is_mobile_push);
tempHtmlArray.push(userElement.outerHTML); // 기존 배열에 추가
});
}
if (update) {
updateElementWithContent(usersSection, tempHtmlArray.join(''));
} else {
usersSection.insertAdjacentHTML('beforeend', tempHtmlArray.join(''));
}
desc_order('.users-section.follow');
showMore('.users-section.follow', 'toggleButton2', update ? openList : displayFollow, displayFollow);
makeThumbnailTooltip();
} catch (error) {
console.error("Error in insertFavoriteChannels:", error);
}
}
const waitForNonEmptyArray = async () => {
const timeout = new Promise((resolve) =>
setTimeout(() => resolve([]), 3000) // 3초 후 빈 배열 반환
);
const checkArray = (async () => {
while (allFollowUserIds.length === 0) {
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms 대기
}
return allFollowUserIds;
})();
return Promise.race([timeout, checkArray]);
};
const insertMyplusChannels = async (update) => {
try {
const response = await fetchBroadList('https://live.sooplive.co.kr/api/myplus/preferbjLiveVodController.php?nInitCnt=6&szRelationType=C');
if (!response || typeof response !== 'object' || response.RESULT === -1 || !response.DATA) {
return;
}
const { DATA } = response;
const myplusIcon = IS_DARK_MODE ?
` `
: ` `;
if (!update) {
const newHtml = `
추천 채널
${myplusIcon}
추천 VOD
${myplusIcon}
`;
document.getElementById('sidebar')?.insertAdjacentHTML('beforeend', newHtml);
}
const openList = document.querySelectorAll('.users-section.myplus .user:not(.show-more)').length;
const openListvod = document.querySelectorAll('.users-section.myplusvod .user:not(.show-more)').length;
const { live_list: channels, vod_list: vods } = DATA;
const usersSection = document.querySelector('.users-section.myplus');
const usersSection_vod = document.querySelector('.users-section.myplusvod');
const tempHtmlArray = [];
const tempHtmlVodArray = [];
const addChannelElements = (channelList, isVod = false) => {
for (const channel of channelList) {
const isWordBlocked = channel.broad_title &&
blockedWords.some(word => channel.broad_title.toLowerCase().includes(word.toLowerCase()));
// 조건 추가: allFollowUserIds와 channel.user_id 비교
if (
allFollowUserIds.includes(channel.user_id) && // allFollowUserIds에 user_id가 포함된 경우
!isVod && // isVod가 false일 때
isDuplicateRemovalEnabled // 중복 제거 기능이 활성화되어 있을 때
) {
continue; // 조건이 충족되면 다음 루프 반복
}
if (
isCategoryBlocked(isVod ? channel.category : channel.broad_cate_no) ||
isUserBlocked(channel.user_id) ||
isWordBlocked ||
(update && isDuplicateRemovalEnabled && isUserInFollowSection(channel.user_id))
) {
continue; // 다른 조건에 따라 건너뛰기
}
const userElement = isVod ? createUserElement_vod(channel) : createUserElement(channel, 0, 0);
(isVod ? tempHtmlVodArray : tempHtmlArray).push(userElement.outerHTML);
}
};
if (isDuplicateRemovalEnabled && displayFollow) await waitForNonEmptyArray();
addChannelElements(channels);
addChannelElements(vods, true);
if (update) {
updateElementWithContent(usersSection, tempHtmlArray.join(''));
updateElementWithContent(usersSection_vod, tempHtmlVodArray.join(''));
} else {
usersSection.insertAdjacentHTML('beforeend', tempHtmlArray.join(''));
usersSection_vod.insertAdjacentHTML('beforeend', tempHtmlVodArray.join(''));
}
makeThumbnailTooltip();
if (!myplusOrder) {
desc_order('.users-section.myplus');
}
const showMoreHandler = () => {
showMore('.users-section.myplus', 'toggleButton', update ? openList : displayMyplus, displayMyplus);
showMore('.users-section.myplusvod', 'toggleButton4', update ? openListvod : displayMyplusvod, displayMyplusvod);
};
showMoreHandler();
} catch (error) {
console.error("Error fetching or processing data:", error);
}
};
const makeThumbnailTooltip = () => {
try {
const elements = document.querySelectorAll('#sidebar a.user');
const tooltipContainer = document.querySelector('.tooltip-container');
const sidebar = document.getElementById('sidebar');
const hoverTimeouts = new Map();
elements.forEach(element => {
const isOffline = element.getAttribute('data-tooltip-listener') === 'false';
if (isOffline) return;
const hasEventListener = element.getAttribute('data-tooltip-listener') === 'true';
if (!hasEventListener) {
element.addEventListener('mouseenter', (e) => {
const uniqueId = `tooltip-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
element.setAttribute('data-hover-tooltip-id', uniqueId);
const timeoutId = setTimeout(() => {
if (element.matches(':hover') && element.getAttribute('data-hover-tooltip-id') === uniqueId) {
showTooltip(element, uniqueId);
}
}, 20);
hoverTimeouts.set(element, timeoutId);
});
element.addEventListener('mouseleave', (e) => {
element.removeAttribute('data-hover-tooltip-id');
const timeoutId = hoverTimeouts.get(element);
if (timeoutId) {
clearTimeout(timeoutId);
hoverTimeouts.delete(element);
}
const to = e.relatedTarget;
const isGoingToAnotherElement = Array.from(elements).some(el => {
const isOffline = el.getAttribute('data-tooltip-listener') === 'false';
return el !== element && el.contains(to) && !isOffline;
});
if (!isGoingToAnotherElement) {
tooltipContainer.classList.remove('visible');
tooltipContainer.removeAttribute('data-tooltip-id');
tooltipContainer.innerHTML = ''; // 초기화
}
});
window.addEventListener('mouseout', (e) => {
if (!e.relatedTarget && !e.toElement) {
tooltipContainer.classList.remove('visible');
tooltipContainer.innerHTML = '';
}
});
element.setAttribute('data-tooltip-listener', 'true');
}
});
async function showTooltip(element, uniqueId) {
// hover 중인지 다시 검사
if (element.getAttribute('data-hover-tooltip-id') !== uniqueId) return;
tooltipContainer.setAttribute('data-tooltip-id', uniqueId);
const topBarHeight = document.getElementById('serviceHeader')?.offsetHeight ?? 0;
const isScreenMode = document.body.classList.contains('screen_mode');
const { left: elementX, top: elementY } = element.getBoundingClientRect();
const offsetX = elementX + sidebar.offsetWidth;
const offsetY = Math.max(elementY - 260, isScreenMode ? 0 : topBarHeight);
let imgSrc = element.getAttribute('broad_thumbnail');
const broadTitle = element.getAttribute('tooltip');
let broadStart = element.getAttribute('broad_start');
const vodDuration = element.getAttribute('vod_duration');
const randomTimeCode = Date.now();
const userId = element.getAttribute('user_id');
if (broadStart === "NotAvailable") {
try {
const getThumbnailJson = await fetchBroadList(`https://api.chzzk.naver.com/service/v1/channels/${userId}/data?fields=topExposedVideos`);
if (getThumbnailJson?.code === 200) {
const topExposedVideos = getThumbnailJson.content?.topExposedVideos;
if (topExposedVideos?.openLive?.liveImageUrl) {
const newThumbnail = topExposedVideos.openLive.liveImageUrl.split('{type}').join('360');
const newBroadStart = topExposedVideos.openLive.openDate;
if (
tooltipContainer.getAttribute('data-tooltip-id') === uniqueId &&
element.getAttribute('data-hover-tooltip-id') === uniqueId
) {
element.setAttribute('broad_thumbnail', newThumbnail);
element.setAttribute('broad_start', newBroadStart);
imgSrc = newThumbnail;
broadStart = newBroadStart;
}
}
}
} catch (error) {
console.error("Error in fetching thumbnail:", error);
}
}
if (element.getAttribute('data-hover-tooltip-id') !== uniqueId) return;
// 방송 시간 && 이미지 && !게시판이미지
if (broadStart && imgSrc?.startsWith("http") && !imgSrc?.startsWith('https://stimg.')) {
imgSrc += `?${Math.floor(randomTimeCode / 10000)}`;
}
let durationText = broadStart
? getElapsedTime(broadStart, "HH:MM")
: vodDuration;
let tooltipText = '';
if (sidebar.offsetWidth === 52) {
const username = element.querySelector('span.username')?.textContent ?? '';
const description = element.querySelector('span.description')?.textContent ?? '';
let watchers = element.querySelector('span.watchers')?.textContent ?? '';
watchers = watchers.replace('●', '').trim();
tooltipText = `${username} · ${description} · ${watchers} ${broadTitle}`;
} else {
tooltipText = broadTitle;
}
const isTooltipVisible = tooltipContainer.classList.contains('visible');
const isSameTooltip = tooltipContainer.getAttribute('data-tooltip-id') === uniqueId;
if (isTooltipVisible && isSameTooltip) {
const imgEl = tooltipContainer.querySelector('img');
if (imgEl) imgEl.src = imgSrc;
else {
const newImg = document.createElement('img');
newImg.src = imgSrc;
tooltipContainer.prepend(newImg);
}
const durationOverlay = tooltipContainer.querySelector('.duration-overlay');
if (durationOverlay) {
durationOverlay.textContent = durationText;
} else if (durationText) {
const newOverlay = document.createElement('div');
newOverlay.className = 'duration-overlay';
newOverlay.textContent = durationText;
tooltipContainer.appendChild(newOverlay);
}
const textEl = tooltipContainer.querySelector('.tooltiptext');
if (textEl) {
textEl.innerHTML = tooltipText;
} else {
const newText = document.createElement('div');
newText.className = 'tooltiptext';
newText.innerHTML = tooltipText;
tooltipContainer.appendChild(newText);
}
} else {
let tooltipContent = ` `;
if (durationText) {
tooltipContent += `${durationText}
`;
}
tooltipContent += `${tooltipText}
`;
tooltipContainer.innerHTML = tooltipContent;
}
Object.assign(tooltipContainer.style, {
left: `${offsetX}px`,
top: `${offsetY}px`
});
tooltipContainer.classList.add('visible');
}
} catch (error) {
console.error('makeThumbnailTooltip 함수에서 오류가 발생했습니다:', error);
}
};
const showMore = (containerSelector, buttonId, n, fixed_n) => {
const userContainer = document.body.querySelector(containerSelector);
const users = Array.from(userContainer?.querySelectorAll('.user') || []);
const displayPerClick = 10;
// n보다 목록이 적으면 함수를 끝낸다
if (users.length <= fixed_n) return false;
// n개를 넘는 모든 요소를 숨긴다
users.slice(n).forEach(user => user.classList.add('show-more'));
const toggleButton = document.createElement('button');
toggleButton.textContent = users.length > n ? `더 보기 (${users.length - n})` : '접기';
toggleButton.id = buttonId;
toggleButton.title = "우클릭시 접기(초기화)";
userContainer.appendChild(toggleButton);
toggleButton.addEventListener('click', () => {
const hiddenUsers = users.filter(user => user.classList.contains('show-more'));
const hiddenCount = hiddenUsers.length;
if (hiddenCount > 0) {
hiddenUsers.slice(0, displayPerClick).forEach(user => user.classList.remove('show-more'));
const remainingHidden = hiddenUsers.length - displayPerClick;
toggleButton.textContent = remainingHidden > 0 ? `더 보기 (${remainingHidden})` : '접기';
} else {
users.slice(fixed_n).forEach(user => user.classList.add('show-more'));
toggleButton.textContent = `더 보기 (${users.length - fixed_n})`;
}
});
toggleButton.addEventListener('contextmenu', event => {
event.preventDefault();
users.slice(fixed_n).forEach(user => user.classList.add('show-more'));
toggleButton.textContent = `더 보기 (${users.length - fixed_n})`;
});
}
const generateBroadcastElements = async (update) => {
//console.log(`방송 목록 갱신: ${new Date().toLocaleString()}`);
try {
if (displayFollow) insertFavoriteChannels(update);
if (displayTop) insertTopChannels(update);
if (displayMyplus || displayMyplusvod) insertMyplusChannels(update);
} catch (error) {
console.error('Error:', error);
}
}
const addModalSettings = () => {
const openModalBtn = document.createElement("div");
openModalBtn.setAttribute("id", "openModalBtn");
const link = document.createElement("button");
link.setAttribute("class", "btn-settings-ui");
openModalBtn.appendChild(link);
const serviceUtilDiv = document.body.querySelector("div.serviceUtil");
serviceUtilDiv.prepend(openModalBtn);
// 모달 컨텐츠를 담고 있는 HTML 문자열
const modalContentHTML = `
`;
// 모달 컨텐츠를 body에 삽입
document.body.insertAdjacentHTML("beforeend", modalContentHTML);
// 모달 열기 버튼에 이벤트 리스너 추가
let isFirstClick = true; // 첫 클릭 여부를 저장하는 변수
openModalBtn.addEventListener("click", () => {
// 모달을 표시
document.getElementById("myModal").style.display = "block";
// 첫 클릭인 경우에만 updateSettingsData 호출
if (isFirstClick) {
updateSettingsData();
isFirstClick = false; // 첫 클릭 후에는 false로 변경
}
});
// 모달 닫기 버튼에 이벤트 리스너 추가
const closeModalBtn = document.body.querySelector(".myModalClose");
closeModalBtn.addEventListener("click", () => {
// 모달을 숨김
const modal = document.getElementById("myModal");
if (modal) {
modal.style.display = "none";
}
});
// 모달 외부를 클릭했을 때 닫기
document.getElementById("myModal").addEventListener("click", (event) => {
const modalContent = document.querySelector('div.modal-content');
const modal = document.getElementById("myModal");
// 모달 콘텐츠가 아닌 곳을 클릭한 경우에만 모달 닫기
if (modal && !modalContent.contains(event.target)) {
modal.style.display = "none";
}
});
}
const updateSettingsData = () => {
const setCheckboxAndSaveValue = (elementId, storageVariable, storageKey) => {
const checkbox = document.getElementById(elementId);
// elementId가 유효한 경우에만 체크박스를 설정
if (checkbox) {
checkbox.checked = (storageVariable === 1);
checkbox.addEventListener("change", (event) => {
GM_setValue(storageKey, event.target.checked ? 1 : 0);
storageVariable = event.target.checked ? 1 : 0;
});
} else {
console.warn(`Checkbox with id "${elementId}" not found.`);
}
}
// 함수를 사용하여 각 체크박스를 설정하고 값을 저장합니다.
setCheckboxAndSaveValue("fixFixedChannel", isPinnedStreamWithPinEnabled, "isPinnedStreamWithPinEnabled");
setCheckboxAndSaveValue("fixNotificationChannel", isPinnedStreamWithNotificationEnabled, "isPinnedStreamWithNotificationEnabled");
setCheckboxAndSaveValue("showBufferTime", isRemainingBufferTimeEnabled, "isRemainingBufferTimeEnabled");
setCheckboxAndSaveValue("mutedInactiveTabs", isAutoChangeMuteEnabled, "isAutoChangeMuteEnabled");
setCheckboxAndSaveValue("switchAutoChangeQuality", isAutoChangeQualityEnabled, "isAutoChangeQualityEnabled");
setCheckboxAndSaveValue("switchNo1440p", isNo1440pEnabled, "isNo1440pEnabled");
setCheckboxAndSaveValue("popularChannelsFirst", myplusPosition, "myplusPosition");
setCheckboxAndSaveValue("mpSortByViewers", myplusOrder, "myplusOrder");
setCheckboxAndSaveValue("removeDuplicates", isDuplicateRemovalEnabled, "isDuplicateRemovalEnabled");
setCheckboxAndSaveValue("openInNewTab", isOpenNewtabEnabled, "isOpenNewtabEnabled");
setCheckboxAndSaveValue("mouseOverSideBar", showSidebarOnScreenMode, "showSidebarOnScreenMode");
setCheckboxAndSaveValue("switchShowSidebarOnScreenModeAlways", showSidebarOnScreenModeAlways, "showSidebarOnScreenModeAlways");
setCheckboxAndSaveValue("chatPosition", isBottomChatEnabled, "isBottomChatEnabled");
setCheckboxAndSaveValue("showPauseButton", isMakePauseButtonEnabled, "isMakePauseButtonEnabled");
setCheckboxAndSaveValue("switchCaptureButton", isCaptureButtonEnabled, "isCaptureButtonEnabled");
setCheckboxAndSaveValue("switchStreamDownload", isStreamDownloadEnabled, "isStreamDownloadEnabled");
setCheckboxAndSaveValue("switchSharpmodeShortcut", isMakeSharpModeShortcutEnabled, "isMakeSharpModeShortcutEnabled");
setCheckboxAndSaveValue("switchLLShortcut", isMakeLowLatencyShortcutEnabled, "isMakeLowLatencyShortcutEnabled");
setCheckboxAndSaveValue("sendLoadBroadCheck", isSendLoadBroadEnabled, "isSendLoadBroadEnabled");
setCheckboxAndSaveValue("selectBestQuality", isSelectBestQualityEnabled, "isSelectBestQualityEnabled");
setCheckboxAndSaveValue("selectHideSupporterBadge", isHideSupporterBadgeEnabled, "isHideSupporterBadgeEnabled");
setCheckboxAndSaveValue("selectHideFanBadge", isHideFanBadgeEnabled, "isHideFanBadgeEnabled");
setCheckboxAndSaveValue("selectHideSubBadge", isHideSubBadgeEnabled, "isHideSubBadgeEnabled");
setCheckboxAndSaveValue("selectHideVIPBadge", isHideVIPBadgeEnabled, "isHideVIPBadgeEnabled");
setCheckboxAndSaveValue("selectHideMngrBadge", isHideManagerBadgeEnabled, "isHideManagerBadgeEnabled");
setCheckboxAndSaveValue("selectHideStreamerBadge", isHideStreamerBadgeEnabled, "isHideStreamerBadgeEnabled");
setCheckboxAndSaveValue("selectBlockWords", isBlockWordsEnabled, "isBlockWordsEnabled");
setCheckboxAndSaveValue("useInterFont", isChangeFontEnabled, "isChangeFontEnabled");
setCheckboxAndSaveValue("autoClaimGem", isAutoClaimGemEnabled, "isAutoClaimGemEnabled");
setCheckboxAndSaveValue("switchVideoSkipHandler", isVideoSkipHandlerEnabled, "isVideoSkipHandlerEnabled");
setCheckboxAndSaveValue("switchSmallUserLayout", isSmallUserLayoutEnabled, "isSmallUserLayoutEnabled");
setCheckboxAndSaveValue("switchChannelFeed", isChannelFeedEnabled, "isChannelFeedEnabled");
setCheckboxAndSaveValue("switchCustomSidebar", isCustomSidebarEnabled, "isCustomSidebarEnabled");
setCheckboxAndSaveValue("switchRemoveCarousel", isRemoveCarouselEnabled, "isRemoveCarouselEnabled");
setCheckboxAndSaveValue("switchDocumentTitleUpdate", isDocumentTitleUpdateEnabled, "isDocumentTitleUpdateEnabled");
setCheckboxAndSaveValue("switchRemoveRedistributionTag", isRemoveRedistributionTagEnabled, "isRemoveRedistributionTagEnabled");
setCheckboxAndSaveValue("switchRemoveWatchLaterButton", isRemoveWatchLaterButtonEnabled, "isRemoveWatchLaterButtonEnabled");
setCheckboxAndSaveValue("switchBroadTitleTextEllipsis", isBroadTitleTextEllipsisEnabled, "isBroadTitleTextEllipsisEnabled");
setCheckboxAndSaveValue("switchRemoveBroadStartTimeTag", isRemoveBroadStartTimeTagEnabled, "isRemoveBroadStartTimeTagEnabled");
setCheckboxAndSaveValue("switchUnlockCopyPaste", isUnlockCopyPasteEnabled, "isUnlockCopyPasteEnabled");
setCheckboxAndSaveValue("switchAlignNicknameRight", isAlignNicknameRightEnabled, "isAlignNicknameRightEnabled");
//setCheckboxAndSaveValue("switchPreviewModal", isPreviewModalEnabled, "isPreviewModalEnabled");
setCheckboxAndSaveValue("switchReplaceEmptyThumbnail", isReplaceEmptyThumbnailEnabled, "isReplaceEmptyThumbnailEnabled");
setCheckboxAndSaveValue("switchAutoScreenMode", isAutoScreenModeEnabled, "isAutoScreenModeEnabled");
setCheckboxAndSaveValue("switchAdjustDelayNoGrid", isAdjustDelayNoGridEnabled, "isAdjustDelayNoGridEnabled");
setCheckboxAndSaveValue("switchHideButtonsAboveChatInput", ishideButtonsAboveChatInputEnabled, "ishideButtonsAboveChatInputEnabled");
setCheckboxAndSaveValue("switchExpandVODChatArea", isExpandVODChatAreaEnabled, "isExpandVODChatAreaEnabled");
setCheckboxAndSaveValue("switchExpandLiveChatArea", isExpandLiveChatAreaEnabled, "isExpandLiveChatAreaEnabled");
setCheckboxAndSaveValue("switchOpenExternalPlayer", isOpenExternalPlayerEnabled, "isOpenExternalPlayerEnabled");
setCheckboxAndSaveValue("switchOpenExternalPlayerFromSidebar", isOpenExternalPlayerFromSidebarEnabled, "isOpenExternalPlayerFromSidebarEnabled");
setCheckboxAndSaveValue("switchRemoveShadowsFromCatch", isRemoveShadowsFromCatchEnabled, "isRemoveShadowsFromCatchEnabled");
setCheckboxAndSaveValue("switchChzzkFollowChannels", isChzzkFollowChannelsEnabled, "isChzzkFollowChannelsEnabled");
setCheckboxAndSaveValue("switchChzzkTopChannels", isChzzkTopChannelsEnabled, "isChzzkTopChannelsEnabled");
setCheckboxAndSaveValue("switchShowSelectedMessages", isShowSelectedMessagesEnabled, "isShowSelectedMessagesEnabled");
setCheckboxAndSaveValue("switchShowDeletedMessages", isShowDeletedMessagesEnabled, "isShowDeletedMessagesEnabled");
setCheckboxAndSaveValue("switchNoAutoVOD", isNoAutoVODEnabled, "isNoAutoVODEnabled");
setCheckboxAndSaveValue("switchHideEsportsInfo", isHideEsportsInfoEnabled, "isHideEsportsInfoEnabled");
setCheckboxAndSaveValue("switchBlockedCategorySorting", isBlockedCategorySortingEnabled, "isBlockedCategorySortingEnabled");
setCheckboxAndSaveValue("switchChatCounter", isChatCounterEnabled, "isChatCounterEnabled");
setCheckboxAndSaveValue("switchRandomSort", isRandomSortEnabled, "isRandomSortEnabled");
setCheckboxAndSaveValue("switchPinnedOnlineOnly", isPinnedOnlineOnlyEnabled, "isPinnedOnlineOnlyEnabled");
const handleRangeInput = (inputId, displayId, currentValue, storageKey) => {
const input = document.getElementById(inputId);
input.value = currentValue;
input.addEventListener("input", (event) => {
const newValue = parseInt(event.target.value); // event.target.value로 변경
if (newValue !== currentValue) {
GM_setValue(storageKey, newValue);
currentValue = newValue;
document.getElementById(displayId).textContent = newValue;
if (inputId === "nicknameWidthDisplay") setWidthNickname(newValue);
}
});
}
handleRangeInput("favoriteChannelsDisplay", "favoriteChannelsDisplayValue", displayFollow, "displayFollow");
handleRangeInput("myPlusChannelsDisplay", "myPlusChannelsDisplayValue", displayMyplus, "displayMyplus");
handleRangeInput("myPlusVODDisplay", "myPlusVODDisplayValue", displayMyplusvod, "displayMyplusvod");
handleRangeInput("popularChannelsDisplay", "popularChannelsDisplayValue", displayTop, "displayTop");
handleRangeInput("nicknameWidthDisplay", "nicknameWidthDisplayValue", nicknameWidth, "nicknameWidth");
// 채팅 단어 차단 입력 상자 설정
const blockWordsInputBox = document.getElementById('blockWordsInput');
blockWordsInputBox.addEventListener('input', () => {
const inputValue = blockWordsInputBox.value.trim();
registeredWords = inputValue;
GM_setValue("registeredWords", inputValue);
});
// 유저 채팅 모아보기 입력 상자 설정
const selectedUsersinputBox = document.getElementById('selectedUsersInput');
selectedUsersinputBox.addEventListener('input', () => {
const inputValue = selectedUsersinputBox.value.trim();
selectedUsers = inputValue;
GM_setValue("selectedUsers", inputValue);
});
}
const checkSidebarVisibility = () => {
let intervalId = null;
let lastExecutionTime = Date.now(); // 마지막 실행 시점 기록
const handleVisibilityChange = () => {
const body = document.body;
const isScreenmode = body.classList.contains('screen_mode');
const isShowSidebar = body.classList.contains('showSidebar');
const isFullScreenmode = body.classList.contains('fullScreen_mode');
const isSidebarHidden = (isScreenmode ? !isShowSidebar : false) || isFullScreenmode;
const webplayer = document.getElementById('webplayer');
const webplayerStyle = webplayer?.style;
const sidebar = document.getElementById('sidebar');
// 스크린 모드에서 사이드바 항상 보이는 옵션
if (webplayer && isScreenmode && showSidebarOnScreenModeAlways && !isShowSidebar) {
body.classList.add('showSidebar');
webplayer.style.left = '0px';
webplayer.style.left = sidebar.offsetWidth + 'px';
webplayer.style.width = `calc(100vw - ${sidebar.offsetWidth}px)`;
}
// 사이드바가 보이는 상태에서 스크린 모드 종료할 때
if (webplayer && !isScreenmode && isShowSidebar) {
body.classList.remove('showSidebar');
webplayerStyle.removeProperty('width');
webplayerStyle.removeProperty('left');
}
if (document.visibilityState === 'visible' && isSidebarHidden) {
//console.log('#sidebar는 숨겨져 있음');
return;
}
const currentTime = Date.now();
const timeSinceLastExecution = (currentTime - lastExecutionTime) / 1000; // 초 단위로 변환
if (document.visibilityState === 'visible' && timeSinceLastExecution >= 60) {
//console.log('탭 활성화됨');
generateBroadcastElements(1);
lastExecutionTime = currentTime; // 갱신 시점 기록
restartInterval(); // 인터벌 재시작
} else if (document.visibilityState === 'visible') {
//console.log('60초 미만 경과: 방송 목록 갱신하지 않음');
} else {
//console.log(`탭 비활성화됨: 마지막 갱신 = ${parseInt(timeSinceLastExecution)}초 전`);
}
};
const restartInterval = () => {
if (intervalId) clearInterval(intervalId); // 기존 인터벌 중단
intervalId = setInterval(() => {
handleVisibilityChange();
}, 60 * 1000); // 60초마다 실행
};
const observeBodyClassChanges = () => {
const body = document.querySelector('body');
const observer = new MutationObserver((mutations) => {
mutations.forEach(({ attributeName }) => {
if (attributeName === 'class') {
handleVisibilityChange();
}
});
});
observer.observe(body, {
attributes: true,
attributeFilter: ['class']
});
};
waitForElement('#sidebar', function (elementSelector, element) {
//console.log('#sidebar가 로드됨!');
observeBodyClassChanges(); // body 클래스 감시 시작
restartInterval(); // 인터벌 시작
document.addEventListener('visibilitychange', handleVisibilityChange);
});
};
const hideUsersSection = () => {
const styles = [
!displayMyplus && '#sidebar .myplus { display: none !important; }',
!displayMyplusvod && '#sidebar .myplusvod { display: none !important; }',
!displayTop && '#sidebar .top { display: none !important; }'
].filter(Boolean).join(' '); // 빈 값 제거 및 합침
if (styles) {
GM_addStyle(styles);
}
}
const removeTargetFromLinks = () => {
try {
const links = document.querySelectorAll('#container a[target], .side_list a[target]');
links.forEach(link => {
link.removeAttribute('target');
});
} catch (error) {
console.error('target 속성 제거 중 오류 발생:', error);
}
}
const runCommonFunctions = () => {
if (isCustomSidebarEnabled) {
orderSidebarSection();
hideUsersSection();
generateBroadcastElements(0);
checkSidebarVisibility();
}
// 본문 방송 목록의 새 탭 열기 방지
if(!isOpenNewtabEnabled){
setInterval(removeTargetFromLinks, 1000);
}
waitForElement('div.serviceUtil', function (elementSelector, element) {
addModalSettings();
manageRedDot();
});
registerMenuBlockingWord();
blockedUsers.forEach(function(user) {
registerUnblockMenu(user);
});
blockedCategories.forEach(function(category) {
registerCategoryUnblockMenu(category);
});
blockedWords.forEach(function(word) {
registerWordUnblockMenu(word);
});
}
const orderSidebarSection = () => {
const style =
`
#sidebar .top-section.top {
order: 3 !important;
}
#sidebar .users-section.top {
order: 4 !important;
}
#sidebar .top-section.myplus {
order: 5 !important;
}
#sidebar .users-section.myplus {
order: 6 !important;
}
#sidebar .top-section.myplusvod {
order: 7 !important;
}
#sidebar .users-section.myplusvod {
order: 8 !important;
}
`;
if (!myplusPosition) {
GM_addStyle(style);
}
}
//=================================공용 함수 끝=================================//
//=================================메인 페이지 함수=================================//
const openHlsStream = (nickname, m3u8Url) => {
// HTML과 JavaScript 코드 생성
const htmlContent = `
${nickname}
`;
// Blob 생성
const blob = new Blob([htmlContent], { type: 'text/html' });
const blobUrl = URL.createObjectURL(blob);
// 새로운 창으로 Blob URL 열기
window.open(blobUrl, "_blank");
};
unsafeWindow.openHlsStream = openHlsStream;
const getBroadAid2 = async (id, broadNumber, quality = 'original') => {
const basePayload = {
bid: id,
bno: broadNumber,
from_api: '0',
mode: 'landing',
player_type: 'html5',
stream_type: 'common',
quality: quality
};
// AID 요청 함수
const requestAid = async (password = '') => {
const payload = {
...basePayload,
type: 'aid',
pwd: password
};
const options = {
method: 'POST',
body: new URLSearchParams(payload),
credentials: 'include',
cache: 'no-store'
};
const res = await fetch('https://live.sooplive.co.kr/afreeca/player_live_api.php', options);
return await res.json();
};
// LIVE 요청 함수
const requestLive = async () => {
const payload = {
...basePayload,
type: 'live',
pwd: ''
};
const options = {
method: 'POST',
body: new URLSearchParams(payload),
credentials: 'include',
cache: 'no-store'
};
const res = await fetch('https://live.sooplive.co.kr/afreeca/player_live_api.php', options);
return await res.json();
};
try {
// 1차: 비밀번호 없이 AID 요청
const result1 = await requestAid('');
if (result1?.CHANNEL?.AID) {
console.log(result1.CHANNEL.AID);
return result1.CHANNEL.AID;
}
// 2차: LIVE 요청으로 BPWD 확인
const result2 = await requestLive();
if (result2?.CHANNEL?.BPWD === 'Y') {
const password = prompt('비밀번호를 입력하세요:');
if (password === null) return null;
// 3차: 입력된 비밀번호로 다시 AID 요청
const retryResult = await requestAid(password);
if (retryResult?.CHANNEL?.AID) {
console.log(result1.CHANNEL.AID);
return retryResult.CHANNEL.AID;
} else {
alert('비밀번호가 틀렸거나 종료된 방송입니다.');
}
}
return null;
} catch (error) {
console.log('오류 발생:', error);
return null;
}
};
unsafeWindow.getBroadAid2 = getBroadAid2;
const openStreamDownloader = async (id, broadNumber) => {
const aid = await getBroadAid2(id, broadNumber);
if (!aid) return;
const m3u8Url = `https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=${aid}`;
const baseUrl = m3u8Url.split('/').slice(0, -1).join('/') + '/';
const html = `
다운로드 준비 중... ${id}
📡 실시간 저장 중...
💾 저장
자동 저장 (저장을 너무 오래 하지 않을 시 작업을 잃을 수 있습니다)
간격 (분):
⏱️ 00:00:00
📦 0 / 0
🍰 최신 조각: 0
🍰 마지막 저장: 0
`;
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
};
unsafeWindow.openStreamDownloader = openStreamDownloader;
const makeExternalLinks = (thumbsBoxLinks) => {
for (const thumbsBoxLink of thumbsBoxLinks) {
if (!thumbsBoxLink.classList.contains("externalPlayer-checked")) {
thumbsBoxLink.classList.add("externalPlayer-checked");
const hrefValue = thumbsBoxLink.getAttribute('href');
if (hrefValue?.includes("play.sooplive.co.kr")) {
const [ , , , id, broadNumber] = hrefValue.split('/');
thumbsBoxLink.addEventListener('contextmenu', async (event) => {
event.preventDefault();
event.stopPropagation();
const nickname = thumbsBoxLink.parentNode.parentNode.querySelector('.nick').innerText;
const aid = await getBroadAid2(id, broadNumber);
if (aid){
openHlsStream(nickname, `https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=${aid}`);
}
});
}
}
}
};
/*
const getBroadAid = async (id, broadNumber) => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
try {
const response = await fetch(`https://live.sooplive.co.kr/api/live_status.php?user_id=${id}&broad_no=${broadNumber}&type=play`, requestOptions);
const result = await response.json();
return result.data.aid || null;
} catch (error) {
console.log('오류 발생:', error);
return null;
}
};
const getBroadDomain = async (id, broadNumber) => {
const requestOptions = {
method: 'GET'
};
try {
const response = await fetch(`https://livestream-manager.sooplive.co.kr/broad_stream_assign.html?return_type=gs_cdn_preview&use_cors=true&cors_origin_url=www.sooplive.co.kr&broad_key=${broadNumber}-common-hd-hls`, requestOptions);
const result = await response.json();
return result.view_url || null;
} catch (error) {
console.log('오류 발생:', error);
return null;
}
};
*/
// 최신 프레임 캡처 함수
const captureLatestFrame = (videoElement) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 캔버스 크기 설정 (480x270)
const canvasWidth = 480;
const canvasHeight = 270;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// 원본 비디오의 비율을 유지하면서 크기 계산
const videoRatio = videoElement.videoWidth / videoElement.videoHeight;
const canvasRatio = canvasWidth / canvasHeight;
let drawWidth, drawHeight;
let offsetX = 0, offsetY = 0;
if (videoRatio > canvasRatio) {
drawWidth = canvasWidth;
drawHeight = canvasWidth / videoRatio;
offsetY = (canvasHeight - drawHeight) / 2;
} else {
drawHeight = canvasHeight;
drawWidth = canvasHeight * videoRatio;
offsetX = (canvasWidth - drawWidth) / 2;
}
// 배경을 검은색으로 채우기
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// 비디오의 현재 프레임을 캔버스에 그림
ctx.drawImage(videoElement, offsetX, offsetY, drawWidth, drawHeight);
// webp 형식으로 변환 후 반환
const dataURL = canvas.toDataURL('image/webp');
resolve(dataURL); // 데이터 URL 반환
});
};
// id와 broadNumber로 이미지 데이터 캡처
const getLatestFrameData = async (id, broadNumber) => {
const videoElement = document.createElement('video');
videoElement.playbackRate = 16; // 빠른 재생 속도 설정
const aid = await getBroadAid2(id, broadNumber, 'sd');
const m3u8url = `https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=${aid}`
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(m3u8url);
hls.attachMedia(videoElement);
return new Promise((resolve) => {
videoElement.addEventListener('canplay', async () => {
const frameData = await captureLatestFrame(videoElement);
resolve(frameData);
videoElement.pause();
videoElement.src = '';
});
});
} else {
console.error('HLS.js를 지원하지 않는 브라우저입니다.');
return null;
}
};
const replaceThumbnails = (thumbsBoxLinks) => {
for (const thumbsBoxLink of thumbsBoxLinks) {
if (!thumbsBoxLink.classList.contains("thumbnail-checked")) {
thumbsBoxLink.classList.add("thumbnail-checked");
const hrefValue = thumbsBoxLink.getAttribute('href');
if (hrefValue && hrefValue.includes("sooplive.co.kr")) {
const [ , , , id, broadNumber] = hrefValue.split('/');
thumbsBoxLink.dataset.lastMouseEnterTime = 0;
thumbsBoxLink.addEventListener('mouseenter', async function(event) {
event.preventDefault();
event.stopPropagation();
const currentTime = Date.now();
const lastMouseEnterTime = Number(thumbsBoxLink.dataset.lastMouseEnterTime);
if (currentTime - lastMouseEnterTime >= 30000) {
thumbsBoxLink.dataset.lastMouseEnterTime = currentTime;
const frameData = await getLatestFrameData(id, broadNumber);
let imgElement = thumbsBoxLink.querySelector('img');
if (!imgElement) {
imgElement = document.createElement('img');
thumbsBoxLink.appendChild(imgElement);
}
imgElement.src = frameData;
}
});
}
}
}
};
/*
// 모달 생성 함수 먼저 정의
const createModal = () => {
if (!CURRENT_URL.startsWith("https://www.sooplive.co.kr")){
return false;
}
window.onclick = function(event) {
if (event.target === modal) {
closeModal(modal, modalElements.videoPlayer);
}
};
const modal = document.createElement('div');
modal.className = 'preview-modal';
const modalContent = document.createElement('div');
modalContent.className = 'preview-modal-content';
const closeButton = document.createElement('span');
closeButton.className = 'preview-close';
closeButton.innerHTML = '×';
closeButton.onclick = () => closeModal(modal, modalElements.videoPlayer); // closeButton에 클릭 이벤트 설정
const videoPlayer = document.createElement('video');
videoPlayer.controls = true;
const infoContainer = document.createElement('div');
infoContainer.className = 'info';
// 방송 정보 및 태그 추가
const streamerName = document.createElement('div');
streamerName.className = 'streamer-name';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
const tagsContainer = document.createElement('div');
tagsContainer.className = 'tags';
const startButton = document.createElement('a');
startButton.className = 'start-button';
startButton.textContent = '참여하기 >'; // 버튼 텍스트 설정
infoContainer.append(streamerName, tagsContainer, videoTitle, startButton);
modalContent.append(closeButton, videoPlayer, infoContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
return { modal, videoPlayer, streamerName, videoTitle, tagsContainer, startButton };
};
// 전역 변수에 modalElements 저장
const modalElements = createModal(); // 모달을 한 번 생성하고 변수에 저장
const makePreviewModalContents = (thumbsBoxLinks) => {
for (const thumbsBoxLink of thumbsBoxLinks) {
if (!thumbsBoxLink.classList.contains("preview-checked")) {
thumbsBoxLink.classList.add("preview-checked");
const hrefValue = thumbsBoxLink.getAttribute('href');
if (hrefValue?.includes("play.sooplive.co.kr")) {
const [ , , , id, broadNumber] = hrefValue.split('/');
thumbsBoxLink.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
// 모달이 이미 표시된 경우 클릭 처리하지 않음
if (modalElements.modal.style.display === 'block') return;
await handleLinkClick(id, broadNumber, thumbsBoxLink);
modalElements.modal.style.display = 'block';
});
}
}
}
};
const closeModal = (modal, videoPlayer) => {
modal.style.display = 'none';
videoPlayer.pause(); // 비디오 정지
videoPlayer.src = ''; // 소스 초기화
};
const handleLinkClick = async (id, broadNumber, thumbsBoxLink) => {
const playerLink = `https://play.sooplive.co.kr/${id}/${broadNumber}`;
try {
const aid = await getBroadAid2(id, broadNumber, 'hd');
const m3u8url = `https://live-global-cdn-v02.sooplive.co.kr/live-stm-12/auth_playlist.m3u8?aid=${aid}`
// 내용 업데이트
updateModalContent(m3u8url, playerLink, thumbsBoxLink);
} catch (error) {
console.error('Error fetching broadcast information:', error);
}
};
const updateModalContent = (m3u8url, playerLink, thumbsBoxLink) => {
const { videoPlayer, streamerName, videoTitle, tagsContainer, startButton } = modalElements;
const hrefTarget = isOpenNewtabEnabled ? "_blank" : "_self";
// 방송 정보 업데이트
const parent = thumbsBoxLink.parentNode.parentNode;
streamerName.textContent = parent.querySelector('.nick').innerText; // 스트리머 이름
videoTitle.textContent = parent.querySelector('.title a').innerText; // 방송 제목
// 태그 추가
updateTags(tagsContainer, thumbsBoxLink);
// startButton의 href 업데이트
startButton.setAttribute('href', playerLink);
startButton.setAttribute('target', hrefTarget);
// 비디오 표시 및 재생
const playVideo = () => {
videoPlayer.style.display = 'block'; // 비디오 표시
videoPlayer.play(); // 비디오 재생 시작
};
// HLS.js를 사용하여 m3u8 재생 설정
const initializeHLS = () => {
const hls = new Hls();
hls.loadSource(m3u8url);
hls.attachMedia(videoPlayer);
hls.on(Hls.Events.MANIFEST_PARSED, playVideo);
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS error: ', data);
});
};
const handleSafariSupport = () => {
videoPlayer.src = m3u8url;
videoPlayer.addEventListener('loadedmetadata', playVideo);
videoPlayer.addEventListener('error', () => {
console.error('Video playback error');
alert('비디오를 로드하는 데 오류가 발생했습니다.');
});
};
// HLS 지원 여부에 따라 초기화
if (Hls.isSupported()) {
initializeHLS();
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
handleSafariSupport();
} else {
console.error('이 브라우저는 HLS를 지원하지 않습니다.');
alert('이 브라우저는 HLS 비디오를 지원하지 않습니다.');
}
};
// 기존 updateTags 함수 유지
const updateTags = (tagsContainer, thumbsBoxLink) => {
const tags = thumbsBoxLink.parentNode.parentNode.querySelectorAll('.tag_wrap a');
tagsContainer.innerHTML = ''; // 이전 태그 제거
tags.forEach(tag => {
const tagElement = document.createElement('a');
tagElement.textContent = tag.innerText;
tagElement.href = tag.getAttribute("class") === "category"
? `https://www.sooplive.co.kr/directory/category/${encodeURIComponent(tag.innerText)}/live`
: `https://www.sooplive.co.kr/search?hash=hashtag&tagname=${encodeURIComponent(tag.innerText)}&hashtype=live&stype=hash&acttype=live&location=live_main&inflow_tab=`;
tagsContainer.appendChild(tagElement);
});
};
*/
const removeUnwantedTags = () =>{
if (isRemoveCarouselEnabled) {
GM_addStyle(`
div[class^="player_player_wrap"] {
display: none !important;
}
`);
}
if (isRemoveRedistributionTagEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .allow {
display: none !important;
}
`);
}
if (isRemoveWatchLaterButtonEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .later {
display: none !important;
}
`);
}
if (isRemoveBroadStartTimeTagEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .time {
display: none !important;
}
`);
}
if (isBroadTitleTextEllipsisEnabled) {
GM_addStyle(`
[data-type=cBox] .cBox-info .title a {
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
`);
}
}
const processStreamers = () => {
const processedLayers = new Set(); // 처리된 레이어를 추적
const createStreamDownloadButton_MainPage = (listItem, optionsLayer) => {
const downloadButton = document.createElement('button'); // "스트림 다운로드" 버튼 생성
downloadButton.type = 'button';
const span = document.createElement('span');
span.textContent = '스트림 다운로드';
downloadButton.appendChild(span);
// 클릭 이벤트 추가
downloadButton.addEventListener('click', () => {
const userIdElement = listItem.querySelector('.cBox-info .title a');
if (userIdElement) {
const urlParts = userIdElement.href.split('/');
const id = urlParts[3];
const broadNumber = urlParts[4];
if (id) {
if (id === "player") alert('VOD는 아직 지원하지 않습니다.');
openStreamDownloader(id, broadNumber); // 스트리밍 다운로드 함수 호출
} else {
//console.log("ID를 찾을 수 없습니다.");
}
} else {
//console.log("userIdElement 또는 broadNumber를 찾을 수 없습니다.");
}
});
// 리스트 맨 앞에 삽입
optionsLayer.insertBefore(downloadButton, optionsLayer.firstChild);
};
// 버튼 생성 및 클릭 이벤트 처리
const createHideButton = (listItem, optionsLayer) => {
const hideButton = document.createElement('button'); // "숨기기" 버튼 생성
hideButton.type = 'button';
const span = document.createElement('span');
span.textContent = '이 브라우저에서 스트리머 숨기기';
hideButton.appendChild(span);
// 클릭 이벤트 추가
hideButton.addEventListener('click', () => {
const userNameElement = listItem.querySelector('a.nick > span'); // 사용자 이름 요소
const userIdElement = listItem.querySelector('.cBox-info > a'); // 사용자 ID 요소
if (userNameElement && userIdElement) {
const userId = userIdElement.href.split('/')[3]; // 사용자 ID 추출
const userName = userNameElement.innerText; // 사용자 이름 추출
//console.log(`Blocking user: ${userName}, ID: ${userId}`); // 로그 추가
if (userId && userName) {
blockUser(userName, userId); // 사용자 차단 함수 호출
listItem.style.display = 'none';
}
} else {
//console.log("User elements not found."); // 요소가 없을 경우 로그 추가
}
});
optionsLayer.appendChild(hideButton); // 옵션 레이어에 버튼 추가
};
const createCategoryHideButton = (listItem, optionsLayer) => {
const hideButton = document.createElement('button'); // "숨기기" 버튼 생성
hideButton.type = 'button';
const span = document.createElement('span');
span.textContent = '이 브라우저에서 해당 카테고리 숨기기';
hideButton.appendChild(span);
// 클릭 이벤트 추가 [data-type=cBox] .cBox-info .tag_wrap a.category
hideButton.addEventListener('click', () => {
const categoryElement = listItem.querySelector('.cBox-info .tag_wrap a.category');
if (categoryElement) {
const categoryName = categoryElement.textContent;
const categoryNo = getCategoryNo(categoryName);
if (categoryName && categoryNo) {
blockCategory(categoryName, categoryNo);
}
} else {
//console.log("User elements not found."); // 요소가 없을 경우 로그 추가
}
});
optionsLayer.appendChild(hideButton); // 옵션 레이어에 버튼 추가
}
// DOM 변경 감지 및 처리
const handleDOMChange = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
const moreOptionsContainer = document.querySelector('div._moreDot_wrapper'); // 추가 옵션 컨테이너
const optionsLayer = moreOptionsContainer ? moreOptionsContainer.querySelector('div._moreDot_layer') : null; // 옵션 레이어
if (optionsLayer && optionsLayer.style.display !== 'none' && !processedLayers.has(optionsLayer)) {
const activeButton = document.querySelector('button.more_dot.on'); // 활성화된 버튼
const listItem = activeButton.closest('li[data-type="cBox"]'); // 가장 가까운 리스트 아이템 찾기
if (listItem) {
if (isStreamDownloadEnabled) createStreamDownloadButton_MainPage(listItem, optionsLayer);
createHideButton(listItem, optionsLayer); // 숨기기 버튼 생성
createCategoryHideButton(listItem, optionsLayer);
processedLayers.add(optionsLayer); // 이미 처리된 레이어로 추가
}
} else if (!optionsLayer) {
processedLayers.clear(); // 요소가 없을 때 처리된 레이어 초기화
}
// cBox-list의 리스트 아이템 처리
const cBoxListItems = document.querySelectorAll('div.cBox-list li[data-type="cBox"]:not(.hide-checked)');
// cBoxListItems를 for...of 루프로 반복
for (const listItem of cBoxListItems) {
listItem.classList.add('hide-checked');
const userIdElement = listItem.querySelector('.cBox-info > a'); // 사용자 ID 요소
const categoryElement = listItem.querySelector('.cBox-info .tag_wrap a.category');
const titleElement = listItem.querySelector('.cBox-info .title a');
if (userIdElement) {
const userId = userIdElement.href.split('/')[3]; // 사용자 ID 추출
// 차단된 사용자일 경우 li 삭제
if (isUserBlocked(userId)) {
listItem.style.display = 'none';
//console.log(`Removed blocked user with ID: ${userId}`); // 로그 추가
}
}
if (categoryElement) {
const categoryName = categoryElement.textContent;
if (isCategoryBlocked(getCategoryNo(categoryName))) {
listItem.style.display = 'none';
//console.log(`Removed blocked category with Name: ${categoryName}`); // 로그 추가
}
}
if (titleElement) {
const broadTitle = titleElement.textContent;
// blockedWords에 포함된 단어가 broadTitle에 있는지 체크
for (const word of blockedWords) {
if (broadTitle.toLowerCase().includes(word.toLowerCase())) {
listItem.style.display = 'none';
//console.log(`Removed item with blocked word in title: ${broadTitle}`); // 로그 추가
break; // 하나의 차단 단어가 발견되면 더 이상 확인할 필요 없음
}
}
}
}
/*
// 프리뷰 모달 사용
if (isPreviewModalEnabled) {
const allThumbsBoxLinks = document.querySelectorAll('[data-type=cBox] .thumbs-box > a[href]:not([href^="https://vod.sooplive.co.kr"])');
if (allThumbsBoxLinks.length) makePreviewModalContents(allThumbsBoxLinks);
}
*/
// 외부 재생기 사용
if (isOpenExternalPlayerEnabled) {
const allThumbsBoxLinks = document.querySelectorAll('[data-type=cBox] .thumbs-box > a[href]:not([href^="https://vod.sooplive.co.kr"])');
if (allThumbsBoxLinks.length) makeExternalLinks(allThumbsBoxLinks);
}
// 빈 썸네일 대체
if (isReplaceEmptyThumbnailEnabled){
const noThumbsBoxLinks = document.querySelectorAll('[data-type=cBox] .thumbs-box > a[href].thumb-adult:not([href^="https://vod.sooplive.co.kr"])');
if (noThumbsBoxLinks.length) replaceThumbnails(noThumbsBoxLinks);
}
}
}
};
const observer = new MutationObserver(handleDOMChange); // DOM 변경 감지기
// 감지할 옵션 설정
const config = { childList: true, subtree: true };
// 관찰 시작
observer.observe(document.body, config);
};
//=================================메인 페이지 함수 끝=================================//
//=================================플레이어 페이지 함수=================================//
const waitForLivePlayer = (timeout = 10000) => {
return new Promise((resolve, reject) => {
const interval = 1500;
let elapsed = 0;
const check = () => {
if (unsafeWindow.livePlayer) {
resolve(unsafeWindow.livePlayer);
} else {
elapsed += interval;
if (elapsed >= timeout) {
reject(new Error('livePlayer 객체를 찾지 못했습니다.'));
} else {
setTimeout(check, interval);
}
}
};
check();
});
};
const createStreamDownloadButton_LivePage = () => {
const moreDotLayers = document.querySelectorAll('._moreDot_layer');
moreDotLayers.forEach(moreDotLayer => {
// 이미 버튼이 있으면 중복 삽입 방지
if (moreDotLayer.querySelector('.stream-download')) return;
const downloadButton = document.createElement('button');
downloadButton.type = 'button';
downloadButton.textContent = '스트림 다운로드';
downloadButton.className = 'stream-download';
downloadButton.addEventListener('click', () => {
const match = location.href.match(/\/([^/]+)\/(\d+)/);
if (!match) {
console.warn("URL에서 id 또는 broadNumber를 추출할 수 없습니다.");
return;
}
const id = match[1];
const broadNumber = match[2];
openStreamDownloader(id, broadNumber);
});
moreDotLayer.insertBefore(downloadButton, moreDotLayer.firstChild);
});
};
const showSidebarOnMouseOver = () => {
const sidebar = document.getElementById('sidebar');
const videoLayer = document.getElementById('player');
const webplayerContents = document.getElementById('webplayer');
const body = document.body;
const handleSidebarMouseOver = () => {
if (body.classList.contains('screen_mode') && !body.classList.contains('showSidebar')) {
body.classList.add('showSidebar');
webplayerContents.style.left = sidebar.offsetWidth + 'px';
webplayerContents.style.width = `calc(100vw - ${sidebar.offsetWidth}px)`;
}
};
const handleSidebarMouseOut = () => {
if (body.classList.contains('screen_mode') && body.classList.contains('showSidebar')) {
body.classList.remove('showSidebar');
webplayerContents.style.left = '0px';
webplayerContents.style.width = '100vw';
}
};
const mouseMoveHandler = (event) => {
const mouseX = event.clientX;
const mouseY = event.clientY;
if (!body.classList.contains('showSidebar')) {
if (mouseX < 52 && mouseY < videoLayer.clientHeight - 150) {
handleSidebarMouseOver();
}
} else {
if (mouseX < sidebar.clientWidth && mouseY < sidebar.clientHeight) {
handleSidebarMouseOver();
} else {
handleSidebarMouseOut();
}
}
};
const windowMouseOutHandler = (event) => {
if (!event.relatedTarget && !event.toElement) {
handleSidebarMouseOut();
}
};
document.addEventListener('mousemove', mouseMoveHandler);
window.addEventListener('mouseout', windowMouseOutHandler); // 창 벗어남 감지
};
const toggleExpandChatShortcut = () => {
setupKeydownHandler("KeyX", toggleExpandChat); // X 키
};
const toggleSharpModeShortcut = () => {
setupKeydownHandler("KeyE", togglesharpModeCheck); // E 키
updateLabel('clear_screen', '선명한 모드', '선명한 모드(e)');
};
const toggleLowLatencyShortcut = () => {
setupKeydownHandler("KeyD", toggleDelayCheck); // D 키
updateLabel('delay_check', '시차 단축', '시차 단축(d)');
};
const setupKeydownHandler = (targetCode, toggleFunction) => {
document.addEventListener('keydown', (event) => {
const active = document.activeElement;
const tag = active?.tagName?.toUpperCase();
const isEditable = (
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
active?.isContentEditable ||
active?.id === 'write_area'
);
if (event.code === targetCode && !isEditable) {
toggleFunction();
}
}, true); // 캡처링 단계에서 이벤트 수신
};
const updateLabel = (forId, oldText, newText) => {
const labelElement = document.body.querySelector(`#player label[for="${forId}"]`);
if (labelElement) {
labelElement.innerHTML = labelElement.innerHTML.replace(oldText, newText);
} else {
console.error('Label element not found.');
}
};
function toggleExpandChat() {
if (!isElementVisible('.expand-toggle-li')) {
return;
}
waitForElement('.expand-toggle-li a', function (elementSelector, element) {
element.click();
})
}
const togglesharpModeCheck = () => {
const sharpModeCheckElement = document.getElementById('clear_screen');
sharpModeCheckElement.click();
showPlayerBar(69); // E 키
};
const toggleDelayCheck = () => {
if (isAdjustDelayNoGridEnabled) {
moveToLatestBufferedPoint();
} else {
const delayCheckElement = document.getElementById('delay_check');
delayCheckElement.click();
showPlayerBar(68); // D 키
}
};
const showPlayerBar = (keyCode) => {
const player = document.getElementById('player');
player.classList.add('mouseover');
let settingButton, settingBoxOn;
if (keyCode === 69) { // E 키
settingButton = document.body.querySelector('#player button.btn_quality_mode');
settingBoxOn = document.body.querySelector('.quality_box.on');
} else if (keyCode === 68) { // D 키
settingButton = document.body.querySelector('#player button.btn_setting');
settingBoxOn = document.body.querySelector('.setting_box.on');
}
if (settingButton) {
if (!settingBoxOn) {
settingButton.click();
}
setTimeout(() => {
if (settingBoxOn) {
settingButton.click();
}
player.classList.remove('mouseover');
}, 1000); // 1초 후에 mouseover 클래스 제거
} else {
console.error('Setting button not found or not visible.');
}
};
// 비디오의 가장 최신 버퍼링 지점에서 2초 전으로 이동 (현재 지점보다 오래된 지점으로는 이동하지 않음)
const moveToLatestBufferedPoint = () => {
const video = document.querySelector('video');
const buffered = video.buffered;
if (buffered.length > 0) {
// 버퍼링된 구간의 마지막 시간
const bufferedEnd = buffered.end(buffered.length - 1);
const targetTime = bufferedEnd - 2; // 2초 전으로 설정
// targetTime이 현재 시간보다 뒤에 있을 경우에만 이동
if (targetTime > video.currentTime) {
video.currentTime = targetTime;
}
}
}
const checkPlayerPageHeaderAd = () => {
waitForElement('#header_ad', function (elementSelector, element) {
element.remove();
})
}
const extractDateTime = (text) => {
const [dateStr, timeStr] = text.split(' '); // split 한 번으로 날짜와 시간을 동시에 얻기
const dateTimeStr = `${dateStr}T${timeStr}Z`; // 문자열 템플릿 사용
return new Date(dateTimeStr);
}
const getElapsedTime = (broadcastStartTimeText, type) => {
const broadcastStartTime = extractDateTime(broadcastStartTimeText);
broadcastStartTime.setHours(broadcastStartTime.getHours() - 9);
const currentTime = new Date();
const timeDiff = currentTime - broadcastStartTime;
const secondsElapsed = Math.floor(timeDiff / 1000);
const hoursElapsed = Math.floor(secondsElapsed / 3600);
const minutesElapsed = Math.floor((secondsElapsed % 3600) / 60);
const remainingSeconds = secondsElapsed % 60;
let formattedTime = '';
if (type === "HH:MM:SS") {
formattedTime = `${String(hoursElapsed).padStart(2, '0')}:${String(minutesElapsed).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
} else if (type === "HH:MM") {
if (hoursElapsed > 0) {
formattedTime = `${String(hoursElapsed)}시간 `;
}
formattedTime += `${String(minutesElapsed)}분`;
}
return formattedTime;
}
// remainingBufferTime을 계산하여 리턴하는 함수
const getRemainingBufferTime = (video) => {
const buffered = video.buffered;
if (buffered.length > 0) {
// 마지막 버퍼의 끝과 현재 시간의 차이를 계산
const remainingBufferTime = buffered.end(buffered.length - 1) - video.currentTime;
// 0초 또는 정수일 경우 소수점 한 자리로 반환
return remainingBufferTime >= 0
? remainingBufferTime.toFixed(remainingBufferTime % 1 === 0 ? 0 : 1)
: '';
}
return ''; // 버퍼가 없으면 빈 문자열 반환
};
// remainingBufferTime 값을 삽입하는 함수
const insertRemainingBuffer = (element) => {
const video = element;
const emptyChat = document.body.querySelector('#empty_chat');
// video의 onprogress 이벤트 핸들러
video.onprogress = () => {
const remainingBufferTime = getRemainingBufferTime(video); // remainingBufferTime 계산
if (emptyChat && remainingBufferTime !== '') {
emptyChat.innerText = `${remainingBufferTime}s 지연됨`;
}
};
};
const isVideoInPiPMode = () => {
const videoElement = document.body.querySelector('video');
return videoElement && document.pictureInPictureElement === videoElement;
}
const handleMuteByVisibility = () => {
if (!isAutoChangeMuteEnabled || isVideoInPiPMode()) return;
const button = document.body.querySelector("#btn_sound");
if (document.hidden) {
// 탭이 비활성화됨
if (!button.classList.contains("mute")) {
button.click();
// console.log("탭이 비활성화됨, 음소거");
}
} else {
// 탭이 활성화됨
if (button.classList.contains("mute")) {
button.click();
// console.log("탭이 활성화됨, 음소거 해제");
}
}
}
const registerVisibilityChangeHandler = () => {
document.addEventListener('visibilitychange', handleMuteByVisibility, true);
}
let qualityChangeTimeout = null;
let previousQualityBeforeDowngrade = null;
let previousIsAutoMode = null;
let didChangeToLowest = false;
// 현재 화질 가져오기
const getCurrentInternalQuality = () => {
try {
const playerInfo = unsafeWindow.LivePlayer.getPlayerInfo();
return playerInfo?.quality || null;
} catch (e) {
//console.warn("[getCurrentInternalQuality] 오류 발생:", e);
return null;
}
};
// 자동 모드 여부 가져오기
const getIsAutoQualityMode = () => {
try {
const playerInfo = unsafeWindow.LivePlayer.getPlayerInfo();
return !!playerInfo?.qualityInfo?.isAuto;
} catch (e) {
//console.warn("[getIsAutoQualityMode] 오류 발생:", e);
return false;
}
};
// 화질 변경 실행
const changeQualityLivePlayer = async (qualityName) => {
const current = getCurrentInternalQuality();
if (current === qualityName) {
//console.log(`[화질 변경 스킵] 현재(${current}) = 요청(${qualityName})`);
return;
}
try {
unsafeWindow.livePlayer.changeQuality(qualityName);
//console.log(`[화질 변경] → ${qualityName}`);
} catch (e) {
//console.warn("[changeQualityLivePlayer] 변경 실패:", e);
}
};
// 비활성화/활성화 핸들러
const handleVisibilityChangeForQuality = async () => {
if (!isAutoChangeQualityEnabled || isVideoInPiPMode()) return;
if (document.hidden) {
//console.log("[탭 상태] 비활성화됨");
previousQualityBeforeDowngrade = getCurrentInternalQuality();
previousIsAutoMode = getIsAutoQualityMode();
if (!previousQualityBeforeDowngrade) {
//console.warn("[현재 화질] 정보를 가져오지 못함");
} else {
//console.log(`[현재 화질 저장] ${previousQualityBeforeDowngrade} (자동모드: ${previousIsAutoMode})`);
}
qualityChangeTimeout = setTimeout(async () => {
await changeQualityLivePlayer('LOW'); // LOW = 최저화질
didChangeToLowest = true;
//console.log("[타이머 실행] 최저화질로 전환됨");
}, 6500);
//console.log("[타이머] 몇 초 후 최저화질로 변경 예약됨");
} else {
//console.log("[탭 상태] 활성화됨");
if (qualityChangeTimeout) {
clearTimeout(qualityChangeTimeout);
qualityChangeTimeout = null;
//console.log("[타이머] 예약된 최저화질 변경 취소됨");
}
if (didChangeToLowest && previousQualityBeforeDowngrade) {
const current = getCurrentInternalQuality();
if (previousIsAutoMode) {
if (getIsAutoQualityMode()) {
//console.log("[복귀] 이미 자동 모드이므로 변경 생략");
} else {
await changeQualityLivePlayer('AUTO');
//console.log("[복귀] 자동 모드 복원됨");
}
} else {
if (current === previousQualityBeforeDowngrade) {
//console.log(`[복귀] 현재 화질(${current})과 동일하여 복원 생략`);
} else {
await changeQualityLivePlayer(previousQualityBeforeDowngrade);
//console.log(`[복귀] 수동 화질 복원됨 → ${previousQualityBeforeDowngrade}`);
}
}
} else {
//console.log("[복귀] 화질 변경 없었으므로 복원 생략");
}
// 상태 초기화
didChangeToLowest = false;
previousQualityBeforeDowngrade = null;
previousIsAutoMode = null;
}
};
const registerVisibilityChangeHandlerForQuality = () => {
document.addEventListener('visibilitychange', handleVisibilityChangeForQuality, true);
};
const qualityNameToInternalType = {
sd: 'LOW',
hd: 'NORMAL',
hd4k: 'HIGH_4000',
hd8k: 'HIGH_8000',
original: 'ORIGINAL',
auto: 'AUTO'
};
const downgradeFrom1440p = async () => {
try {
const livePlayer = await waitForLivePlayer();
const info = await livePlayer.getLiveInfo();
const presets = info.CHANNEL.VIEWPRESET.filter(p => p.name !== 'auto' && p.bps);
const index1440 = presets.findIndex(p => p.label_resolution === '1440');
if (index1440 === -1) {
//console.warn('1440p 화질 정보를 찾을 수 없습니다.');
return;
}
if (index1440 === 0) {
//console.log('1440p가 최저 화질이라서 더 낮출 수 없습니다.');
return;
}
const lowerPreset = presets[index1440 - 1];
const targetName = qualityNameToInternalType[lowerPreset.name];
if (!targetName) {
//console.warn(`하위 화질 ${lowerPreset.name}에 대한 매핑이 없습니다.`);
return;
}
//console.log(`1440p에서 ${lowerPreset.label}(${targetName})로 다운그레이드 시도`);
livePlayer.changeQuality(targetName);
} catch (e) {
console.error(e.message);
}
};
const appendPauseButton = () => {
try {
const checkInterval = 250;
let elapsedTime = 0;
let intervalId;
let buttonCreated = false; // 버튼 생성 여부 체크 변수 추가
const checkLiveViewStatus = () => {
const closeStreamButton = document.body.querySelector("#closeStream");
const playerDiv = document.body.querySelector("#player");
const isPlayerPresent = !!playerDiv; // null 체크를 간단히
const isMouseoverClass = isPlayerPresent && playerDiv.classList.contains("mouseover");
const isTimeover = isPlayerPresent && (elapsedTime > 30);
if (closeStreamButton) {
// 버튼이 이미 존재하면 체크
if (!isMouseoverClass && !isTimeover) {
// 마우스 오버 상태가 아니고 시간 초과가 아닐 때 버튼 제거
closeStreamButton.remove();
buttonCreated = false; // 버튼이 제거됨
}
} else if ((!closeStreamButton && isMouseoverClass) || isTimeover) {
// 버튼이 없고 마우스 오버 상태이거나 시간 초과일 때만 생성
if (!buttonCreated) {
createCloseStreamButton();
buttonCreated = true; // 버튼이 생성됨
}
}
elapsedTime += checkInterval / 1000; // 초 단위로 변환하여 증가
};
const createCloseStreamButton = () => {
waitForElement('button#time_shift_play', (elementSelector, element) => {
if (window.getComputedStyle(element).display === 'none') { // Time Shift 기능이 비활성화된 경우
const ctrlDiv = document.body.querySelector('div.ctrl');
const newCloseStreamButton = document.createElement("button");
newCloseStreamButton.type = "button";
newCloseStreamButton.id = "closeStream";
newCloseStreamButton.className = "pause on";
const tooltipDiv = document.createElement("div");
tooltipDiv.className = "tooltip";
const spanElement = document.createElement("span");
spanElement.textContent = "일시정지";
tooltipDiv.appendChild(spanElement);
newCloseStreamButton.appendChild(tooltipDiv);
ctrlDiv.insertBefore(newCloseStreamButton, ctrlDiv.firstChild);
newCloseStreamButton.addEventListener("click", (e) => {
e.preventDefault();
toggleStream(newCloseStreamButton, spanElement);
});
}
});
};
const toggleStream = (button, spanElement) => {
try {
if (button.classList.contains("on")) {
unsafeWindow.livePlayer.closeStreamConnector();
button.classList.remove("on", "pause");
button.classList.add("off", "play");
spanElement.textContent = "재생";
} else {
unsafeWindow.livePlayer._startBroad();
button.classList.remove("off", "play");
button.classList.add("on", "pause");
spanElement.textContent = "일시정지";
}
} catch (error) {
console.log(error);
}
};
// setInterval을 사용해 일정 간격으로 체크
intervalId = setInterval(checkLiveViewStatus, checkInterval);
} catch (error) {
console.error(error);
}
};
const detectPlayerChangeAndAppendPauseButton = () => {
const updateBjIdIfMismatch = () => {
const currentUrl = window.location.href;
const urlBjId = currentUrl.split('/')[3];
const infoNickName = document.querySelector('#infoNickName');
const dataBjId = infoNickName.getAttribute('data-bj_id');
const streamerNick = document.querySelector('#streamerNick');
if (dataBjId !== urlBjId) {
infoNickName.setAttribute('data-bj_id', urlBjId);
streamerNick.setAttribute('data-bj_id', urlBjId);
}
};
const handleMutations = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
appendPauseButton();
emptyViewStreamer();
updateBjIdIfMismatch();
setTimeout(downgradeFrom1440p, 4000);
}
}
};
appendPauseButton();
const targetNode = document.body.querySelector('#infoNickName');
const observer = new MutationObserver(handleMutations);
observer.observe(targetNode, { childList: true, subtree: true });
}
const emptyViewStreamer = () => {
const viewStreamer = document.getElementById('view_streamer');
if (viewStreamer) {
viewStreamer.innerHTML = '';
}
}
const setWidthNickname = (wpx) => {
if (typeof wpx === 'number' && wpx > 0) { // wpx가 유효한 값인지 확인
GM_addStyle(`
.starting-line .chatting-list-item .message-container .username {
width: ${wpx}px !important;
}
`);
} else {
//console.warn('Invalid width value provided for setWidthNickname.'); // 유효하지 않은 값 경고
}
}
const hideBadges = () => {
const badgeSettings = [
{ key: 'isHideSupporterBadgeEnabled', className: 'support' },
{ key: 'isHideFanBadgeEnabled', className: 'fan' },
{ key: 'isHideSubBadgeEnabled', className: 'sub' },
{ key: 'isHideVIPBadgeEnabled', className: 'vip' },
{ key: 'isHideManagerBadgeEnabled', className: 'manager' },
{ key: 'isHideStreamerBadgeEnabled', className: 'streamer' }
];
// 각 배지 숨김 설정 값 가져오기
const settings = badgeSettings.map(setting => ({
key: setting.key,
enabled: GM_getValue(setting.key),
className: setting.className
}));
// 모든 배지 숨김 설정이 비활성화된 경우 종료
if (!settings.some(setting => setting.enabled)) {
return;
}
// 활성화된 설정에 대한 CSS 규칙 생성
let cssRules = settings
.filter(setting => setting.enabled)
.map(setting => `[class^="grade-badge-${setting.className}"] { display: none !important; }`)
.join('\n');
// 서브 배지용 CSS 규칙 추가
if (settings.find(s => s.className === 'sub' && s.enabled)) {
const thumbSpanSelector = CURRENT_URL.startsWith("https://play.sooplive.co.kr/")
? '#chat_area div.username > button > span.thumb'
: '#chatMemo div.username > button > span.thumb';
cssRules += `\n${thumbSpanSelector} { display: none !important; }`;
}
// CSS 규칙 한 번만 적용
GM_addStyle(cssRules);
};
// 디바운스 함수 구현
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// registeredWords 배열 초기화 - 여러 번 실행되지 않도록 전역에서 한 번만 설정
const rw = registeredWords ? registeredWords.split(',').map(word => word.trim()).filter(Boolean) : [];
const observeChat = (elementSelector, elem) => {
hideBadges();
if (!isBlockWordsEnabled) return;
if (rw.length === 0) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach(({ addedNodes }) => {
addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// 메시지 요소를 직접 감지하거나 하위에서 찾음
const message = node.matches?.('div.message-text > p.msg')
? node
: node.querySelector?.('div.message-text > p.msg');
if (message) {
deleteMessages([message]);
}
});
});
});
observer.observe(elem, { childList: true, subtree: true });
};
const deleteMessages = (messages) => {
if (!Array.isArray(messages) || messages.length === 0) return;
// 필터 단어가 없으면 아무것도 하지 않음
if (rw.length === 0) return;
for (const message of messages) {
const messageText = message.textContent.trim();
let shouldRemove = false;
for (const word of rw) {
const isExactCheck = word.startsWith("e:");
const wordToCheck = isExactCheck ? word.slice(2) : word;
if ((isExactCheck && messageText === wordToCheck) ||
(!isExactCheck && messageText.includes(wordToCheck))) {
shouldRemove = true;
break;
}
}
if (shouldRemove) {
const listItem = message.closest('.chatting-list-item');
if (listItem) {
listItem.remove();
}
}
}
};
const autoClaimGem = () => {
const element = document.querySelector('#actionbox > div.ic_gem');
// 요소가 존재하고, display 속성이 'none'이 아닌 경우 클릭
if (element && getComputedStyle(element).display !== 'none') {
element.click();
}
}
// 비디오 재생 건너뛰기 및 입력란 확인 함수
const videoSkipHandler = (e) => {
const activeElement = document.activeElement;
const tagName = activeElement.tagName.toLowerCase();
// 입력란 활성화 여부 체크
const isInputActive = (tagName === 'input') ||
(tagName === 'textarea') ||
(activeElement.id === 'write_area') ||
(activeElement.contentEditable === 'true');
// 입력란이 활성화되어 있지 않은 경우 비디오 제어
if (!isInputActive) {
const video = document.querySelector('video');
if (video) {
switch (e.code) {
case 'ArrowRight':
// 오른쪽 방향키: 동영상을 1초 앞으로 이동
video.currentTime += 1;
break;
case 'ArrowLeft':
// 왼쪽 방향키: 동영상을 1초 뒤로 이동
video.currentTime -= 1;
break;
}
}
}
}
const homePageCurrentTab = () => {
waitForElement('#logo > a', function (elementSelector, element) {
element.removeAttribute("target");
});
}
const useBottomChat = () => {
const toggleBottomChat = () => {
const playerArea = document.querySelector('#player_area');
if (!playerArea) {
console.warn('#player_area 요소를 찾을 수 없습니다.');
return;
}
const playerHeight = playerArea.getBoundingClientRect().height;
const browserHeight = window.innerHeight;
const isPortrait = window.innerHeight * 1.1 > window.innerWidth;
document.body.classList.toggle('bottomChat', isPortrait);
};
window.addEventListener('resize', debounce(toggleBottomChat, 500));
toggleBottomChat();
};
// #nAllViewer의 숫자를 변환하여 리턴하는 함수
const getViewersNumber = (raw = false) => {
const element = document.querySelector('#nAllViewer');
if (!element) return '0';
// 요소의 텍스트에서 쉼표 제거 후 숫자 처리
const rawNumber = element.innerText.replace(/,/g, '').trim();
// raw가 truthy한 값이면 (1, true 등) 숫자를 변환하지 않고 원본 그대로 반환
if (Boolean(raw)) {
return rawNumber;
}
return addNumberSeparator(rawNumber);
};
let previousViewers = 0; // 이전 시청자 수를 저장할 변수
let previousTitle = ''; // 이전 제목을 저장할 변수
// 제목 표시줄을 업데이트하는 함수
const updateTitleWithViewers = () => {
const originalTitle = document.title.split(' ')[0]; // 기존 제목의 첫 번째 단어
const viewers = getViewersNumber(true); // 현재 시청자 수 갱신
const formattedViewers = addNumberSeparatorAll(viewers); // 형식화된 시청자 수
let title = originalTitle;
if (originalTitle !== previousTitle) {
previousViewers = 0; // 제목이 변경되면 이전 시청자 수 초기화
}
if (viewers && previousViewers) {
if (viewers > previousViewers) {
title += ` 🔺${formattedViewers}`;
} else if (viewers < previousViewers) {
title += ` 🔻${formattedViewers}`;
} else {
title += ` • ${formattedViewers}`; // 시청자 수가 변동 없을 때
}
} else {
title += ` • ${formattedViewers}`; // 시청자 수가 변동 없을 때
}
document.title = title; // 제목을 업데이트
previousViewers = viewers; // 이전 시청자 수 업데이트
previousTitle = originalTitle; // 현재 제목을 이전 제목으로 업데이트
};
const unlockCopyPaste = () => {
const writeArea = document.getElementById('write_area');
// 복사 기능
const handleCopy = (event) => {
event.preventDefault(); // 기본 복사 동작 막기
const selectedText = window.getSelection().toString(); // 선택된 텍스트 가져오기
if (selectedText) {
event.clipboardData.setData('text/plain', selectedText); // 클립보드에 텍스트 쓰기
}
};
// 잘라내기 기능
const handleCut = (event) => {
event.preventDefault(); // 기본 잘라내기 동작 막기
const selectedText = window.getSelection().toString(); // 선택된 텍스트 가져오기
if (selectedText) {
event.clipboardData.setData('text/plain', selectedText); // 클립보드에 텍스트 쓰기
document.execCommand("delete"); // 선택된 텍스트 삭제
}
};
// 붙여넣기 기능
const handlePaste = (event) => {
event.preventDefault(); // 기본 붙여넣기 동작 막기
const text = (event.clipboardData || window.clipboardData).getData('text'); // 클립보드에서 텍스트 가져오기
document.execCommand("insertText", false, text); // 텍스트를 수동으로 삽입
};
// 이벤트 리스너 등록
writeArea.addEventListener('copy', handleCopy);
writeArea.addEventListener('cut', handleCut);
writeArea.addEventListener('paste', handlePaste);
}
const alignNicknameRight = () => {
GM_addStyle(`
.starting-line .chatting-list-item .message-container .username > button {
float: right !important;
white-space: nowrap;
}
`);
}
const hideButtonsAboveChatInput = () => {
const style = `
.chatbox .actionbox .chat_item_list {
display: none !important;
}
.chatbox .actionbox {
height: auto !important;
}
`;
GM_addStyle(style);
}
const addStyleExpandLiveChat = () => {
const style = `
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) #serviceHeader,
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) .broadcast_information,
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) .section_selectTab,
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) .wrapping.player_bottom{
display: none !important;
}
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) #webplayer_contents,
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) #sidebar {
top: 0 !important;
margin-top: 0 !important;
min-height: 100vh !important;
}
body.expandLiveChat:not(.screen_mode,.fullScreen_mode) #webplayer #webplayer_contents .wrapping.side {
padding: 0 !important;
}
`;
GM_addStyle(style);
}
const makeExpandChatButton = (el, css_class) => {
if (!el) return;
// li 요소 생성
const li = document.createElement('li');
li.className = 'expand-toggle-li';
// a 요소 생성
const a = document.createElement('a');
a.href = 'javascript:;';
a.setAttribute('tip', '확장/축소(x)');
a.textContent = '확장/축소(x)';
// 클릭 이벤트 등록 (a에 등록해도 되고 li에 등록해도 됨)
a.addEventListener('click', () => {
document.body.classList.toggle(css_class);
});
// li에 a 추가, 그리고 el에 li 추가
li.appendChild(a);
el.appendChild(li);
};
function setupChatMessageTrackers(element) {
const OriginalWebSocket = window.WebSocket;
const targetUrlPattern = /^wss:\/\/chat-[\w\d]+\.sooplive\.co\.kr/;
const MAX_MESSAGES = 500;
const messageHistory = [];
const bannedMessages = [];
const targetUserMessages = [];
let bannedWindow = null;
let targetWindow = null;
let banIcon = null;
let highlightIcon = null;
const selectedUsersArray = selectedUsers ? selectedUsers.split(',').map(user_id => user_id.trim()).filter(Boolean) : [];
const targetUserIdSet = new Set([...allFollowUserIds, ...selectedUsersArray]);
const highlightPosition = isShowDeletedMessagesEnabled ? "40px" : "10px" ;
let totalChatCount = 0;
let lastChatCount = 0;
let last10Intervals = [];
if (isChatCounterEnabled) {
// 채팅 속도 디스플레이
// 1. CPS 표시용 div 생성 및 삽입
const container = document.querySelector('.chatting-item-wrap');
const cpsDisplay = document.createElement('div');
cpsDisplay.id = 'cps_display';
container.appendChild(cpsDisplay);
// 2. 스타일 적용
Object.assign(cpsDisplay.style, {
position: 'absolute',
top: '8px',
left: '8px',
background: 'rgba(0, 0, 0, 0.3)',
color: '#fff',
fontSize: '14px',
padding: '4px 8px',
borderRadius: '4px',
zIndex: '10',
pointerEvents: 'none'
});
// 3. CPS 계산 및 표시
setInterval(() => {
const delta = totalChatCount - lastChatCount;
lastChatCount = totalChatCount;
last10Intervals.push(delta);
if (last10Intervals.length > 10) {
last10Intervals.shift();
}
const sum = last10Intervals.reduce((a, b) => a + b, 0);
const avg = sum / 5;
cpsDisplay.textContent = `${Math.round(avg)}개/s`;
}, 500);
// 채팅 속도 디스플레이 끝
}
// 스타일 추가
GM_addStyle(`
#cps_display {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
pointer-events: none;
}
.chat-icon {
position: absolute;
bottom: 10px;
right: 6px;
width: 24px;
height: 24px;
cursor: pointer;
z-index: 1000;
background-size: contain;
background-repeat: no-repeat;
}
.chat-icon.highlight {
right: 7px;
width: 22px;
height: 22px;
bottom: ${highlightPosition};
}
html:not([dark="true"]) .trash-icon {
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2264%22%20height%3D%2264%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23000%22%20stroke-width%3D%220%22%3E%3Cg%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke%3D%22%23CCC%22%20stroke-width%3D%22.192%22%2F%3E%3Cg%20fill%3D%22%236A6A75%22%20stroke%3D%22none%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M10.31%202.25h3.38c.217%200%20.406%200%20.584.028a2.25%202.25%200%200%201%201.64%201.183c.084.16.143.339.212.544l.111.335.03.085a1.25%201.25%200%200%200%201.233.825h3a.75.75%200%200%201%200%201.5h-17a.75.75%200%200%201%200-1.5h3.09a1.25%201.25%200%200%200%201.173-.91l.112-.335c.068-.205.127-.384.21-.544a2.25%202.25%200%200%201%201.641-1.183c.178-.028.367-.028.583-.028Zm-1.302%203a3%203%200%200%200%20.175-.428l.1-.3c.091-.273.112-.328.133-.368a.75.75%200%200%201%20.547-.395%203%203%200%200%201%20.392-.009h3.29c.288%200%20.348.002.392.01a.75.75%200%200%201%20.547.394c.021.04.042.095.133.369l.1.3.039.112q.059.164.136.315z%22%2F%3E%3Cpath%20d%3D%22M5.915%208.45a.75.75%200%201%200-1.497.1l.464%206.952c.085%201.282.154%202.318.316%203.132.169.845.455%201.551%201.047%202.104s1.315.793%202.17.904c.822.108%201.86.108%203.146.108h.879c1.285%200%202.324%200%203.146-.108.854-.111%201.578-.35%202.17-.904.591-.553.877-1.26%201.046-2.104.162-.813.23-1.85.316-3.132l.464-6.952a.75.75%200%200%200-1.497-.1l-.46%206.9c-.09%201.347-.154%202.285-.294%202.99-.137.685-.327%201.047-.6%201.303-.274.256-.648.422-1.34.512-.713.093-1.653.095-3.004.095h-.774c-1.35%200-2.29-.002-3.004-.095-.692-.09-1.066-.256-1.34-.512-.273-.256-.463-.618-.6-1.302-.14-.706-.204-1.644-.294-2.992z%22%2F%3E%3Cpath%20d%3D%22M9.425%2010.254a.75.75%200%200%201%20.821.671l.5%205a.75.75%200%200%201-1.492.15l-.5-5a.75.75%200%200%201%20.671-.821m5.15%200a.75.75%200%200%201%20.671.82l-.5%205a.75.75%200%200%201-1.492-.149l.5-5a.75.75%200%200%201%20.82-.671Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
html[dark="true"] .trash-icon {
background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2264%22%20height%3D%2264%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23000%22%20stroke-width%3D%220%22%3E%3Cg%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke%3D%22%23CCC%22%20stroke-width%3D%22.192%22%2F%3E%3Cg%20fill%3D%22%2394949C%22%20stroke%3D%22none%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M10.31%202.25h3.38c.217%200%20.406%200%20.584.028a2.25%202.25%200%200%201%201.64%201.183c.084.16.143.339.212.544l.111.335.03.085a1.25%201.25%200%200%200%201.233.825h3a.75.75%200%200%201%200%201.5h-17a.75.75%200%200%201%200-1.5h3.09a1.25%201.25%200%200%200%201.173-.91l.112-.335c.068-.205.127-.384.21-.544a2.25%202.25%200%200%201%201.641-1.183c.178-.028.367-.028.583-.028Zm-1.302%203a3%203%200%200%200%20.175-.428l.1-.3c.091-.273.112-.328.133-.368a.75.75%200%200%201%20.547-.395%203%203%200%200%201%20.392-.009h3.29c.288%200%20.348.002.392.01a.75.75%200%200%201%20.547.394c.021.04.042.095.133.369l.1.3.039.112q.059.164.136.315z%22%2F%3E%3Cpath%20d%3D%22M5.915%208.45a.75.75%200%201%200-1.497.1l.464%206.952c.085%201.282.154%202.318.316%203.132.169.845.455%201.551%201.047%202.104s1.315.793%202.17.904c.822.108%201.86.108%203.146.108h.879c1.285%200%202.324%200%203.146-.108.854-.111%201.578-.35%202.17-.904.591-.553.877-1.26%201.046-2.104.162-.813.23-1.85.316-3.132l.464-6.952a.75.75%200%200%200-1.497-.1l-.46%206.9c-.09%201.347-.154%202.285-.294%202.99-.137.685-.327%201.047-.6%201.303-.274.256-.648.422-1.34.512-.713.093-1.653.095-3.004.095h-.774c-1.35%200-2.29-.002-3.004-.095-.692-.09-1.066-.256-1.34-.512-.273-.256-.463-.618-.6-1.302-.14-.706-.204-1.644-.294-2.992z%22%2F%3E%3Cpath%20d%3D%22M9.425%2010.254a.75.75%200%200%201%20.821.671l.5%205a.75.75%200%200%201-1.492.15l-.5-5a.75.75%200%200%201%20.671-.821m5.15%200a.75.75%200%200%201%20.671.82l-.5%205a.75.75%200%200%201-1.492-.149l.5-5a.75.75%200%200%201%20.82-.671Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
}
html:not([dark="true"]) .highlight-icon {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAADyUlEQVR4nO1YTWhdVRA+/TH+Ba3gorgQFBS0ulEsqItXFCWLmuZ8k0GSvDtzo/C0YFu0+IfIU7rQnSKCIIo7XYmRIiL+oCsRBbFimwr+ddOVWKy2aovK5OYl552Q9+59P7kp3A8O5OWe+c7MnJk5c45zFSpUqFBhLTA9PX3Z5KTMAjoHyDygfwByGtAfAX3Te/HMvKkbj80hSgHIW5nsAodxzRPJO4Cmu3bploEpzswXAvIkICeI9L/OQ44wJ7euxkWU3mZzcvD8BugTqnpBX8qPj993BaBfdl+wbZwxg51zG1o8zWZzI6BP2bciXIB+YTr0rDyRHIsIvyHSvcyz19fr9YvNQ/Y3oPuJ5OfIiy+1uAB5Ofr2E5E8PDGRXGccxkUk2wDZB8ihaO6xwkZkYRN6Xv6anJQHzJOdZEzpyIvPEMmz0f9e7BQazWZzI5E+aGuGO1EonCwEQuWZk1p+WX109ZBIHsnLQ6Q7IiMey11twoQ1z+dddHlxeW2l8vJqUR5AdoeJnas6WakMY75T2KyGnTsbFxHpt4HyhyzEivIw86b2nEi0q5DV4sBze12PmJhIrl2s7UeA9JpeeQDZFzji7a4CRHq0JWAVxpUMItkWGDDfVQDQk8sG8KgrGcw8GoT0ya4Cdqy3BGZmZi5xJYO5cWlBAyxulzL/blcyvNexICcPdxWwclcoaYYMQOcCfV7pKkCkdwYW/8ss211JYNabTYdAnx155DYs9jytMPq4l7OgXzSzluKTwPtf5xb2Pr0jauL2uzUGVrQkBfORSN4IrP9nLUPJ+/QmQP8O1n+9MEnW4urhsAVm1q1uyGDWrVm7vaT8d9aa9EiW3gjoqSCUvhrm4cbMo7ZGsN4p7+s39EUKKAF6NtiJ92q12mY3YNRqtc3GHSh/1u7OAyG3ljpK6oNjY3vOHwi5c67RaJxnZ064hve6xw0SRHogMmKOmUf65WXmkfCwWhwH3DAQG0Ek7/fzcsDMI0T6bsT5ghsmsntu20580MtlxUIQ0INrqnwLRPp0tOUfFil1NheQj0pRvgUieTzaiU/ztN82h0g/i5R/zpWBxUv3UrNlNXxqauryTg8GRPJ5ZPjzrkwA8lC7EXaBX3liZyfs8kXfZPygS2WvIJJ6+GyYPdrOXt36zpxcSSTfh4cUoPe79QQgudeavkDJX+xlgrl+FZH8EHj+DKCJW4/wXsbbX9PkuI3g92kgvcetZ1D2JPh7/DJHJH96n97lzgV4n94CyK+B508Aers7l8AL99mFxD1qF5Sy9alQoUIFNxT8D3rbJQHjF4hlAAAAAElFTkSuQmCC");
}
html[dark="true"] .highlight-icon {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAD1ElEQVR4nO2YXYhVVRTHd6NZ1tAH9CA9CAYJZb4oCdVDUhS9GFQPvSgWPViQiklmRJzCh3orIgijGFAo2Gf9z5mud85aZ6Y69SSSEBrlFGTli0+hqGkf0sSac+/Mvnvy3nvu5wjnBxcunLP+e6191tp77W1MSUlJSckgqFartwLyLJGMAzIN8AWALwF8EpBPgOQJa+2SVjr6ThTxk0T8aW47q3GhphkT8TNxnN3SM8ettcuJ+FWAzwIy0/zHJ6KI77uSVhjK/fpOKx0iPkMke8fGsuu7cn58fPJ2gL9p7XjD4P9owDMzM9fUdYIgGCGS1/RZQa0j6kPHzhPxKU/0GMA7rE3uPnAgvVFnSP8TyW5AfvUGf6+uRSTvNz6TX4B0VxRN3qUaqhWGvIaIdxLJcU/nVOEgNG3cmSeSPwHepjPZzEad9lLqDYDf9Cbh3WapEQTBSBTx8/mY81+iUDrlOT/vfBQlDxawfblJSrzUrk4Y8kYviD0FVhu3YHmbKQggH/2P8x8W1SGSF9zCbmt10qXSzflmaXMlKpXKDUT8nfMVj2uKFdWx1i5xayIM061tBMCxM/s7TIdE0cTq2tp+gmjyzk51iHinMxFow0B+rBvoCmOGTBjyGicjplsaAHx+PoBs1AwZa7NRJyPOtzSobeuzBgcPJjeZIWPt1M0FA9C8nSuaRwfiZRMAecxZiX4wrdDlrlDR9BnKm8e6Px+0NAjD5GEn4n+jSDaYIRHH6Xr1YT4jeGNLI23C8p5nLogvOtkLuiUIghGAv3T8+LZtYyB9yGu+dpsBQ15LUrgeARlzqv/vQaYSwOsA+csZ/+PCItriatW7LbC11RWmz1hbXZG323Oz/722Jh2JxfHEWkAuOnl4tJ+bm7XZqI7hOH8RSO/pSpRIngLksiM6kWXZUtNjsixbqtrOOJf17NwTcW2pvdb4UJIk1/VE3Bizf//Ra3XP8Vrw7aaXALLPW5nGrbXLutW11i5zN6vab5/pBwuDYO7m5sDmzn/mab5j+kl+zm0496adHFY0BYn40ECdrwPw694nnyqy1Om7gHw+FOfrAPKKVxNftdN+6zsAf+19xbfMMNBDt9ts6RpeqVRua3ZhQCSHvZl/2wwTInmxMQg9wC/csfMd1j3oz9psN4uBMEw2N14b8kmiqTvqz62dWgnwT+4mRZQ8ZxYTRPK0Nn1OEL/pzYS16SqAf/buTreYxQjAj7u3aYCcrv3qQV0iSjeZxUyYXwmeW3gzJ38A8oi5GgCSewH+3Zn5s0TpA+ZqIo7T9Vq4+UUZrxu2PyUlJSWmL/wH5eEJ5sFzGH4AAAAASUVORK5CYII=");
}
`);
// 빨간 점 토글 함수
const toggleRedDot = (icon, shouldShow) => {
if (!icon) return;
const existingDot = icon.querySelector(".red-dot");
if (shouldShow && !existingDot) {
const redDot = document.createElement("div");
redDot.classList.add("red-dot");
Object.assign(redDot.style, {
position: "absolute",
top: "0px",
right: "0px",
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "red",
zIndex: 1001
});
icon.appendChild(redDot);
} else if (!shouldShow && existingDot) {
existingDot.remove();
}
};
const recordMessage = (userId, userName, message, timestamp) => {
messageHistory.push({ userId, userName, message, timestamp });
if (messageHistory.length > MAX_MESSAGES) {
messageHistory.shift();
}
if (isShowSelectedMessagesEnabled && targetUserIdSet.has(userId)) {
targetUserMessages.push({ userId, userName, message, timestamp });
updateTargetMessages();
}
};
const decodeMessage = (data) => {
const decoder = new TextDecoder("utf-8");
const decodedText = decoder.decode(data);
const parts = decodedText.split("\x0c");
const now = new Date();
const timestamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
if (parts.length === 13 || parts.length === 14) {
const message = parts[1];
const userId = parts[2].split('(')[0];
const userName = parts[6];
if (userId.includes('|') || userName.includes('|') || !userId || !userName || userName === '0') return;
recordMessage(userId, userName, message, timestamp);
if(isChatCounterEnabled) totalChatCount++; // ✅ 여기서 1씩 증가
} else if (parts[1] === '-1' && parts[4] === '2') {
const userId = parts[2].split('(')[0];
const userName = parts[3];
if (userId.includes('|') || userName.includes('|') || !userId || !userName) return;
if (isShowDeletedMessagesEnabled) {
const userMessages = messageHistory.filter(msg => msg.userId === userId);
bannedMessages.push(...userMessages);
bannedMessages.push({
userId,
userName,
message: "[강제퇴장 됨]",
timestamp
});
updateBannedMessages();
}
}
};
// WebSocket Hook
unsafeWindow.WebSocket = function(url, protocols) {
const ws = new OriginalWebSocket(url, protocols);
if (targetUrlPattern.test(url)) {
ws.addEventListener("message", (event) => decodeMessage(event.data));
}
return ws;
};
unsafeWindow.WebSocket.prototype = OriginalWebSocket.prototype;
// 아이콘 생성
const createIcon = (type, onClick) => {
const icon = document.createElement("div");
icon.classList.add("chat-icon", type === "highlight" ? "highlight-icon" : "trash-icon", type);
icon.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
onClick();
});
element.appendChild(icon);
return icon;
};
const showBannedMessages = () => {
if (bannedWindow && !bannedWindow.closed) {
bannedWindow.focus();
return;
}
const width = 600;
const height = 600;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
const features = `width=${width},height=${height},left=${left},top=${top}`;
bannedWindow = window.open("", "_blank", features);
bannedWindow.document.write(`
강제퇴장된 유저의 채팅
`);
bannedWindow.document.close();
updateBannedMessages();
};
const updateBannedMessages = () => {
if (!bannedWindow || bannedWindow.closed) {
toggleRedDot(banIcon, true);
return;
}
toggleRedDot(banIcon, false);
const messageList = bannedWindow.document.getElementById("bannedMessages");
messageList.replaceChildren();
if (!Array.isArray(bannedMessages) || bannedMessages.length === 0) {
const noMessageItem = document.createElement("li");
noMessageItem.textContent = "메시지가 없습니다.";
messageList.appendChild(noMessageItem);
} else {
bannedMessages.forEach(msg => {
const systemMessage = msg.message === `[강제퇴장 됨]`;
const listItem = document.createElement("li");
const timestampSpan = document.createElement("span");
timestampSpan.className = "message-timestamp";
timestampSpan.textContent = `[${msg.timestamp}]`;
const nameTag = systemMessage
? document.createElement("i")
: document.createElement("strong");
nameTag.textContent = systemMessage
? `${msg.userName} (${msg.userId}) 님이 강제 퇴장 되었습니다.`
: `${msg.userName} (${msg.userId})`;
const text = systemMessage ? '' : `: ${msg.message}`;
const messageText = document.createTextNode(text);
listItem.appendChild(timestampSpan);
listItem.appendChild(document.createTextNode(" "));
listItem.appendChild(nameTag);
listItem.appendChild(messageText);
messageList.insertBefore(listItem, messageList.firstChild);
});
}
};
const showTargetMessages = () => {
if (targetWindow && !targetWindow.closed) {
targetWindow.focus();
return;
}
const width = 600;
const height = 600;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
const features = `width=${width},height=${height},left=${left},top=${top}`;
targetWindow = window.open("", "_blank", features);
targetWindow.document.write(`
지정 유저 채팅 | 즐겨찾기 ${allFollowUserIds.length}명 | 수동 입력 ${selectedUsersArray.length}명
`);
targetWindow.document.close();
updateTargetMessages();
};
const updateTargetMessages = () => {
if (!targetWindow || targetWindow.closed) {
toggleRedDot(highlightIcon, true);
return;
}
toggleRedDot(highlightIcon, false);
const messageList = targetWindow.document.getElementById("targetUserMessages");
messageList.replaceChildren();
if (!Array.isArray(targetUserMessages) || targetUserMessages.length === 0) {
const noMessageItem = document.createElement("li");
noMessageItem.textContent = "메시지가 없습니다.";
messageList.appendChild(noMessageItem);
} else {
targetUserMessages.forEach(msg => {
const li = document.createElement("li");
li.textContent = `[${msg.timestamp}] ${msg.userName} (${msg.userId}): ${msg.message}`;
messageList.insertBefore(li, messageList.firstChild);
});
}
};
const resetChatData = () => {
messageHistory.length = 0;
bannedMessages.length = 0;
targetUserMessages.length = 0;
updateBannedMessages();
updateTargetMessages();
toggleRedDot(banIcon, false);
toggleRedDot(highlightIcon, false);
};
unsafeWindow.resetChatData = resetChatData;
// 조건부 실행
if (isShowDeletedMessagesEnabled) {
banIcon = createIcon("trash", showBannedMessages);
}
if (isShowSelectedMessagesEnabled) {
highlightIcon = createIcon("highlight", showTargetMessages);
}
}
const disableAutoVOD = () => {
const container = unsafeWindow.liveView?.aContainer?.[1];
if (container) {
if (container.autoPlayVodBanner) {
container.autoPlayVodBanner.show = () => {};
}
} else {
setTimeout(disableAutoVOD, 1000); // container가 없으면 재시도
}
};
function isElementVisible(selector) {
const el = document.querySelector(selector);
if (!el) return false; // 요소가 없음
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false; // CSS로 숨겨진 경우
}
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false; // 크기가 0인 경우
}
// 화면 안에 일부라도 보이는 경우
return (
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
rect.left < (window.innerWidth || document.documentElement.clientWidth)
);
}
function updateBodyClass(targetClass) {
if (!window.matchMedia("(orientation: portrait)").matches) {
document.body.classList.remove(targetClass);
document.querySelector('.expand-toggle-li').style.display = 'none';
} else {
document.querySelector('.expand-toggle-li').style.display = 'block';
}
}
function makeCaptureButton() {
const svgDataUrl = 'data:image/svg+xml,%3Csvg%20width%3D%2264%22%20height%3D%2264%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23fff%22%3E%3Cg%20stroke-width%3D%220%22%2F%3E%3Cg%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke%3D%22%23CCC%22%20stroke-width%3D%22.048%22%2F%3E%3Cg%20stroke-width%3D%221.488%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M21%2013c0-2.667-.5-5-1-5.333-.32-.214-1.873-.428-4-.553C14.808%207.043%2017%205%2012%205S9.192%207.043%208%207.114c-2.127.125-3.68.339-4%20.553C3.5%208%203%2010.333%203%2013s.5%205%201%205.333S8%2019%2012%2019s7.5-.333%208-.667c.5-.333%201-2.666%201-5.333%22%2F%3E%3Cpath%20d%3D%22M12%2016a3%203%200%201%200%200-6%203%203%200%200%200%200%206%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E';
// 1. CSS 삽입
const style = document.createElement('style');
style.textContent = `
#player .imageCapture {
overflow: visible;
color: rgba(0, 0, 0, 0);
width: 32px;
height: 32px;
margin: 0;
font-size: 0;
opacity: 0.9;
background: url("${svgDataUrl}") 50% 50% no-repeat;
background-size: 82%;
border: none;
padding: 0;
cursor: pointer;
position: relative;
}
#player .imageCapture:hover {
opacity: 1;
}
`;
document.head.appendChild(style);
const captureAndOpenBlob = () => {
const video = document.getElementById('livePlayer') || document.getElementById('video');
if (!video) return;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
canvas.toBlob(blob => {
if (!blob) return;
const imgURL = URL.createObjectURL(blob);
const html = `
ScreenShot (${video.videoWidth}x${video.videoHeight})
다운로드 capture_${timestamp}.jpg
`;
const blobURL = URL.createObjectURL(new Blob(
[html],
{ type: 'text/html;charset=UTF-8' }
));
window.open(blobURL, '_blank');
}, 'image/jpeg', 0.92);
};
// 2. 버튼 생성
const createButton = () => {
const btn = document.createElement('button');
btn.className = 'imageCapture';
btn.type = 'button';
btn.title = '비디오 스크린샷';
btn.addEventListener('click', () => {
try {
captureAndOpenBlob();
} catch (err) {
console.error('캡처 실패:', err);
}
});
return btn;
};
// 3. 버튼 삽입
const insertButton = () => {
const container = document.querySelector('#player .player_ctrlBox .ctrlBox .right_ctrl');
if (container && !container.querySelector('.imageCapture')) {
const btn = createButton();
container.insertBefore(btn, container.firstChild);
}
};
// 4. DOM 변화 감지 + 초기 실행
const observer = new MutationObserver(() => {
insertButton();
const container = document.querySelector('#player .player_ctrlBox .ctrlBox .right_ctrl');
if (container && container.querySelector('.imageCapture')) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
//=================================플레이어 페이지 함수 끝=================================//
//============================ VOD 페이지 함수 시작 ============================//
// 미디어 정보 확인 함수
const checkMediaInfo = async (mediaName) => {
if (mediaName !== 'original') { // 원본 화질로 설정되지 않은 경우
const player = await waitForElementAsync('#player');
player.className = 'video mouseover ctrl_output';
// 설정 버튼 클릭
const settingButton = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box > button.btn_setting');
settingButton.click();
// 화질 변경 리스트 대기
const settingList = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box.on .setting_list');
const spanElement = Array.from(settingList.querySelectorAll('span')).find(el => el.textContent.includes("화질 변경"));
const buttonElement = spanElement.closest('button');
buttonElement.click();
// 두 번째 설정 대기
const resolutionButton = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box .setting_list_subLayer ul > li:nth-child(2) > button');
resolutionButton.click();
resolutionButton.className = 'video';
}
};
const addStyleExpandVODChat = () => {
const style = `
.expandVODChat:not(.screen_mode,.fullScreen_mode) #serviceHeader,
.expandVODChat:not(.screen_mode,.fullScreen_mode) .broadcast_information,
.expandVODChat:not(.screen_mode,.fullScreen_mode) .section_selectTab,
.expandVODChat:not(.screen_mode,.fullScreen_mode) .wrapping.player_bottom{
display: none !important;
}
.expandVODChat:not(.screen_mode,.fullScreen_mode) #webplayer_contents {
margin: 0 auto !important;
min-height: 100vh !important;
}
`;
GM_addStyle(style);
}
const addStyleRemoveShadowsFromCatch = () => {
const style = `
.catch_webplayer_wrap .vod_player:after {
background-image: none !important;
}
`;
GM_addStyle(style);
}
//============================ VOD 페이지 함수 끝 ============================//
//============================ 메인 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://www.sooplive.co.kr")) {
GM_addStyle(CommonStyles);
GM_addStyle(mainPageCommonStyles);
if (isReplaceEmptyThumbnailEnabled) {
loadHlsScript();
}
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
waitForElement('#serviceLnb', function (elementSelector, element) {
if (isCustomSidebarEnabled) makeTopNavbarAndSidebar("main");
runCommonFunctions();
});
removeUnwantedTags();
processStreamers();
return;
}
//============================ 플레이어 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://play.sooplive.co.kr")) {
const blankA = !!document.getElementById("bannedMessages");
const blankB = !!document.getElementById("targetUserMessages");
// Embed 페이지에서는 실행하지 않음
const pattern = /^https:\/\/play.sooplive.co.kr\/.*\/.*\/embed(\?.*)?$/;
if (pattern.test(CURRENT_URL) || CURRENT_URL.includes("vtype=chat") || blankA || blankB) {
return;
}
GM_addStyle(CommonStyles);
GM_addStyle(playerCommonStyles);
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
if (isCustomSidebarEnabled) {
makeTopNavbarAndSidebar("player");
insertFoldButton();
if(showSidebarOnScreenMode && !showSidebarOnScreenModeAlways) {
showSidebarOnMouseOver();
}
}
if(isBottomChatEnabled) useBottomChat();
if(isMakePauseButtonEnabled) detectPlayerChangeAndAppendPauseButton();
if(isMakeSharpModeShortcutEnabled) toggleSharpModeShortcut();
if(isMakeLowLatencyShortcutEnabled) toggleLowLatencyShortcut();
if(isRemainingBufferTimeEnabled){
waitForElement('#livePlayer', function (elementSelector, element) {
insertRemainingBuffer(element);
});
}
if(isCaptureButtonEnabled){
makeCaptureButton();
}
if(isStreamDownloadEnabled){
createStreamDownloadButton_LivePage();
}
if(isAutoClaimGemEnabled){
setInterval(autoClaimGem, 30000);
}
if(isVideoSkipHandlerEnabled){
waitForElement('#livePlayer', function (elementSelector, element) {
window.addEventListener('keydown', videoSkipHandler);
});
}
registerVisibilityChangeHandler();
registerVisibilityChangeHandlerForQuality();
if (isNo1440pEnabled) {
downgradeFrom1440p();
}
checkPlayerPageHeaderAd();
if(!isOpenNewtabEnabled){
homePageCurrentTab();
}
if(isDocumentTitleUpdateEnabled){
setTimeout(updateTitleWithViewers, 10000);
setInterval(updateTitleWithViewers, 60000);
}
runCommonFunctions();
// LIVE 채팅창
waitForElement('#chat_area', function (elementSelector, element) {
observeChat(elementSelector,element);
});
if (isUnlockCopyPasteEnabled) {
waitForElement('#write_area', function (elementSelector, element) {
unlockCopyPaste();
});
}
if (isAlignNicknameRightEnabled) {
alignNicknameRight();
}
if (isAutoScreenModeEnabled) {
waitForElement('#livePlayer', function (elementSelector, element) {
if (!document.body.classList.contains('screen_mode')) {
document.body.querySelector('#player .btn_screen_mode').click();
}
});
}
if (ishideButtonsAboveChatInputEnabled) {
hideButtonsAboveChatInput();
}
if (isExpandLiveChatAreaEnabled) {
waitForElement('#chatting_area div.area_header > div.chat_title > ul', function (elementSelector, element) {
// MutationObserver 생성
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && document.body.classList.contains('ratio169_mode')) {
// 클래스가 추가된 것을 감지했을 때 함수 실행
addStyleExpandLiveChat();
makeExpandChatButton(element, 'expandLiveChat');
toggleExpandChatShortcut();
updateBodyClass('expandLiveChat');
window.addEventListener('resize', debounce(() => updateBodyClass('expandLiveChat'), 500));
observer.disconnect(); // 더 이상 감시하지 않도록 종료
break;
}
}
});
// body의 클래스 속성 변화를 감시
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
});
}
if (isShowDeletedMessagesEnabled || isShowSelectedMessagesEnabled) {
waitForElement(".chatting-item-wrap", function (elementSelector, element) {
setupChatMessageTrackers(element);
});
}
if (isNoAutoVODEnabled) {
disableAutoVOD();
}
if (isHideEsportsInfoEnabled) {
GM_addStyle(`
body:not(.screen_mode,.fullScreen_mode,.embeded_mode)
#webplayer #webplayer_contents #player_area
.broadcast_information.detail_open .esports_info {
display: none !important;
}
.broadcast_information .esports_info {
display: none !important;
}
`
);
}
return;
}
//============================ VOD 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://vod.sooplive.co.kr/player/")) {
const isBaseUrl = (url) => /https:\/\/vod\.sooplive\.co\.kr\/player\/\d+/.test(url) && !isCatchUrl(url);
const isCatchUrl = (url) => /https:\/\/vod\.sooplive\.co\.kr\/player\/\d+\/catch/.test(url) || /https:\/\/vod\.sooplive\.co\.kr\/player\/catch/.test(url);
// 다시보기 페이지
if (isBaseUrl(CURRENT_URL)) {
GM_addStyle(CommonStyles);
// vodCore 변수가 선언될 때까지 대기하는 함수
const waitForVodCore = () => {
const checkVodCore = setInterval(() => {
if (unsafeWindow.vodCore?.playerController?._currentMediaInfo?.name) {
clearInterval(checkVodCore); // setInterval 정지
checkMediaInfo(unsafeWindow.vodCore.playerController._currentMediaInfo.name); // vodCore 변수가 정의되면 미디어 정보 확인 함수 호출
}
}, 500); // 500ms 주기로 확인
};
if(isSelectBestQualityEnabled){
waitForVodCore();
}
if(isCaptureButtonEnabled){
makeCaptureButton();
}
// VOD 채팅창
waitForElement('#webplayer_contents', function (elementSelector, element) {
observeChat(elementSelector,element);
});
waitForElement('div.serviceUtil', function (elementSelector, element) {
addModalSettings();
manageRedDot();
});
if (isAlignNicknameRightEnabled) {
alignNicknameRight();
}
if (isExpandVODChatAreaEnabled) {
waitForElement('#chatting_area div.area_header > div.chat_title > ul', function (elementSelector, element) {
addStyleExpandVODChat();
makeExpandChatButton(element, 'expandVODChat');
toggleExpandChatShortcut();
updateBodyClass('expandVODChat');
window.addEventListener('resize', debounce(() => updateBodyClass('expandVODChat'), 500));
});
}
// 캐치 페이지
} else if (isCatchUrl(CURRENT_URL)) {
GM_addStyle(CommonStyles);
GM_addStyle(mainPageCommonStyles);
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
waitForElement('#serviceLnb', function (elementSelector, element) {
if (isCustomSidebarEnabled) makeTopNavbarAndSidebar("main");
runCommonFunctions();
});
if (isRemoveShadowsFromCatchEnabled) addStyleRemoveShadowsFromCatch();
}
}
})();