后来又发现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; | |
} |