// ==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();
})();