您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
让牛牛聊天支持发送图片、解析图片;支持插件专属表情;支持自定义聊天界面;支持屏蔽复读机
当前为
// ==UserScript== // @name 牛牛聊天增强插件 // @namespace https://www.milkywayidle.com/ // @version 0.1.16 // @description 让牛牛聊天支持发送图片、解析图片;支持插件专属表情;支持自定义聊天界面;支持屏蔽复读机 // @author HouGuoYu // @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-radius:4px;padding:1px 5px;white-space:nowrap;border:1px solid var(--color-space-400);background:var(--color-space-600)} .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[aria-labelledby]>span{display:flex} .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>*:not(:nth-child(-n+2)){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))} body[data-window="1"] .GamePage_gamePage__ixiPl .GamePage_gamePanel__3uNKN .GamePage_contentPanel__Zx4FH .GamePage_middlePanel__uDts7 .GamePage_chatPanel__mVaVt{position:fixed!important;width:unset;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);z-index:100} body[data-window="1"] .TabsComponent_tabsContainer__3BDUp.TabsComponent_wrap__3fEC7{cursor:move} .resize-handle{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;z-index:10;overflow:hidden;display:none} body[data-window="1"] .resize-handle{display:block} .resize-handle:after{content:'';position:absolute;width:0;height:0;border:6px solid transparent;left:0;top:0;border-right-color:var(--color-midnight-100);border-bottom-color:var(--color-midnight-100);transition:all .2s} .resize-handle:hover:after{border-right-color:var(--color-space-300);border-bottom-color:var(--color-space-300)} body[data-window="1"] .gutter-vertical{display:none} .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;transition:all .2s} .chat-img:hover img{background-color:var(--color-midnight-300);border-color:var(--color-space-300)} .chat-img.chat-emoji img{border:0;background:0 0;padding:0;margin:0} .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:flex;flex-wrap:wrap;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;width:100px;transition:background .2s} .emoji-item:hover{background:var(--color-space-600)} .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);display:flex;justify-content:center;align-items:center} .chat-config-mask{position:absolute;height:100%;width:100%;background-color:var(--color-midnight-800);opacity:.8} .chat-config-window{position:absolute;display:flex;max-height:100%;max-width:100%} .chat-config-window-npc{z-index:1;margin-left:-110px;position:absolute;bottom:0} @media (max-width:600px){.chat-config-window-npc{display:none}} .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{width:500px;min-height:100px;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:12px;right:12px;height:18px;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;max-height:100%;overflow-y:scroll} .chat-config-item:not(:last-child){border-bottom:2px dashed var(--color-divider);padding-bottom:6px;margin-bottom:4px} .chat-config-item-title{display:flex;align-items:flex-end;font-size:18px;padding-bottom:6px} .chat-config-item-title>svg{width:20px;height:20px;margin-right:4px} .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-bottom{display:flex;justify-content:space-between;align-items:center} .startpoint{color:var(--color-success);font-size:24px;padding:0 6px;line-height:12px} .chat-config-tip{color:var(--color-success);font-size:14px;padding:6px} .ic-icon-default{color:var(--color-neutral-300)} .duplicate-marker{color:var(--color-disabled)} .chat-my .ChatMessage_clickable__58ej2{border-color:var(--color-jade-500)!important;background:var(--color-jade-600)!important} `); const gamePageChatPanel = '.GamePage_chatPanel__mVaVt'; const tabsComponent = '.TabsComponent_wrap__3fEC7'; 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; let initialHeight = window.innerHeight; let cleanupDraggable = null; let chatPanelConfig; let chatUserNameMy = false; 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"]], ["Eva", "2025/05/15", ["6824e3deb52c2","6824e3f38115d","6824e3f89d882","6824e3f6976d9","6824e3dea72c1","6824e3e014ff1","6824e3f15eb6a","6824e3df8d3a1","6824e3e024a4e","6824e3e4bba98","6824e3e46dd22","6824e3e666692"]], ]; 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 = emoji ? 'chat-img chat-emoji' : 'chat-img'; var img; if(emoji || GM_getValue('option_img',0) == 1){ img = document.createElement('img'); img.src = url; img.alt = altText; }else{ 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) => { tooltip.querySelector('.GuideTooltip_title__1QDN9').textContent = '网页链接'; tooltip.querySelector('.GuideTooltip_paragraph__18Zcq').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; } if (chatUserNameMy && node.textContent.trim() === getChatName()) { const clickableElement = node.parentNode.closest('.ChatMessage_chatMessage__2wev4'); if (clickableElement) { clickableElement.classList.add('chat-my'); } } 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 getChatName(){ const nameElement = document.querySelector('.Header_name__227rJ .CharacterName_name__1amXp'); const name = nameElement.dataset.name; return name; } 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 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 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 = 'flex'; 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/chat_icons_sprite.0bff9247.svg#anniversary_purple"></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"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#book"></use></svg> 图片预览 <span class="startpoint">*</span> </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"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#custom_cosmetic"></use></svg> 界面设置 <span class="startpoint">*</span> </div> <div class="chat-config-item-set"> <span> <label><input type="checkbox" id="option_username"> 强调用户名</label> </span> <span> <label><input type="checkbox" id="option_my"> 强调自己</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"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#patch_notes"></use></svg> 文本大小 </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"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/chat_icons_sprite.0bff9247.svg#iron_cow"></use></svg> 铁牛图标 </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 preview-my"> <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> </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">[IC]</div> </div> </span> <span>: </span> <span>@Stella 闪!</span> </div> </div> </div> <div class="chat-config-item"> <div class="chat-config-item-title"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/misc_sprite.4fc0598b.svg#task_block_slot"></use></svg> 屏蔽复读机 <span class="startpoint">*</span> </div> <div class="chat-config-item-set"> <span> <label><input type="checkbox" id="option_duplicate"> 屏蔽复读</label> </span> </div> <div class="chat-config-preview"> <div class="ChatMessage_chatMessage__2wev4 preview-my"> <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> </span> <span>: </span> <span class="duplicate-marker">(复读)</span> </div> </div> </div> <div class="chat-config-item"> <div class="chat-config-item-title"> <svg class="Icon_icon__2LtL_" width="100%" height="100%"><use href="/static/media/skills_sprite.57eb3a30.svg#magic"></use></svg> 聊天窗口化 </div> <div class="chat-config-item-set"> <span> <label><input type="checkbox" id="option_window"> 启用</label> </span> </div> </div> </div> <div class="chat-config-bottom"> <div class="chat-config-tip">出现<span class="startpoint">*</span>的配置项表示该配置的部分功能需要收起并重新打开聊天栏才会生效</div> <button id="saveConfig">保存</button> </div> </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); const option_my = GM_getValue('option_my',false); document.getElementById('option_my').checked = option_my; document.getElementById('img_title').value = GM_getValue('img_title','[图片]'); document.getElementById('option_duplicate').checked = GM_getValue('option_duplicate',false); document.getElementById('option_window').checked = GM_getValue('option_window',false); 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); GM_setValue('option_duplicate', document.getElementById('option_duplicate').checked); const option_window = document.getElementById('option_window').checked; document.body.setAttribute('data-window', option_window ? 1 : 0); GM_setValue('option_window', option_window); if(option_window){ enableWindow() }else{ disableWindow() } refreshAllChats(); document.getElementById('configDialog').style.display = 'none'; }); const preview = document.querySelectorAll('.preview-my'); preview.forEach(preview => { preview.classList.toggle('chat-my',option_my); }); 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_my').addEventListener('click', function() { var my = document.getElementById('option_my').checked ; GM_setValue('option_my', my); const preview = document.querySelectorAll('.preview-my'); preview.forEach(preview => { preview.classList.toggle('chat-my',my); }); chatUserNameMy = my; }); 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('option_duplicate').addEventListener('click', function() { var duplicate = document.getElementById('option_duplicate').checked; GM_setValue('option_duplicate', duplicate); duplicateStatus = duplicate; }); document.getElementById('option_window').addEventListener('click', function() { var window = document.getElementById('option_window').checked; GM_setValue('option_window', window); }); 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')); duplicateStatus = GM_getValue('option_duplicate',false); chatUserNameMy = GM_getValue('option_my',false); if(GM_getValue('option_window',false)){ enableWindow() document.body.setAttribute('data-window', 1); }else{ disableWindow() } 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); } } // ---------- 复读过滤 ---------- const duplicateProcessors = new WeakMap(); let duplicateStatus = false; function refreshAllChats() { document.querySelectorAll(chatHistorySelector).forEach(chatHistory => { const processor = duplicateProcessors.get(chatHistory); if (processor) { processor.processMessages(); } }); } function initDuplicateCheck(){ document.querySelectorAll(chatHistorySelector).forEach(chatHistory => { if (!duplicateProcessors.has(chatHistory)) { const processor = new DuplicateProcessor(chatHistory); duplicateProcessors.set(chatHistory, processor); processor.init(); } }); setTimeout(initDuplicateCheck, 1000); } class DuplicateProcessor { constructor(chatHistory) { this.chatHistory = chatHistory; this.observer = null; this.isProcessing = false; } init() { this.observer = new MutationObserver(mutations => { if (!duplicateStatus) return; if (!this.isProcessing && mutations.some(m => m.addedNodes.length > 0)) { this.processMessages(); } }); this.observer.observe(this.chatHistory, { childList: true, subtree: true }); this.processMessages(); } processMessages() { if (!duplicateStatus) return; this.isProcessing = true; const messages = Array.from( this.chatHistory.querySelectorAll(chatMessageSelector) ); messages.forEach((currentMsg, currentIndex) => { if (currentMsg._processed) return; const currentKey = this.getMessageKey(currentMsg); if (!currentKey) return; const isDuplicate = messages.slice(0, currentIndex).some(prevMsg => { const prevKey = this.getMessageKey(prevMsg); return prevKey === currentKey; }); if (isDuplicate) { this.markAsDuplicate(currentMsg); } currentMsg._processed = true; }); this.isProcessing = false; } getMessageKey(msg) { const children = Array.from(msg.children).filter(el => el.tagName === 'SPAN').slice(2); if (children.length === 0) return null; return children.map(el => el.textContent.trim()).join('|'); } markAsDuplicate(msg) { const children = Array.from(msg.children); if (children.length <= 2) return; if (children.slice(2).some(el => el.tagName === 'DIV')) return; const contentElements = children.slice(2); const originalContent = contentElements.map(el => el.textContent.trim()).join(' '); contentElements.forEach(el => el.remove()); const marker = document.createElement('span'); marker.className = 'duplicate-marker'; marker.textContent = '(复读)'; msg.appendChild(marker); marker.addEventListener('mouseover', (e) => { tooltip.querySelector('.GuideTooltip_title__1QDN9').textContent = '原文'; tooltip.querySelector('.GuideTooltip_paragraph__18Zcq').textContent = originalContent; positionTooltip(e.target); }); marker.addEventListener('mouseout', () => { tooltip.style.display = 'none'; }); } } // ---------- 聊天窗口 ---------- const defaultConfig = { top: 130, left: 240, width: 600, height: 600 }; function getChatPanelConfig() { const savedConfig = GM_getValue('chatPanel', '{}'); return { ...defaultConfig, ...JSON.parse(savedConfig) }; } function setChatPanelConfig(newConfig) { const currentConfig = getChatPanelConfig(); const mergedConfig = { ...currentConfig, ...newConfig }; GM_setValue('chatPanel', JSON.stringify(mergedConfig)); } function initchatPanelDraggable(panelElement, handleElement) { addResizeHandle(panelElement); let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; chatPanelConfig = getChatPanelConfig(); const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; chatPanelConfig.width = Math.min(chatPanelConfig.width, screenWidth); chatPanelConfig.height = Math.min(chatPanelConfig.height, screenHeight); chatPanelConfig.left = Math.max(0, Math.min(chatPanelConfig.left, screenWidth - chatPanelConfig.width)); chatPanelConfig.top = Math.max(0, Math.min(chatPanelConfig.top, screenHeight - chatPanelConfig.height)); panelElement.style.top = `${chatPanelConfig.top}px`; panelElement.style.left = `${chatPanelConfig.left}px`; panelElement.style.width = `${chatPanelConfig.width}px`; panelElement.style.height = `${chatPanelConfig.height}px`; const dragMouseDown = (e) => { e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.addEventListener('mouseup', closeDragElement); document.addEventListener('mousemove', elementDrag); }; function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panelElement.style.top = (panelElement.offsetTop - pos2) + "px"; panelElement.style.left = (panelElement.offsetLeft - pos1) + "px"; } function closeDragElement() { document.removeEventListener('mousemove', elementDrag); document.removeEventListener('mouseup', closeDragElement); setChatPanelConfig({top:parseInt(panelElement.style.top),left:parseInt(panelElement.style.left)}); } handleElement.addEventListener('mousedown', dragMouseDown); return () => { handleElement.removeEventListener('mousedown', dragMouseDown); }; } function addResizeHandle(panelElement) { const existingHandle = panelElement.querySelector('.resize-handle'); if (existingHandle) return; const handle = document.createElement('div'); handle.className = 'resize-handle'; panelElement.appendChild(handle); let startX, startY, startWidth, startHeight; handle.onmousedown = function(e) { e.preventDefault(); e.stopPropagation(); startX = e.clientX; startY = e.clientY; startWidth = panelElement.offsetWidth; startHeight = panelElement.offsetHeight; document.onmousemove = resize; document.onmouseup = stopResize; }; function resize(e){ const newWidth = startWidth + (e.clientX - startX); const newHeight = startHeight + (e.clientY - startY); panelElement.style.width = `${Math.max(400, newWidth)}px`; panelElement.style.height = `${Math.max(150, newHeight)}px`; } function stopResize(e) { document.onmousemove = null; document.onmouseup = null; const newWidth = startWidth + (e.clientX - startX); const newHeight = startHeight + (e.clientY - startY); setChatPanelConfig({width:newWidth,height:newHeight}); } } function enableWindow() { const chatPanel = document.querySelector(gamePageChatPanel); const dragHandle = chatPanel?.querySelector(tabsComponent); if (chatPanel && dragHandle){ if (cleanupDraggable) cleanupDraggable(); cleanupDraggable = initchatPanelDraggable(chatPanel, dragHandle); } } function disableWindow() { if (cleanupDraggable) cleanupDraggable(); cleanupDraggable = null; const chatPanel = document.querySelector(gamePageChatPanel); if(chatPanel){ chatPanel.style.top = 'unset'; chatPanel.style.left = 'unset'; chatPanel.style.width = 'unset'; chatPanel.style.height = 'height'; } } // ---------- 插件,启动! ---------- function init() { document.querySelectorAll(chatHistorySelector).forEach(setupHistoryObserver); const chatHistories = document.querySelectorAll(chatHistorySelector); if (chatHistories.length === 0) { setTimeout(init, 1000); return; } chatHistories.forEach(observeChatHistory); 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); } }); }); }); 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()); }); window.addEventListener('resize', function() { if (window.innerHeight > initialHeight) { tooltip.style.display = 'none'; } }); 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(); initEmojiButtonSystem(); initDuplicateCheck(); if (!emojiPanel) { emojiPanel = initEmojiPanel(); } })();