// ==UserScript==
// @name translator
// @namespace https://lufei.so
// @supportURL https://github.com/intellilab/translator.user.js
// @description 划词翻译
// @version 1.6.8
// @run-at document-start
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@1,npm/@violentmonkey/ui@0.4
// @include *
// ==/UserScript==
(function () {
'use strict';
function dumpQuery(query) {
return Object.entries(query).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
}
function request({
method = 'GET',
url,
params,
responseType,
data,
headers
}) {
return new Promise((resolve, reject) => {
if (params) {
const sep = url.includes('?') ? '&' : '?';
url += sep + dumpQuery(params);
}
GM_xmlhttpRequest({
method,
url,
responseType,
data,
headers,
onload(res) {
if (res.status >= 300) return reject();
resolve(res.response);
},
onerror: reject
});
});
}
const provider = {
name: 'youdao',
handle: async text => {
const payload = {
type: 'data',
doctype: 'json',
version: '1.1',
relatedUrl: 'http://fanyi.youdao.com/',
keyfrom: 'fanyiweb',
key: null,
translate: 'on',
q: text,
ts: Date.now()
};
const result = await request({
url: 'https://fanyi.youdao.com/openapi.do',
params: payload,
responseType: 'json'
});
if (result.errorCode) throw result;
const {
basic,
query,
translation
} = result;
if (basic) {
const noPhonetic = '♥';
const {
explains,
'us-phonetic': us,
'uk-phonetic': uk
} = basic;
return {
query,
phonetic: [{
html: `UK: [${uk || noPhonetic}]`,
url: `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(query)}&type=1`
}, {
html: `US: [${us || noPhonetic}]`,
url: `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(query)}&type=2`
}],
explains,
detailUrl: `http://dict.youdao.com/search?q=${encodeURIComponent(query)}`
};
}
if (translation != null && translation[0]) {
return {
translations: translation
};
}
}
};
const LANG_EN = 'en';
const LANG_ZH_HANS = 'zh-Hans';
async function translate(text, to) {
const [data] = await request({
method: 'POST',
url: 'https://cn.bing.com/ttranslatev3',
responseType: 'json',
data: dumpQuery({
fromLang: 'auto-detect',
to,
text
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const {
detectedLanguage,
translations
} = data;
return {
language: {
from: detectedLanguage.language,
to
},
translations: translations.map(item => item.text)
};
}
const provider$1 = {
name: 'bing',
handle: async source => {
let data = await translate(source, LANG_ZH_HANS);
if (data.language.from === LANG_ZH_HANS) data = await translate(source, LANG_EN);
return data;
}
};
const LANG_EN$1 = 'en';
const LANG_ZH_CN = 'zh-CN';
async function translate$1(text, to) {
var _data$;
const data = await request({
url: 'https://translate.google.cn/translate_a/single',
params: {
q: text,
client: 'gtx',
sl: 'auto',
tl: to,
dt: 'at'
},
responseType: 'json'
});
const language = {
from: data[8][0][0],
to
};
const translations = (_data$ = data[5]) == null ? void 0 : _data$.map(item => {
var _item$, _item$$;
return (_item$ = item[2]) == null ? void 0 : (_item$$ = _item$[0]) == null ? void 0 : _item$$[0];
}).filter(Boolean);
return {
language,
translations
};
}
const provider$2 = {
name: 'google',
handle: async source => {
let data = await translate$1(source, LANG_ZH_CN);
if (data.language.from === LANG_ZH_CN) data = await translate$1(source, LANG_EN$1);
return data;
}
};
var styles = {"link":"style-module_link__YVV7m","section":"style-module_section__1Eiq1","block-top":"style-module_block__1NZsy","block-bottom":"style-module_block__1NZsy","label":"style-module_label__JD9KX","content":"style-module_content__1MvKK","phonetic":"style-module_phonetic__2SIsx","item":"style-module_item__2QPXK"};
var stylesheet=":host .style-module_link__YVV7m{position:relative;color:#7cbef0;cursor:pointer}:host .style-module_link__YVV7m:hover{text-decoration:underline}:host .style-module_section__1Eiq1{display:flex;align-items:flex-start;font-size:12px;line-height:1.2}:host .style-module_section__1Eiq1:not(:first-child){border-top:1px solid #eee}:host .style-module_block__1NZsy{display:block}:host .style-module_label__JD9KX{display:block;margin:8px 8px 8px 0;padding:2px 0;color:#fff;background:#bbb;border-radius:4px;font-size:12px;line-height:1.4;text-transform:uppercase;writing-mode:vertical-rl}:host .style-module_content__1MvKK{flex:1;min-width:0;padding:8px 0}:host .style-module_content__1MvKK>*{display:block}:host .style-module_content__1MvKK>:not(:first-child){margin-top:8px}:host .style-module_phonetic__2SIsx{display:inline-block;margin-left:8px}:host .style-module_item__2QPXK{display:block}:host .style-module_item__2QPXK~.style-module_item__2QPXK{margin-top:8px}";
const React = VM;
let audio;
function play(url) {
if (!audio) audio = /*#__PURE__*/React.createElement("audio", {
autoPlay: true
});
audio.src = url;
}
function getPlayer(url) {
return () => {
play(url);
};
}
function handleOpenUrl(e) {
const {
href
} = e.target.dataset;
const a = /*#__PURE__*/React.createElement("a", {
href: href,
target: "_blank",
rel: "noopener noreferrer"
});
a.click();
}
function render(results, {
event,
panel
}) {
panel.clear();
for (const [name, result] of Object.entries(results)) {
const {
query,
phonetic,
detailUrl,
explains,
translations
} = result;
panel.append( /*#__PURE__*/React.createElement(panel.id, {
className: styles.section
}, /*#__PURE__*/React.createElement(panel.id, {
className: styles.label
}, name), /*#__PURE__*/React.createElement(panel.id, {
className: styles.content
}, !!(query || phonetic != null && phonetic.length) && /*#__PURE__*/React.createElement(panel.id, {
className: styles.block
}, query && /*#__PURE__*/React.createElement(panel.id, null, query), phonetic == null ? void 0 : phonetic.map(({
html,
url
}) => /*#__PURE__*/React.createElement(panel.id, {
className: `${styles.phonetic} ${styles.link}`,
dangerouslySetInnerHTML: {
__html: html
},
onClick: getPlayer(url)
}))), explains && /*#__PURE__*/React.createElement(panel.id, {
className: styles.block
}, explains.map(item => /*#__PURE__*/React.createElement(panel.id, {
className: styles.item,
dangerouslySetInnerHTML: {
__html: item
}
}))), detailUrl && /*#__PURE__*/React.createElement(panel.id, {
className: styles.block
}, /*#__PURE__*/React.createElement(panel.id, {
className: styles.link,
"data-href": detailUrl,
onClick: handleOpenUrl
}, "\u66F4\u591A...")), translations && /*#__PURE__*/React.createElement(panel.id, {
className: styles.block
}, translations.map(item => /*#__PURE__*/React.createElement(panel.id, {
className: styles.item,
dangerouslySetInnerHTML: {
__html: item
}
}))))));
}
const {
wrapper
} = panel;
const {
innerWidth,
innerHeight
} = window;
const {
clientX,
clientY
} = event;
if (clientY > innerHeight * 0.5) {
wrapper.style.top = 'auto';
wrapper.style.bottom = `${innerHeight - clientY + 10}px`;
} else {
wrapper.style.top = `${clientY + 10}px`;
wrapper.style.bottom = 'auto';
}
if (clientX > innerWidth * 0.5) {
wrapper.style.left = 'auto';
wrapper.style.right = `${innerWidth - clientX}px`;
} else {
wrapper.style.left = `${clientX}px`;
wrapper.style.right = 'auto';
}
panel.show();
}
const providers = [provider, provider$1, provider$2];
let session;
function translate$2(context) {
const sel = window.getSelection();
const text = sel.toString().trim();
if (/^\s*$/.test(text)) return;
const {
activeElement
} = document;
if (['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) < 0 && !activeElement.contains(sel.getRangeAt(0).startContainer)) return;
context.source = text;
const results = {};
session = results;
providers.forEach(async provider => {
const result = await provider.handle(text);
if (!result || session !== results) return;
results[provider.name] = result;
render(results, context);
});
}
function debounce(func, delay) {
let timer;
function exec(...args) {
timer = null;
func(...args);
}
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(exec, delay, ...args);
};
}
function initialize() {
const panel = VM.getPanel({
css: stylesheet,
shadow: false
});
const panelStyle = panel.body.style;
panelStyle.maxHeight = '50vh';
panelStyle.padding = '0 8px';
panelStyle.overflow = 'auto';
panelStyle.overscrollBehavior = 'contain';
const debouncedTranslate = debounce(event => translate$2({
event,
panel
}));
let isSelecting;
document.addEventListener('mousedown', e => {
isSelecting = false;
if (panel.body.contains(e.target)) return;
panel.hide();
session = null;
}, true);
document.addEventListener('mousemove', () => {
isSelecting = true;
}, true);
document.addEventListener('mouseup', e => {
if (panel.body.contains(e.target) || !isSelecting) return;
debouncedTranslate(e);
}, true);
document.addEventListener('dblclick', e => {
if (panel.body.contains(e.target)) return;
debouncedTranslate(e);
}, true);
}
initialize();
}());