语言切换快捷键|适配大部分在线翻译网站

语言切换快捷键 Ctrl + Shift + S

// ==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

})();