inDarkness / AC-有道取词+翻译

// ==UserScript==
// @name  AC-有道取词+翻译
// @description 一个可以在浏览器中自由使用的屏幕取词脚本-alt+鼠标翻译,或者选中按Q翻译
// @version 2.1
// @namespace youdao
// @license         GPL-3.0-only
// @author  AC -modified from :Liu Yuyang(sa@linuxer.me)
// @include *
// @icon    https://raw.githubusercontent.com/langren1353/zbImg/master/img0/icon.jpg
// @require https://greasyfork.org/scripts/10783-md5/code/md5.js?version=60242
// @run-at  document-end
// @note    V2.1 2019-05-25 继续修复翻译失效的问题,后来发现是Resouece无法即时的刷新,所以现在采用脚本自动的获取数据,同时也支持了其他脚本的运行
// @note    V2.0 2019-03-24 只支持Tampermonkey。由于GM_getResourceText在GM下的不支持的原因,所以暂时只支持Tampermonkey的运行
// @note    V1.9 2019-02-21 继续更新搜狗API更新导致的无法翻译的bug -- 变更为库文件更新模式,可以很方便的更新token
// @note    V1.8 2018-12-19 更新搜狗API的key,且修复了部分特殊关键词无法翻译的bug
// @note    V1.7 2018-12-04 修复搜狗搜索API更新的问题--已经更换为搜狗的翻译API;如果搜狗更新的话,那估计又要换回必应翻译了
// @note    V1.6 2018-05-23 修复部分英文单词翻译不出来的bug,同时修复特殊的BOLD标签
// @note    V1.5 2018-05-16 修复英文翻译单词的bug,同时修复语音的问题,暂时就这样了,最近不更新了,有需要的话提出来,以后再说
// @note    V1.4 2018-05-16 修复上一次更新导致的bug,同时修复多种语言翻译,结果却翻译成了英文的bug
// @note    V1.3 2018-05-16 修复word词组的时候翻译出错的问题
// @note    V1.2 2018-05-16 修复没有添加connect的错误问题
// @note    V1.1 2018-05-12 有道自己不更新~~HTTPS证书过期,现在使用搜狗翻译吧,老司机们说搜狗翻译还是很有优势的,至于百度翻译,再说吧
// @note    V1.0 2017-11-22 修复firefox57的翻译的BUG
// @note    V0.9 2017-11-15 修复了一大堆,优化了下界面的展示问题
// @note    V0.8 2017-2-21 修正了由于z-index过低导致的淘宝页面翻译过于底层而看不见
// @note    V0.7 2016-9-29 修正了部分格式错误,换行的时候稍微好了点
// @note    V0.6 2016-1-12 终于修正了,之前只改了句子翻译中的文字颜色,才发现单词翻译后的颜色简直看不清。。。
// @note    V0.5 2016-1-9 修改默认背景色
// @note    V0.4 2016-1-8 修正chrome的支持
// @note    V0.3 2016-1-8 修改窗体默认大小,尽可能的显示回车功能,避免了以前的全部显示在同一行--依旧有bug
// @note    V0.2 2015-11-26 修改部分地方适合自己使用,选中文字,按下Q翻译,或者alt+鼠标选中,自动翻译
// @grant GM_xmlhttpRequest
// @connect fanyi.youdao.com
// @connect fanyi.sogou.com
// @connect xbaidu.ntaow.com
// @grant   GM_addStyle
// @grant   GM_getValue
// @grant   GM.getValue
// @grant   GM_setValue
// @grant   GM.setValue
// ==/UserScript==

var curX;
var curY;
var timeout;
var transResult;

if (typeof(GM) == "undefined") {
	// 这个是ViolentMonkey的支持选项
	GM = {};
	GM.setValue = GM_setValue;
	GM.getValue = GM_getValue;
}
var Config = {};
var DefaultConfig = {
	SoGouKey : "",
	updateTime: "" // 每隔5个小时,可以刷新一次
};
Promise.all([GM.getValue("Config")]).then(function (data) {
	if (data[0] != null) {
		Config = data[0];
	}else{
		Config = DefaultConfig;
	}
	// 初始化完成之后才能调用正常函数
	callback();
}).catch(function (except) {
	console.log(except);
});

function callback() {
	periodCheckKeyTime();
	document.body.addEventListener("mousemove", function(e){curX = e.clientX; curY = e.clientY;}, false);
	document.body.addEventListener("keypress", callbackWrapper, false);
	document.body.addEventListener("mouseup", callbackWrapper, false);
	document.body.addEventListener("dblclick", callbackWrapper, false);
	GM_addStyle("bold{font-weight: bold;}");
}

function callbackWrapper(e) {
	if(e.type === 'dblclick' && isInACTransPop()){
		// console.log("双击操作:复制目标文本");
		// console.log(transResult);
		return;
	}
	if(e.charCode == 113  || e.type == "mouseup"){ //Q
		// remove previous .ACyoudaoPopup if exists
		var previous = document.querySelector(".ACyoudaoPopup");
		if(previous && isInACTransPop()){
			document.body.removeChild(previous);
			transResult = "";
		}
		// quick fix. with ctrl key held
		if (e.type == "mouseup" && !e.altKey) {
			return;
		}
		clearTimeout(timeout);
		timeout = setTimeout(function(){
			translate();
		}, 120);
		reNewSogouKey();
	}
}
function periodCheckKeyTime(){
	var delta = (new Date().getTime() - Config.updateTime) /  1000; // 秒钟
	if(delta > 172800){ // 2 * 24 * 3600
		reNewSogouKey();
	}
}
function reNewSogouKey(){
	GM_xmlhttpRequest({
		url: "http://xbaidu.ntaow.com/newcss/sogoukey.php",
		method: "GET",
		timeout: 1000,
		onreadystatechange: function (response) {
			var data = response.responseText;
			if(/[\d\w]+/.test(data)){
				Config.SoGouKey = data;
				Config.updateTime = new Date().getTime();
				GM.setValue("Config", Config);
			}
		}
	});
}

