Skip to content

Instantly share code, notes, and snippets.

@KnIfER
Last active July 21, 2024 05:44
Show Gist options
  • Save KnIfER/84ff08a1ce175efe58d6db569aac1ef1 to your computer and use it in GitHub Desktop.
Save KnIfER/84ff08a1ce175efe58d6db569aac1ef1 to your computer and use it in GitHub Desktop.
抹布翻译

【DIY】魔改扩展,实现悬浮查词、划词翻译 + 自动切换目标语言!【脚本分享】【saladict 替代】 · immersive-translate/immersive-translate · Discussion #1916


后来又发现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;">原文&ensp;</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="">
译文&ensp;
</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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment