后来又发现immersive推荐的 kt 翻译器,他的面板更好,还能查词。
https://github.com/fishjar/kiss-translator
而且,开启kt翻译器的csp规则,还能绕过github对于跨域iframe的限制。
但他的设置过于麻烦。而且翻译面板只有顶部能拖动,别不小心拖到上面去。
后来又发现immersive推荐的 kt 翻译器,他的面板更好,还能查词。
https://github.com/fishjar/kiss-translator
而且,开启kt翻译器的csp规则,还能绕过github对于跨域iframe的限制。
但他的设置过于麻烦。而且翻译面板只有顶部能拖动,别不小心拖到上面去。
| // ==UserScript== | |
| // @name 沉浸翻译 - 面板样式调整 | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.1 | |
| // @description try to take over the world! | |
| // @author You | |
| // @include https://app.immersivetranslate.com/text* | |
| // @grant none | |
| // @run-at document-start | |
| // @icon https://immersive-translate.owenyoung.com/favicon.png | |
| // ==/UserScript== | |
| // var hash = location.hash; | |
| // location.hash = ''; | |
| debug('initBridge???') | |
| if(window.innerWidth>800) { | |
| return | |
| } | |
| var addSty = _=>US_addStyle(` | |
| .h-10 { | |
| height: 35px; | |
| } | |
| ._text-clear-btn_6f0kr_26 { | |
| top: unset; | |
| } | |
| ._text-out-box_6f0kr_2{ | |
| padding: 0; | |
| } | |
| ._text-out-box-service_6f0kr_124{ | |
| padding-left: 9px; | |
| // 标题 | |
| } | |
| ._drag-icon_6f0kr_286{ | |
| top: 5px; | |
| left: 0px; | |
| } | |
| img.w-5{ | |
| width: 1rem; | |
| } | |
| ._translated-text_6f0kr_115{ | |
| margin: 0; | |
| padding-left: 9px; | |
| } | |
| ._text-in-box-container_6f0kr_1{ | |
| height: 35px; | |
| min-height: 35px; | |
| display:none; | |
| } | |
| ._service-container_191fn_86{ | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| margin: auto; | |
| z-index: 999; | |
| background: white; | |
| width: auto; | |
| max-width: 175px; | |
| } | |
| `.replaceAll(';', '!important;'), `fanyi`) | |
| function initBridge() { | |
| debug('initBridge') | |
| addEventListener('hashchange', function(event) { | |
| var text = location.hash; | |
| debug('hashchange', location.hash) | |
| if(text) { | |
| text = decodeURIComponent(text.slice(1+text.indexOf('/', 1+text.indexOf('/')))) | |
| var textBox = gt('TEXTAREA'); | |
| if(textBox.value!=text) { | |
| textBox.value = text | |
| textBox.dispatchEvent(new Event('input', { | |
| bubbles: true | |
| })); | |
| } | |
| } | |
| }); | |
| // targetNode.parentNode.remove(); | |
| } | |
| var poor = true, headview, svcView, topped; | |
| addEventListener('scroll', function(e){ | |
| if(poor) { | |
| if(!headview) headview = ge('imt-navbar'); | |
| if(!svcView) svcView = gc('_service-container_191fn_86'); | |
| if(headview) poor = 0; | |
| } | |
| if(headview) { | |
| var b = doc.documentElement.scrollTop<=headview.offsetHeight; | |
| if(topped != b) { | |
| topped = b; | |
| headview.style.opacity = topped?1:0; | |
| headview.style.transition = topped?'':'opacity 0.4s'; | |
| if(svcView) svcView.style.opacity = topped?1:0; | |
| } | |
| } | |
| }); | |
| var targetNode, cc=0, sty=1; | |
| var initTm = setInterval(() => { | |
| targetNode = doc.head; | |
| // debug('targetNode', targetNode) | |
| if(sty && doc.head) { | |
| addSty(); | |
| sty = 0; | |
| } | |
| if(targetNode || cc++>999) { | |
| clearInterval(initTm) | |
| initBridge(); | |
| } | |
| }, 1000); | |
| setTimeout(_=>doc.documentElement.scrollTop=80, 250) | |
| // ==UserScript== | |
| // @name 翻译面板 | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.1 | |
| // @description try to take over the world! | |
| // @author You | |
| // @match *://*/* | |
| // @icon https://immersive-translate.owenyoung.com/favicon.png | |
| // @grant win | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_xmlhttpRequest | |
| // @grant unsafeWindow | |
| // @run-at document-start | |
| // ==/UserScript== | |
| var d = document, win=window.unsafeWindow||window, bank=win._log_bank; | |
| win.doc = document; | |
| win.log = console.log; | |
| win.debug = console.log; | |
| // debug(111, unsafeWindow) | |
| if(!bank) { | |
| bank = win._log_bank = {}; | |
| } else { | |
| bank.unreg(); | |
| } | |
| bank.unreg = uninstall; | |
| var unregs = []; | |
| function uninstall() { | |
| for(var i=0;i<unregs.length;i++) { | |
| unregs[i](); | |
| } | |
| return 0; | |
| } | |
| function addEvent(a, b, c, d) { | |
| if(!d) d = win; | |
| d.addEventListener(a, b, c); | |
| unregs.push(function(){ d.removeEventListener(a, b, c)} ); | |
| } | |
| function delEvent(a, b, c, d) { | |
| if(!d) d = win; | |
| d.removeEventListener(a, b, c); | |
| } | |
| function gc(c, d) { | |
| return (d||document).getElementsByClassName(c)[0]; | |
| } | |
| function gt(c, d) { | |
| return (d||document).getElementsByTagName(c)[0]; | |
| } | |
| function ge(id) { | |
| return document.getElementById(id); | |
| } | |
| win.gc = gc; | |
| win.gt = gt; | |
| win.ge = ge; | |
| function gcs(c, d) { | |
| return (d||document).getElementsByClassName(c); | |
| } | |
| function gts(c, d) { | |
| return (d||document).getElementsByTagName(c); | |
| } | |
| function gcp(c, d, max) { | |
| var p = d||document; | |
| if(!max) max=99999; | |
| while(p) { | |
| if(p.classList && p.classList.contains(c)) return p; | |
| p = p.parentNode; | |
| if(--max<=0) return null; | |
| } | |
| return p; | |
| } | |
| win.gcs = gcs; | |
| win.gts = gts; | |
| win.gcp = gcp; | |
| // 这里开始 | |
| var fanyUI; | |
| // GM_setValue('test', {happy:"yes or no"}) | |
| // var test = GM_getValue('test', 'happy'); | |
| // debug(test, typeof test) | |
| function readFromHere() { | |
| var bkEvt = new CustomEvent('ext', { detail: {type:'read'}}); | |
| win.dispatchEvent(bkEvt, function(data) { }); | |
| } | |
| addEvent('keydown', (e)=>{ | |
| debug(e) | |
| if(e.key==='Pause') // break' 一键翻译原文,需要魔改扩展+调用ahk发送快捷键 | |
| { | |
| var bkEvt = new CustomEvent('_transx', { detail: {type:'fanyi',doit:true}}); | |
| win.dispatchEvent(bkEvt, function(data) { }); | |
| } | |
| if(e.key==='a' && e.altKey || e.key=='ScrollLock') | |
| { | |
| // 注:原快捷键改为 ctrl+shift+a (翻译输入框),alt+a启动的是翻译面板 | |
| if(!win._transx) { | |
| win._transx = 1; | |
| // 翻译器,启动! | |
| var bkEvt = new CustomEvent('_transx', { detail: {type:'fanyi'}}); | |
| win.dispatchEvent(bkEvt, function(data) { }); | |
| } | |
| floatTranslate(); | |
| } | |
| }); | |
| var lastCtx=0, x=0, y=0; | |
| addEvent('contextmenu', (e)=>{ | |
| if(e.ctrlKey || e.altKey || e.shiftKey) | |
| { | |
| return | |
| } | |
| var now = Date.now(); | |
| if(now-lastCtx<450 && now-lastCtx>0 && _dist(x-e.pageX, y-e.pageY)<25) | |
| { | |
| stopX(e); | |
| // 双击翻译段落,需要魔改扩展+调用ahk发送快捷键 | |
| setTimeout(_=>{var bkEvt = new CustomEvent('_transx', { detail: {type:'fanyi',doit:110}}); | |
| win.dispatchEvent(bkEvt, function(data) { });}, 100) | |
| } | |
| x = e.pageX; y=e.pageY; | |
| lastCtx = now; | |
| },true,d); | |
| var toastTimer=-1, toastView; | |
| function fadeToast() { | |
| clearTimeout(toastTimer); | |
| var toast = ge("toastview"); | |
| toast.style.opacity=0; | |
| toastTimer=setTimeout(function(){toastTimer=-1;toast.style.display='none'}, 300); | |
| } | |
| function craft(t, p, c, s) { | |
| t = d.createElement(t||'DIV'); | |
| if(c) t.className=c; | |
| if(s) t.style=s; | |
| if(p)p.appendChild(t); | |
| return t; | |
| } | |
| function toast(msg, severity, time, parent, alpha){ | |
| if(toastTimer!=-1)clearTimeout(toastTimer); | |
| if(!parent) parent=d.body; | |
| var toast = ge("toastview"); | |
| if(!toast) { | |
| toast = craft(0,parent); | |
| toast.id="toastview"; | |
| craft(0,toast).id="toasttext"; | |
| craft('STYLE', d.head).textContent=`#toastview { | |
| display:none; | |
| position:fixed; | |
| left:50%; | |
| top:89%; | |
| width:100%; | |
| opacity:0; | |
| position:absolute; | |
| transform:translate(-50%,-50%); | |
| -webkit-transform:translate(-50%,-50%); | |
| transition:opacity 0.3s; | |
| overflow:auto; | |
| text-align:center; | |
| z-index: 999999999999999; | |
| position: fixed; | |
| pointer-events:none; | |
| } | |
| #toasttext{ | |
| display:inline-block; | |
| margin:0 0px; | |
| padding:7px 15px; | |
| font-size:16px; | |
| color:#FFFFFF; | |
| letter-spacing:0; | |
| line-height:22px; | |
| border-radius:30px; | |
| -moz-border-radius:30px; | |
| -moz-user-select:text; | |
| -webkit-user-select:text; | |
| -ms-user-select:text; | |
| -khtml-user-select:text; | |
| user-select:text; | |
| } | |
| #toasttext.warn{ | |
| background-image: linear-gradient(-45deg, #ff9569 0%, #e92758 100%); | |
| } | |
| #toasttext.info{ | |
| background-image: linear-gradient(0deg, #27bdc9 0%, #296ade 21%); | |
| box-shadow: inset 0px 0px 4px 0px #ffffff9c; | |
| }`; | |
| } | |
| var p=toast.parentNode; | |
| if(p!=parent){ | |
| //debug("re-add toast!"); | |
| if(p) toast.remove(); | |
| parent.appendChild(toast); | |
| } | |
| var tt=toast.firstChild; | |
| tt.innerHTML=msg; | |
| tt.className=severity>=1?"warn":"info"; | |
| toastTimer=setTimeout(fadeToast, time||1500); | |
| setTimeout(function(){toast.style.opacity=1}, 16); | |
| toast.style.display='block' | |
| tt.style.opacity=alpha||1; | |
| } | |
| win.toast = toast; | |
| win.US_addStyle = function(text, id, h, t){ | |
| var el = 0; | |
| if(id) el = ge(id); | |
| if(!el) { | |
| el = document.createElement(t||'STYLE'); | |
| if(id) el.id = id; | |
| (h||document.head).append(el); | |
| } | |
| else if(h && h!=el.parentNode) { | |
| h.append(el); | |
| } | |
| if(t) el.innerText = text; | |
| else el.textContent = text; | |
| }; | |
| addEvent("toast", function(e){ | |
| toast(e.detail.text, e.detail.warn); | |
| }); | |
| function editing() { | |
| return d.activeElement?.tagName==='INPUT' | |
| || d.activeElement?.contentEditable==='true' | |
| || d.activeElement?.tagName==='TEXTAREA' | |
| } | |
| win.editing = editing; | |
| function stopX(e) { | |
| try{ | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| } catch(e) {debug(e)} | |
| } | |
| win.stop = stopX; | |
| win.stem = (f,t)=>setTimeout(f,t); | |
| win.addEventListenerRaw_ = addEventListener; | |
| var s = getSelection() | |
| function getRange(){ | |
| return s.rangeCount==0?0:s.getRangeAt(0); | |
| } | |
| function setRange(r){ | |
| s.empty(); s.addRange(r); | |
| } | |
| function parentCount(n){ | |
| var cc=0; while(n=n.parentNode) cc++; return cc; | |
| } | |
| win.getRange = getRange; | |
| win.setRange = setRange; | |
| var ext = ''; | |
| var darkOnly; | |
| win._editing = editing; | |
| win._dist = (x,y)=>x*x + y*y; | |
| function getNextNode(n, e) { | |
| var a = n.firstChild; | |
| if (a) return a; | |
| while (n && n!=e) { | |
| if (a = n.nextSibling) { | |
| return a | |
| } | |
| n = n.parentNode | |
| } | |
| } | |
| function floatTranslate() { | |
| if(!fanyUI) { | |
| fanyUI = newUIFanyi() | |
| fanyUI.id = '_fanyUI'; | |
| US_addStyle(`#_fanyUI { | |
| background-color: #fff; | |
| border: 0; | |
| border-radius: 8px; | |
| box-shadow: 0 0 16px rgba(0,0,0,.12), 0 16px 16px rgba(0,0,0,.24); | |
| position: absolute; | |
| width: 80%; | |
| padding: 10px; | |
| margin: auto; | |
| max-width: 512px; | |
| left: 50%; | |
| transform: translate(-50%,0); | |
| outline: none; | |
| position: fixed; | |
| top: 0; | |
| z-index: 999999; | |
| }`, 'fanySt') | |
| US_addStyle(`.eta { | |
| word-wrap: break-word; | |
| word-break: keep-all; | |
| padding: 5px 2px 2px 14px; | |
| border: 1px solid #b2b5be; | |
| outline: 0; | |
| border-radius: 12px; | |
| width: 100%; | |
| height: 100%; | |
| font-size: 1.5em; | |
| height: 30px; | |
| line-height: 25px; | |
| } | |
| `, '', fanyUI.s) | |
| unregs.push(function(){ fanyUI.remove() } ); | |
| }; | |
| if(!fanyUI.parentNode) | |
| doc.body.append(fanyUI); | |
| var text = getSelection()+''; | |
| if(text) { | |
| fanyUI.doms['et'].value = text; | |
| fanyUI.fy(); | |
| } | |
| } | |
| function newUIFanyi() { | |
| var ret = craft(0, 0, 'UiTab'); | |
| ret.s = ret.attachShadow&&0?ret.attachShadow({mode: 'open'}):ret; | |
| ret.s.innerHTML = ` | |
| <div class="UiHead" style="user-select:none;"> | |
| <div style="margin-top:8px;display:flex;justify-content: space-between;flex-direction: row;"> | |
| <div id='yy' style=" white-space:nowrap;padding-left:5px;font-size:.89em;width:100px;text-align:center;padding-top:7.9px;">原文 </div> | |
| <textarea id='et' class='eta' style="resize:vertical;min-height:1em"></textarea> | |
| <div class="tools" style="margin-top:2.8px;height:30px"> | |
| <button class="btn" id="sync" title="翻译…">🔄</button> | |
| </div> | |
| </div> | |
| <div id='hr' style="padding: 10px;"> | |
| <HR style='margin:0px;opacity:.8;'> | |
| <div id='hrt'style="position: absolute; left: 50%; font-size: 13px; transform: translate(-50%, -50%); background-color: white; padding: 0 10px; color: #434343;"> | |
| 翻译</div> | |
| </div> | |
| <div style="display:flex;justify-content: space-between;flex-direction: row;"> | |
| <div id='yw' style="user-select:none; hite-space:nowrap;padding-left:5px;font-size:.89em;width:100px;text-align:center;padding-top:7.9px;" title=""> | |
| 译文  | |
| </div> | |
| <div style="position: relative; flex: 1; margin-right: -25px;"> | |
| <textarea id='etFy' class='eta' style="resize:vertical;min-height:145px;margin-right: 8px;"></textarea> | |
| <iframe id='web' style="position: absolute; top: 0; height: 100%; width: 100%;display:none" ></iframe> | |
| </div> | |
| <div class="tools" style="margin-top:2.8px;height:30px;width: 55px;"> | |
| <button class="btn" id="read" title="朗读" style="margin-bottom:5px">🔊</button> | |
| <button class="btn" id="cpPtBin" title="复制">📋</button> | |
| </div> | |
| </div> | |
| <HR style='margin:1px;opacity:0;'> | |
| </div> | |
| <div class="UITabo"> | |
| <div class="ListView"></div> | |
| </div>`; | |
| var doms = {},n=ret.s; | |
| while(n=getNextNode(n,ret.s)) | |
| if(n.id)doms[n.id]=n; | |
| doms['et'].value = 'happy'; | |
| let isDragging = false, dragged; | |
| let offsetX=0, offsetY=0, tmpX, tmpY; | |
| var fromX, fromY; | |
| var tmode = parseInt(GM_getValue('tmode', 0)); | |
| function fnMove(e) { | |
| if (isDragging) { | |
| dragged = true; | |
| tmpX = offsetX + e.clientX - fromX; | |
| tmpY = offsetY + e.clientY - fromY; | |
| ret.style.transform = 'translate(calc('+parseInt(tmpX)+'px - 50%), 0)'; | |
| ret.style.top = parseInt(tmpY) + 'px'; | |
| // debug(parseInt(tmpX), parseInt(tmpY)) | |
| } | |
| } | |
| function fnDrop(e) { | |
| var padH=50, padV=50, W=win.innerWidth, H=win.innerHeight, w=ret.offsetWidth, h=ret.offsetHeight; | |
| var minX = -W/2 + w/2, maxX = W/2 + padH; | |
| // var minY = -(H - h + padV), maxY = h - padV; | |
| // var minY = -(h - padV), maxY = H - h + padV; | |
| var minY = -(padV), maxY = H - padV; | |
| if(tmpX<minX) tmpX=minX; | |
| if(tmpX>maxX) tmpX=maxX; | |
| if(tmpY<minY) tmpY=minY; | |
| if(tmpY>maxY) tmpY=maxY; | |
| offsetX = tmpX; | |
| offsetY = tmpY; | |
| ret.style.transform = 'translate(calc('+parseInt(tmpX)+'px - 50%), 0)'; | |
| ret.style.top = parseInt(tmpY) + 'px'; | |
| isDragging = false; | |
| ret.style.cursor = ''; | |
| delEvent('mousemove', fnMove, 1, d); | |
| delEvent('mouseup', fnDrop, 1, d); | |
| } | |
| ret.fy = doms['sync'].onclick = () => | |
| { | |
| var text = doms['et'].value; | |
| var yz = guessLanguage(text); | |
| if(tmode) { | |
| var tar = 'zh-CN'; | |
| if(yz=='zh') tar = 'en'; | |
| // tar = 'en'; | |
| // document.querySelector("#web").src = 'https://app.immersivetranslate.com/text#auto/en/x' | |
| doms['web'].src = 'https://app.immersivetranslate.com/text#auto/'+tar+'/'+text; | |
| return; | |
| } | |
| doms['etFy'].value = text; | |
| doms['etFy'].focus(); | |
| debug('yz::', yz); | |
| if(yz=='en') { | |
| US_addStyle('zh-CN', 'yzyz', 0, 'yzyz') // 强制翻为中文 | |
| } else if(yz=='zh') { | |
| US_addStyle('en', 'yzyz', 0, 'yzyz') // 强制翻为英文 | |
| } else { | |
| US_addStyle('', 'yzyz', 0, 'yzyz') | |
| } | |
| var bkEvt = new CustomEvent('_transx', { detail: {type:'fanyi',doit:111}}); | |
| win.dispatchEvent(bkEvt, function(data) { }); | |
| }; | |
| function hit(x,y,el) { | |
| var doms = [el, d]; | |
| for (var i = 0; i < doms.length; i++) { | |
| el = doms[i]; | |
| if(el.elementFromPoint) | |
| return el.elementFromPoint(x, y); | |
| } | |
| } | |
| ret.addEventListener('mousedown', (e) => { | |
| var el = hit(e.clientX, e.clientY, ret.s); | |
| debug(el, e.target, doms['hr'].contains(e.target)) | |
| var box = ret.getBoundingClientRect(); | |
| if(e.button==0 && el.tagName!='TEXTAREA' | |
| && (e.clientX < box.left+80 || e.clientX > box.right-38 || doms['hr'].contains(el))) { | |
| fromX = e.clientX; | |
| fromY = e.clientY; | |
| isDragging = true; | |
| ret.style.cursor = 'grabbing'; | |
| // toast('grabbing') | |
| addEvent('mousemove', fnMove, 1, d); | |
| addEvent('mouseup', fnDrop, 1, d); | |
| stop(e); | |
| } | |
| }, 1); | |
| doms['hr'].addEventListener('mousedown', (e) => { | |
| dragged = false; | |
| }); | |
| function setTmd() { | |
| doms['yw'].style.display = tmode?'none':''; | |
| doms['web'].hidden = !tmode; | |
| doms['web'].style.display = ''; | |
| doms['hrt'].innerText = '翻译面板 · '+(tmode?'IFRAME':'INPUT'); | |
| } | |
| doms['hr'].addEventListener('click', (e) => { | |
| if(!dragged) { | |
| // toast('切换!') | |
| tmode = (tmode+1)%2; | |
| GM_setValue('tmode', tmode); | |
| setTmd(); | |
| ret.fy(); | |
| } | |
| }); | |
| ret.addEventListener('contextmenu', (e) => { | |
| if(e.clientX < ret.getBoundingClientRect().left+80) { | |
| ret.remove(); | |
| stopX(e); | |
| } | |
| }); | |
| offsetY = win.innerHeight / 2; | |
| ret.style.top = parseInt(offsetY) + 'px'; | |
| ret.doms = doms; | |
| if(tmode) { | |
| setTmd(); | |
| } | |
| return ret; | |
| } | |
| var YuZhong = [ | |
| 'zh' | |
| , 'en' | |
| ] | |
| function countSpaces(str) { | |
| var count = 0; | |
| for (var i = 0; i < str.length; i++) { | |
| if (str[i] === ' ') { | |
| count++; | |
| } | |
| } | |
| return count; | |
| } | |
| function hasChinese(str) { | |
| return /[\u4E00-\u9FA5]/.test(str); | |
| } | |
| function guessLanguage(text) { // todo use code point to test language block | |
| var size = text.length; | |
| if (size > 50) size = 50; | |
| var weights = {}; | |
| for (var i = 0; i < size; i++) { | |
| var c = text.charAt(i); | |
| var yz = 0; | |
| if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')) { | |
| yz = 'en'; // eng | |
| } | |
| if (!yz) { | |
| if(hasChinese(text.slice(i, i+1))) { | |
| yz = 'zh'; | |
| } | |
| } | |
| if (yz) { | |
| weights[yz] = (weights[yz]||0) + 1; | |
| } | |
| } | |
| var max = 0, guess = 0, keys=Object.keys(weights), key; | |
| for (var k in keys) { | |
| key = keys[k]; | |
| var value = weights[key]; | |
| if (value > max) { | |
| max = value; | |
| guess = key; | |
| } | |
| } | |
| if(weights.zh && guess!='zh') { | |
| if(max>weights.zh*500 || countSpaces(text)-3>weights.zh) { | |
| // valid guess | |
| } else { | |
| guess = 'zh'; | |
| } | |
| } | |
| debug('guess::', guess, max, weights); | |
| return guess; | |
| } |