Compare commits

...

12 Commits

Author SHA1 Message Date
Gabe
2650e5cf7c Update version number: 2.0.12 2025-11-22 21:17:06 +08:00
Gabe
da1fa3e8ed fix: clean content html 2025-11-22 21:16:35 +08:00
Gabe
e256314d4f fix: add context for setting provider 2025-11-22 18:15:07 +08:00
Gabe
f8f7d5955f fix: handle mouseup (#400) 2025-11-22 17:24:02 +08:00
Gabe
fa6f68fec3 fix: add global context type 2025-11-22 01:36:03 +08:00
Gabe
0705e8a65a fix: add baidu audio for youdao dict 2025-11-22 00:20:58 +08:00
Gabe
9d9bbd3821 fix: throw hook error 2025-11-20 22:27:27 +08:00
Gabe
86a312ea6b fix: runtime oninstalled 2025-11-20 17:36:02 +08:00
Gabe
e4771e795b fix: Trigger shortcut key when keyup (#410) 2025-11-20 17:30:55 +08:00
Gabe
1504830142 fix: content does not render elements during loading 2025-11-20 16:33:49 +08:00
XYenon
7e99bc7aad fix: restore context menu configuration after extension update (#408) 2025-11-19 20:03:08 +08:00
Gabe
abca8cb26d fix: default select style 2025-11-16 21:59:01 +08:00
26 changed files with 230 additions and 413 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=2.0.11
REACT_APP_VERSION=2.0.12
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "2.0.11",
"version": "2.0.12",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {

View File

@@ -4,321 +4,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>%REACT_APP_NAME%</title>
<style>
img {
width: 1.2em;
height: 1.2em;
}
svg {
max-width: 1.2em;
max-height: 1.2em;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
// (() => {
// var shadow = document.querySelector("#shadow1");
// var root = shadow.attachShadow({ mode: "open" });
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// root.appendChild(newLine);
// })();
// setTimeout(function () {
// var shadow = document.querySelector("#shadow2");
// var root = shadow.attachShadow({ mode: "open" });
// }, 1000);
// setTimeout(() => {
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 2000);
// setTimeout(() => {
// var newLine = document.createElement("div");
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 3000);
// setTimeout(function () {
// var el = document.querySelector("h2");
// el.innerText = "hello world";
// var title = document.querySelector("#addtitle");
// title.innerHTML =
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
// }, 1000);
setTimeout(function () {
var el = document.querySelector('h2>p>span');
el.innerText = 'hello world';
}, 1000);
});
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div id="content">
<p>You need to enable JavaScript to run <span>this app.</span></p>
The <span>embargo</span> has just lifted to confirm that AmpereOne is coming to
Google Cloud with the C3A instances.
<br />
But these upcoming instances for now are only in private preview form.
<br />
<br />
Needless to say I also haven't had any AmpereOne access to check out the
performance and power efficiency of these new Arm server processors from Ampere
Computing.
<br />
</div>
<h2>
<p>
<span>React is a JavaScript library for building user interfaces.</span>
</p>
</h2>
<hr />
<input id="input1" style="width: 80%" />
<hr />
<textarea id="textarea1" style="width: 80%">test</textarea>
<hr />
<div id="addtitle"></div>
<h2>Shadow 1</h2>
<div id="shadow1"></div>
<h2>Shadow 2</h2>
<div id="shadow2"></div>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<h2>
React Server Components (or RSC) is a new application architecture designed by the
React team.
</h2>
<iframe
id="iframe1"
width="800px"
height="600px"
src="http://localhost:3000/index.html"></iframe>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<h2>Weve first shared our research on RSC in an introductory talk and an RFC.</h2>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<h2>
To recap them, we are introducing a new kind of component—Server Components—that
run ahead of time and are excluded from your JavaScript bundle.
</h2>
<iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="cont cont1">
<h2>
Server Components can run during the build, letting you read from the filesystem
or fetch static content.
</h2>
<ul>
<li>
They can also run on the server, letting you access your data layer without
having to build an API. You can pass data by props from Server Components to
the interactive Client Components in the browser.
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="cont cont2">
<h2>
Since our last update, we have merged the React Server Components RFC to ratify
the proposal.
</h2>
<ul>
<li>
RSC combines the simple “request/response” mental model of server-centric
Multi-Page Apps with the seamless interactivity of client-centric Single-Page
Apps, giving you the best of both worlds.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
</div>
<!--
This HTML file is a template.

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.11",
"version": "2.0.12",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.11",
"version": "2.0.12",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "2.0.11",
"version": "2.0.12",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",

View File

@@ -739,6 +739,13 @@ export const genTransReq = async ({ reqHook, ...args }) => {
// 执行 request hook
if (reqHook?.trim() && !events) {
try {
const req = {
url,
body,
headers,
userMsg,
method,
};
interpreter.run(`exports.reqHook = ${reqHook}`);
const hookResult = await interpreter.exports.reqHook(
{
@@ -747,20 +754,16 @@ export const genTransReq = async ({ reqHook, ...args }) => {
defaultSubtitlePrompt,
defaultNobatchPrompt,
defaultNobatchUserPrompt,
req,
},
{
url,
body,
headers,
userMsg,
method,
}
req
);
if (hookResult && hookResult.url) {
return genInit(hookResult);
}
} catch (err) {
kissLog("run req hook", err);
throw new Error(`Request hook error: ${err.message}`);
}
}
@@ -817,6 +820,7 @@ export const parseTransRes = async (
}
} catch (err) {
kissLog("run res hook", err);
throw new Error(`Response hook error: ${err.message}`);
}
}

View File

@@ -15,8 +15,6 @@ import {
MSG_UPDATE_CSP,
MSG_BUILTINAI_DETECT,
MSG_BUILTINAI_TRANSLATE,
DEFAULT_CSPLIST,
DEFAULT_ORILIST,
CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
@@ -37,7 +35,7 @@ import { injectInlineJsBg, injectInternalCss } from "./libs/injector";
import { kissLog, logger } from "./libs/log";
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
globalThis.ContextType = "BACKGROUND";
globalThis.__KISS_CONTEXT__ = "background";
const CSP_RULE_START_ID = 1;
const ORI_RULE_START_ID = 10000;
@@ -193,19 +191,21 @@ async function registerMsgDisplayScript() {
/**
* 插件安装
*/
browser.runtime.onInstalled.addListener(() => {
tryInitDefaultData();
browser.runtime.onInstalled.addListener(async () => {
await tryInitDefaultData();
//在thunderbird中注册脚本
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
registerMsgDisplayScript();
}
const { contextMenuType, csplist, orilist } = await getSettingWithDefault();
// 右键菜单
addContextMenus();
addContextMenus(contextMenuType);
// 禁用CSP
updateCspRules({ csplist: DEFAULT_CSPLIST, orilist: DEFAULT_ORILIST });
updateCspRules({ csplist, orilist });
});
/**

View File

@@ -8,8 +8,8 @@ export const SHADOW_KEY = ">>>";
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "font";
// export const DEFAULT_SELECT_STYLE =
// "-webkit-line-clamp: unset; max-height: none; height: auto;";
export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;";
export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
@@ -108,7 +108,7 @@ export const GLOBLA_RULE = {
textExtStyle: "", // 译文附加样式
termsStyle: "font-weight: bold;", // 专业术语样式
highlightStyle: "color: red;", // 高亮词汇样式
selectStyle: "", // 选择器节点样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: "", // 选择器父节点样式
grandStyle: "", // 选择器祖节点样式
injectJs: "", // 注入JS

View File

@@ -1,3 +1,5 @@
import { run } from "./common";
globalThis.__KISS_CONTEXT__ = "content";
run();

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { apiBaiduTTS } from "../apis";
import { kissLog } from "../libs/log";
import { logger } from "../libs/log";
import { fetchData } from "../libs/fetch";
/**
* 声音播放hook
@@ -12,50 +12,97 @@ export function useAudio(src) {
const [error, setError] = useState(null);
const [ready, setReady] = useState(false);
const [playing, setPlaying] = useState(false);
const [loading, setLoading] = useState(false);
const onPlay = useCallback(() => {
audioRef.current?.play();
const onPlay = useCallback(async () => {
if (!audioRef.current) return;
try {
await audioRef.current.play();
} catch (err) {
logger.info("Playback failed:", err);
setPlaying(false);
}
}, []);
const onPause = useCallback(() => {
audioRef.current?.pause();
}, []);
useEffect(() => {
if (!src) {
return;
}
const audio = new Audio(src);
audio.addEventListener("error", (err) => setError(err));
audio.addEventListener("canplaythrough", () => setReady(true));
audio.addEventListener("play", () => setPlaying(true));
audio.addEventListener("ended", () => setPlaying(false));
if (!src) return;
let ignore = false;
let objectUrl = null;
setReady(false);
setError(null);
setPlaying(false);
setLoading(true);
const audio = new Audio();
audioRef.current = audio;
const handleCanPlay = () => setReady(true);
const handlePlay = () => setPlaying(true);
const handlePause = () => setPlaying(false);
const handleEnded = () => setPlaying(false);
const handleError = (e) => {
if (!ignore) {
setError(audio.error || e);
setReady(false);
setLoading(false);
}
};
audio.addEventListener("canplaythrough", handleCanPlay);
audio.addEventListener("play", handlePlay);
audio.addEventListener("pause", handlePause);
audio.addEventListener("ended", handleEnded);
audio.addEventListener("error", handleError);
const loadAudio = async () => {
try {
const data = await fetchData(src, {}, { expect: "audio" });
if (ignore) return;
audio.src = data;
setLoading(false);
} catch (err) {
if (!ignore) {
logger.info("Audio fetch failed:", err);
setError(err);
setLoading(false);
}
}
};
loadAudio();
return () => {
ignore = true;
audio.pause();
audio.removeAttribute("src");
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
audio.removeEventListener("canplaythrough", handleCanPlay);
audio.removeEventListener("play", handlePlay);
audio.removeEventListener("pause", handlePause);
audio.removeEventListener("ended", handleEnded);
audio.removeEventListener("error", handleError);
};
}, [src]);
return {
loading,
error,
ready,
playing,
onPlay,
onPause,
};
}
/**
* 获取语音hook
* @param {*} text
* @param {*} lan
* @param {*} spd
* @returns
*/
export function useTextAudio(text, lan = "uk", spd = 3) {
const [src, setSrc] = useState("");
useEffect(() => {
(async () => {
try {
setSrc(await apiBaiduTTS(text, lan, spd));
} catch (err) {
kissLog("baidu tts", err);
}
})();
}, [text, lan, spd]);
return useAudio(src);
}

View File

@@ -25,17 +25,15 @@ const SettingContext = createContext({
reloadSetting: () => {},
});
export function SettingProvider({ children, isSettingPage }) {
export function SettingProvider({ children, context }) {
const isOptionsPage = useMemo(() => context === "options", [context]);
const {
data: setting,
isLoading,
update,
reload,
} = useStorage(
STOKEY_SETTING,
DEFAULT_SETTING,
isSettingPage ? KV_SETTING_KEY : ""
);
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
useEffect(() => {
if (typeof setting?.darkMode === "boolean") {
@@ -47,7 +45,7 @@ export function SettingProvider({ children, isSettingPage }) {
}, [setting?.darkMode, update]);
useEffect(() => {
if (!isSettingPage) return;
if (!isOptionsPage) return;
(async () => {
try {
@@ -59,7 +57,7 @@ export function SettingProvider({ children, isSettingPage }) {
logger.error("Failed to fetch log level, using default.", error);
}
})();
}, [isSettingPage, setting?.logLevel]);
}, [isOptionsPage, setting?.logLevel]);
const updateSetting = useCallback(
(objOrFn) => {
@@ -81,28 +79,31 @@ export function SettingProvider({ children, isSettingPage }) {
const value = useMemo(
() => ({
context,
setting,
updateSetting,
updateChild,
reloadSetting: reload,
}),
[setting, updateSetting, updateChild, reload]
[context, setting, updateSetting, updateChild, reload]
);
if (isLoading) {
return <Loading />;
return isOptionsPage ? <Loading /> : null;
}
if (!setting) {
<center>
<Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
<p>数据加载出错请刷新页面或卸载后重新安装</p>
<p>
Data loading error, please refresh the page or uninstall and
reinstall.
</p>
</Alert>
</center>;
return isOptionsPage ? (
<center>
<Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
<p>数据加载出错请刷新页面或卸载后重新安装</p>
<p>
Data loading error, please refresh the page or uninstall and
reinstall.
</p>
</Alert>
</center>
) : null;
}
return (

View File

@@ -3,6 +3,7 @@ import { storage } from "../libs/storage";
import { kissLog } from "../libs/log";
import { syncData } from "../libs/sync";
import { useDebouncedCallback } from "./DebouncedCallback";
import { isOptions } from "../libs/browser";
/**
* 用于将组件状态与 Storage 同步
@@ -79,7 +80,7 @@ export function useStorage(key, defaultVal = null, syncKey = "") {
});
// 触发远端同步
if (syncKey) {
if (syncKey && isOptions()) {
debouncedSync(syncKey, data);
}
}, [key, syncKey, isLoading, data, debouncedSync]);

View File

@@ -14,7 +14,30 @@ function _browser() {
export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
export const getContext = () => {
const context = globalThis.__KISS_CONTEXT__;
if (context) return context;
// if (typeof window === "undefined" || typeof document === "undefined") {
// return "background";
// }
// const extensionOrigin = browser.runtime.getURL("");
// if (!window.location.href.startsWith(extensionOrigin)) {
// return "content";
// }
// const pathname = window.location.pathname;
// if (pathname.includes("popup")) return "popup";
// if (pathname.includes("options")) return "options";
// if (pathname.includes("sidepanel")) return "sidepanel";
// if (pathname.includes("background")) return "background";
return "undefined";
};
export const isBg = () => getContext() === "background";
export const isOptions = () => getContext() === "options";
export const isBuiltinAIAvailable =
"LanguageDetector" in globalThis && "Translator" in globalThis;

View File

@@ -16,7 +16,7 @@ import { blobToBase64 } from "./utils";
*/
export const tryClearCaches = async () => {
try {
if (isExt && !isBg) {
if (isExt && !isBg()) {
await sendBgMsg(MSG_CLEAR_CACHES);
} else {
await caches.delete(CACHE_NAME);
@@ -50,13 +50,13 @@ const newCacheReq = async (input, init) => {
* @param {*} init
* @returns
*/
export const getHttpCache = async ({ input, init }) => {
export const getHttpCache = async ({ input, init, expect }) => {
try {
const request = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(request);
if (response) {
const res = await parseResponse(response);
const res = await parseResponse(response, expect);
return res;
}
} catch (err) {
@@ -99,7 +99,7 @@ export const putHttpCache = async ({
* @param {*} res
* @returns
*/
export const parseResponse = async (res) => {
export const parseResponse = async (res, expect = null) => {
if (!res) {
throw new Error("Response object does not exist");
}
@@ -108,21 +108,45 @@ export const parseResponse = async (res) => {
const msg = {
url: res.url,
status: res.status,
statusText: res.statusText,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
try {
const errorText = await res.clone().text();
try {
msg.response = JSON.parse(errorText);
} catch {
msg.response = errorText;
}
} catch (e) {
msg.response = "Unable to read error body";
}
throw new Error(JSON.stringify(msg));
}
const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) {
return res.json();
} else if (contentType?.includes("audio")) {
const contentType = res.headers.get("Content-Type") || "";
if (expect === "blob") return res.blob();
if (expect === "text") return res.text();
if (expect === "json") return res.json();
if (
expect === "audio" ||
contentType.includes("audio") ||
contentType.includes("image") ||
contentType.includes("video")
) {
const blob = await res.blob();
return blobToBase64(blob);
}
return res.text();
const text = await res.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch (err) {
return text;
}
};
/**

View File

@@ -99,7 +99,7 @@ export const fetchPatcher = async (input, init = {}, opts) => {
*/
export const fetchHandle = async ({ input, init, opts }) => {
const res = await fetchPatcher(input, init, opts);
return parseResponse(res);
return parseResponse(res, opts.expect);
};
/**

View File

@@ -61,14 +61,25 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
if (targetKeys.length === 0) return () => {};
const targetKeySet = new Set(targetKeys);
let hasInterference = false;
const onKeyDown = (pressedKeys, event) => {
if (isSameSet(targetKeySet, pressedKeys)) {
// event.preventDefault(); // 阻止浏览器的默认行为
// event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
fn();
// if (isSameSet(targetKeySet, pressedKeys)) {
// // event.preventDefault(); // 阻止浏览器的默认行为
// // event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
// fn();
// }
if (!targetKeySet.has(event.code)) {
hasInterference = true;
}
};
const onKeyUp = (pressedKeys, event) => {
if (isSameSet(targetKeySet, pressedKeys) && !hasInterference) {
fn();
}
if (pressedKeys.size === 1) {
hasInterference = false;
}
};
const onKeyUp = () => {};
return shortcutListener(onKeyDown, onKeyUp, target);
};

View File

@@ -2,6 +2,8 @@ import React from "react";
import ReactDOM from "react-dom/client";
import Options from "./views/Options";
globalThis.__KISS_CONTEXT__ = "options";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>

View File

@@ -4,10 +4,12 @@ import { SettingProvider } from "./hooks/Setting";
import ThemeProvider from "./hooks/Theme";
import Popup from "./views/Popup";
globalThis.__KISS_CONTEXT__ = "popup";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<SettingProvider>
<SettingProvider context="popup">
<ThemeProvider>
<Popup />
</ThemeProvider>

View File

@@ -45,7 +45,7 @@ export default function ContentFab({
);
return (
<SettingProvider>
<SettingProvider context="fab">
<ThemeProvider>
<Draggable
key="fab"

View File

@@ -70,7 +70,7 @@ export default function Action({ translator, processActions }) {
}, [windowSize]);
return (
<SettingProvider>
<SettingProvider context="contentPopup">
<ThemeProvider>
{showPopup && (
<Draggable

View File

@@ -99,7 +99,7 @@ export default function Options() {
}
return (
<SettingProvider isSettingPage={true}>
<SettingProvider context="options">
<ThemeProvider>
<AlertProvider>
<ConfirmProvider>

View File

@@ -1,8 +1,9 @@
import IconButton from "@mui/material/IconButton";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import { useAudio } from "../../hooks/Audio";
import queryString from "query-string";
export default function AudioBtn({ src }) {
export function AudioBtn({ src }) {
const { error, ready, playing, onPlay } = useAudio(src);
if (error || !ready) {
@@ -27,3 +28,10 @@ export default function AudioBtn({ src }) {
</IconButton>
);
}
export function BaiduAudioBtn({ text, lan = "uk", spd = 3 }) {
if (!text) return null;
const src = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
return <AudioBtn src={src} />;
}

View File

@@ -1,5 +1,5 @@
import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
import { AudioBtn, BaiduAudioBtn } from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
@@ -48,12 +48,14 @@ export const dictHandlers = {
style={{ display: "inline-block", paddingRight: "1em" }}
>
<Typography component="span">{`UK [${data?.ec?.word?.ukphone}]`}</Typography>
<BaiduAudioBtn text={data?.ec?.word?.["return-phrase"]} lan="uk" />
</Typography>
<Typography
component="div"
style={{ display: "inline-block", paddingRight: "1em" }}
>
<Typography component="span">{`US [${data?.ec?.word?.usphone}]`}</Typography>
<BaiduAudioBtn text={data?.ec?.word?.["return-phrase"]} lan="en" />
</Typography>
</Typography>
),

View File

@@ -141,7 +141,7 @@ export default function TranBox({
const [mouseHover, setMouseHover] = useState(false);
// todo: 这里的 SettingProvider 不应和 background 的共用
return (
<SettingProvider>
<SettingProvider context="tranbox">
<ThemeProvider styles={extStyles}>
{showBox && (
<DraggableResizable

View File

@@ -133,7 +133,7 @@ export default function Slection({
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();
// e.stopPropagation();
await sleep(200);
const selection = window.getSelection();