您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
SOOP (숲)의 사이드바 UI를 변경합니다.
// ==UserScript== // @name SOOP (숲) - 사이드바 UI 변경 // @name:ko SOOP (숲) - 사이드바 UI 변경 // @namespace https://gf.qytechs.cn/ko/scripts/484713 // @version 20250619 // @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 // ==/UserScript== (function() { 'use strict'; const NEW_UPDATE_DATE = 20250609; 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 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 = `<button type="button" class="left_nav_button">${data.text}</button>`; 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 = ` <div id="sidebar" class="max"></div> `; const serviceLnbElement = document.getElementById('soop-gnb'); if (serviceLnbElement) { serviceLnbElement.insertAdjacentHTML('afterend', newHtml); } document.body.appendChild(tooltipContainer); } if (page === "player") { const sidebarHtml = ` <div id="sidebar" class="${sidebarClass}"></div> `; 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'); // <span class="dot" role="img">●</span> 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 = ` <div class="button-fold-sidebar" role="button"></div> <div class="button-unfold-sidebar" role="button"></div> `; 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 ? `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAExUlEQVR4nO1ZaYgcVRB+HlkVjyheibckxgNRQZSomB8RFC8MElGiP0T9LSgiASELgkpERRGPxeBqghhfV3XPrpN+1bsx8yMRVOKJENR4H9FVo+sRjbKulDPtfN07Mz1s98xOYD94sDtd7+v63lGv6rUxs5hFb8L35Xxm97k2/dvsSSCKlhDJOLNMVpv7zfPCS8yeAM+LLmWWXXXnq41Iftdnppfhee5idbTutPtaG4ogii4yvQhmOZtIdsKo77A2PINo5BQi+RJEjAdBdK7pJRCVTyRy38HIf2GtLIyf699JEe5ba0dPML0AaysHMcu74NwPRHLaVDtZyCxjYPf22rXRgWYm0d/fvzeR+LjGmcPFzew9Ty7EDU4krBxmpkAkq2BE/2EOb2ijz4pUdFplZgLM4XnM7i+I9Q+031dWg/C/fd9dYLqJUql0MJFsB+c3WWv3abe/2jJLBWZhu+4l0y0QuWfB+Z+nE1GYw+NSYXeN6QZ8X65KreEV0+VilhuTXOEVppMYHKzsz+w+hvX7Yl5OZrceZvOjMAz3M50CkdwDI7/T2vDIvJzM0VG6DIF3pekEgqByaHLNutuL4maO7gDeH60dnWuKBpH0w0s+GRjYOqcobmttH7N81rGzQddlMtcJb812yp2qa5rZbfP9DYuy7JndbclcyfYVJsDzwpuSWWZr8kqlsi+RvAF93sqaMWttnzpe75N9qrcNIjcC03tvtr2snFrQuLvb6Hcf2IeFOK8bill2x8S+Hy7IOqCY3a9pAfqbtS8f26qv729YBAP1p574uQUwR8uB9L1se3kJ7D/QBiLWt9H//fpguWsLEFBPvJjlwda20VKwndAkTUvIaqYa/x4tbc3hHppOgthKwEZ4+fJmdpOTk3vhxtV8CZx6Dje02jbjIZLrQUCUWwBmnRoam9npdOM1CrObDwLmJ4t9WdacZ+R0TC1yC9CTsb4mRw9vZkfk3oFRXj2VRx4GAW824xkeHj4Cy9PcArTgiAk1vje2iZaA87sa5UjWlucly8nG1ysDA1vnYLGTW0B1OcRLqHGOghklkTzTggv2gnuhkU25XD4MuMZzC0hWXtGZ6edaSWnMjm1a3flowQ8C/mh0K+F54Vlg82FuAcxuGKb05vRz33fXYNzP4sNzgSi6eqoAuQX4SgUIkLuA0G/g0NOw/u/PFuAegRF+sgHfEAzYnbkF6PEeH0S6VNLpALPbAg5dmc0XXQ4DshmfBYEcH6ct+k683csFvEFIjxrm8Rj7mwsIF4CAT1vMZqUQ56tOhoshHZjAEIgbuJ0ip1pX1xM2kwzFE/Gzwr8naAEPL/5Gs87a7z/Fvw8NbTw6i6d2HvxfV5v60tkBMxmYolEqjRzDLF/hMR8E7iRm9ypkj9dl8aRynS3WRidjqK7ebJfnmU4gCKJzMNcnku91I4JDr7W6rNUbuWTCJ5uVA/7/Rc8B00kQucv0RbDZ0u3RRtlmLVt9rFk/+s/5Ln2Cqn55wUJlSvn4ijqjFZW26nczt6m5aLdNs9CuOF8XMTqXSJ7AcnMabTeze3zduvCQrjqfFKKb0D2P4TSr1WqDQQ0CplegN3e165c1tY06VpsdbWNE7nUieUpPa2vtATPt7yxmYar4F3KMj24yKDCnAAAAAElFTkSuQmCC" style="width:22px">` : `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEzklEQVR4nO1ZaYgcVRB+URMVb7zibTDGA1FBFA90QSHGY3emq/qhO9Nd1TuR+aegiAhCFgSViIoiHmCI+kOMP4zHD0Ex8UciqOCJENR4H4nX6iYao5JVKjOdru6Znp5s98xOYD940DOvXr2q96req6pnzCxmMZiwli4A4K+lybfZkwBAlwHwJCL/Jw2A/0DkK8yeAERaDMDbQuGjRn9KnxlkIAaXNgTdJfT3jRYpAcCXmEGE44ydA8ATkdnQJmvHzgQITkXkb5U5TVrL55lBAkDtJET+Ua30N9Z6C8N++dZKINJma/0TzSDAWnsgAH+o7P0XAD69lc5bCEA/qR163/O8A8xMYnx8fC9EXq1t3HH4wjR6xOBi7eAA9LzwMDMFAF6mVn7Kdfn67DFBRZ9OALzMzAQcJzgfgP5Rq39Pt2MRablS4l9r/YtMPzEyUjsIkTcqIdZaa/fudrzQAtAbavxG8SXTLwDwSmXHv0/nRHGc6vH62EWkFaYfQPSvjdtwUJkuL9flaoLX1aaXYOb9EOlztfrP5uWJyKuUQ3+2ZMmN+5peAYDvUJNNWDt2ZF6ejuMdJWao+N5ueoFSiQ+N2yzfVBRvRLpZ7eqv1tYPMUUDgMbVKn1Rr9fnFsXbWjsPkb7q2d0gdqljHQBemi1U7TSxaUTaUC77i7LoXZduiMdKdl5hCiCSF48yOzMfGhraB5HeUQq/l7VjtrELm8Mx3dzqu6PAa0qBO7PoxRGTCQ0A39bFuLsU/SuFCC8OBcB/h4zLZTqliwtqaxsFtlpLx3UaWy77i5QZbZcbP7cCjkOuWv2PsugB+Dkl+CfNFv5e1cX4jyP6AHIrEA+86N5OtI4TXK6U3SFBmqSQEqmG/wtNxnz3TSdATAUAvR5NTm4H0jkJx10Z8eCntEMLbfp8/nWK9tXcCuioU47GdLoAdBnFcarHhH3yrZN9AC6n8SmX/TN0aJFbAbkZIwf2D09XgD5QW7+8tZ/vV/3vpvEZHR09QqenuRWQhCNkKOd7WiFLrdq2djGStTw/nk62L6/U6/W5OtnJrUCzqtY0ofYxSiKifKIDr12+gEjPtKOpVCqHKV6ThfqA43hnJfslk5IzO1IyveYjCb86pf5qV5VApLOVkp8WoAC9rFYkaO33S/rcz+YX3QsAwXBrf1BT871YhAK3KgFXJ/sB+HHVf3cWPwB+QNE/2jofvxQp4N+SW4Hm9d68iGh7MhxApPWRifE1WfwQ6Sq1wut0X6kUnKDClild3cuFRAUhtmo6jtdnfxokllI2/mXabsqcpig0nW8qDBH0EagduJskp5FXRwGbib0p0A61UMW+J0gCr5j/IFFnU4Hfwv+Hh5cencVH7gNlQhOR6dAmpdgLpmiMjNSOBeDv9DVfKvHJAPRm+J/rBjaLj451EGm9td6CeJFMKts83/QC1vK5OtYH4J/FEdXvtzoVa6Uilwj41gkPJfwWuQdMLwHgX9mYKJ6wqPZgSrQ5B4AeSh9HW/r2BCUvL4lEJSnMGhFGMqpGHZUWS/20A/0GiUL7Inwi1XxEp5u726Ax9uFqtXpwX4WPK+ItAOCn9XGa3Xa+WD4ph4AZFEjlrlF+oRXiqPKcJCvcaDu/3wagx+S2ttbuP9PyzmIWpoH/AeaUFWmyMuZqAAAAAElFTkSuQmCC" style="width:22px">`; const newHtml = ` <div class="top-section top"> <span class="max"><a href="https://www.sooplive.co.kr/live/all">인기 채널</a></span> <span class="min"><a href="https://www.sooplive.co.kr/live/all">${topIcon}</a></span> </div> <div class="users-section top"></div> `; 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 ? `<img src="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=" style="width:20px">` : `<img src="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" style="width:20px">`; if (!update) { const newHtml = ` <div class="top-section follow"> <span class="max"><a href="https://www.sooplive.co.kr/my/favorite">즐겨찾기 채널</a></span> <span class="min"><a href="https://www.sooplive.co.kr/my/favorite">${followIcon}</a></span> </div> <div class="users-section follow"></div> `; 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 ? `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAABkUlEQVR4nO2Wu0oDQRSGExQsBO/BC2Kl4CNY+ABW4hMIFpZi4RuI4gtYCRYm3eE/m4th5uiKKQWDoiAiIqKlqIjaCBYjQyaYFIYsJruI+eHAzuwM/zdnDjMTi7X0l0XkdwNyBOgnQG+lUqorNPN0utADyDGzmIpIhmKez+d7mXWxZKpvmWXGfgPy2nRzIukD5MQZ3hD5Y0QyXmrrh6aae57fzyynbuXXzGrUGBNn1tsOYKdp5rlcbgDQZ26vrzKZ/RHbzyxLDugd2J9oijmRSjDLuVvlJVF+qPwP0Lu23/NksWGGXF3ZlXGRzR4MlscVCoV2QN5qjLeZeQZknYjafg1ApBLV49RUbfPvAGQ1MEDsh3YQeZ6adgCPkQAw62FXOy9RAcy6WjiMCmDNAWxEBCC+q4G50AGMMXG793Zu+eAKFQCQSZf++0ATuUEAzDLv0k+RAACy6QBWIgFg92awh1HoAEqpDkA+AP2ZTO51hgpgjInb29EVYDGQuVW9F0ydsRALKm6M8R2glwObt9TSv9MXwO1y9weCI98AAAAASUVORK5CYII=" style="width:24px">` : `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAABmElEQVR4nO2Wu0oDQRSGNyhYCN6DF8RKwUewENKmyWXO2VMkuznHGLQLFr6BKL6AlWChpa2lYCsYFAUREREtRUXURrBQVk+ICpEsJruI+WFgZ3aG/5t/Z2fGspr6yyKa7UTkXUS5BeBVx3E6AjNPpaQLkfcQ5bVSeCMQ82w22w0gJc8UQC6Mkbg+PzTcnKjQg8j7OutzotwIkTuq9euGmqfTuV4AOdC4z4xxhi3LigDwmiaw3jDzTCbTh8iHH0Z8mkxOD3ntxkhRzZ8ApsYaYk6UjwLIkcZ8QiQD5XcAsqXtM3UzxC8ru1IA5DiRKPSX+8VisVZEfqzWX9O6A5AlImr5NQBRPvq5nzEy8ZP5N5AF3wBWlbofEeUmNb2bUACMcQb1r7kPCYCTOn4nFABEXtQElkMCkG1dA+kwACLet/fGljeuQAEAZFxnf+VrINYJwLaZdQ/YDAUAgFc0gfmQAOT9zuBtRoEDxOPFNkR+RpQX13XbgwaIeKejxl/yZe6p1gOmlmLbnLf8CutizpcAPOfbvKmm/p3eAMYsiMeMK5ANAAAAAElFTkSuQmCC" style="width:24px">`; if (!update) { const newHtml = ` <div class="top-section myplus"> <span class="max">추천 채널</span> <span class="min">${myplusIcon}</span> </div> <div class="users-section myplus"></div> <div class="top-section myplusvod"> <span class="max">추천 VOD</span> <span class="min">${myplusIcon}</span> </div> <div class="users-section myplusvod"></div> `; 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}<br>${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 = `<img src="${imgSrc}">`; if (durationText) { tooltipContent += `<div class="duration-overlay">${durationText}</div>`; } tooltipContent += `<div class="tooltiptext">${tooltipText}</div>`; 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 = ` <div id="myModal" class="modal"> <div class="modal-content"> <span class="myModalClose">×</span> <h2 style="font-size: 24px;">확장 프로그램 설정</h2> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">방송 목록 옵션</h3> <div class="option"> <label class="switch"> <input type="checkbox" id="switchPreviewModal"> <span class="slider round"></span> </label> <label for="switchPreviewModal">[썸네일] 클릭시 프리뷰 열기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchOpenExternalPlayer"> <span class="slider round"></span> </label> <label for="switchOpenExternalPlayer">[썸네일] 오른쪽 클릭시 외부 재생기 열기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchReplaceEmptyThumbnail"> <span class="slider round"></span> </label> <label for="switchReplaceEmptyThumbnail">[썸네일] 마우스 오버시 연령 제한 썸네일 보기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRemoveRedistributionTag"> <span class="slider round"></span> </label> <label for="switchRemoveRedistributionTag">[썸네일] 탐방 허용 태그 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRemoveWatchLaterButton"> <span class="slider round"></span> </label> <label for="switchRemoveWatchLaterButton">[썸네일] 나중에 보기 버튼 숨기기 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRemoveBroadStartTimeTag"> <span class="slider round"></span> </label> <label for="switchRemoveBroadStartTimeTag">[썸네일] 방송 시작 시간 숨기기 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRemoveCarousel"> <span class="slider round"></span> </label> <label for="switchRemoveCarousel">[메인 페이지] 자동 재생되는 채널 전광판 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchBroadTitleTextEllipsis"> <span class="slider round"></span> </label> <label for="switchBroadTitleTextEllipsis">방송 제목이 긴 경우 ...으로 생략하기 👍</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">사이드바 옵션</h3> <div class="option"> <label class="switch"> <input type="checkbox" id="switchCustomSidebar"> <span class="slider round"></span> </label> <label for="switchCustomSidebar">사이드바 사용 (해제시 기본 사이드바) 👍</label> </div> <div class="option"> <label for="favoriteChannelsDisplay" title="0 = 숨김">[즐겨찾기 채널] 표시 수</label> <input type="range" id="favoriteChannelsDisplay" min="0" max="30" title="0 = 숨김"> <span id="favoriteChannelsDisplayValue">${displayFollow}</span> </div> <div class="option"> <label for="myPlusChannelsDisplay" title="0 = 숨김">[추천 채널] 표시 수</label> <input type="range" id="myPlusChannelsDisplay" min="0" max="30" title="0 = 숨김"> <span id="myPlusChannelsDisplayValue">${displayMyplus}</span> </div> <div class="option"> <label for="myPlusVODDisplay" title="0 = 숨김">[추천 VOD] 표시 수</label> <input type="range" id="myPlusVODDisplay" min="0" max="30" title="0 = 숨김"> <span id="myPlusVODDisplayValue">${displayMyplusvod}</span> </div> <div class="option"> <label for="popularChannelsDisplay" title="0 = 숨김">[인기 채널] 표시 수</label> <input type="range" id="popularChannelsDisplay" min="0" max="30" title="0 = 숨김"> <span id="popularChannelsDisplayValue">${displayTop}</span> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchSmallUserLayout"> <span class="slider round"></span> </label> <label for="switchSmallUserLayout">미니 방송 목록</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchOpenExternalPlayerFromSidebar"> <span class="slider round"></span> </label> <label for="switchOpenExternalPlayerFromSidebar">오른쪽 클릭시 외부 재생기 열기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="sendLoadBroadCheck"> <span class="slider round"></span> </label> <label for="sendLoadBroadCheck">새로고침 없는 방송 전환 사용 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRandomSort"> <span class="slider round"></span> </label> <label for="switchRandomSort">[즐겨찾기 채널] 랜덤 정렬 (해제시 시청자 많은 순)</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchChannelFeed"> <span class="slider round"></span> </label> <label for="switchChannelFeed">[즐겨찾기 채널] 오프라인 채널의 최신 글 보기 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchBlockedCategorySorting"> <span class="slider round"></span> </label> <label for="switchBlockedCategorySorting">[즐겨찾기 채널] 차단된 카테고리를 하단으로 이동</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="fixNotificationChannel"> <span class="slider round"></span> </label> <label for="fixNotificationChannel">[즐겨찾기 채널] 알림 설정된 채널을 상단 고정</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="fixFixedChannel"> <span class="slider round"></span> </label> <label for="fixFixedChannel" title="MY 페이지에서 스트리머 고정 버튼(핀 모양)을 누르면 사이드바에 고정이 됩니다.">[즐겨찾기 채널] 스트리머 관리에서 고정된 채널을 상단 고정<sup>1)</sup></label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchPinnedOnlineOnly"> <span class="slider round"></span> </label> <label for="switchPinnedOnlineOnly">[즐겨찾기 채널] 온라인일 때만 상단 고정하기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="popularChannelsFirst"> <span class="slider round"></span> </label> <label for="popularChannelsFirst">[추천 채널]을 [인기 채널]보다 위에 표시</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="mpSortByViewers"> <span class="slider round"></span> </label> <label for="mpSortByViewers">[추천 채널] 정렬을 추천순으로 변경 (해제시 시청자순)</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="removeDuplicates"> <span class="slider round"></span> </label> <label for="removeDuplicates">[추천 채널] 즐겨찾기 중복 제거</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="openInNewTab"> <span class="slider round"></span> </label> <label for="openInNewTab">방송 목록 클릭 시 새 탭으로 열기 👎🏼</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">LIVE 플레이어 옵션</h3> <div class="option"> <label class="switch"> <input type="checkbox" id="switchNoAutoVOD"> <span class="slider round"></span> </label> <label for="switchNoAutoVOD">방송 종료 후 자동으로 VOD 재생하는 기능 차단 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchHideEsportsInfo"> <span class="slider round"></span> </label> <label for="switchHideEsportsInfo">E-Sports 정보 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="autoClaimGem"> <span class="slider round"></span> </label> <label for="autoClaimGem">젬 자동 획득 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="showPauseButton"> <span class="slider round"></span> </label> <label for="showPauseButton">[플레이어] 일시정지 버튼 표시 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchCaptureButton"> <span class="slider round"></span> </label> <label for="switchCaptureButton">[플레이어] LIVE / VOD 스크린샷 버튼 표시 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchStreamDownload"> <span class="slider round"></span> </label> <label for="switchStreamDownload">[플레이어] LIVE 스트림 다운로드 버튼 표시<sup>5)</sup></label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="showBufferTime"> <span class="slider round"></span> </label> <label for="showBufferTime">[채팅창] 방송 딜레이 (남은 버퍼 시간) 표시 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchVideoSkipHandler"> <span class="slider round"></span> </label> <label for="switchVideoSkipHandler">[단축키] 좌/우 방향키를 눌러 1초 전/후로 이동 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchSharpmodeShortcut"> <span class="slider round"></span> </label> <label for="switchSharpmodeShortcut">[단축키] '선명한 모드'(e) 활성화 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchLLShortcut"> <span class="slider round"></span> </label> <label for="switchLLShortcut">[단축키] '시차 단축'(d) 활성화** 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchAdjustDelayNoGrid"> <span class="slider round"></span> </label> <label for="switchAdjustDelayNoGrid">[단축키] (d)를 '앞당기기'로 변경 👎🏼<br>(위 옵션** 활성화 필수, 비 그리드 사용자만)</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="mutedInactiveTabs"> <span class="slider round"></span> </label> <label for="mutedInactiveTabs">[브라우저 탭] 전환 시 방송 음소거</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchDocumentTitleUpdate"> <span class="slider round"></span> </label> <label for="switchDocumentTitleUpdate">[브라우저 탭] 제목에 시청자 수 표시</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchShowSidebarOnScreenModeAlways"> <span class="slider round"></span> </label> <label for="switchShowSidebarOnScreenModeAlways">[스크린 모드] 항상 사이드바 보기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="mouseOverSideBar"> <span class="slider round"></span> </label> <label for="mouseOverSideBar">[스크린 모드] 좌측 마우스 오버시 사이드바 사용 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="chatPosition"> <span class="slider round"></span> </label> <label for="chatPosition">[스크린 모드] 세로로 긴 화면에서 채팅창을 아래에 위치 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchAutoScreenMode"> <span class="slider round"></span> </label> <label for="switchAutoScreenMode">[스크린 모드] 자동 스크린 모드</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">VOD 플레이어 옵션</h3> <div class="option"> <label class="switch"> <input type="checkbox" id="selectBestQuality"> <span class="slider round"></span> </label> <label for="selectBestQuality">최고화질 자동 선택 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchRemoveShadowsFromCatch"> <span class="slider round"></span> </label> <label for="switchRemoveShadowsFromCatch">[플레이어] CATCH 플레이어 하단의 그림자 효과 없애기</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">채팅창 옵션</h3> <div class="option"> <label for="nicknameWidthDisplay">[닉네임] 가로 크기 (채팅 메시지 정렬시)</label> <input type="range" id="nicknameWidthDisplay" min="86" max="186"> <span id="nicknameWidthDisplayValue">${nicknameWidth}</span> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchAlignNicknameRight"> <span class="slider round"></span> </label> <label for="switchAlignNicknameRight">[닉네임] 우측 정렬하기 (채팅 메시지 정렬시)</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="selectHideSupporterBadge"> <span class="slider round"></span> </label> <label for="selectHideSupporterBadge">[닉네임] 서포터 배지 숨기기</label> <label class="switch"> <input type="checkbox" id="selectHideFanBadge"> <span class="slider round"></span> </label> <label for="selectHideFanBadge">[닉네임] 팬 배지 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="selectHideSubBadge"> <span class="slider round"></span> </label> <label for="selectHideSubBadge">[닉네임] 구독팬 배지 숨기기</label> <label class="switch"> <input type="checkbox" id="selectHideVIPBadge"> <span class="slider round"></span> </label> <label for="selectHideVIPBadge">[닉네임] 열혈팬 배지 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="selectHideMngrBadge"> <span class="slider round"></span> </label> <label for="selectHideMngrBadge">[닉네임] 매니저 배지 숨기기</label> <label class="switch"> <input type="checkbox" id="selectHideStreamerBadge"> <span class="slider round"></span> </label> <label for="selectHideStreamerBadge">[닉네임] 스트리머 배지 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchUnlockCopyPaste"> <span class="slider round"></span> </label> <label for="switchUnlockCopyPaste">[채팅 입력란] 복사/붙여넣기 기능 복원</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchHideButtonsAboveChatInput"> <span class="slider round"></span> </label> <label for="switchHideButtonsAboveChatInput">[채팅 입력란] 상단 버튼 탭 숨기기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="selectBlockWords"> <span class="slider round"></span> </label> <label for="selectBlockWords">[메시지] 단어로 차단<sup>2)</sup></label> </div> <div class="option"> <textarea id="blockWordsInput" placeholder="콤마(,)로 구분하여 단어 입력" style="width: 100%; height: 34px; border: 1px solid #ccc;">${registeredWords}</textarea> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchShowSelectedMessages"> <span class="slider round"></span> </label> <label for="switchShowSelectedMessages">[메시지] 유저 채팅 모아보기</label> </div> <div class="option"> <textarea id="selectedUsersInput" placeholder="대상 유저 추가: 콤마(,)로 구분하여 유저 아이디 입력하세요 \n즐겨찾기는 자동 등록이므로 따로 입력할 필요 없음" style="width: 100%; height: 34px; border: 1px solid #ccc;">${selectedUsers}</textarea> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchShowDeletedMessages"> <span class="slider round"></span> </label> <label for="switchShowDeletedMessages">[메시지] 강제퇴장 된 유저의 채팅 모아보기</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchChatCounter"> <span class="slider round"></span> </label> <label for="switchChatCounter">[메시지] 초당 채팅 수 카운터 표시<sup>3)</sup></label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchExpandLiveChatArea"> <span class="slider round"></span> </label> <label for="switchExpandLiveChatArea">세로로 긴 화면에서 LIVE 채팅창 확장 버튼 추가 👍</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchExpandVODChatArea"> <span class="slider round"></span> </label> <label for="switchExpandVODChatArea">세로로 긴 화면에서 VOD 채팅창 확장 버튼 추가 👍</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">기타 옵션</h3> <div class="option"> <label class="switch"> <input type="checkbox" id="switchChzzkFollowChannels"> <span class="slider round"></span> </label> <label for="switchChzzkFollowChannels">치지직 팔로우 채널 통합<sup>4)</sup></label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="switchChzzkTopChannels"> <span class="slider round"></span> </label> <label for="switchChzzkTopChannels">치지직 인기 채널 통합</label> </div> <div class="option"> <label class="switch"> <input type="checkbox" id="useInterFont"> <span class="slider round"></span> </label> <label for="useInterFont">트위치 폰트 (Inter) 사용</label> </div> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">차단 관리</h3> <span style="margin-bottom: 15px; font-size: 12px; display: block;">채널 차단: 본문 방송 목록 -> 점 세개 버튼 -> [이 브라우저에서 ... 숨기기]</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">차단 해제: Tampermonkey 아이콘을 눌러서 가능합니다.</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">단어 등록: Tampermonkey 아이콘을 눌러서 가능합니다.</span> <div class="divider"></div> <h3 style="margin-bottom: 15px; font-size: 16px;">부가 설명</h3> <span style="margin-bottom: 15px; font-size: 12px; display: block;">* 👍 (대부분 추천), 👎🏼 (대부분 비추천)</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">1) MY 페이지에서 스트리머 고정 버튼(핀 모양)을 누르면 사이드바에 고정이 됩니다.</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">2) 해당 단어를 포함하는 메시지 숨김. 완전 일치할 때만 숨김은 단어 앞에 e:를 붙이기. <br>예시) ㄱㅇㅇ,ㅔㅔ,e:ㅇㅇ,e:ㅇㅎ,e:극,e:나,e:락</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">3) [메시지] 유저 채팅 모아보기, [메시지] 강제퇴장 된 유저의 채팅 모아보기 둘 중 하나를 먼저 켜야 작동 함</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">4) 치지직 로그인이 되어있지 않으면 응답지연이 생겨서 느려집니다</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">5) 본문 방송 목록 or 플레이어 페이지 -> 점 세개 버튼 -> [스트림 다운로드]</span> <span style="margin-bottom: 15px; font-size: 12px; display: block;">버그 신고는 <a href="https://gf.qytechs.cn/ko/scripts/484713" target="_blank">https://gf.qytechs.cn/ko/scripts/484713</a>에서 가능합니다.</span> </div> </div> `; // 모달 컨텐츠를 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("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 = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${nickname}</title> <style> body { background-color: black; margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; position: relative; /* 자식 요소 위치 조정을 위해 추가 */ } #video { position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; margin: auto; max-height: 100%; max-width: 100%; } #overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); /* 반투명 배경 */ display: flex; justify-content: center; align-items: center; z-index: 5; /* 비디오보다 위에 보이도록 설정 */ } #muteButton { background-color: rgba(255, 255, 255, 0.8); border: none; border-radius: 50%; padding: 30px; /* 버튼 크기 증가 */ cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 36px; /* 아이콘 크기 증가 */ z-index: 10; /* 버튼이 다른 요소 위에 보이도록 설정 */ } </style> <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> </head> <body> <video id="video" controls autoplay muted></video> <div id="overlay"> <button id="muteButton"><i class="fas fa-volume-mute"></i></button> </div> <script> const video = document.getElementById("video"); const muteButton = document.getElementById("muteButton"); const overlay = document.getElementById("overlay"); if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource("${m3u8Url}"); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } else if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = "${m3u8Url}"; video.addEventListener("loadedmetadata", function () { video.play(); }); } const toggleMute = () => { video.muted = !video.muted; muteButton.innerHTML = video.muted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>'; overlay.style.display = 'none'; // 버튼 클릭 후 레이어 사라지도록 설정 }; // 버튼 클릭 시 음소거 해제 muteButton.addEventListener("click", (event) => { event.stopPropagation(); // 클릭 이벤트 전파 방지 toggleMute(); }); // 문서의 아무 곳을 클릭해도 음소거 해제 overlay.addEventListener("click", toggleMute); </script> </body> </html> `; // 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) => { const basePayload = { bid: id, bno: broadNumber, from_api: '0', mode: 'landing', player_type: 'html5', stream_type: 'common', quality: 'original' }; // 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) { 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) { 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 = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>다운로드 준비 중... ${id}</title> <style> body { font-family: sans-serif; padding: 20px; } #info { margin-top: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 8px; } #info p { margin: 4px 0; } label { display: block; margin-top: 10px; } input[type="number"] { width: 60px; } </style> </head> <body> <h2>📡 실시간 저장 중...</h2> <button id="saveBtn">💾 저장</button> <label> <input type="checkbox" id="autoSaveCheckbox"> 자동 저장 (저장을 너무 오래 하지 않을 시 작업을 잃을 수 있습니다) </label> <label> 간격 (분): <input type="number" id="autoSaveInterval" min="1" value="30"> </label> <div id="info"> <p>⏱️ <span id="elapsedTime">00:00:00</span></p> <p>📦 <span id="downloadedSegments">0</span> / <span id="totalSegments">0</span></p> <p>🍰 최신 조각: <span id="currentSegment">0</span></p> <p>🍰 마지막 저장: <span id="lastSavedSegment">0</span></p> </div> <script> const m3u8Url = ${JSON.stringify(m3u8Url)}; const baseUrl = ${JSON.stringify(baseUrl)}; const id = ${JSON.stringify(id)}; const broadNumber = ${JSON.stringify(broadNumber)}; let fetchedSet = new Set(); let downloaded = new Set(); let stopped = false; let lastSavedSegment = 0; let segmentQueue = []; let autoSave = false; let autoSaveInterval = 30 * 60 * 1000; // 30분 let startTime = Date.now(); let lastAutoSaveTime = Date.now(); let maxSegNum = 0; const checkbox = document.getElementById("autoSaveCheckbox"); const intervalInput = document.getElementById("autoSaveInterval"); checkbox.addEventListener("change", e => { autoSave = e.target.checked; lastAutoSaveTime = Date.now(); }); intervalInput.addEventListener("input", () => { const minutes = parseInt(intervalInput.value); if (!isNaN(minutes) && minutes > 0) { autoSaveInterval = minutes * 60 * 1000; } }); document.getElementById("saveBtn").addEventListener("click", () => { saveToFile(); }); function getTimeString(d = new Date()) { d = new Date(d.getTime() + 9 * 60 * 60 * 1000); // KST 보정 return d.toISOString().slice(0, 19).replace(/[-T:]/g, ''); } function updateInfo() { const elapsed = Math.floor((Date.now() - startTime) / 1000); const h = String(Math.floor(elapsed / 3600)).padStart(2, '0'); const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0'); const s = String(elapsed % 60).padStart(2, '0'); const downloadedCount = downloaded.size; const totalCount = fetchedSet.size; // 탭 제목 업데이트 document.title = \`[\${downloadedCount}/\${totalCount}] \${id}\`; // 정보 패널 업데이트 document.getElementById("elapsedTime").textContent = \`\${h}:\${m}:\${s}\`; document.getElementById("totalSegments").textContent = totalCount; document.getElementById("downloadedSegments").textContent = downloadedCount; document.getElementById("lastSavedSegment").textContent = lastSavedSegment; document.getElementById("currentSegment").textContent = maxSegNum; } async function fetchSegments() { try { const res = await fetch(m3u8Url + '&_=' + Date.now()); const text = await res.text(); const lines = text.split('\\n').map(l => l.trim()).filter(l => l && !l.startsWith('#') && l.includes('.TS')); for (const line of lines) { const tsUrl = new URL(line, baseUrl).href; if (!fetchedSet.has(tsUrl)) { fetchedSet.add(tsUrl); const match = line.match(/hls_(\\d+)_/i); const segNum = match ? parseInt(match[1], 10) : null; if (segNum !== null && segNum > lastSavedSegment) { maxSegNum = Math.max(maxSegNum, segNum); downloadSegment(tsUrl, segNum); } } } } catch (err) { console.error('m3u8 fetch error', err); } } async function downloadSegment(url, segNum) { try { const res = await fetch(url); if (!res.ok) return; const buf = await res.arrayBuffer(); segmentQueue.push({ segNum, buf }); downloaded.add(segNum); // 다운로드 후 즉시 정보 업데이트를 호출하여 더 빠르게 반영 updateInfo(); } catch (err) { console.error('segment download error', err); } } function saveToFile() { if (segmentQueue.length === 0) { alert("저장할 세그먼트가 없습니다."); return; } segmentQueue.sort((a, b) => a.segNum - b.segNum); const start = segmentQueue[0].segNum; const end = segmentQueue[segmentQueue.length - 1].segNum; const timeString = getTimeString(); const blob = new Blob(segmentQueue.map(s => s.buf), { type: 'video/MP2T' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = \`\${id}_\${broadNumber}_\${timeString}_\${start}_to_\${end}.ts\`; a.click(); lastSavedSegment = end; segmentQueue = []; updateInfo(); } setInterval(fetchSegments, 2000); setInterval(updateInfo, 1000); setInterval(() => { if (autoSave && Date.now() - lastAutoSaveTime >= autoSaveInterval) { saveToFile(); lastAutoSaveTime = Date.now(); } }, 5000); </script> </body> </html> `; 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; // 빠른 재생 속도 설정 // 병렬로 broadAid와 broadDomain 가져오기 const [broadAid, broadDomain] = await Promise.all([ getBroadAid(id, broadNumber), getBroadDomain(id, broadNumber) ]); const m3u8url = `${broadDomain}?aid=${broadAid}`; 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 { // 병렬로 broadAid와 broadDomain 가져오기 const [broadAid, broadDomain] = await Promise.all([ getBroadAid(id, broadNumber), getBroadDomain(id, broadNumber) ]); if (broadAid && broadDomain) { const m3u8url = `${broadDomain}?aid=${broadAid}`; // 내용 업데이트 updateModalContent(m3u8url, playerLink, thumbsBoxLink); } else { console.error('Invalid broadAid or broadDomain'); } } 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 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 handleMuteByVisibility = () => { 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 isVideoInPiPMode = () => { const videoElement = document.body.querySelector('video'); return videoElement && document.pictureInPictureElement === videoElement; } const registerVisibilityChangeHandler = () => { document.addEventListener('visibilitychange', () => { if (!isVideoInPiPMode() && isAutoChangeMuteEnabled) { handleMuteByVisibility(); } }, true); } 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(); } } }; 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(` <html> <head> <title>강제퇴장된 유저의 채팅</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f4f9; color: #333; margin: 20px; } #bannedMessages { list-style: none; padding: 0; } #bannedMessages li { background-color: #fff; border-radius: 8px; padding: 10px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .message-timestamp { font-size: 0.9em; color: #888; } </style> </head> <body> <ul id='bannedMessages'></ul> </body> </html> `); 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(` <html> <head> <title>지정 유저 채팅 | 즐겨찾기 ${allFollowUserIds.length}명 | 수동 입력 ${selectedUsersArray.length}명</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f4f9; color: #333; margin: 20px; } #targetUserMessages { list-style: none; padding: 0; } #targetUserMessages li { background-color: #fff; border-radius: 8px; padding: 10px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .message-timestamp { font-size: 0.9em; color: #888; } </style> </head> <body> <ul id='targetUserMessages'></ul> </body> </html> `); 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 = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ScreenShot (${video.videoWidth}x${video.videoHeight})</title> <style> body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; position: relative; } img { max-width: 100%; max-height: 100%; } #downloadBtn { position: absolute; top: 16px; right: 16px; padding: 8px 12px; background-color: #ffffffcc; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; font-weight: bold; } </style> </head> <body> <img id="capturedImg" src="${imgURL}" alt="영상 캡쳐 이미지"> <button id="downloadBtn">다운로드 capture_${timestamp}.jpg</button> <script> const timestamp = "${timestamp}"; document.getElementById('downloadBtn').addEventListener('click', () => { const a = document.createElement('a'); a.href = document.getElementById('capturedImg').src; a.download = \`capture_\${timestamp}.jpg\`; a.click(); }); </script> </body> </html> `; 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 (isPreviewModalEnabled || 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(); 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(); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址