Greasy Fork

Greasy Fork is available in English.

牛牛聊天增强插件

让牛牛聊天支持发送图片,发送任何外链图片链接时,自动转换为图片,可以点击查看大图;支持粘贴图片自动上传

// ==UserScript==
// @name         牛牛聊天增强插件
// @namespace    https://www.milkywayidle.com/
// @version      0.1.12
// @description  让牛牛聊天支持发送图片,发送任何外链图片链接时,自动转换为图片,可以点击查看大图;支持粘贴图片自动上传
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @icon         https://www.milkywayidle.com/favicon.svg
// @license MIT
// ==/UserScript==

(function() {
	'use strict';
	GM_addStyle(`
body[data-fontsize="1"] .ChatMessage_chatMessage__2wev4{font-size:18px}
body[data-fontsize="2"] .ChatMessage_chatMessage__2wev4{font-size:20px}
body[data-fontsize="3"] .ChatMessage_chatMessage__2wev4{font-size:24px;line-height:24px}
body[data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{display:inline-flex;height:18px;grid-gap:4px;gap:4px;align-items:center;margin:1px 4px;width:-moz-fit-content;width:fit-content;border:1px solid #778be1;border-radius:4px;padding:1px 5px;white-space:nowrap;background:#4357af}
.ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:auto;}
body[data-fontsize="1"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:18px;}
body[data-fontsize="2"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:20px;}
body[data-fontsize="3"][data-username="1"] .ChatMessage_name__1W9tB.ChatMessage_clickable__58ej2{height:24px;}
body[data-fontsize="1"] .ChatMessage_chatMessage__2wev4 .CharacterName_chatIcon__22lxV{height:20px;width:20px}
body[data-fontsize="2"] .ChatMessage_chatMessage__2wev4 .CharacterName_chatIcon__22lxV{height:24px;width:24px}
body[data-fontsize="3"] .ChatMessage_chatMessage__2wev4 .CharacterName_chatIcon__22lxV{height:28px;width:28px}
body[data-chattime="1"] .ChatMessage_chatMessage__2wev4 .ChatMessage_timestamp__1iRZO{display:none}
.ChatMessage_chatMessage__2wev4 span>span>span{display:flex}
body[data-at="1"] .ChatMessage_chatMessage__2wev4.ChatMessage_mention__1pKLW{border:2px dashed var(--color-midnight-100)}
body[data-at="1"] .ChatMessage_chatMessage__2wev4.ChatMessage_mention__1pKLW>span:last-child{color:var(--color-scarlet-100)}
body[data-ic="1"] .ChatMessage_chatMessage__2wev4 .CharacterName_gameMode__2Pvw8,.ic-icon{display:inline-block;border-radius:50%;color:#000!important;width:14px;height:14px;font-size:0;margin:3px 0 0 2px;border:1px solid #fff;background:linear-gradient(61deg,var(--color-neutral-300),var(--color-neutral-300) 3%,var(--color-neutral-100) 15%,var(--color-neutral-0) 50%,var(--color-neutral-200) 70%,var(--color-neutral-300) 95%,var(--color-neutral-300))}
.CharacterName_characterName__2FqyZ{font-size:unset;align-items:center}
.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,.chat-conf{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,.chat-conf: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}
.chat-config{z-index:1000;position:absolute;top:0;left:0;height:100%;width:100%;color:var(--color-text-dark-mode)}
.chat-config-mask{height:100%;width:100%;background-color:var(--color-midnight-800);opacity:.8}
.chat-config-window{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;align-items:flex-end}
.chat-config-window-npc{z-index:1;margin-left:-40px;margin-right:-20px;vertical-align:bottom}
.chat-config-window-npc svg{width:130px;height:100px}
.chat-config-window-npc-name{margin:0 10px;border-radius:4px;font-size:14px;font-weight:500;background-color:var(--color-space-600);text-align:center}
.chat-config-window-content{min-width:300px;max-width:98%;min-height:100px;max-height:98%;font-weight:400;overflow:auto;display:flex;flex-direction:column;font-size:16px;line-height:18px;background:#2d2d2d;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)}
.chat-config-title{font-size:18px;font-weight:600;text-align:center}
.chat-config-close{background:0 0;border:none;position:absolute;top:6px;right:6px;height:22px;width:22px;padding:4px;cursor:pointer}
#saveConfig{border-radius:4px;width:-moz-fit-content;width:fit-content;min-width:50px;border:none;font-family:Roboto;font-weight:600;text-align:center;overflow:hidden;cursor:pointer;display:flex;align-items:center;justify-content:center;background-color:var(--color-success);color:#000;height:36px;padding:0 10px;font-size:14px;line-height:15px;align-self:flex-end}
#saveConfig:hover{background-color:var(--color-success-hover)}
.chat-config-main{display:flex;gap:8px;padding:12px;background-color:var(--color-midnight-700);border-radius:4px;margin:13px 0;flex-direction:column}
.chat-config-item:not(:last-child){border-bottom:1px dashed var(--color-midnight-100);padding-bottom:10px}
.chat-config-item-title{font-size:18px;padding-bottom:6px}
.chat-config-item-set{display:flex;align-items:center;padding:6px 0 6px 10px}
.chat-config-item-set>span{flex:auto}
.chat-config-preview{background:#191b24;margin-top:10px;padding:10px;font-size:14px;line-height:20px;text-align:left;border-radius:4px}
.chat-config-preview .ChatMessage_chatMessage__2wev4{white-space:unset;padding:10px 0}
.chat-config-tip{color:var(--color-success);font-size:14px;padding-top:6px}
.ic-icon-default{color:var(--color-neutral-300)}
    `);
	const chatHistorySelector = '.ChatHistory_chatHistory__1EiG3';
	const chatMessageSelector = '.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", ["6822cb1434582","6822cb1577f35","6822cb0b55713","6822cb1641e76","6822cb0ca4e96","6822cb0c9bb38","6822cb18ecf07","6822cb0d58e5f","6822cb0e73a13","6822cb11e7275","6822cb13c0bcb","6822cb1ad69a6"]],
		["Ersha", "2025/05/13", ["6822cb873ce8f","6822cb86a51a4","6822cb8a2a04e","6822cb8366e28","6822cb8747d70","6822cb8466238"]],
	];
	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,emoji) {//创建可预览的链接
		emoji = emoji || null;
		const link = document.createElement('a');
		link.href = url;
		link.target = '_blank';
		link.rel = 'noreferrer noopener nofollow';
		link.className = 'chat-img';
		if(emoji || GM_getValue('option_img',0) == 1){
			var img = document.createElement('img');
			img.src = url;
			img.alt = altText;
		}else{
			var img = document.createElement('span');
			img.innerHTML = GM_getValue('img_title','[图片]');
		}
		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;
			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">${href}</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);
			});
			link.addEventListener('mouseout', () => {
				tooltip.style.display = 'none';
			});	
			return
		}
		if (link.querySelector('.chat-img') || link.querySelector('img')) return;

		const newLink = createPreviewableLink(href, '图片预览');
		link.parentNode.replaceChild(newLink, 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}`,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);
							convertEmojiCodes(msg);
						});
					}
				});
			});
		});
		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);
			convertEmojiCodes(msg);
		});
	}
	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`;
			let left = btnRect.left;
			let width = document.querySelector('.Chat_chatInputContainer__2euR8').getBoundingClientRect().width - 4;
			if(width < 600 && window.innerWidth >= 600) width = 600;
			if(window.innerWidth < left + width) left = window.innerWidth - width;
			emojiPanel.style.left = `${left}px`;
			emojiPanel.style.width = `${width}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(chatInputSelector);
				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 showConfigDialog() {
		if (document.getElementById('configDialog')) {
			document.getElementById('configDialog').style.display = 'block';
			return;
		}
		let html = `
			<div id="configDialog" class="chat-config">
				<div class="chat-config-mask"></div>
				<div class="chat-config-window">
					<div class="chat-config-window-npc">
						<svg role="img" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#purple_cow_hello"></use></svg>
						<div class="chat-config-window-npc-name">小牛紫</div>
					</div>
					<div class="chat-config-window-content">
						<span class="chat-config-title">插件配置</span>
						<button id="closeConfig" class="chat-config-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 class="chat-config-main">
							<div class="chat-config-item">
								<div class="chat-config-item-title">图片预览</div>
								<div class="chat-config-tip">图片预览相关设置需要收起并重新打开聊天栏才会生效</div>
								<div class="chat-config-item-set">
									<span>
										<input id="chatconf_1" checked="checked" value="1" name="chatconf_img" type="radio">
										<label for="chatconf_1">直接显示图片</label>
									</span>
									<span>
										<input id="chatconf_2" value="0" name="chatconf_img" type="radio">
										<label for="chatconf_2">显示为文本:<input type="text" id="img_title"></label>
									</span>
								</div>
							</div>
							<div class="chat-config-item">
								<div class="chat-config-item-title">文本设置</div>
								<div class="chat-config-item-set">
									<span>
										<label><input type="checkbox" id="option_username"> 突出用户名</label>
									</span>
									<span>
										<label><input type="checkbox" id="option_chattime"> 不显示时间</label>
									</span>
									<span>
										<label><input type="checkbox" id="option_at"> 突出@消息</label>
									</span>
								</div>
								<div class="chat-config-item-title">文本大小</div>
								<div class="chat-config-item-set">
									<span>
										<input id="chatconf_3" checked="checked" value="0" name="chatconf_fontsize" type="radio">
										<label for="chatconf_3" style="font-size:14px">小</label>
									</span>
									<span>
										<input id="chatconf_4" value="1" name="chatconf_fontsize" type="radio">
										<label for="chatconf_4" style="font-size:18px">中</label>
									</span>
									<span>
										<input id="chatconf_5" value="2" name="chatconf_fontsize" type="radio">
										<label for="chatconf_5" style="font-size:20px">大</label>
									</span>
									<span>
										<input id="chatconf_6" value="3" name="chatconf_fontsize" type="radio">
										<label for="chatconf_6" style="font-size:24px">超大</label>
									</span>
								</div>
								<div class="chat-config-item-title">铁牛图标</div>
								<div class="chat-config-item-set">
									<span>
										<input id="chatconf_7" checked="checked" value="0" name="chatconf_ic" type="radio">
										<label for="chatconf_7">默认 <span class="ic-icon-default">[IC]</span></label>
									</span>
									<span>
										<input id="chatconf_8" value="1" name="chatconf_ic" type="radio">
										<label for="chatconf_8" style="display:inline-flex">图标 <span class="ic-icon">[IC]</span></label>
									</span>
								</div>
								<div class="chat-config-preview">
									<div class="ChatMessage_chatMessage__2wev4">
										<span class="ChatMessage_timestamp__1iRZO">[11:45:14] </span>
										<span class="ChatMessage_name__1W9tB ChatMessage_clickable__58ej2">
											<div class="CharacterName_characterName__2FqyZ" translate="no">
												<div class="CharacterName_chatIcon__22lxV">
													<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#moderator"></use></svg>
												</div>
												<div class="CharacterName_chatIcon__22lxV">
													<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#anniversary_purple"></use></svg>
												</div>
												<div class="CharacterName_name__1amXp CharacterName_rainbow__1GTos" data-name="Stella">
													<span>Stella</span>
												</div>
												<div class="CharacterName_gameMode__2Pvw8">[IC]</div>
											</div>
										</span>
										<span>: </span>
										<span>杀!</span>
									</div>
									<div class="ChatMessage_chatMessage__2wev4 ChatMessage_mention__1pKLW">
										<span class="ChatMessage_timestamp__1iRZO">[11:45:14] </span>
										<span class="ChatMessage_name__1W9tB ChatMessage_clickable__58ej2">
											<div class="CharacterName_characterName__2FqyZ" translate="no">
												<div class="CharacterName_chatIcon__22lxV">
													<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#moderator"></use></svg>
												</div>
												<div class="CharacterName_chatIcon__22lxV">
													<svg role="img" aria-label="chat icon" class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#ice_sorcerer"></use></svg>
												</div>
												<div class="CharacterName_name__1amXp CharacterName_fancy_blue__Vk2EJ" data-name="AlphB">
													<span>AlphB</span>
												</div>
												<div class="CharacterName_gameMode__2Pvw8"></div>
											</div>
										</span>
										<span>: </span>
										<span>@Stella 闪!</span>
									</div>
								</div>
							</div>
						</div>
						<button id="saveConfig">保存</button>
					</div>
				</div>
			</div>
		`;
		document.body.insertAdjacentHTML('beforeend', html);
		document.getElementById('option_username').checked = GM_getValue('option_username',0);
		document.getElementById('option_chattime').checked = GM_getValue('option_chattime',0);
		document.getElementById('option_at').checked = GM_getValue('option_at',0);
		document.getElementById('img_title').value = GM_getValue('img_title','[图片]');
		document.querySelector(`input[name="chatconf_img"][value="${GM_getValue('option_img', '0')}"]`).checked = true;
		document.querySelector(`input[name="chatconf_fontsize"][value="${GM_getValue('option_fontsize', '0')}"]`).checked = true;
		document.querySelector(`input[name="chatconf_ic"][value="${GM_getValue('option_ic', '0')}"]`).checked = true;
		document.getElementById('saveConfig').addEventListener('click', function() {
			GM_setValue('img_title', document.getElementById('img_title').value);
			document.getElementById('configDialog').style.display = 'none';
		});
		document.querySelectorAll('input[name="chatconf_img"]').forEach(radio => {
			radio.addEventListener('click', function() {
				GM_setValue('option_img', this.value);
			});
		});
		document.querySelectorAll('input[name="chatconf_fontsize"]').forEach(radio => {
			radio.addEventListener('click', function() {
				const font_val = this.value;
				document.body.setAttribute('data-fontsize', font_val);
				GM_setValue('option_fontsize', font_val);
			});
		});
		document.querySelectorAll('input[name="chatconf_ic"]').forEach(radio => {
			radio.addEventListener('click', function() {
				const ic_val = this.value;
				document.body.setAttribute('data-ic', ic_val);
				GM_setValue('option_ic', ic_val);
			});
		});
		document.getElementById('option_chattime').addEventListener('click', function() {
			var chattime = document.getElementById('option_chattime').checked ? 1 : 0;
			GM_setValue('option_chattime', chattime);
			document.body.setAttribute('data-chattime', chattime);
		});
		document.getElementById('option_at').addEventListener('click', function() {
			var at = document.getElementById('option_at').checked ? 1 : 0;
			GM_setValue('option_at', at);
			document.body.setAttribute('data-at', at);
		});
		document.getElementById('option_username').addEventListener('click', function() {
			var username = document.getElementById('option_username').checked ? 1 : 0;
			GM_setValue('option_username', username);
			document.body.setAttribute('data-username', username);
		});
		document.getElementById('closeConfig').addEventListener('click', function() {
			document.getElementById('configDialog').style.display = 'none';
		});
		document.querySelector('.chat-config-mask').addEventListener('click', function() {
			document.getElementById('configDialog').style.display = 'none';
		});
	}
	GM_registerMenuCommand("配置", showConfigDialog);
	function chatConfigInit() {
		document.body.setAttribute('data-fontsize', GM_getValue('option_fontsize', '0'));
		document.body.setAttribute('data-username', GM_getValue('option_username', '0'));
		document.body.setAttribute('data-chattime', GM_getValue('option_chattime', '0'));
		document.body.setAttribute('data-at', GM_getValue('option_at', '0'));
		document.body.setAttribute('data-ic', GM_getValue('option_ic', '0'));
		let btn = document.querySelector('.chat-conf');
		if (!btn) {
			btn = document.createElement('div');
			btn.innerHTML = '<svg width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#settings"></use></svg>';
			btn.className = 'chat-conf';
			btn.addEventListener('click', showConfigDialog);
		}
		const insertButton = () => {
			const targetElement = document.querySelector('.TabsComponent_expandCollapseButton__6nOWk');
			
			if (targetElement && targetElement.parentNode) {
				if (btn.parentNode && btn.parentNode !== targetElement.parentNode) {
					btn.remove();
				}
				targetElement.parentNode.insertBefore(btn, targetElement);
				return true;
			}
			return false;
		};
		if (!insertButton()) {
			const observer = new MutationObserver((mutations) => {
				const targetElement = document.querySelector('.TabsComponent_expandCollapseButton__6nOWk');
				if (targetElement && !btn.isConnected) {
					insertButton();
				}
			});
			observer.observe(document.body, {
				childList: true,
				subtree: true,
			});
			setTimeout(() => observer.disconnect(), 1000);
		}
	}
	// ---------- 插件,启动! ----------
	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);
					}
				});
			});
		});
		if(document.readyState === 'complete') {
			chatConfigInit();
		}else{
			window.addEventListener('load', chatConfigInit);
		}
		globalObserver.observe(document.body, {
			childList: true,
			subtree: true
		});
	}
	window.addEventListener('unload', () => {
		globalObserver?.disconnect();
		historyObservers.forEach(obs => obs.disconnect());
	});
	if (window.ReactRouter) {
		window.ReactRouter.useEffect(() => {
			chatConfigInit();
		}, [window.location.pathname]);
	}
	const spaContentObserver = new MutationObserver(() => {
		if (!document.querySelector('.chat-conf')) {
			chatConfigInit();
		}
	});
	spaContentObserver.observe(document.getElementById('root'), { childList: true, subtree: true });
	init();
	initClipboardUpload();
	setupGlobalObserver();
	initEmojiButtonSystem();
	if (!emojiPanel) {
		emojiPanel = initEmojiPanel();
	}
})();