// ==UserScript==
// @name 牛牛聊天发图片插件
// @namespace https://www.milkywayidle.com/
// @version 0.1.8
// @description 让牛牛聊天支持发送图片,发送任何外链图片链接时,自动转换为图片,可以点击查看大图;支持粘贴图片自动上传
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const imageType = 1;//改成0则不显示图片,而是显示为[图片],手动点击即可查看
const imageDefaultText = '[图片]';//会显示为[图片],也可以改成[image]等,按需,需要imageType改成0才生效
GM_addStyle(`
.chat-img{display:inline-block}
.chat-img img{display:inline-flex;margin:1px 4px;max-height:60px;max-width:100px;width:fit-content;border:2px solid #778be1;border-radius:4px;padding:1px;white-space:nowrap;background:#000;cursor:pointer}
.chat-img span{padding:0 1px;border:0;margin:0;background:unset}
.chat-img-preview{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:zoom-out}
.chat-img-preview img{max-width:90%;max-height:90%;border:2px solid #fff;border-radius:4px}
.upload-status{position:fixed;bottom:20px;right:20px;padding:10px 15px;background:#4caf50;color:#fff;border-radius:4px;z-index:10000;box-shadow:0 2px 10px rgba(0,0,0,.2)}
.emoji-btn{width:28px;height:28px;display:flex;justify-content:center;align-items:center;cursor:pointer;position:relative;border-radius:4px;padding:4px;background-color:var(--color-midnight-500);margin:2px}
.emoji-btn:hover{background-color:var(--color-midnight-300)}
.emoji-panel{display:none;position:absolute;width:450px;background:#2d2d2d;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.3);z-index:10000;border:solid 2px var(--color-midnight-100);border-radius:4px;padding:12px;background-color:var(--color-midnight-900);box-shadow:rgba(0,0,0,.3) 2px 2px 10px 6px;color:var(--color-text-dark-mode)}
.emoji-panel.show{display:block}
.emoji-header{align-items:center;font-size:18px;font-weight:600;text-align:center;padding-bottom:10px}
.emoji-tabbar{display:flex;flex-wrap:wrap}
.emoji-tab{background:0 0;border:none;padding:5px 10px;border-radius:4px;cursor:pointer}
.emoji-tab.active{background:var(--color-space-600)}
.emoji-tab:hover{background:var(--color-midnight-300)}
.emoji-content{padding-top:10px}
.emoji-close{background:0 0;border:none;position:absolute;top:6px;right:6px;height:22px;width:22px;padding:4px;cursor:pointer}
.emoji-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;padding:12px;background-color:var(--color-midnight-700);border-radius:4px;max-height:300px;overflow-y:scroll}
.emoji-item{cursor:pointer;padding:4px;border-radius:4px;transition:background .2s}
.emoji-item:hover{background:#3d3d3d}
.emoji-item img{width:100%;height:auto}
.link-tooltip{position:absolute}
.link-tooltip .GuideTooltip_paragraph__18Zcq{white-space:normal;overflow-wrap:break-word}
`);
const chatHistorySelector = 'div.ChatHistory_chatHistory__1EiG3';
const chatMessageSelector = 'div.ChatMessage_chatMessage__2wev4';
const chatInputSelector = '.Chat_chatInput__16dhX';
const historyObservers = new WeakMap();
let inputObserver = null;
let globalObserver;
const handledInputs = new WeakSet();
let isProcessing = false;
let emojiBtnObserver;
let emojiPanel;
let tooltip;
const COMPRESSED_EMOJI_DATA = [
["Adela", "2025/05/13", ["6822787e1f9a3", "682278801af4c", "68227876259d7","68227880e8b5b", "682278829c625", "682278b51e4ce"]],
["Adriana", "2025/05/13", ["682278dc64bc8", "682278df2aec2", "682278df9f902","682278dec78c1", "682278e19aeef", "682278e57cc98","682278e80ef54", "68227924b3820", "682279269379c","6822792ad0801", "68227927a2726", "6822792e99f9d","6822792495782"]],
["Aiden", "2025/05/13", ["682279ee7d34a", "682279e92545b", "682279ef77d86","682279fbc0d07", "682279e9083d6", "682279f2098d4","682279fbbcad6"]],
["Alex", "2025/05/13", ["68227a8437342", "68227a836aedc", "68227a7e3b1e4","68227a85b5b3b", "68227a89a93e4", "68227a7e4090e","68227ac730057", "68227aca492d5", "68227ac87c6a9","68227acc230e7", "68227ad188f42", "68227ac961e61"]],
["Angelika", "2025/05/13", ["68227b12b2d6b", "68227b08f0c8f", "68227b09592c0","68227b1146ce5", "68227b101397b", "68227b0a74f09"]],
["Arda", "2025/05/13", ["68227b5a04178", "68227b42d179b", "68227b42dc9ee","68227b43504c5", "68227b43aa87c", "68227b4d2723d"]],
["Aya", "2025/05/13", ["68227bae53837", "68227bb199b76", "68227baf9e9f6","68227bbe4ca1e", "68227bc80c410", "68227bae947b1","68227baf17e05", "68227bed10bad", "68227bef7fbe2","68227bf144282", "68227befad25f", "68227bf5c9372","68227bedf39e0"]],
["Azuko", "2025/05/13", ["68227c26ec0fe", "68227c26e940c", "68227c28409ae","68227c2b464a5", "68227c309dca2", "68227c26ed151"]],
["Barbara", "2025/05/13", ["68227c61bf4c6", "68227c6237b22", "68227c57ebdfd","68227c5d4423b", "68227c591a910", "68227c59b5200","68227c5c26676"]],
["Bernice", "2025/05/13", ["68227ca1a7788", "68227c981d78b", "68227c9b7ac7c","68227c9886e21", "68227c9c8be28", "68227c992289b"]],
["Bianca", "2025/05/13", ["6822858c83f7e", "6822856d2124a", "68228560c0411","6822856bed04b", "6822857080c78", "6822856132aa0"]],
["Camilo", "2025/05/13", ["682285dda8f27", "682285ee5344b", "682285f009ce2","682285f1550dc", "682285c91b753", "682285c5e5d85","68228617e9e7c", "6822861e34e07", "6822861964d77","6822861aa750e", "6822861f903f5", "6822862391bd6"]],
["Cathy", "2025/05/13", ["6822acb40087a", "6822acba3cd86", "6822acbceb492","6822acc0d932b", "6822acb69050b", "6822acb64842b"]],
["Celine", "2025/05/13", ["6822acfcb9a01", "6822ad00edbae", "6822ad0279376","6822ad044678c", "6822acfcd1fdf", "6822acfd162d9"]],
["Chiara", "2025/05/13", ["6822ad395e9e6", "6822aea264109", "6822aea5c9b49","6822ae8c74312", "6822aea71a27a", "6822aea2dc7b7","6822ae8deec53"]],
["Chloe", "2025/05/13", ["6822aee5d8781", "6822aee543990", "6822aee6163ad","6822aef034223", "6822aee60e506", "6822aee7ee8fb"]],
["Dailin", "2025/05/13", ["6822af2e22c67", "6822af243c5dc", "6822af2475945","6822af2701928", "6822af297f693", "6822af25ed9c9"]],
["Daniel", "2025/05/13", ["6822af6cca77a","6822af6e3408b","6822af6964153","6822af72aaa16","6822af69c393e","6822af6cca77a"]],
["Echion", "2025/05/13", ["6822afcab90f4","6822afcb91e5b","6822afce7977b","6822afbc30f0b","6822afcbe1d9e","6822afcab90f4"]],
["Elena", "2025/05/13", ["6822b010afdcd","6822b009b3312","6822b00f64656","6822b010d46df","6822b009ed6fe","6822b00ba6d2a"]],
["Eleven", "2025/05/13", ["6822b04e22136","6822b05403d3a","6822b0596558f","6822b0520afb4","6822b0520470c","6822b04e85508"]],
["Emma", "2025/05/13", ["6822b04e22136","6822b05403d3a","6822b0596558f","6822b0520afb4","6822b0520470c","6822b04e85508"]],
];
function decompressEmojiData() {
const baseUrl = "https://tupian.li/images/";
return COMPRESSED_EMOJI_DATA.map(([name, date, files]) => ({
name,
list: files.map(file => `${baseUrl}${date}/${file}.png`)
}));
}
const emojiData = decompressEmojiData();
function isImageUrl(url) {// 检查链接是否是图片
return url && /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
}
function createPreviewOverlay(imgSrc) {//创建预览
const overlay = document.createElement('div');
overlay.className = 'chat-img-preview';
const previewImg = document.createElement('img');
previewImg.src = imgSrc;
overlay.appendChild(previewImg);
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => {// 点击后关闭图片预览
if (e.target === overlay || e.target === previewImg) {
document.body.removeChild(overlay);
}
});
document.addEventListener('keydown', function handleEsc(e) {// ESC关闭图片预览
if (e.key === 'Escape') {
document.body.removeChild(overlay);
document.removeEventListener('keydown', handleEsc);
}
});
}
function createPreviewableLink(url, altText) {//创建可预览的链接
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noreferrer noopener nofollow';
link.className = 'chat-img';
const img = document.createElement('img');
img.src = url;
img.alt = altText;
link.appendChild(img);
link.addEventListener('click', function(e) {
if (e.ctrlKey || e.metaKey) return; // 允许Ctrl+点击在新标签打开
e.preventDefault();
e.stopImmediatePropagation();
createPreviewOverlay(url);
});
return link;
}
function replaceLinkContentWithImage(link) {//修改A标签内的图片
const href = link.getAttribute('href');
if (!isImageUrl(href)){//普通链接
if (link.querySelector('.chat-link')) return;
const newLink = createALink(href, '网页链接');
link.parentNode.replaceChild(newLink, link);
return
}
if (link.querySelector('.chat-img')) return;
const newLink = createPreviewableLink(href, '图片预览');
link.parentNode.replaceChild(newLink, link);
}
function createALink(url, altText) {//普通链接
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noreferrer noopener nofollow';
link.className = 'chat-link';
link.innerHTML = '[网页链接]';
if(!tooltip) {
tooltip = document.createElement('div');
tooltip.className = 'link-tooltip MuiPopper-root MuiTooltip-popper css-112l0a2';
tooltip.innerHTML = `
<div class="MuiTooltip-tooltip MuiTooltip-tooltipPlacementBottom css-1spb1s5">
<div class="GuideTooltip_guideTooltipText__PhA_Q">
<div class="GuideTooltip_title__1QDN9">网页链接</div>
<div class="GuideTooltip_content__1_yqJ">
<div class="GuideTooltip_paragraph__18Zcq">${url}</div>
</div>
</div>
</div>
`;
document.body.appendChild(tooltip);
}
link.addEventListener('mouseover', (e) => {
const contentEl = tooltip.querySelector('.GuideTooltip_paragraph__18Zcq');
contentEl.textContent = e.target.href;
positionTooltip(e.target);
/*
const rect = e.target.getBoundingClientRect();
tooltip.style.left = `${rect.left + window.scrollX}px`;
tooltip.style.top = `${rect.top + window.scrollY - tooltip.offsetHeight - 5}px`;
tooltip.style.display = 'block';
*/
});
link.addEventListener('mouseout', () => {
tooltip.style.display = 'none';
});
return link;
}
function positionTooltip(link) {
tooltip.style.display = 'block';
tooltip.style.left = '0';
tooltip.style.top = '0';
const linkRect = link.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const windowWidth = window.innerWidth;
let left = linkRect.left + (linkRect.width - tooltipRect.width) / 2;
let top = linkRect.top - tooltipRect.height - 5;
if(left + tooltipRect.width > windowWidth) left = windowWidth - tooltipRect.width - 5;
if(left < 5) left = 5;
if(top < window.scrollY) top = linkRect.bottom + window.scrollY + 5;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
}
function convertEmojiCodes(container) {
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
if (node.parentNode.classList?.contains('processed-emoji')) {
return NodeFilter.FILTER_REJECT;
}
return /{::\d+_\d+}/.test(node.nodeValue) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
}
},
false
);
let node;
while ((node = walker.nextNode())) {
const fragment = document.createDocumentFragment();
const parts = node.nodeValue.split(/({::\d+_\d+})/);
parts.forEach(part => {
if (!part) return;
const emojiMatch = part.match(/{::(\d+)_(\d+)}/);
if (emojiMatch) {
const groupIndex = parseInt(emojiMatch[1]) - 1;
const emojiIndex = parseInt(emojiMatch[2]) - 1;
// 安全检查
if (emojiData[groupIndex]?.list[emojiIndex]) {
const url = emojiData[groupIndex].list[emojiIndex];
const link = createPreviewableLink(url, `emoji:${groupIndex+1}_${emojiIndex+1}`);
fragment.appendChild(link);
return;
}
}
fragment.appendChild(document.createTextNode(part));
});
if (node.parentNode) {
const wrapper = document.createElement('span');
wrapper.className = 'processed-emoji';
wrapper.appendChild(fragment);
node.parentNode.replaceChild(wrapper, node);
}
}
}
function processExistingMessages(container) {//聊天页面消息处理
const messages = container.querySelectorAll(chatMessageSelector);
messages.forEach(msg => {
const links = msg.querySelectorAll('a');
links.forEach(replaceLinkContentWithImage);
convertEmojiCodes(msg);
});
}
function observeChatHistory(chatHistory) {//监听聊天页面变化
processExistingMessages(chatHistory);
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const messages = node.matches(chatMessageSelector) ? [node] : node
.querySelectorAll(chatMessageSelector);
messages.forEach(msg => {
const links = msg.querySelectorAll('a');
links.forEach(replaceLinkContentWithImage);
});
}
});
});
});
observer.observe(chatHistory, {
childList: true,
subtree: true
});
}
function initClipboardUpload() {
if (inputObserver && typeof inputObserver.disconnect === 'function') {
inputObserver.disconnect();
}
const chatInput = document.querySelector(chatInputSelector);
if (chatInput && !handledInputs.has(chatInput)) {
setupPasteHandler(chatInput);
return;
}
inputObserver = new MutationObserver((mutations) => {
initEmojiPanel();
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const input = node.matches(chatInputSelector) ? node : node.querySelector(chatInputSelector);
if (input && !handledInputs.has(input)) {
setupPasteHandler(input);
}
}
});
});
});
inputObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function setupPasteHandler(inputElement) {
handledInputs.add(inputElement);
inputElement.removeEventListener('paste', handlePaste);
inputElement.addEventListener('paste', handlePaste);
let isProcessing = false;
async function handlePaste(e) {
if (isProcessing) {
e.preventDefault();
return;
}
isProcessing = true;
try {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.preventDefault();
const blob = items[i].getAsFile();
if (blob) await uploadAndInsertImage(blob, inputElement);
break;
}
}
} finally {
isProcessing = false;
}
}
}
function uploadAndInsertImage(blob, inputElement) {//上传图片
const statusDiv = document.createElement('div');
statusDiv.className = 'upload-status';
statusDiv.textContent = '正在上传图片...';
document.body.appendChild(statusDiv);
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
const formParts = [];
function appendFile(name, file) {
formParts.push(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"; filename="${file.name}"\r\nContent-Type: ${file.type}\r\n\r\n`);
formParts.push(file);
formParts.push('\r\n');
}
appendFile('file', blob);
formParts.push(`--${boundary}--\r\n`);
const bodyBlob = new Blob(formParts);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://tupian.li/api/v1/upload',
data: bodyBlob,
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Accept': 'application/json'
},
binary: true,
onload: function(response) {
statusDiv.remove();
if (response.status === 200) {
try {
const result = JSON.parse(response.responseText);
if (result.status) {
const url = result.data.links.url;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
const currentValue = inputElement.value;
const newValue = currentValue ? `${currentValue} ${url}` : url;
nativeInputValueSetter.call(inputElement, newValue);
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.focus();
const successDiv = document.createElement('div');
successDiv.className = 'upload-status';
successDiv.textContent = '上传成功!';
document.body.appendChild(successDiv);
setTimeout(() => successDiv.remove(), 2000);
} else {
throw new Error(result.message || '上传失败');
}
} catch (e) {
showError('解析失败: ' + e.message);
}
} else {
showError('服务器错误: ' + response.status);
}
},
onerror: function(error) {
statusDiv.remove();
showError('上传失败: ' + error.statusText);
}
});
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'upload-status error';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 3000);
console.error(message);
}
}
function insertAtCursor(inputElement, text) {//插入文本,兼容SB VUE
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
).set;
nativeInputValueSetter.call(inputElement,inputElement.value.substring(0, start) + text + inputElement.value.substring(end)
);
const event = new Event('input', {
bubbles: true,
cancelable: true
});
inputElement.dispatchEvent(event);
inputElement.selectionStart = inputElement.selectionEnd = start + text.length;
inputElement.focus();
}
function showStatus(message, duration = 3000, isError = false) {//上传状态
const existingStatus = document.querySelector('.upload-status');
if (existingStatus) existingStatus.remove();
const statusDiv = document.createElement('div');
statusDiv.className = 'upload-status';
statusDiv.textContent = message;
statusDiv.style.background = isError ? '#F44336' : '#4CAF50';
document.body.appendChild(statusDiv);
setTimeout(() => {
statusDiv.remove();
}, duration);
}
function setupHistoryObserver(historyElement) {//设置聊天记录监听
if (historyObservers.has(historyElement)) {
historyObservers.get(historyElement).disconnect();
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const messages = node.matches(chatMessageSelector) ? [node] : node.querySelectorAll(chatMessageSelector);
messages.forEach((msg) => {
const links = msg.querySelectorAll('a');
links.forEach(replaceLinkContentWithImage);
convertEmojiCodes(msg);
});
}
});
});
});
observer.observe(historyElement, {
childList: true,
subtree: true
});
historyObservers.set(historyElement, observer);
const messages = historyElement.querySelectorAll(chatMessageSelector);
messages.forEach((msg) => {
const links = msg.querySelectorAll('a');
links.forEach(replaceLinkContentWithImage);
});
}
function setupGlobalObserver() {//全局监听
globalObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const newHistories = node.matches(chatHistorySelector) ? [node] : node.querySelectorAll(chatHistorySelector);
newHistories.forEach(setupHistoryObserver);
}
});
});
});
globalObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function initEmojiPanel() {
const chatInput = document.querySelector(chatInputSelector);
if (!chatInput || chatInput.previousElementSibling?.classList.contains('emoji-btn')) {
return;
}
const emojiBtn = document.createElement('div');
emojiBtn.className = 'emoji-btn';
emojiBtn.innerHTML = '<svg role="img" aria-label="action icon" width="100%" height="100%"><use href="/static/media/actions_sprite.e6388cbc.svg#cow"></use></svg>';
chatInput.parentNode.insertBefore(emojiBtn, chatInput);
const panelContainer = document.createElement('div');
panelContainer.innerHTML = `
<div class="emoji-panel">
<div class="emoji-header">
<span>选择表情</span>
<button class="emoji-close"><svg role="img" aria-label="Close" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#close_menu"></use></svg></button>
</div>
${createEmojiPanelHTML()}
</div>`;
document.body.appendChild(panelContainer);
emojiPanel = document.querySelector('.emoji-panel');
if (!emojiPanel) return;
emojiBtn.addEventListener('click', (e) => {//打开表情按钮
e.stopPropagation();
emojiPanel.classList.toggle('show');
const btnRect = emojiBtn.getBoundingClientRect();
emojiPanel.style.bottom = `${window.innerHeight - btnRect.top + 3}px`;
emojiPanel.style.left = `${btnRect.left}px`;
emojiPanel.style.width = `${document.querySelector('.Chat_chatInputContainer__2euR8').offsetWidth - 4}px`
});
const closeBtn = emojiPanel.querySelector('.emoji-close');//关闭表情面板
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
emojiPanel.classList.remove('show');
});
}
emojiPanel.querySelectorAll('.emoji-tab').forEach(tab => {
tab.addEventListener('click', () => {
const groupIndex = parseInt(tab.dataset.group);
if (isNaN(groupIndex)) return;
emojiPanel.querySelector('.emoji-content').innerHTML = createEmojiGroupHTML(groupIndex);
emojiPanel.querySelectorAll('.emoji-tab').forEach(t =>
t.classList.remove('active'));
tab.classList.add('active');
});
});
if(!emojiPanel._hasEmojiListener) {//表情按钮 修复一个重复执行的BUG
emojiPanel.addEventListener('click', (e) => {
const emojiItem = e.target.closest('.emoji-item');
if (!emojiItem) return;
e.stopPropagation();
e.stopImmediatePropagation();
const chatInput = document.querySelector('.Chat_chatInput__16dhX');
if (chatInput) {
const groupId = emojiItem.dataset.group;
const emojiId = emojiItem.dataset.emoji;
insertAtCursor(chatInput, `{::${groupId}_${emojiId}}`);
}
emojiPanel.classList.remove('show');
});
emojiPanel._hasEmojiListener = true;
}
document.addEventListener('click', (e) => {//关闭面板
if (!emojiPanel.contains(e.target) && e.target !== emojiBtn) {
emojiPanel.classList.remove('show');
}
});
return panelContainer;
}
function createEmojiPanelHTML() {
return `
<div class="emoji-tabbar">
${emojiData.map((group, index) => `
<button class="emoji-tab ${index === 0 ? 'active' : ''}"
data-group="${index}">
${group.name}
</button>
`).join('')}
</div>
<div class="emoji-content">
${createEmojiGroupHTML(0)}
</div>
`;
}
function createEmojiGroupHTML(groupIndex) {
const group = emojiData[groupIndex];
if (!group) return '';
return `
<div class="emoji-grid" data-group="${groupIndex}">
${group.list.map((url, emojiIndex) => `
<div class="emoji-item"
data-group="${groupIndex + 1}"
data-emoji="${emojiIndex + 1}">
<img src="${url}" alt="{::${groupIndex + 1}_${emojiIndex + 1}}">
</div>
`).join('')}
</div>
`;
}
function setupEmojiPanel() {
document.querySelectorAll('.emoji-item').forEach(item => {
item.addEventListener('click', () => {
const key = item.dataset.key;
const input = document.querySelector(chatInputSelector);
if (input) {
insertAtCursor(input, `{::${key}}`);
document.querySelector('.emoji-panel')?.classList.remove('show');
}
});
});
}
function initEmojiButtonSystem() {
emojiBtnObserver?.disconnect();
tryAddEmojiButton();
emojiBtnObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
tryAddEmojiButton();
}
});
});
emojiBtnObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function tryAddEmojiButton() {
const chatInput = document.querySelector(chatInputSelector);
if (!chatInput || chatInput.previousElementSibling?.classList.contains('emoji-btn')) {
return;
}
if (!document.querySelector('.emoji-panel')) {
initEmojiPanel();
}
}
// ---------- 插件,启动! ----------
function init() {
document.querySelectorAll(chatHistorySelector).forEach(setupHistoryObserver);
const chatHistories = document.querySelectorAll(chatHistorySelector);
if (chatHistories.length === 0) {
setTimeout(init, 1000);
return;
}
chatHistories.forEach(observeChatHistory);
const globalObserver = new MutationObserver(mutations => {
initEmojiButtonSystem()
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const newHistories = node.querySelectorAll?.(chatHistorySelector) || [];
newHistories.forEach(observeChatHistory);
}
});
});
});
globalObserver.observe(document.body, {
childList: true,
subtree: true
});
}
window.addEventListener('unload', () => {
globalObserver?.disconnect();
historyObservers.forEach(obs => obs.disconnect());
});
init();
initClipboardUpload();
setupGlobalObserver();
initEmojiButtonSystem();
if (!emojiPanel) {
emojiPanel = initEmojiPanel();
}
})();