// ==UserScript==
// @name 语言切换快捷键|适配大部分在线翻译网站
// @namespace https://github.com/CandyTek
// @version 1.1
// @license MIT
// @description 语言切换快捷键 Ctrl + Shift + S
// @author CandyTek
// @match *://translate.yandex.com/?*
// @match *://zh.pons.com/*
// @match *://fanyi.so.com/*
// @match *://fanyi.xfyun.cn/console/trans/*
// @match *://translate.volcengine.com/*
// @match *://translation.imtranslator.net/*
// @match *://www.iciba.com/translate*
// @match *://fanyi.baidu.com/*
// @match *://dictionary.cambridge.org/*
// @match *://www.baidu.com/s?*
// @match *://fanyi.youdao.com/*
// @match *://www.deepl.com/*
// @match *://translation2.paralink.com/*
// @match *://cn.bing.com/search?*
// @match *://cn.bing.com/translator?*
// @match *://fanyi.sogou.com/text?*
// @match *://www.amz123.com/tools-translate/sougou
// @match *://fanyi.caiyunapp.com/*
// @match *://niutrans.com/trans?*
// @match *://translate.alibaba.com/*
// @match *://dict.eudic.net/home/translation
// @match *://www.fanyi1234.com/lang/*
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik02Ljk5IDExTDMgMTVsMy45OSA0di0zSDE0di0ySDYuOTl2LTN6TTIxIDlsLTMuOTktNHYzSDEwdjJoNy4wMXYzTDIxIDl6Ii8+PC9zdmc+
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// ==/UserScript==
// 网站规则列表
const myMatchWebsites = [
{
// https://translate.yandex.com/?source_lang=en&target_lang=zh
match: ["*://translate.yandex.com/?*"],
button:"[aria-label=\"Switch direction\"]",
},
{
// https://zh.pons.com/%E7%BF%BB%E8%AF%91/
match: ["*://zh.pons.com/*"],
button:"[data-e2e=\"switch-language\"]",
},
{
// https://fanyi.so.com/#
match: ["*://fanyi.so.com/*"],
button:".exchange",
},
{
// https://fanyi.xfyun.cn/console/trans/text
match: ["*://fanyi.xfyun.cn/console/trans/*"],
button:".anticon-swap",
},
{
// https://translate.volcengine.com/
match: ["*://translate.volcengine.com/*"],
button:".reverse-img",
},
{
// https://translation.imtranslator.net/
match: ["*://translation.imtranslator.net/*"],
button:"[title=\"Change translation direction of a selected language pair\"]",
},
{
// https://www.iciba.com/translate
match: ["*://www.iciba.com/translate*"],
button:".select_exchange__AfzH4",
},
{
// fanyi.baidu.com
match: ["*://fanyi.baidu.com/*"],
button:"svg.mg3bUrpQ",
},
{
// https://dictionary.cambridge.org/zhs/%E8%AF%8D%E5%85%B8/%E8%8B%B1%E8%AF%AD-%E6%B1%89%E8%AF%AD-%E7%B9%81%E4%BD%93/exchange
match: ["*://dictionary.cambridge.org/*"],
button:".i-exchange",
},
{
// https://www.baidu.com/s?tn=68018901_3_dg&ie=UTF-8&wd=%E7%BF%BB%E8%AF%91%E7%BD%91%E7%AB%99
match: ["*://www.baidu.com/s?*"],
button:".op_translation_exchange_img",
},
{
// https://fanyi.youdao.com/#/
match: ["*://fanyi.youdao.com/*"],
button:".ic_language_exchange",
},
{
// https://fanyi.sogou.com/text?keyword=&transfrom=en&transto=zh-CHS&model=general&exchange=true
match: ["*://fanyi.sogou.com/*"],
button:".btn-switch",
},
{
// https://www.deepl.com/zh/translator
match: ["*://www.deepl.com/*"],
button:"[data-testid=\"lmt_language_switch\"]",
},
{
// https://translation2.paralink.com/
match: ["*://translation2.paralink.com/*"],
button:"#switch",
},
{
// https://cn.bing.com/search?q=%E4%BD%A0%E5%A5%BD%20%E8%8B%B1%E8%AF%AD%E7%BF%BB%E8%AF%91
match: ["*://cn.bing.com/search?*","*://cn.bing.com/translator?*"],
button:"#tta_revIcon",
},
// {
// // https://www.amz123.com/tools-translate/sougou
// match: ["*://www.amz123.com/tools-translate/sougou"],
// button:".btn-switch",
// iframe:"#translate",
// },
{
// https://fanyi.caiyunapp.com/
match: ["*://fanyi.caiyunapp.com/*"],
button:".changeImg",
},
{
// https://niutrans.com/trans?type=text
match: ["*://niutrans.com/trans?*"],
button:".nt-icon-qiehuan",
},
{
// https://translate.alibaba.com/
match: ["*://translate.alibaba.com/*"],
button:".anticon-swap",
},
{
// https://dict.eudic.net/home/translation
match: ["*://dict.eudic.net/home/translation"],
button:".switchBtn",
},
{
// https://www.fanyi1234.com/lang/
match: ["*://www.fanyi1234.com/lang/*"],
button:"[alt=\"转换\"]",
},
];
/** 设置工具类 */
class CandyTekPreferenceUtil {
/** 是否已向网页添加过设置面板了 */
isAlreadyAddSettingPanel = false;
/** 设置面板根元素 */
rootShadow = null;
/** 存放设置值的地方。获取 prefValues[key] */
prefValues;
/** 源 pref 配置数组 */
preferenceList;
constructor(preferenceList) {
this.preferenceList = preferenceList;
this.refreshPrefValues();
}
/** 刷新设置值 */
refreshPrefValues() {
this.prefValues = this.preferenceList.reduce((list, curr) => {
list[curr.preference] = GM_getValue(curr.preference, curr.defaultValue);
return list;
}, {});
}
/** 获取设置值 */
get(key) {
return this.prefValues.hasOwnProperty(key) ? this.prefValues[key] : GM_getValue(key, "");
}
/** 写入设置值,未适配 boolean */
set(key, value) {
GM_setValue(key, value);
this.prefValues[key] = value;
}
/** 显示设置面板在网页右上角 */
show() {
if (this.isAlreadyAddSettingPanel) {
this.rootShadow.querySelector(".setting_panel").style.display = "block";
return;
}
if (!document.body.createShadowRoot) {
console.warn("可能不能创建 ShadowRoot");
//return;
}
// 创建设置面板
const host = document.createElement('div');
host.id = "simplify_article_settings_panel";
document.body.appendChild(host);
const root = host.attachShadow({ mode: 'open' });
this.rootShadow = root;
this.isAlreadyAddSettingPanel = true;
root.innerHTML = `
<style>
.preference_title {
width: fit-content;
height: 40px;
font-size: 20px;
margin: 0px;
line-height: 40px;
padding-left: 16px;
font-weight: bold;
}
.preference_item {
display: flex;
padding: 12px 8px;
}
.preference_item_title {
padding: 0px 0px 0px 10px;
margin: 0px;
font-size: 15px;
line-height: 40px;
letter-spacing: 2px;
height: 40px;
width: 140px;
}
.preference_item_edittext {
font-size: 14px;
margin-left: auto;
line-height: 36px;
height: 36px;
padding: 0px;
border: 2px solid #c4c7ce;
border-radius: 6px;
text-align: center;
width: 138px;
}
.preference_item_textarea {
text-align: unset;
line-height: 20px;
}
.preference_item_edittext_color {
width: 100px;
border-radius: 6px 0px 0px 6px;
border-right: 0;
}
.hoverbutton {
background: none;
}
.hoverbutton:hover {
background: #CCC;
background-size: 80% 80%;
border-radius: 4px;
}
.input_select_color {
width: 40px;
height: 40px;
margin: 0px;
padding:0px 2px 0px 4px;
box-sizing: border-box;
background-color:#ffffff;
border-width: 2px;
border-radius: 0px 6px 6px 0px;
border-left: 0px;
border-color: #c4c7ce;
}
.checkbox_input {
width: 24px;
height: 40px;
margin: 0px 0px 0px auto;
}
.setting_panel {
position: fixed;
right: 20px;
top: 20px;
width: fit-content;
height: fit-content;
border-radius: 8px;
background: #FFFFFF;
padding: 8px;
box-shadow: 0 10px 20px rgb(0 0 0 / 15%);
z-index:9999;
}
.container {
background: #F0F0F0;
border-radius: 8px;
margin-top: 0px;
padding-top: 8px;
padding-right: 8px;
}
</style>
<div class="setting_panel">
<div class="preference_item" style="padding-top: 0px;">
<button id="close" title="关闭并保存" class="hoverbutton" type="submit"
style="width: 40px;height: 40px;display: flex;align-items: center; justify-content: center; border: unset;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#5f6368"
viewBox="0 -960 960 960">
<path
d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
</svg>
</button>
<p class="preference_title">设置</p>
</div>
<div class="container" id="container">
</div>
</div>
`;
const container = root.querySelector("#container");
// 动态创建设置项
for (const index in this.preferenceList) {
const item = this.preferenceList[index];
const itemDiv = document.createElement("div");
itemDiv.className = "preference_item";
const itemTitle = document.createElement("p");
itemTitle.className = "preference_item_title";
itemTitle.innerText = item.text;
itemDiv.appendChild(itemTitle);
if (item.type == "number") {
const input = document.createElement("input");
input.type = "number";
input.className = "preference_item_edittext";
input.id = item.preference;
input.value = GM_getValue(item.preference, item.defaultValue);
itemDiv.appendChild(input);
} else if (item.type == "color") {
const inputText = document.createElement("input");
inputText.type = "text";
inputText.className = "preference_item_edittext preference_item_edittext_color";
inputText.id = item.preference;
inputText.value = GM_getValue(item.preference, item.defaultValue);
inputText.maxLength = 50;
itemDiv.appendChild(inputText);
const inputColor = document.createElement("input");
inputColor.type = "color";
inputColor.className = "input_select_color";
if (this.isValidHexColor(inputText.value)) {
inputColor.value = inputText.value;
}
itemDiv.appendChild(inputColor);
inputText.addEventListener('input', () => this.inputTextAndChangeDisplayColor(inputText, inputColor));
inputColor.addEventListener('input', () => this.selectColorAndChangeText(inputText, inputColor));
} else if (item.type == "checkbox") {
const input = document.createElement("input");
input.type = "checkbox";
input.id = item.preference;
const checkValue = GM_getValue(item.preference, item.defaultValue);
input.checked = checkValue;
input.className = "checkbox_input";
itemDiv.appendChild(input);
} else if (item.type == "textarea") {
const input = document.createElement("textarea");
input.id = item.preference;
input.value = GM_getValue(item.preference, item.defaultValue);
input.className = "preference_item_edittext preference_item_textarea";
itemDiv.appendChild(input);
}
container.appendChild(itemDiv);
}
root.querySelector("#close").onclick = () => {
root.querySelector(".setting_panel").style.display = "none";
// 动态创建设置项
for (const index in this.preferenceList) {
const item = this.preferenceList[index];
if (item.type == "color" || item.type == "textarea") {
try {
GM_setValue(item.preference, root.querySelector(`#${item.preference}`).value);
} catch (error) {
console.error(`保存配置失败:${item.preference}`);
}
} else if (item.type == "number") {
try {
GM_setValue(item.preference, parseFloat(root.querySelector(`#${item.preference}`).value));
} catch (error) {
console.error(`保存配置失败:${item.preference}`);
}
} else if (item.type == "checkbox") {
try {
GM_setValue(item.preference, root.querySelector(`#${item.preference}`).checked);
} catch (error) {
console.error(`保存配置失败:${item.preference}`);
}
}
}
this.refreshPrefValues();
};
}
/** input 颜色选择器更改颜色时,同时更改文本框 */
selectColorAndChangeText(inputText, inputColor) {
inputText.value = inputColor.value;
};
/** 文本框更改值时,同时更改颜色显示 */
inputTextAndChangeDisplayColor(inputText, inputColor) {
const color = inputText.value;
if (this.isValidHexColor(color)) {
inputColor.value = color;
}
};
/** 用于校验 6 位的十六进制颜色值 */
isValidHexColor(hex) {
try {
const hexPattern = /^#?([a-fA-F0-9]{6})$/;
return hexPattern.test(hex);
} catch (error) {
return false;
}
}
}
(() => {
let p;
// 开始匹配网站
for (const website of myMatchWebsites) {
let hit = false;
hit = Array.isArray(website.match) ? website.match.some((s) => matchRule(window.location.href, s)) : matchRule(window.location.href, website.match);
if (hit) {
//p = new CandyTekPreferenceUtil(myPreferenceList);
// 添加设置菜单
// GM_registerMenuCommand("快捷键设置", () => {
// p.show();
// });
// 添加语音调转快捷键
document.addEventListener("keydown", function(event) {
if (event.ctrlKey && event.shiftKey && !event.altKey && event.key === "S") {
let el = document.querySelector(website.button);
if(website.iframe){
const iframe = document.querySelector(website.iframe);
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
el=iframeDocument.querySelector(website.button);
}
customClick(el);
if(website.secondButton){
let el2 = document.querySelector(website.secondButton);
customClick(el2);
}
}
});
console.info(`匹配成功 ${website.match}`);
break;
}
};
function customClick(el){
if(el){
try{
el.click();
}catch{
el.dispatchEvent(new MouseEvent('click', {bubbles: true,cancelable: true,}));
}
}
}
/** match匹配方法 */
function matchRule(str, rule) {
const escapeRegex = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
return new RegExp(`^${rule.split("*").map(escapeRegex).join(".*")}$`).test(str);
}
// todo:https://www.reverso.net/text-translation
// todo:google
// node:http://www.sowang.com/sogou/fanyi.htm
// todo:https://www.amz123.com/tools-translate/tenxunjiaohu
// todo:https://fanyi.pdf365.cn/free
// todo:https://www.fanyi1234.com/
// todo:https://dict.cnki.net/
// todo:https://www.ichacha.net/
// todo:https://www.tangpafanyi.com/text.html
// todo:https://tran.httpcn.com/FanyiWeb/
// todo:https://fanyi.zou.la/
// todo:https://fanyi.dict.cn/
// todo:https://www.99yee.cn/
// todo:https://transmart.qq.com/zh-CN/index
// todo:https://fanyi.atman360.com/index
// todo:https://fanyi.qq.com/
// todo:https://www.medsci.cn/sci/translation.do
// todo:https://www.worldlingo.com/en/products/text_translator.html
})();