Soop(숲) 채팅 확장 스크립트

이모티콘 창 및 버튼 위치 조정, 채팅 붙여넣기 허용, 채팅창 너비 조절, 커스텀 이모티콘 박스

// ==UserScript==
// @name         Soop(숲) 채팅 확장 스크립트
// @namespace    https://greasyfork.org/scripts/512780
// @icon         https://res.sooplive.co.kr/afreeca.ico
// @version      1.1.3
// @description  이모티콘 창 및 버튼 위치 조정, 채팅 붙여넣기 허용, 채팅창 너비 조절, 커스텀 이모티콘 박스
// @match        https://play.sooplive.co.kr/*
// @license      MIT
// @author       ekzmchoco
// @grant        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
    };
    
 
    const userSettings = JSON.parse(localStorage.getItem('soopChatSettings')) || DEFAULT_SETTINGS;
    const savedSettings = JSON.parse(localStorage.getItem('soopChatSettings')) || {};

 
    function saveSettings() {
        localStorage.setItem('soopChatSettings', JSON.stringify(userSettings));
    }
 
    function initSettingsUI() {
        const chattingArea = document.querySelector("#chatting_area");
        const personSettingEl = chattingArea.querySelector(".chat_layer.sub.person .contents > ul");
    
        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개 연속 입력' }
        ];
    
        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 '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");
        const chatTitleDiv = chattingArea.querySelector(".chat_title");
        if (chatTitleDiv && !document.getElementById('chatWidthSlider')) {
            const ul = chatTitleDiv.querySelector("ul");
            const viewerLi = ul.querySelector("#setbox_viewer");
 
            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")
              : chattingArea.offsetWidth;
            rangeInput.style.width = "80px";
            rangeInput.style.marginRight = "1px";
            rangeInput.id = 'chatWidthSlider';
 
            const rangeLabel = document.createElement("span");
            rangeLabel.style.color = "#fff";
            rangeLabel.style.fontSize = "12px";
 
            rangeInput.addEventListener("input", () => {
                changeChatAreaWidth(rangeInput.value);
                localStorage.setItem("customChattingAreaWidth", rangeInput.value);
            });
 
            sliderLi.appendChild(rangeInput);
            sliderLi.appendChild(rangeLabel);
 
            ul.insertBefore(sliderLi, viewerLi);
 
            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;
                    }
                `;
            }
            const storedWidth = localStorage.getItem("customChattingAreaWidth") || chattingArea.offsetWidth;
            changeChatAreaWidth(storedWidth);
        }
    }
 
    function removeChatWidthAdjustment() {
        const chatWidthSlider = document.getElementById('chatWidthSlider');
        if (chatWidthSlider) {
            chatWidthSlider.parentElement.remove();
        }
        const chatStyleEl = document.getElementById('custom-chat-width-style');
        if (chatStyleEl) {
            chatStyleEl.remove();
        }
    }
 
    function initCustomEmoticonBox() {
        const chattingArea = document.querySelector("#chatting_area");
        const actionBox = chattingArea.querySelector("#actionbox");
        if (!document.querySelector(".customEmojiBtn")) {
            const emoticonBox = document.querySelector("#emoticonBox");
            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']");
    
            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;
    
            function renderEmoticon(type = "default") {
              type === "default" ? defaultEmoticonClick() : OGQEmoticonClick();
              if (isLoading) return;
              isLoading = true;
              setTimeout(() => {
                isLoading = false;
    
                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";
                  return;
                }
    
                const emoticonItemBox = emoticonBox.querySelector(".emoticon_item");
                const itemList = [];
                emoticonItemBox.querySelectorAll("span > a")?.forEach((item, index) => {
                  if (index < 21) {
                    const itemClone = item.cloneNode(true);
    
                    itemClone.addEventListener("click", () => {
                      const repeatCount = (userSettings.emoticonTripleInput && type !== "OGQ") ? 3 : 1;
                      for (let i = 0; i < repeatCount; i++) {
                        item.click();
                      }
                    });
    
                    itemList.push(itemClone);
                  }
                });
                if (isDiffOn) {
                  customEmojiBox.classList.remove(diffType);
                  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 + 8 + "px";
              }, 200);
            }
    
            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");
            });
    
            actionBox
              .querySelector(".item_box")
              .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;
              bottom: 0;
              left: 0;
              width: 100%;
              display: flex;
              flex-wrap: wrap;
              gap: 8px 4px;
              padding: 8px 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);
            }
            `;
            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 styleEl = document.querySelector('#custom-emoticon-style');
        if (styleEl) styleEl.remove();
    }
 
    function enablePasteInChat() {
        $("#write_area").off("cut copy paste");
        $("#write_area").on("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(
                    `<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='none'>
                        <g opacity='1'>
                            <path fill='${iconColor}' d='M19.56 18.396a.498.498 0 1 1 .86.506c-.598 1.015-1.973 2.735-4.421 2.735-2.445 0-3.82-1.717-4.418-2.73a.497.497 0 0 1 .176-.684.5.5 0 0 1 .684.176c.498.845 1.617 2.24 3.558 2.24 1.943 0 3.063-1.397 3.56-2.243Z'/>
                            <path stroke='${iconColor}' stroke-width='.4' d='M11.581 18.906c.598 1.014 1.973 2.732 4.418 2.732 2.448 0 3.823-1.72 4.42-2.736a.498.498 0 1 0-.86-.506c-.497.846-1.617 2.243-3.56 2.243-1.94 0-3.06-1.395-3.559-2.24a.5.5 0 0 0-.683-.176.497.497 0 0 0-.176.683Zm0 0 .078-.045'/>
                            <path fill='${iconColor}' stroke='${iconColor}' stroke-width='.45' d='M19.527 15.805a1.227 1.227 0 1 1 0-2.455 1.227 1.227 0 0 1 0 2.455ZM12.477 15.805a1.228 1.228 0 1 1 .001-2.456 1.228 1.228 0 0 1 0 2.456Z'/>
                            <path stroke='${iconColor}' stroke-width='1.4' d='M16 25.8a9.3 9.3 0 1 1 0-18.6 9.3 9.3 0 0 1 0 18.6Z'/>
                        </g>
                    </svg>`
                );
    
                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 (typeof livePlayer !== 'undefined' && livePlayer.mainMedia) {
            init();
        } else {
            setTimeout(startScript, 500);
        }
    }
 
    startScript();
 
})();