function isInACTransPop(){
	var checkNode = document.getSelection().anchorNode.parentNode;
	return checkNode && (""+checkNode.className).indexOf("ACyoudao") < 0;
}
function translate() {
	var selectObj = document.getSelection();
	if (selectObj.anchorNode.nodeType == 3) {
		var word = selectObj.toString();
		if (word == "") {
			return;
		}
		word = word.replace(new RegExp('(\r\n)+', 'g'), 'QWER');// 特殊标记便于替换
		var ts = new Date().getTime();
		var mx = curX;
		var my = curY;
		translate(word, ts);
	}

	function popup(mx, my, result) {
		var youdaoWindow = document.createElement('div');
		youdaoWindow.classList.toggle("ACyoudaoPopup");
		// parse
		var dictJSON = JSON.parse(result)['data'];
		console.log(dictJSON);
		var query = dictJSON['translate']['dit'];
		// var errorCode = dictJSON['errorCode'];
		console.log(query);
		if (dictJSON['dictionary']) {
			word();
		} else {
			sentence();
		}
		// main window
		// first insert into dom then there is offsetHeight!IMPORTANT!

		document.body.appendChild(youdaoWindow);
		youdaoWindow.style.color = "#000000";
		youdaoWindow.style.textAlign = "left";
		youdaoWindow.style.display = "block";
		youdaoWindow.style.position = "fixed";
		youdaoWindow.style.background = "#EDF6FF";
		youdaoWindow.style.borderRadius = "3px";
		youdaoWindow.style.boxShadow = "0 0 5px 0";
		youdaoWindow.style.opacity = "1";
		youdaoWindow.style.minWidth = "300px";
		youdaoWindow.style.maxWidth = "550px";
		youdaoWindow.style.wordWrap = "break-word";
		youdaoWindow.style.fontSize = "15px";
		youdaoWindow.style.left = mx + 10 + "px";
		if (mx + 200 + 30 >= window.innerWidth) {
			youdaoWindow.style.left = parseInt(youdaoWindow.style.left) - 200 + "px";
		}
		//alert(window.innerHeight);
		if (my + youdaoWindow.offsetHeight + 30 >= window.innerHeight) {
			youdaoWindow.style.bottom = "20px";
		} else {
			youdaoWindow.style.top = my + "px";
		}
		youdaoWindow.style.padding = "3px";
		youdaoWindow.style.zIndex = '9999999999';

		function word() {
			function play(soundURL) {
				if(soundURL.indexOf("http") != 0) soundURL = "https:" + soundURL;
				function playSound(buffer) {
					var source = context.createBufferSource();
					source.buffer = buffer;
					source.connect(context.destination);
					source.start(0);
				}
				var context = new AudioContext();
				// var soundUrl = 'https://dict.youdao.com/dictvoice?type=2&audio='+encodeURIComponent(word);
				console.log(soundURL);
				var soundUrl = soundURL;
				var p = new Promise(function(resolve, reject) {
					var ret = GM_xmlhttpRequest({
						method: "GET",
						url: soundUrl,
						responseType: 'arraybuffer',
						onload: function(res) {
							try {
								context.decodeAudioData(res.response, function(buffer) {
									resolve(buffer);
								});
							} catch(e) {
								reject(e);
							}
						}
					});
				});
				p.then(playSound, function(e) {
					console.log(e);
				});
			}
			var basic = dictJSON['basic'] != null ? dictJSON['basic']:dictJSON['dictionary']['content'][0];
			var header = document.createElement('div');
			header.className = "ACyoudaoPopupDIV";
			// header
			var span = document.createElement('span');
			span.className = "ACyoudaoPopupSpan";
			span.innerHTML = query;
			header.appendChild(span);
			// phonetic if there is
			var phonetic = basic['phonetic'];
			var transWord;
			try{
				transWord = basic['category'][0]['sense'][0]['description'];
			}catch (e) {
				transWord =  basic['usual'][0]['values'];
			}
			var phoneticText = [];
			if(typeof(phonetic) == "object"){
				var i = 0;
				phonetic.map(function(one){
					phoneticText[i] = one['text']+";";
					i++;
				});
			}else{
				phoneticText[0] = phonetic;
			}
			for(var i = 0; i < phoneticText.length; i++){
				var phoneticNode = document.createElement('span');
				if(i == phoneticText.length-1)
					phoneticNode.innerHTML = '[' + phoneticText[i] + ']:'+transWord;
				else
					phoneticNode.innerHTML = '[' + phoneticText[i] + ']';
				phoneticNode.style.cursor = "pointer";
				header.appendChild(phoneticNode);
				var playLogo = document.createElement('span');
				header.appendChild(phoneticNode);
				(function(index){
					phoneticNode.addEventListener('mouseup', function(e){
						if (e.target === phoneticNode) { }
						e.stopPropagation();
						play(phonetic[index]['filename']);
					}, false);
				})(i);
			}
			header.style.lineHeight = "1.8";
			header.style.color = "black";
			header.style.margin = "0";
			header.style.padding = "0";
			span.style.fontSize = "15px";
			span.style.color = "black";

			youdaoWindow.appendChild(header);
			var hr = document.createElement('hr');
			hr.className = "ACyoudaoPopupHR";
			hr.style.margin = "0";
			hr.style.padding = "0";
			hr.style.height = "1px";
			hr.style.borderTop = "dashed 1px black";
			youdaoWindow.appendChild(hr);
			var ul = document.createElement('ul');
			ul.className = "ACyoudaoPopupUL";
			// ul style
			ul.style.margin = "0";
			ul.style.padding = "0";
			var mapNode;
			try{
				mapNode = basic['category'][0]['sense'][0]['example'];
			}catch (e) {
				mapNode = basic['content'][0]['item']['core'];
			}
			mapNode.map(function(trans) {
				var li = document.createElement('li');
				li.className = "ACyoudaoPopupLi";
				li.style.listStyle = "none";
				li.style.margin = "0";
				li.style.padding = "0";
				li.style.background = "none";
				li.style.color = "#008CD8";
				var innerHTML = "";
				if(trans['source'] != undefined){
					innerHTML = trans['source']+":"+trans['target'];
				}else{
					innerHTML = trans['detail']['zh']+":" + trans['detail']['en']+":";
				}
				var divNode = document.createElement("div");
				divNode.innerHTML = innerHTML;
				li.appendChild(divNode);
				ul.appendChild(li);
			});
			youdaoWindow.appendChild(ul);
		}

		function sentence() {
			var ul = document.createElement('ul');
			ul.className = "ACyoudaoPopupUL";
			// ul style
			ul.style.margin = "0";
			ul.style.padding = "0";
			var trans = dictJSON['translate']['dit'];
			var oriText = dictJSON['translate']['text'];
			var li = document.createElement('li');
			li.style.listStyle = "none";
			li.style.margin = "0";
			li.style.padding = "0";
			li.style.background = "none";
			li.style.color = "#008CD8";
			li.className = "ACyoudaoPopupLi";
			trans = trans.replaceAll('QWER', '\n').replaceAll(",", ", ");
			console.log(trans);
			if(oriText.length < 70 && !/\n/.test(oriText)) // 如果长度比较短,并且没有回车符
				trans = oriText+"\n"+trans;
			transResult += trans;
			var reg = new RegExp("\n","gm");
			while(reg.exec(trans) != null){// 如果找不到回车了,那么最后重复一次新建<p>
				//找到一个回车,那么添加一个新的<p>,内容为:trans.sub(0, ?); trans = trans.sub(?);
				var newNode = document.createElement("li");
				newNode.className = "ACyoudaoPopupLi";
				newNode.style.listStyle = "none";
				newNode.style.margin = "0";
				newNode.style.padding = "0";
				newNode.style.background = "none";
				newNode.style.color = "#008CD8";
				newNode.style.fontSize = "15px";
				newNode.innerHTML = trans.substring(0, reg.lastIndex-1);
				li.appendChild(newNode);
				trans = trans.substring(reg.lastIndex);
			}
			var newNode = document.createElement("div");
			newNode.className = "ACyoudaoPopupDiv";
			newNode.innerHTML = trans;
			newNode.style = "line-height: 1.8;color:#008CD8 !important;font-size:15px !important;margin-left:10px;margin-right:10px;";
			li.appendChild(newNode);
			ul.appendChild(li);
			youdaoWindow.appendChild(ul);
		}
	}
	function ToCDB(str) {
		var tmp = "";
		for(var i=0;i<str.length;i++)
		{
			if(str.charCodeAt(i)>65248&&str.charCodeAt(i)<65375)
			{
				tmp += String.fromCharCode(str.charCodeAt(i)-65248);
			}
			else
			{
				tmp += String.fromCharCode(str.charCodeAt(i));
			}
		}
		return tmp
	}
	function translate(word, ts) {
		var ToLANGUAGE = "en";
		if(!/[\u4E00-\u9FA5]/g.test(word)) // 如果没有中文,那么说明是外文--> to=中文
			ToLANGUAGE = "zh-CHS";
		// word = encodeURIComponent(word); // 搜狗的这个API,不需要中文的encode处理

		// https://dlweb.sogoucdn.com/translate/pc/static/js/app.bf879f5f.js
		// https://fanyi.sogou.com/reventondc/translateV1?from=auto&to=en&client=pc&fr=browser_pc&text=中文&pid=sogou-dict-vr&useDetect=on&useDetectResult=on&needQc=1&oxford=on&isReturnSugg=on&s=0174d87d45e00a83a2dea5a27718472a
		var reqUrl = `https://fanyi.sogou.com/reventondc/translateV1?from=auto&to=${ToLANGUAGE}&client=pc&fr=browser_pc&text=${encodeURIComponent(word)}&pid=sogou-dict-vr&useDetect=on&useDetectResult=on&needQc=1&oxford=on&isReturnSugg=on&s=`
			+ CryptoJS.MD5("auto"+ToLANGUAGE+word+Config.SoGouKey);
		var ret = GM_xmlhttpRequest({
			method: "GET",
			url: reqUrl,
			headers: {"Accept": "application/json"},  // can be omitted...
			onload: function(res) {
				var retContent = res.response;
				retContent = ToCDB(retContent);
				//console.log(retContent)
				popup(mx, my, retContent);
			},
			onerror: function(res) {
				console.log("error");
			}
		});
	}
	String.prototype.replaceAll = function(s1,s2){
		return this.replace(new RegExp(s1,"gm"),s2);
	};
}