🏠 Home 

[SNOLAB] [Mulango] Point Speaker

[SNOLAB] Mulango Point Speaker, 按 alt+t 翻译鼠标所在元素到浏览器第二语言

// ==UserScript==
// @name               [SNOLAB] [Mulango] Point Speaker
// @name:zh            [SNOLAB] [Mulango] 点读笔
// @namespace          [email protected]
// @author             [email protected]
// @version            1.0.3
// @description        [SNOLAB] Mulango Point Speaker, 按 alt+t 翻译鼠标所在元素到浏览器第二语言
// @description:zh     [SNOLAB] Mulango 点读笔, 按 alt+t 翻译鼠标所在元素到浏览器第二语言
// @match              https://*.google.com/search?*
// @match              https://*.bing.com/search?*
// @match              https://*/search*
// @grant              none
// @run-at             document-start
// @license            GPL-3.0+
// @supportURL         https://github.com/snomiao/userscript.js/issues
// @contributionURL    https://snomiao.com/donate
// @grant              GM_getValue
// @grant              GM_setValue
// ==/UserScript==
(async function () {
const translate = await useTranslator();
hotkeys({
"alt+t": async () => await pointedTranslate(),
});
async function pointedTranslate() {
return await elementAltTranslate(getPointedElement());
}
async function elementAltTranslate(e) {
console.log("translating", e);
const transcript = await translate(
speaked(e.textContent),
navigator.languages[1]
);
e.setAttribute("title", transcript);
}
})();
async function speaked(text) {
return (
speechSynthesis.speak(
Object.assign(new SpeechSynthesisUtterance(), { text })
),
text
);
}
function hotkeys(mapping) {
Object.entries(mapping).map(([hotkey, handler]) =>
window.addEventListener("keydown", hotkeyHandler(hotkey, handler))
);
function hotkeyHandler(hotkey, fn) {
return (e) => {
e[e.key + "Key"] = true;
const falseKeys = "meta+alt+shift+ctrl";
const conds = (falseKeys + "+" + hotkey)
.replace(/win|command|search/, "meta")
.replace(/control/, "ctrl")
.split("+")
.map((k, i) => [k, (i < 4) ^ e[k + "Key"]]);
const covered = Object.entries(Object.fromEntries(conds));
console.log(covered);
const matched = covered.every(([keyName, pass]) => pass);
if (!matched) return;
e.stopPropagation();
e.preventDefault();
return fn();
};
}
}
async function useTranslator(initLang = navigator.language) {
const translateAPI = (
await import(
"https://cdn.skypack.dev/@snomiao/google-translate-api-browser"
)
).setCORS("https://google-translate-cors.vercel.app/api?url=", {
encode: true,
});
const translate = async (s, lang = initLang) => {
if (!s) return;
return await translateAPI(s, { to: lang.replace(/-.*/, "") })
.then((e) => e.text)
.catch(console.error);
};
return localforageCached(limiter(translate, 1e3));
}
function validPipor(fn) {
// requires the first param is not undefined otherwise return the undefined
return (s, ...args) => (s === undefined ? undefined : fn(s, ...args));
}
function limiter(fn, wait = 1e3, last = 0) {
return async (...args) => {
const remain = () => last + wait - +new Date();
while (remain() > 0) await new Promise((r) => setTimeout(r, remain()));
const r = await fn(...args);
last = +new Date();
return r;
};
}
function edgeFilter(init) {
return (e) => (e !== init ? (init = e) : undefined);
}
async function localforageCached(fn) {
const hash = (s) => s.slice(0, 16) + s.slice(-16);
const { default: cache } = await import(
"https://cdn.skypack.dev/@luudjanssen/localforage-cache"
);
const in3day = 86400e3 * 3;
const cacheName = hash(String(fn));
const cacheInstance = cache.createInstance({
name: cacheName,
defaultExpiration: in3day,
});
return validPipor(cachedFn);
async function cachedFn(...args) {
const r###lt =
(await cacheInstance?.getItem(JSON.stringify(args))) ||
(await fn(...args));
await cacheInstance?.setItem(JSON.stringify(args), r###lt); //refresh cache
return r###lt;
}
}
function getPointedElement() {
return [...document.querySelectorAll(":hover")].reverse()[0];
}