// ==UserScript== // @name Soop(숲) 채팅 확장 스크립트 // @namespace https://greasyfork.org/scripts/512780 // @icon https://res.sooplive.co.kr/afreeca.ico // @version 1.2.1 // @description 이모티콘 창 및 버튼 위치 조정, 채팅 붙여넣기 허용, 채팅창 너비 조절, 커스텀 이모티콘 박스, 이모티콘 입력 후 자동 전송 기능 추가 // @match https://play.sooplive.co.kr/* // @match https://vod.sooplive.co.kr/* // @license MIT // @author ekzmchoco // @grant none // @downloadURL none // ==/UserScript== // Referenced Code: https://greasyfork.org/scripts/512724 (function() { 'use strict'; const DEFAULT_SETTINGS = { chatWidthAdjustment: true, customEmoticonBox: true, allowPasteInChat: true, emoticonButtonReposition: true, emoticonButtonColor: false, emoticonWindowPositionChange: true, emoticonTripleInput: false, autoSendAfterEmoticons: false }; const userSettings = JSON.parse(localStorage.getItem('soopChatSettings')) || DEFAULT_SETTINGS; function saveSettings() { localStorage.setItem('soopChatSettings', JSON.stringify(userSettings)); } function initSettingsUI() { const chattingArea = document.querySelector("#chatting_area"); if (!chattingArea) return; const personSettingEl = chattingArea.querySelector(".chat_layer.sub.person .contents > ul"); if (!personSettingEl) return; if (document.getElementById('script-settings')) return; const settingsLI = document.createElement("li"); settingsLI.id = 'script-settings'; const settingsOptions = [ { key: 'chatWidthAdjustment', label: '채팅 너비 조절 기능' }, { key: 'customEmoticonBox', label: '커스텀 이모티콘 박스*' }, { key: 'allowPasteInChat', label: '채팅 붙여넣기 허용*' }, { key: 'emoticonButtonReposition', label: '이모티콘 버튼 위치 변경*' }, { key: 'emoticonButtonColor', label: '이모티콘 버튼 색상 (밝게/어둡게)' }, { key: 'emoticonWindowPositionChange', label: '이모티콘 창 위치 변경*' }, { key: 'emoticonTripleInput', label: '이모티콘 3개 연속 입력' }, { key: 'autoSendAfterEmoticons', label: '이모티콘 입력 후 자동 전송' } ]; settingsOptions.forEach(option => { const div = document.createElement("div"); div.classList.add("checkbox_wrap"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = option.key; checkbox.checked = userSettings[option.key]; const label = document.createElement("label"); label.htmlFor = option.key; label.textContent = option.label; checkbox.addEventListener("change", () => { userSettings[option.key] = checkbox.checked; saveSettings(); applySettings(option.key); if (option.label.includes('*')) { alert('이 설정은 페이지를 새로고침해야 적용됩니다.'); } }); div.appendChild(checkbox); div.appendChild(label); settingsLI.appendChild(div); }); personSettingEl.appendChild(settingsLI); } function applySettings(optionKey) { switch(optionKey) { case 'chatWidthAdjustment': if (userSettings.chatWidthAdjustment) { initChatWidthAdjustment(); } else { removeChatWidthAdjustment(); } break; case 'customEmoticonBox': if (userSettings.customEmoticonBox) { initCustomEmoticonBox(); } else { removeCustomEmoticonBox(); } break; case 'allowPasteInChat': if (userSettings.allowPasteInChat) { enablePasteInChat(); } else { alert('이 설정은 페이지를 새로고침해야 적용됩니다.'); } break; case 'emoticonButtonColor': if (userSettings.emoticonButtonReposition) { alert('이 설정은 페이지를 새로고침해야 적용됩니다.'); } break; case 'autoSendAfterEmoticons': break; case 'emoticonTripleInput': case 'emoticonButtonReposition': case 'emoticonWindowPositionChange': break; default: break; } } function init() { initSettingsUI(); if (userSettings.allowPasteInChat) { enablePasteInChat(); } if (userSettings.chatWidthAdjustment) { initChatWidthAdjustment(); } if (userSettings.customEmoticonBox) { initCustomEmoticonBox(); } if (userSettings.emoticonButtonReposition || userSettings.emoticonWindowPositionChange) { initEmoticonRelatedFeatures(); } } function initChatWidthAdjustment() { const chattingArea = document.querySelector("#chatting_area"); if (!chattingArea) return; const chatTitleDiv = chattingArea.querySelector(".chat_title"); if (!chatTitleDiv) return; if (document.getElementById('chatWidthSlider')) return; let ul = chatTitleDiv.querySelector("ul"); if (!ul) { ul = document.createElement("ul"); chatTitleDiv.appendChild(ul); } let insertBeforeElement = ul.querySelector("#setbox_viewer") || ul.querySelector("#setbox_set"); if (!insertBeforeElement) { insertBeforeElement = ul.querySelector("li.set"); } if (!insertBeforeElement) { insertBeforeElement = ul.firstChild; } const sliderLi = document.createElement("li"); sliderLi.style.padding = "0 10px"; sliderLi.style.display = "flex"; sliderLi.style.alignItems = "center"; const rangeInput = document.createElement("input"); rangeInput.type = "range"; rangeInput.min = 300; rangeInput.max = 450; rangeInput.step = 5; rangeInput.value = localStorage.getItem("customChattingAreaWidth") ? localStorage.getItem("customChattingAreaWidth") : 380; rangeInput.style.width = "80px"; rangeInput.style.marginRight = "1px"; rangeInput.id = 'chatWidthSlider'; const rangeLabel = document.createElement("span"); rangeLabel.style.color = "#fff"; rangeLabel.style.fontSize = "12px"; rangeLabel.textContent = `${rangeInput.value}px`; rangeInput.addEventListener("input", () => { changeChatAreaWidth(rangeInput.value); rangeLabel.textContent = `${rangeInput.value}px`; localStorage.setItem("customChattingAreaWidth", rangeInput.value); }); sliderLi.appendChild(rangeInput); sliderLi.appendChild(rangeLabel); if (insertBeforeElement && insertBeforeElement.nextSibling) { ul.insertBefore(sliderLi, insertBeforeElement.nextSibling); } else { ul.appendChild(sliderLi); } const chatStyleEl = document.createElement("style"); chatStyleEl.id = 'custom-chat-width-style'; document.head.append(chatStyleEl); function changeChatAreaWidth(width) { chatStyleEl.textContent = ` #webplayer.chat_open { --chatting_W: ${width}px; } #webplayer.chat_open .chatting_area { width: var(--chatting_W); } .vod #chatting_area { --chatting_W: ${width}px; width: var(--chatting_W); position: fixed; right: 0; left: auto; } `; } const storedWidth = localStorage.getItem("customChattingAreaWidth") || 380; changeChatAreaWidth(storedWidth); rangeInput.value = storedWidth; rangeLabel.textContent = `${storedWidth}px`; } function removeChatWidthAdjustment() { const chatWidthSlider = document.getElementById('chatWidthSlider'); if (chatWidthSlider) { chatWidthSlider.parentElement.remove(); } const chatStyleEl = document.getElementById('custom-chat-width-style'); if (chatStyleEl) { chatStyleEl.remove(); } const chattingArea = document.querySelector("#chatting_area"); if (chattingArea) { chattingArea.style.width = ''; chattingArea.style.position = ''; chattingArea.style.right = ''; chattingArea.style.left = ''; } } function initCustomEmoticonBox() { const chattingArea = document.querySelector("#chatting_area"); const actionBox = chattingArea.querySelector("#actionbox"); if (!actionBox) return; if (document.querySelector(".customEmojiBtn")) return; const emoticonBox = document.querySelector("#emoticonBox"); if (!emoticonBox) return; const recentEmoticonBtn = emoticonBox.querySelector( ".tab_area .item_list ul > li[data-type='RECENT'] .ic_clock" ); const subTabArea = emoticonBox.querySelector(".subTab_area"); const defaultSubTab = subTabArea.querySelector("li[data-type='DEFAULT']"); const OGQSubTab = subTabArea.querySelector("li[data-type='OGQ']"); if (!recentEmoticonBtn || !defaultSubTab || !OGQSubTab) return; function defaultEmoticonClick() { recentEmoticonBtn.click(); setTimeout(() => { defaultSubTab.click(); }, 100); } function OGQEmoticonClick() { recentEmoticonBtn.click(); setTimeout(() => { OGQSubTab.click(); }, 100); } const chattingItemWrap = chattingArea.querySelector(".chatting-item-wrap"); const chatArea = chattingItemWrap.querySelector("#chat_area"); const customEmojiBox = document.createElement("div"); customEmojiBox.classList.add("customEmojiBox"); let isLoading = false; let minSliderValue = parseInt(localStorage.getItem('minSliderValue')) || 3; let maxSliderValue = parseInt(localStorage.getItem('maxSliderValue')) || 5; const sliderContainer = document.createElement('div'); sliderContainer.classList.add('slider-container'); sliderContainer.style.display = 'none'; const sliderWrapper = document.createElement('div'); sliderWrapper.classList.add('slider-wrapper'); const sliderTrack = document.createElement('div'); sliderTrack.classList.add('slider-track'); const sliderRange = document.createElement('div'); sliderRange.classList.add('slider-range'); const minSlider = document.createElement('input'); minSlider.type = 'range'; minSlider.min = '1'; minSlider.max = '10'; minSlider.value = minSliderValue; minSlider.classList.add('slider'); minSlider.id = 'slider-min'; const maxSlider = document.createElement('input'); maxSlider.type = 'range'; maxSlider.min = '1'; maxSlider.max = '10'; maxSlider.value = maxSliderValue; maxSlider.classList.add('slider'); maxSlider.id = 'slider-max'; const rangeDisplay = document.createElement('div'); rangeDisplay.classList.add('range-display'); rangeDisplay.textContent = `${minSliderValue}-${maxSliderValue}`; sliderWrapper.appendChild(sliderTrack); sliderWrapper.appendChild(sliderRange); sliderWrapper.appendChild(minSlider); sliderWrapper.appendChild(maxSlider); sliderContainer.appendChild(sliderWrapper); sliderContainer.appendChild(rangeDisplay); function updateRange() { const min = parseInt(minSlider.value); const max = parseInt(maxSlider.value); if (min > max) { if (this === minSlider) { maxSlider.value = min; } else { minSlider.value = max; } } minSliderValue = parseInt(minSlider.value); maxSliderValue = parseInt(maxSlider.value); const leftPercent = ((minSlider.value - 1) / 9) * 100; const rightPercent = ((maxSlider.value - 1) / 9) * 100; sliderRange.style.left = leftPercent + '%'; sliderRange.style.right = (100 - rightPercent) + '%'; rangeDisplay.textContent = `${minSliderValue}-${maxSliderValue}`; localStorage.setItem('minSliderValue', minSliderValue); localStorage.setItem('maxSliderValue', maxSliderValue); } minSlider.addEventListener('input', updateRange); maxSlider.addEventListener('input', updateRange); updateRange(); function renderEmoticon(type = "default") { if (isLoading) return; isLoading = true; type === "default" ? defaultEmoticonClick() : OGQEmoticonClick(); waitForEmoticonItems().then(emoticonItemBox => { proceedWithRender(emoticonItemBox, type); isLoading = false; }).catch(() => { console.error("이모티콘 아이템을 로드하지 못했습니다."); isLoading = false; }); } function waitForEmoticonItems() { return new Promise((resolve, reject) => { const maxAttempts = 20; let attempts = 0; const checkEmoticonItems = () => { const emoticonItemBox = emoticonBox.querySelector(".emoticon_item"); if (emoticonItemBox && emoticonItemBox.querySelectorAll("span > a").length > 0) { resolve(emoticonItemBox); } else { attempts++; if (attempts < maxAttempts) { setTimeout(checkEmoticonItems, 200); } else { reject(); } } }; checkEmoticonItems(); }); } function proceedWithRender(emoticonItemBox, type) { const diffType = type === "default" ? "OGQ" : "default"; const isOn = customEmojiBox.classList.contains(type); const isDiffOn = customEmojiBox.classList.contains(diffType); if (isOn) { customEmojiBox.classList.remove(type); customEmojiBox.innerHTML = ""; customEmojiBox.style.display = "none"; chatArea.style.bottom = "0"; sliderContainer.style.display = "none"; return; } const itemList = []; emoticonItemBox.querySelectorAll("span > a")?.forEach((item, index) => { if (index < 21) { const itemClone = item.cloneNode(true); itemClone.addEventListener("click", () => { const minCount = minSliderValue; const maxCount = maxSliderValue; const repeatCount = Math.floor(Math.random() * (maxCount - minCount + 1)) + minCount; for (let i = 0; i < repeatCount; i++) { item.click(); } if (userSettings.autoSendAfterEmoticons) { setTimeout(() => { const sendBtn = document.querySelector("#btn_send"); if (sendBtn) sendBtn.click(); }, 100); } }); itemList.push(itemClone); } }); if (isDiffOn) { customEmojiBox.classList.remove(diffType); customEmojiBox.innerHTML = ""; } customEmojiBox.innerHTML = ''; customEmojiBox.append(...itemList); if (!chattingItemWrap.contains(customEmojiBox)) { chattingItemWrap.append(customEmojiBox); } customEmojiBox.style.display = "flex"; customEmojiBox.classList.add(type); chatArea.style.position = "relative"; chatArea.style.bottom = `${customEmojiBox.offsetHeight + sliderContainer.offsetHeight + 8}px`; if (!chattingItemWrap.contains(sliderContainer)) { chattingItemWrap.append(sliderContainer); } sliderContainer.style.display = 'flex'; chatArea.style.bottom = `${customEmojiBox.offsetHeight + sliderContainer.offsetHeight + 8}px`; } const recentEmoticonCustomBtnLI = document.createElement("li"); const recentEmoticonCustomBtn = document.createElement("a"); recentEmoticonCustomBtn.href = "javascript:;"; recentEmoticonCustomBtn.classList.add("customEmojiBtn"); recentEmoticonCustomBtn.textContent = "최근"; recentEmoticonCustomBtnLI.append(recentEmoticonCustomBtn); const OGQEmoticonCustomBtnLI = document.createElement("li"); const OGQEmoticonCustomBtn = document.createElement("a"); OGQEmoticonCustomBtn.href = "javascript:;"; OGQEmoticonCustomBtn.classList.add("customEmojiBtn"); OGQEmoticonCustomBtn.textContent = "OGQ"; OGQEmoticonCustomBtnLI.append(OGQEmoticonCustomBtn); recentEmoticonCustomBtnLI.addEventListener("click", () => { renderEmoticon("default"); }); OGQEmoticonCustomBtnLI.addEventListener("click", () => { renderEmoticon("OGQ"); }); const itemBox = actionBox.querySelector(".item_box"); if (itemBox) { itemBox.append(recentEmoticonCustomBtnLI, OGQEmoticonCustomBtnLI); } const iconColor = userSettings.emoticonButtonColor ? '#333' : '#D5D7DC'; const defaultStyleEl = document.createElement("style"); const defaultStyle = ` .chatbox .actionbox .chat_item_list .item_box li a.customEmojiBtn { line-height: 32px; font-size: 15px; font-weight: bold; color: ${iconColor}; background-color: transparent; } .chatbox .actionbox .chat_item_list .item_box li a.customEmojiBtn:hover { color: ${iconColor}; background-color: transparent; } .chatting-item-wrap .customEmojiBox { position: absolute; margin-bottom: 20px; bottom: 0; left: 0; width: 100%; display: flex; flex-wrap: wrap; gap: 8px 4px; padding: 8px 8px 0 8px; background-color: #fefefe; } [dark="true"] .chatting-item-wrap .customEmojiBox { background-color: #222; border-top: 1px solid #444; } .chatting-item-wrap .customEmojiBox a { width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; } .chatting-item-wrap .customEmojiBox a:hover { background-color: rgba(117, 123, 138, 0.2); } .slider-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 20px; margin: 0; background-color: #fefefe; display: flex; align-items: center; padding: 0 8px 8px 8px; flex-wrap: nowrap; } [dark="true"] .slider-container { background-color: #222; } .slider-wrapper { position: relative; width: 90%; height: 20px; } .range-display { width: 10%; text-align: center; font-size: 12px; color: #fff; margin-left: 8px; } .slider-track { position: absolute; top: 50%; left: 0; right: 0; height: 3px; background: #ddd; border-radius: 10px; transform: translateY(-50%); } .slider { position: absolute; top: 50%; left: 0; width: 100%; -webkit-appearance: none; appearance: none; background: none; pointer-events: none; transform: translateY(-50%); } .slider-range { position: absolute; top: 50%; height: 3px; background: #4444ff; border-radius: 10px; pointer-events: none; transform: translateY(-50%); } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; border-radius: 50%; background: #ffffff; border: 2px solid #4444ff; cursor: pointer; pointer-events: all; box-shadow: 0 0 5px rgba(0,0,0,0.2); position: relative; z-index: 1; } .slider::-moz-range-thumb { width: 15px; height: 15px; border-radius: 50%; background: #ffffff; border: 2px solid #4444ff; cursor: pointer; pointer-events: all; box-shadow: 0 0 5px rgba(0,0,0,0.2); position: relative; z-index: 1; } `; defaultStyleEl.textContent = defaultStyle; document.head.append(defaultStyleEl); } function removeCustomEmoticonBox() { const customEmojiBtns = document.querySelectorAll('.customEmojiBtn'); customEmojiBtns.forEach(btn => btn.parentElement.remove()); const customEmojiBox = document.querySelector('.customEmojiBox'); if (customEmojiBox) customEmojiBox.remove(); const sliderContainer = document.querySelector('.slider-container'); if (sliderContainer) sliderContainer.remove(); const styleEl = document.querySelector('#custom-chat-width-style'); if (styleEl) { styleEl.remove(); } const styleCustomEl = document.querySelector('style'); if (styleCustomEl && styleCustomEl.textContent.includes('.customEmojiBox')) { styleCustomEl.remove(); } } function enablePasteInChat() { const writeArea = document.querySelector("#write_area"); if (!writeArea) return; writeArea.removeAttribute("readonly"); writeArea.removeAttribute("disabled"); writeArea.addEventListener("paste", function(e) { e.preventDefault(); const clipboardData = (e.originalEvent || e).clipboardData || window.clipboardData; const pastedData = clipboardData.getData('text'); document.execCommand('insertText', false, pastedData); }); } function initEmoticonRelatedFeatures() { const observer = new MutationObserver((mutations, obs) => { const ul = document.querySelector('ul.item_box'); if (!ul) return; const btnStarLi = document.getElementById('btn_star'); const btnAdballoonLi = document.getElementById('btn_adballoon'); const sooptoreLi = ul.querySelector('li.sooptore'); const btnEmo = document.getElementById('btn_emo'); if (!btnStarLi || !btnAdballoonLi || !sooptoreLi || !btnEmo) return; btnStarLi.classList.remove('off'); btnAdballoonLi.classList.remove('off'); sooptoreLi.classList.remove('off'); btnEmo.classList.remove('off'); if (userSettings.emoticonButtonReposition) { const chatWriteDiv = document.getElementById('chat_write'); if (chatWriteDiv && chatWriteDiv.contains(btnEmo)) { chatWriteDiv.removeChild(btnEmo); } let btnEmoLi = document.createElement('li'); btnEmoLi.id = 'btn_emo_li'; btnEmoLi.className = 'emoticon'; btnEmoLi.appendChild(btnEmo); if (ul.firstChild !== btnEmoLi) { ul.insertBefore(btnEmoLi, ul.firstChild); } ul.appendChild(btnStarLi); ul.appendChild(btnAdballoonLi); ul.appendChild(sooptoreLi); btnStarLi.classList.add('right-align'); const iconColor = userSettings.emoticonButtonColor ? '#333' : '#D5D7DC'; const svgIcon = encodeURIComponent( ` ` ); const dataURL = `data:image/svg+xml,${svgIcon}`; btnEmo.style.backgroundImage = `url("${dataURL}")`; btnEmo.style.backgroundRepeat = 'no-repeat'; btnEmo.style.backgroundPosition = 'center'; btnEmo.style.backgroundSize = 'contain'; btnEmo.style.width = '32px'; btnEmo.style.height = '32px'; btnEmo.style.border = 'none'; btnEmo.style.cursor = 'pointer'; btnEmo.style.padding = '0'; btnEmo.style.margin = '0'; btnEmo.style.backgroundColor = 'transparent'; btnEmo.textContent = ''; } if (userSettings.emoticonWindowPositionChange) { const emoticonContainer = document.getElementById('emoticonContainer'); if (emoticonContainer) { const styleEl = document.createElement('style'); styleEl.id = 'custom-emoticon-position-style'; styleEl.textContent = ` .chatbox #emoticonContainer { bottom: 10px; transform: translateX(0); transition: none !important; } .chatbox #emoticonContainer.on { bottom: 10px; max-width: 360px; min-width: 320px; right: unset; left: 0; transform: translateX(-105%); transition: none !important; } `; document.head.appendChild(styleEl); } } if (!document.getElementById('sooplive-custom-style')) { const style = document.createElement('style'); style.id = 'sooplive-custom-style'; style.innerHTML = ` ul.item_box { display: flex; align-items: center; } ul.item_box li { margin: 0 3px; } ul.item_box li.right-align { margin-left: auto; } `; document.head.appendChild(style); } obs.disconnect(); }); observer.observe(document.body, { childList: true, subtree: true }); } function startScript() { if (document.querySelector("#chatting_area")) { init(); } else { setTimeout(startScript, 500); } } startScript(); })();