// ==UserScript== // @name 能不能好好说话?美化版 // @namespace https://lab.magiconch.com/nbnhhsh // @version 0.16.1 // @description 首字母缩写划词翻译工具 - 兼容性优化版 // @author itorr (美化 by Ace) // @license MIT // @icon https://lab.magiconch.com/favicon.ico // @match *://weibo.com/* // @match *://*.weibo.com/* // @match *://*.weibo.cn/* // @match *://tieba.baidu.com/* // @match *://*.bilibili.com/ // @match *://*.bilibili.com/* // @match *://*.douban.com/group/* // @match *://*.xiaoheihe.cn/* // @match *://xiaoheihe.cn/* // @require https://lab.magiconch.com/vue.2.6.11.min.js // @inject-into content // @grant GM_addStyle // ==/UserScript== let Nbnhhsh = ((htmlText, cssText) => { const API_URL = 'https://lab.magiconch.com/api/nbnhhsh/'; // 使用GM_addStyle注入CSS,避免样式冲突 GM_addStyle(cssText); const request = (method, url, data, onOver) => { let x = new XMLHttpRequest(); x.open(method, url); x.setRequestHeader('content-type', 'application/json'); x.withCredentials = true; x.onload = () => onOver(x.responseText ? JSON.parse(x.responseText) : null); x.send(JSON.stringify(data)); return x; }; const Guess = {}; const guess = (text, onOver) => { text = text.match(/[a-z0-9]{2,}/ig).join(','); if (Guess[text]) { return onOver(Guess[text]); } if (guess._request) { guess._request.abort(); } app.loading = true; guess._request = request('POST', API_URL + 'guess', { text }, data => { Guess[text] = data; onOver(data); app.loading = false; }); }; // 更健壮的模态框实现 const createModal = (name, callback) => { // 创建模态框容器 const modal = document.createElement('div'); modal.className = 'nbnhhsh-modal'; modal.setAttribute('data-nbnhhsh-modal', ''); // 添加自定义属性便于识别 // 创建shadow DOM隔离样式 let shadowRoot; try { shadowRoot = modal.attachShadow({ mode: 'open' }); } catch (e) { shadowRoot = modal; // 回退方案 } // 模态框内容 shadowRoot.innerHTML = `
`; // 添加到body最外层 document.documentElement.appendChild(modal); // 获取DOM元素 const overlay = shadowRoot.querySelector('.modal-overlay'); const closeBtn = shadowRoot.querySelector('.close-btn'); const cancelBtn = shadowRoot.querySelector('.cancel-btn'); const submitBtn = shadowRoot.querySelector('.submit-btn'); const input = shadowRoot.querySelector('.modal-input'); // 关闭模态框函数 const closeModal = () => { modal.style.opacity = '0'; setTimeout(() => { if (modal.parentNode) { modal.parentNode.removeChild(modal); } }, 200); }; // 事件监听 const handleSubmit = () => { const text = input.value.trim(); if (text) { closeModal(); callback(text); // 显示提交成功的提示 showNotice('感谢您的贡献!翻译提交成功,审核通过后生效。'); } else { input.focus(); } }; const handleKeyDown = (e) => { if (e.key === 'Escape') { closeModal(); } else if (e.key === 'Enter' && e.ctrlKey) { handleSubmit(); } }; overlay.addEventListener('click', closeModal); closeBtn.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); submitBtn.addEventListener('click', handleSubmit); input.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown); // 显示动画 setTimeout(() => { modal.style.opacity = '1'; input.focus(); }, 10); // 清理事件监听 modal._cleanup = () => { document.removeEventListener('keydown', handleKeyDown); }; }; // 显示通知 const showNotice = (message) => { const notice = document.createElement('div'); notice.className = 'nbnhhsh-notice'; notice.setAttribute('data-nbnhhsh-notice', ''); notice.textContent = message; // 添加到body最外层 document.documentElement.appendChild(notice); setTimeout(() => { notice.style.opacity = '0'; setTimeout(() => { if (notice.parentNode) { notice.parentNode.removeChild(notice); } }, 300); }, 3000); }; const submitTran = name => { createModal(name, text => { request('POST', API_URL + 'translation/' + name, { text }, () => { showNotice('感谢您的贡献!翻译提交成功,审核通过后生效。'); }); }); }; const transArrange = trans => { return trans.map(tran => { const match = tran.match(/^(.+?)([(\(](.+?)[)\)])?$/); if (match.length === 4) { return { text: match[1], sub: match[3] } } else { return { text: tran } } }) }; const getSelectionText = _ => { let text = getSelection().toString().trim(); if (!!text && /[a-z0-9]/i.test(text)) { return text; } else { return null; } }; const fixPosition = _ => { let rect = getSelection().getRangeAt(0).getBoundingClientRect(); const activeEl = document.activeElement; if (['TEXTAREA', 'INPUT'].includes(activeEl.tagName)) rect = activeEl.getBoundingClientRect(); let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; let scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; let top = Math.floor(scrollTop + rect.top + rect.height + 5); let left = Math.floor(scrollLeft + rect.left); // 防止弹出框超出屏幕右侧 const boxWidth = 360; if (left + boxWidth > window.innerWidth) { left = window.innerWidth - boxWidth - 10; } // 防止弹出框超出屏幕底部 const estimatedHeight = 200; if (top + estimatedHeight > window.innerHeight + scrollTop) { top = Math.floor(scrollTop + rect.top - estimatedHeight - 5); if (top < scrollTop) top = scrollTop; } if (top === 0 && left === 0) { app.show = false; } app.top = top; app.left = left; }; const timer = _ => { if (getSelectionText()) { setTimeout(timer, 300); } else { app.show = false; } }; const nbnhhsh = _ => { let text = getSelectionText(); app.show = !!text && /[a-z0-9]/i.test(text); if (!app.show) { return; } fixPosition(); guess(text, data => { if (!data || !data.length) { app.show = false; } else { app.tags = data; } }); setTimeout(timer, 300); }; const _nbnhhsh = _ => { setTimeout(nbnhhsh, 1); }; document.body.addEventListener('mouseup', _nbnhhsh); document.body.addEventListener('keyup', _nbnhhsh); const createEl = html => { createEl._el.innerHTML = html; let el = createEl._el.children[0]; document.body.appendChild(el); return el; }; createEl._el = document.createElement('div'); // 创建主界面 const el = createEl(htmlText); const app = new Vue({ el, data: { tags: [], show: false, loading: false, top: 0, left: 0, }, methods: { submitTran, transArrange, } }); return { guess, submitTran, transArrange, } })(`