feat: Add more shortcut keys to popup
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
OPT_LANGS_LIST,
|
||||
DEFAULT_API_SETTING,
|
||||
} from "../config";
|
||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||
import { genEventName, removeEndchar, matchInputStr } from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { loadingSvg } from "./svg";
|
||||
@@ -18,34 +18,20 @@ function isEditAbleNode(node) {
|
||||
return node.hasAttribute("contenteditable");
|
||||
}
|
||||
|
||||
function selectContent(node) {
|
||||
function replaceContentEditableText(node, newText) {
|
||||
node.focus();
|
||||
const selection = window.getSelection();
|
||||
if (!selection) return;
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
function pasteContentEvent(node, text) {
|
||||
node.focus();
|
||||
const data = new DataTransfer();
|
||||
data.setData("text/plain", text);
|
||||
range.deleteContents();
|
||||
const textNode = document.createTextNode(newText);
|
||||
range.insertNode(textNode);
|
||||
|
||||
const event = new ClipboardEvent("paste", { clipboardData: data });
|
||||
document.dispatchEvent(event);
|
||||
data.clearData();
|
||||
}
|
||||
|
||||
function pasteContentCommand(node, text) {
|
||||
node.focus();
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
|
||||
function collapseToEnd(node) {
|
||||
node.focus();
|
||||
const selection = window.getSelection();
|
||||
selection.collapseToEnd();
|
||||
}
|
||||
|
||||
@@ -57,144 +43,205 @@ function getNodeText(node) {
|
||||
}
|
||||
|
||||
function addLoading(node, loadingId) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const div = document.createElement("div");
|
||||
div.id = loadingId;
|
||||
div.innerHTML = loadingSvg;
|
||||
div.style.cssText = `
|
||||
width: ${node.offsetWidth}px;
|
||||
height: ${node.offsetHeight}px;
|
||||
line-height: ${node.offsetHeight}px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: ${node.offsetLeft}px;
|
||||
top: ${node.offsetTop}px;
|
||||
z-index: 2147483647;
|
||||
position: fixed;
|
||||
left: ${rect.left}px;
|
||||
top: ${rect.top}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
line-height: ${rect.height}px;
|
||||
text-align: center;
|
||||
z-index: 2147483647;
|
||||
pointer-events: none; /* 允许点击穿透 */
|
||||
`;
|
||||
node.offsetParent?.appendChild(div);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
function removeLoading(node, loadingId) {
|
||||
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
||||
if (div) {
|
||||
div.remove();
|
||||
}
|
||||
function removeLoading(loadingId) {
|
||||
const div = document.getElementById(loadingId);
|
||||
if (div) div.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框翻译
|
||||
*/
|
||||
export default function inputTranslate({
|
||||
inputRule: {
|
||||
transOpen,
|
||||
triggerShortcut,
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
triggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = DEFAULT_INPUT_RULE,
|
||||
transApis,
|
||||
}) {
|
||||
if (!transOpen) {
|
||||
return;
|
||||
export class InputTranslator {
|
||||
#config;
|
||||
#unregisterShortcut = null;
|
||||
#isEnabled = false;
|
||||
#triggerShortcut; // 用于缓存快捷键
|
||||
|
||||
constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) {
|
||||
this.#config = { inputRule, transApis };
|
||||
|
||||
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||
if (initialTriggerShortcut && initialTriggerShortcut.length > 0) {
|
||||
this.#triggerShortcut = initialTriggerShortcut;
|
||||
} else {
|
||||
this.#triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
}
|
||||
|
||||
if (this.#config.inputRule.transOpen) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
const apiSetting =
|
||||
transApis.find((api) => api.apiSlug === apiSlug) || DEFAULT_API_SETTING;
|
||||
if (triggerShortcut.length === 0) {
|
||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
triggerCount = 1;
|
||||
/**
|
||||
* 启用输入翻译功能
|
||||
*/
|
||||
enable() {
|
||||
if (this.#isEnabled || !this.#config.inputRule.transOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { triggerCount, triggerTime } = this.#config.inputRule;
|
||||
this.#unregisterShortcut = stepShortcutRegister(
|
||||
this.#triggerShortcut,
|
||||
this.#handleTranslate.bind(this),
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
|
||||
this.#isEnabled = true;
|
||||
kissLog("Input Translator enabled.");
|
||||
}
|
||||
|
||||
stepShortcutRegister(
|
||||
triggerShortcut,
|
||||
async () => {
|
||||
let node = document.activeElement;
|
||||
/**
|
||||
* 禁用输入翻译功能
|
||||
*/
|
||||
disable() {
|
||||
if (!this.#isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (this.#unregisterShortcut) {
|
||||
this.#unregisterShortcut();
|
||||
this.#unregisterShortcut = null;
|
||||
}
|
||||
this.#isEnabled = false;
|
||||
kissLog("Input Translator disabled.");
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 切换启用/禁用状态
|
||||
*/
|
||||
toggle() {
|
||||
if (this.#isEnabled) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
while (node.shadowRoot) {
|
||||
node = node.shadowRoot.activeElement;
|
||||
}
|
||||
/**
|
||||
* 翻译核心逻辑
|
||||
* @private
|
||||
*/
|
||||
async #handleTranslate() {
|
||||
let node = document.activeElement;
|
||||
if (!node) return;
|
||||
|
||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||
return;
|
||||
}
|
||||
while (node.shadowRoot && node.shadowRoot.activeElement) {
|
||||
node = node.shadowRoot.activeElement;
|
||||
}
|
||||
|
||||
let initText = getNodeText(node);
|
||||
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
||||
// todo: remove multiple char
|
||||
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
||||
}
|
||||
if (!initText.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = initText;
|
||||
if (transSign) {
|
||||
const res = matchInputStr(text, transSign);
|
||||
if (res) {
|
||||
let lang = res[1];
|
||||
if (lang === "zh" || lang === "cn") {
|
||||
lang = "zh-CN";
|
||||
} else if (lang === "tw" || lang === "hk") {
|
||||
lang = "zh-TW";
|
||||
}
|
||||
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||
toLang = lang;
|
||||
}
|
||||
text = res[2];
|
||||
const { apiSlug, transSign, triggerCount } = this.#config.inputRule;
|
||||
let { fromLang, toLang } = this.#config.inputRule;
|
||||
|
||||
let initText = getNodeText(node);
|
||||
|
||||
if (
|
||||
this.#triggerShortcut.length === 1 &&
|
||||
this.#triggerShortcut[0].length === 1
|
||||
) {
|
||||
initText = removeEndchar(
|
||||
initText,
|
||||
this.#triggerShortcut[0],
|
||||
triggerCount
|
||||
);
|
||||
}
|
||||
|
||||
if (!initText.trim()) return;
|
||||
|
||||
let text = initText;
|
||||
if (transSign) {
|
||||
const res = matchInputStr(text, transSign);
|
||||
if (res) {
|
||||
let lang = res[1];
|
||||
if (lang === "zh" || lang === "cn") lang = "zh-CN";
|
||||
else if (lang === "tw" || lang === "hk") lang = "zh-TW";
|
||||
|
||||
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||
toLang = lang;
|
||||
}
|
||||
text = res[2];
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("input -->", text);
|
||||
const apiSetting =
|
||||
this.#config.transApis.find((api) => api.apiSlug === apiSlug) ||
|
||||
DEFAULT_API_SETTING;
|
||||
const loadingId = "kiss-loading-" + genEventName();
|
||||
|
||||
const loadingId = "kiss-" + genEventName();
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
apiSlug,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
if (!trText || isSame) {
|
||||
return;
|
||||
}
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSlug,
|
||||
apiSetting,
|
||||
});
|
||||
|
||||
if (isInputNode(node)) {
|
||||
node.value = trText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!trText || isSame) return;
|
||||
|
||||
selectContent(node);
|
||||
await sleep(200);
|
||||
|
||||
pasteContentEvent(node, trText);
|
||||
await sleep(200);
|
||||
|
||||
// todo: use includes?
|
||||
if (getNodeText(node).startsWith(initText)) {
|
||||
pasteContentCommand(node, trText);
|
||||
await sleep(100);
|
||||
} else {
|
||||
collapseToEnd(node);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("translate input", err);
|
||||
} finally {
|
||||
removeLoading(node, loadingId);
|
||||
if (isInputNode(node)) {
|
||||
node.value = trText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
} else {
|
||||
replaceContentEditableText(node, trText);
|
||||
}
|
||||
},
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
} catch (err) {
|
||||
kissLog("Translate input error:", err);
|
||||
} finally {
|
||||
removeLoading(loadingId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig({ inputRule, transApis }) {
|
||||
const wasEnabled = this.#isEnabled;
|
||||
if (wasEnabled) {
|
||||
this.disable();
|
||||
}
|
||||
|
||||
if (inputRule) {
|
||||
this.#config.inputRule = inputRule;
|
||||
}
|
||||
if (transApis) {
|
||||
this.#config.transApis = transApis;
|
||||
}
|
||||
|
||||
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||
this.#triggerShortcut =
|
||||
initialTriggerShortcut && initialTriggerShortcut.length > 0
|
||||
? initialTriggerShortcut
|
||||
: DEFAULT_INPUT_SHORTCUT;
|
||||
|
||||
if (wasEnabled) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
"hasShadowroot",
|
||||
"transTag",
|
||||
"transTitle",
|
||||
"transSelected",
|
||||
// "detectRemote",
|
||||
// "fixerFunc",
|
||||
].forEach((key) => {
|
||||
@@ -153,7 +152,6 @@ export const checkRules = (rules) => {
|
||||
// transTiming,
|
||||
transTag,
|
||||
transTitle,
|
||||
transSelected,
|
||||
// detectRemote,
|
||||
// skipLangs,
|
||||
// fixerSelector,
|
||||
@@ -186,7 +184,6 @@ export const checkRules = (rules) => {
|
||||
// transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
|
||||
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
|
||||
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
|
||||
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
|
||||
// detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
|
||||
// skipLangs: type(skipLangs) === "array" ? skipLangs : [],
|
||||
// fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
|
||||
|
||||
95
src/libs/tranbox.js
Normal file
95
src/libs/tranbox.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import Slection from "../views/Selection";
|
||||
import { DEFAULT_TRANBOX_SETTING, APP_CONSTS } from "../config";
|
||||
|
||||
export class TransboxManager {
|
||||
#container = null;
|
||||
#reactRoot = null;
|
||||
#shadowContainer = null;
|
||||
#props = {};
|
||||
|
||||
constructor(initialProps = {}) {
|
||||
this.#props = initialProps;
|
||||
|
||||
const { tranboxSetting = DEFAULT_TRANBOX_SETTING } = this.#props;
|
||||
if (tranboxSetting?.transOpen) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return (
|
||||
!!this.#container && document.body.parentElement.contains(this.#container)
|
||||
);
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.isEnabled()) {
|
||||
this.#container = document.createElement("div");
|
||||
this.#container.setAttribute("id", APP_CONSTS.boxID);
|
||||
this.#container.style.cssText =
|
||||
"font-size: 0; width: 0; height: 0; border: 0; padding: 0; margin: 0;";
|
||||
document.body.parentElement.appendChild(this.#container);
|
||||
|
||||
this.#shadowContainer = this.#container.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowRootElement.classList.add(`${APP_CONSTS.boxID}_warpper`);
|
||||
this.#shadowContainer.appendChild(emotionRoot);
|
||||
this.#shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: APP_CONSTS.boxID,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
this.#reactRoot = ReactDOM.createRoot(shadowRootElement);
|
||||
this.CacheProvider = ({ children }) => (
|
||||
<CacheProvider value={cache}>{children}</CacheProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const AppProvider = this.CacheProvider;
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<AppProvider>
|
||||
<Slection {...this.#props} />
|
||||
</AppProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (!this.isEnabled() || !this.#reactRoot) {
|
||||
return;
|
||||
}
|
||||
this.#reactRoot.unmount();
|
||||
this.#container.remove();
|
||||
this.#container = null;
|
||||
this.#reactRoot = null;
|
||||
this.#shadowContainer = null;
|
||||
this.CacheProvider = null;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isEnabled()) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
update(newProps) {
|
||||
this.#props = { ...this.#props, ...newProps };
|
||||
if (this.isEnabled()) {
|
||||
if (!this.#props.tranboxSetting?.transOpen) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,14 @@ import {
|
||||
// DEFAULT_MOUSEHOVER_KEY,
|
||||
OPT_STYLE_NONE,
|
||||
DEFAULT_API_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
} from "../config";
|
||||
import interpreter from "./interpreter";
|
||||
import { ShadowRootMonitor } from "./shadowroot";
|
||||
@@ -25,6 +33,10 @@ import { genTextClass } from "./style";
|
||||
import { loadingSvg } from "./svg";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { tryDetectLang } from "./detect";
|
||||
import { browser } from "./browser";
|
||||
import { isIframe, sendIframeMsg } from "./iframe";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
|
||||
/**
|
||||
* @class Translator
|
||||
@@ -269,6 +281,10 @@ export class Translator {
|
||||
#textClass = {}; // 译文样式class
|
||||
#textSheet = ""; // 译文样式字典
|
||||
|
||||
#isUserscript = false;
|
||||
#transboxManager = null; // 划词翻译
|
||||
#inputTranslator = null; // 输入框翻译
|
||||
|
||||
#observedNodes = new WeakSet(); // 存储所有被识别出的、可翻译的 DOM 节点单元
|
||||
#translationNodes = new WeakMap(); // 存储所有插入到页面的译文节点
|
||||
#viewNodes = new Set(); // 当前在可视范围内的单元
|
||||
@@ -293,9 +309,10 @@ export class Translator {
|
||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
constructor(rule = {}, setting = {}) {
|
||||
constructor(rule = {}, setting = {}, isUserscript) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#isUserscript = isUserscript;
|
||||
this.#eventName = genEventName();
|
||||
this.#docInfo = {
|
||||
title: document.title,
|
||||
@@ -326,6 +343,19 @@ export class Translator {
|
||||
this.#enableMouseHover();
|
||||
}
|
||||
|
||||
if (!isIframe) {
|
||||
// 监听后端事件
|
||||
if (!isUserscript) {
|
||||
this.#runtimeListener();
|
||||
}
|
||||
|
||||
// 划词翻译
|
||||
this.#transboxManager = new TransboxManager(this.setting);
|
||||
|
||||
// 输入框翻译
|
||||
this.#inputTranslator = new InputTranslator(this.setting);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => this.#run());
|
||||
} else {
|
||||
@@ -368,6 +398,43 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听后端事件
|
||||
#runtimeListener() {
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this.toggle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this.toggleStyle();
|
||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this.updateRule(args);
|
||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this.toggleTransbox();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this.toggleInputTranslate();
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { rule: this.rule, setting: this.setting };
|
||||
});
|
||||
}
|
||||
|
||||
#createPlaceholderRegex() {
|
||||
const escapedStart = Translator.escapeRegex(
|
||||
Translator.PLACEHOLDER.startDelimiter
|
||||
@@ -671,14 +738,16 @@ export class Translator {
|
||||
|
||||
// 开始/重新监控节点
|
||||
#startObserveNode(node) {
|
||||
if (this.#observedNodes.has(node)) {
|
||||
// 已监控,但未处理状态,且在可视范围
|
||||
if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) {
|
||||
this.#reIO(node);
|
||||
}
|
||||
} else {
|
||||
// 未监控
|
||||
if (!this.#observedNodes.has(node)) {
|
||||
this.#observedNodes.add(node);
|
||||
this.#io.observe(node);
|
||||
return;
|
||||
}
|
||||
|
||||
// 已监控,但未处理状态,且在可视范围
|
||||
if (!this.#processedNodes.has(node) && this.#viewNodes.has(node)) {
|
||||
this.#reIO(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1369,6 +1438,19 @@ export class Translator {
|
||||
this.updateRule({ textStyle });
|
||||
}
|
||||
|
||||
// 切换划词翻译
|
||||
toggleTransbox() {
|
||||
this.#setting.tranboxSetting.transOpen =
|
||||
!this.#setting.tranboxSetting.transOpen;
|
||||
this.#transboxManager?.toggle();
|
||||
}
|
||||
|
||||
// 切换输入框翻译
|
||||
toggleInputTranslate() {
|
||||
this.#setting.inputRule.transOpen = !this.#setting.inputRule.transOpen;
|
||||
this.#inputTranslator?.toggle();
|
||||
}
|
||||
|
||||
// 停止运行
|
||||
stop() {
|
||||
this.disable();
|
||||
|
||||
Reference in New Issue
Block a user