Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189b7f480a | ||
|
|
5e3aa7e2d1 | ||
|
|
730be678ef | ||
|
|
9293f422f3 | ||
|
|
6e8158bb34 | ||
|
|
3078d3ca91 | ||
|
|
947e1c7f08 | ||
|
|
938c123412 | ||
|
|
e7a57ad3b2 | ||
|
|
1e40f81bf7 | ||
|
|
72b2f44e32 | ||
|
|
76f54461e7 | ||
|
|
14ca13e31d | ||
|
|
556fd71275 | ||
|
|
a8002bba9f | ||
|
|
ddd9371fbd | ||
|
|
0ea97b73e3 | ||
|
|
f8c8a4ebeb | ||
|
|
5f613ab558 | ||
|
|
56281f9e82 | ||
|
|
5e8743dbb7 | ||
|
|
f4e4c84712 |
2
.env
2
.env
@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.7.0
|
||||
REACT_APP_VERSION=1.7.1
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft/DeepL/OpenAI
|
||||
- [x] Custom translation interface
|
||||
- [x] Support input box translation
|
||||
- [x] Support YouTube subtitle translation
|
||||
- [x] Data synchronization function
|
||||
- [x] Custom rules + rule subscription
|
||||
- [x] Custom style
|
||||
@@ -32,6 +34,7 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Popup
|
||||
- `Alt+O` Open Options
|
||||
- `Alt+I` Input Box Translation
|
||||
|
||||
## Download
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft/DeepL/OpenAI
|
||||
- [x] 自定义翻译接口
|
||||
- [x] 支持输入框翻译
|
||||
- [x] 支持 YouTube 字幕翻译
|
||||
- [x] 数据同步功能
|
||||
- [x] 自定义规则 + 规则订阅
|
||||
- [x] 自定义样式
|
||||
@@ -32,6 +34,7 @@
|
||||
- `Alt+C` 切换样式
|
||||
- `Alt+K` 打开弹窗
|
||||
- `Alt+O` 打开设置
|
||||
- `Alt+I` 输入框翻译
|
||||
|
||||
## 下载
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
|
||||
@@ -547,4 +547,44 @@ export const I18N = {
|
||||
zh: `全局规则`,
|
||||
en: `Global Rule`,
|
||||
},
|
||||
input_setting: {
|
||||
zh: `输入框设置`,
|
||||
en: `Input Box Setting`,
|
||||
},
|
||||
input_box_translation: {
|
||||
zh: `启用输入框翻译`,
|
||||
en: `Input Box Translation`,
|
||||
},
|
||||
input_selector: {
|
||||
zh: `输入框选择器`,
|
||||
en: `Input Selector`,
|
||||
},
|
||||
input_selector_helper: {
|
||||
zh: `用于输入框翻译。`,
|
||||
en: `Used for input box translation.`,
|
||||
},
|
||||
trigger_trans_shortcut: {
|
||||
zh: `触发翻译快捷键`,
|
||||
en: `Trigger Translation Shortcut Keys`,
|
||||
},
|
||||
trigger_trans_shortcut_help: {
|
||||
zh: `默认为单击“Alt+i”`,
|
||||
en: `Default is "Alt+i"`,
|
||||
},
|
||||
shortcut_press_count: {
|
||||
zh: `快捷键连击次数`,
|
||||
en: `Shortcut Press Number`,
|
||||
},
|
||||
combo_timeout: {
|
||||
zh: `连击超时时间 (10-1000ms)`,
|
||||
en: `Combo Timeout (10-1000ms)`,
|
||||
},
|
||||
input_trans_start_sign: {
|
||||
zh: `翻译起始标识`,
|
||||
en: `Translation Start Sign`,
|
||||
},
|
||||
input_trans_start_sign_help: {
|
||||
zh: `标识后面可以加目标语言代码,如: “/en 你好”、“/zh hello”`,
|
||||
en: `The target language code can be added after the sign, such as: "/en 你好", "/zh hello"`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ export const OPT_LANGS_SPECIAL = {
|
||||
),
|
||||
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
|
||||
};
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
@@ -198,6 +199,20 @@ export const GLOBLA_RULE = {
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||
export const DEFAULT_INPUT_SHORTCUT = ["Alt", "i"];
|
||||
export const DEFAULT_INPUT_RULE = {
|
||||
transOpen: true,
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "en",
|
||||
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
||||
triggerCount: 1,
|
||||
triggerTime: 200,
|
||||
transSign: OPT_INPUT_TRANS_SIGNS[0],
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
@@ -273,6 +288,7 @@ export const DEFAULT_SETTING = {
|
||||
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
hideFab: false, // 是否隐藏按钮
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
18
src/hooks/InputRule.js
Normal file
18
src/hooks/InputRule.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_INPUT_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useInputRule() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
||||
|
||||
const updateInputRule = useCallback(
|
||||
async (obj) => {
|
||||
Object.assign(inputRule, obj);
|
||||
await updateSetting({ inputRule });
|
||||
},
|
||||
[inputRule, updateSetting]
|
||||
);
|
||||
|
||||
return { inputRule, updateInputRule };
|
||||
}
|
||||
@@ -65,3 +65,48 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||
}
|
||||
}, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册连续快捷键
|
||||
* @param {*} targetKeys
|
||||
* @param {*} fn
|
||||
* @param {*} step
|
||||
* @param {*} timeout
|
||||
* @param {*} target
|
||||
* @returns
|
||||
*/
|
||||
export const stepShortcutRegister = (
|
||||
targetKeys = [],
|
||||
fn,
|
||||
step = 3,
|
||||
timeout = 500,
|
||||
target = document
|
||||
) => {
|
||||
let count = 0;
|
||||
let pre = Date.now();
|
||||
let timer;
|
||||
return shortcutListener((curkeys, allkeys) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
count = 0;
|
||||
}, timeout);
|
||||
|
||||
if (targetKeys.length > 0 && curkeys.length === 0) {
|
||||
const now = Date.now();
|
||||
if (
|
||||
(count === 0 || now - pre < timeout) &&
|
||||
isSameSet(new Set(targetKeys), new Set(allkeys))
|
||||
) {
|
||||
count++;
|
||||
if (count === step) {
|
||||
count = 0;
|
||||
fn();
|
||||
}
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
pre = now;
|
||||
}
|
||||
}, target);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
updateSync,
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
updateSetting,
|
||||
} from "./storage";
|
||||
import { apiFetch } from "../apis";
|
||||
import { checkRules } from "./rules";
|
||||
import { isAllchar } from "./utils";
|
||||
import { syncWebfix } from "./webfix";
|
||||
|
||||
/**
|
||||
* 更新缓存同步时间
|
||||
@@ -63,13 +63,13 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
// 同步订阅规则
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
|
||||
// 同步修复规则
|
||||
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
|
||||
}
|
||||
subrulesList.forEach((item) => {
|
||||
item.syncAt = now;
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
}
|
||||
|
||||
34
src/libs/svg.js
Normal file
34
src/libs/svg.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export const loadingSvg = `
|
||||
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
|
||||
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 15 ; 0 -15; 0 15"
|
||||
repeatCount="indefinite"
|
||||
begin="0.1"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="30" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 10 ; 0 -10; 0 10"
|
||||
repeatCount="indefinite"
|
||||
begin="0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill="#209CEE" stroke="none" cx="54" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 5 ; 0 -5; 0 5"
|
||||
repeatCount="indefinite"
|
||||
begin="0.3"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
`;
|
||||
@@ -9,16 +9,101 @@ import {
|
||||
SHADOW_KEY,
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_MOUSEKEY_MOUSEOVER,
|
||||
DEFAULT_INPUT_RULE,
|
||||
DEFAULT_TRANS_APIS,
|
||||
DEFAULT_INPUT_SHORTCUT,
|
||||
OPT_LANGS_LIST,
|
||||
} from "../config";
|
||||
import Content from "../views/Content";
|
||||
import { updateFetchPool, clearFetchPool } from "./fetch";
|
||||
import { debounce, genEventName } from "./utils";
|
||||
import {
|
||||
debounce,
|
||||
genEventName,
|
||||
removeEndchar,
|
||||
matchInputStr,
|
||||
sleep,
|
||||
} from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { tryDetectLang } from ".";
|
||||
import { loadingSvg } from "./svg";
|
||||
|
||||
function isInputNode(node) {
|
||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||
}
|
||||
|
||||
function isEditAbleNode(node) {
|
||||
return node.hasAttribute("contenteditable");
|
||||
}
|
||||
|
||||
function selectContent(node) {
|
||||
node.focus();
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function getNodeText(node) {
|
||||
if (isInputNode(node)) {
|
||||
return node.value;
|
||||
}
|
||||
return node.innerText || node.textContent || "";
|
||||
}
|
||||
|
||||
function addLoading(node, loadingId) {
|
||||
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;
|
||||
`;
|
||||
node.offsetParent?.appendChild(div);
|
||||
}
|
||||
|
||||
function removeLoading(loadingId) {
|
||||
const div = document.getElementById(loadingId);
|
||||
if (div) {
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译类
|
||||
*/
|
||||
export class Translator {
|
||||
_rule = {};
|
||||
_inputRule = {};
|
||||
_setting = {};
|
||||
_rootNodes = new Set();
|
||||
_tranNodes = new Map();
|
||||
@@ -101,6 +186,11 @@ export class Translator {
|
||||
if (rule.transOpen === "true") {
|
||||
this._register();
|
||||
}
|
||||
|
||||
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
|
||||
if (this._inputRule.transOpen) {
|
||||
this._registerInput();
|
||||
}
|
||||
}
|
||||
|
||||
get setting() {
|
||||
@@ -243,6 +333,116 @@ export class Translator {
|
||||
});
|
||||
};
|
||||
|
||||
_registerInput = () => {
|
||||
const {
|
||||
triggerShortcut: initTriggerShortcut,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang: initToLang,
|
||||
triggerCount: initTriggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = this._inputRule;
|
||||
const apiSetting = (this._setting.transApis || DEFAULT_TRANS_APIS)[
|
||||
translator
|
||||
];
|
||||
|
||||
let triggerShortcut = initTriggerShortcut;
|
||||
let triggerCount = initTriggerCount;
|
||||
if (triggerShortcut.length === 0) {
|
||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
triggerCount = 1;
|
||||
}
|
||||
|
||||
stepShortcutRegister(
|
||||
triggerShortcut,
|
||||
async () => {
|
||||
const node = document.activeElement;
|
||||
if (!node || !(isInputNode(node) || isEditAbleNode(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let text = initText;
|
||||
let toLang = initToLang;
|
||||
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 loadingId = "kiss-" + genEventName();
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
|
||||
const deLang = await tryDetectLang(text);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
if (!trText || isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInputNode(node)) {
|
||||
node.value = trText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
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) {
|
||||
console.log("[translate input]", err.message);
|
||||
} finally {
|
||||
removeLoading(loadingId);
|
||||
}
|
||||
},
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
};
|
||||
|
||||
_handleMouseover = (e) => {
|
||||
const key = this._setting.mouseKey.slice(3);
|
||||
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
|
||||
|
||||
@@ -179,3 +179,47 @@ export const isSameSet = (a, b) => {
|
||||
const s = new Set([...a, ...b]);
|
||||
return s.size === a.size && s.size === b.size;
|
||||
};
|
||||
|
||||
/**
|
||||
* 去掉字符串末尾某个字符
|
||||
* @param {*} s
|
||||
* @param {*} c
|
||||
* @param {*} count
|
||||
* @returns
|
||||
*/
|
||||
export const removeEndchar = (s, c, count = 1) => {
|
||||
let i = s.length;
|
||||
while (i > s.length - count && s[i - 1] === c) {
|
||||
i--;
|
||||
}
|
||||
return s.slice(0, i);
|
||||
};
|
||||
|
||||
/**
|
||||
* 匹配字符串及语言标识
|
||||
* @param {*} str
|
||||
* @param {*} sign
|
||||
* @returns
|
||||
*/
|
||||
export const matchInputStr = (str, sign) => {
|
||||
let reg = /\/([\w-]+)\s+([^]+)/;
|
||||
switch (sign) {
|
||||
case "//":
|
||||
reg = /\/\/([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case "\\":
|
||||
reg = /\\([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case "\\\\":
|
||||
reg = /\\\\([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case ">":
|
||||
reg = />([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
case ">>":
|
||||
reg = />>([\w-]+)\s+([^]+)/;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return str.match(reg);
|
||||
};
|
||||
|
||||
@@ -1,65 +1,51 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
import { isMobile } from "../../libs/mobile";
|
||||
import { setFab } from "../../libs/storage";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
const getEdgePosition = (
|
||||
{ x: left, y: top, edge },
|
||||
const getEdgePosition = ({
|
||||
x: left,
|
||||
y: top,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
width,
|
||||
height
|
||||
) => {
|
||||
hover,
|
||||
}) => {
|
||||
const right = windowWidth - left - width;
|
||||
const bottom = windowHeight - top - height;
|
||||
const min = Math.min(left, top, right, bottom);
|
||||
switch (min) {
|
||||
case right:
|
||||
edge = "right";
|
||||
left = windowWidth - width;
|
||||
left = hover ? windowWidth - width : windowWidth - width / 2;
|
||||
break;
|
||||
case left:
|
||||
edge = "left";
|
||||
left = 0;
|
||||
left = hover ? 0 : -width / 2;
|
||||
break;
|
||||
case bottom:
|
||||
edge = "bottom";
|
||||
top = windowHeight - height;
|
||||
top = hover ? windowHeight - height : windowHeight - height / 2;
|
||||
break;
|
||||
default:
|
||||
edge = "top";
|
||||
top = 0;
|
||||
top = hover ? 0 : -height / 2;
|
||||
}
|
||||
left = limitNumber(left, 0, windowWidth - width);
|
||||
top = limitNumber(top, 0, windowHeight - height);
|
||||
return { x: left, y: top, edge, hide: false };
|
||||
return { x: left, y: top };
|
||||
};
|
||||
|
||||
const getHidePosition = (
|
||||
{ x: left, y: top, edge },
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
width,
|
||||
height
|
||||
) => {
|
||||
switch (edge) {
|
||||
case "right":
|
||||
left = windowWidth - width / 2;
|
||||
break;
|
||||
case "left":
|
||||
left = -width / 2;
|
||||
break;
|
||||
case "bottom":
|
||||
top = windowHeight - height / 2;
|
||||
break;
|
||||
default:
|
||||
top = -height / 2;
|
||||
function DraggableWrapper({ children, usePaper, ...props }) {
|
||||
if (usePaper) {
|
||||
return (
|
||||
<Paper {...props} elevation={4}>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return { x: left, y: top, edge, hide: true };
|
||||
};
|
||||
return <div {...props}>{children}</div>;
|
||||
}
|
||||
|
||||
export default function Draggable({
|
||||
windowSize,
|
||||
windowSize: { w: windowWidth, h: windowHeight },
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
@@ -70,66 +56,38 @@ export default function Draggable({
|
||||
onMove,
|
||||
handler,
|
||||
children,
|
||||
usePaper,
|
||||
}) {
|
||||
const [origin, setOrigin] = useState({
|
||||
x: left,
|
||||
y: top,
|
||||
px: left,
|
||||
py: top,
|
||||
});
|
||||
const [position, setPosition] = useState({
|
||||
x: left,
|
||||
y: top,
|
||||
edge: null,
|
||||
hide: false,
|
||||
});
|
||||
const [edgeTimer, setEdgeTimer] = useState(null);
|
||||
|
||||
const goEdge = useCallback((w, h, width, height) => {
|
||||
setPosition((pre) => getEdgePosition(pre, w, h, width, height));
|
||||
|
||||
setEdgeTimer(
|
||||
setTimeout(() => {
|
||||
setPosition((pre) => getHidePosition(pre, w, h, width, height));
|
||||
}, 1500)
|
||||
);
|
||||
}, []);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [origin, setOrigin] = useState(null);
|
||||
const [position, setPosition] = useState({ x: left, y: top });
|
||||
const setFabPosition = useMemo(() => debounce(setFab, 500), []);
|
||||
|
||||
const handlePointerDown = (e) => {
|
||||
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||
onStart && onStart();
|
||||
edgeTimer && clearTimeout(edgeTimer);
|
||||
const { x, y } = position;
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
setOrigin({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
px: clientX,
|
||||
py: clientY,
|
||||
});
|
||||
setOrigin({ x, y, clientX, clientY });
|
||||
};
|
||||
|
||||
const handlePointerMove = (e) => {
|
||||
onMove && onMove();
|
||||
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||
if (origin) {
|
||||
const dx = clientX - origin.px;
|
||||
const dy = clientY - origin.py;
|
||||
const dx = clientX - origin.clientX;
|
||||
const dy = clientY - origin.clientY;
|
||||
let x = origin.x + dx;
|
||||
let y = origin.y + dy;
|
||||
const { w, h } = windowSize;
|
||||
x = limitNumber(x, 0, w - width);
|
||||
y = limitNumber(y, 0, h - height);
|
||||
setPosition({ x, y, edge: null, hide: false });
|
||||
x = limitNumber(x, -width / 2, windowWidth - width / 2);
|
||||
y = limitNumber(y, 0, windowHeight - height / 2);
|
||||
setPosition({ x, y });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerUp = (e) => {
|
||||
e.stopPropagation();
|
||||
setOrigin(null);
|
||||
if (!snapEdge) {
|
||||
return;
|
||||
}
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -138,35 +96,48 @@ export default function Draggable({
|
||||
|
||||
const handleMouseEnter = (e) => {
|
||||
e.stopPropagation();
|
||||
if (snapEdge && position.hide) {
|
||||
edgeTimer && clearTimeout(edgeTimer);
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
}
|
||||
setHover(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e) => {
|
||||
e.stopPropagation();
|
||||
setHover(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOrigin(null);
|
||||
if (!snapEdge) {
|
||||
if (!snapEdge || !!origin) {
|
||||
return;
|
||||
}
|
||||
goEdge(windowSize.w, windowSize.h, width, height);
|
||||
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (position.hide) {
|
||||
setFab({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
setPosition((pre) => {
|
||||
const edgePosition = getEdgePosition({
|
||||
...pre,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
hover,
|
||||
});
|
||||
}
|
||||
}, [position.x, position.y, position.hide]);
|
||||
setFabPosition(edgePosition);
|
||||
return edgePosition;
|
||||
});
|
||||
}, [
|
||||
origin,
|
||||
hover,
|
||||
width,
|
||||
height,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
snapEdge,
|
||||
setFabPosition,
|
||||
]);
|
||||
|
||||
const opacity = useMemo(() => {
|
||||
if (snapEdge) {
|
||||
return position.hide ? 0.2 : 1;
|
||||
return hover || origin ? 1 : 0.2;
|
||||
}
|
||||
return origin ? 0.8 : 1;
|
||||
}, [origin, snapEdge, position.hide]);
|
||||
}, [origin, snapEdge, hover]);
|
||||
|
||||
const touchProps = isMobile
|
||||
? {
|
||||
@@ -181,7 +152,8 @@ export default function Draggable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<DraggableWrapper
|
||||
usePaper={usePaper}
|
||||
style={{
|
||||
opacity,
|
||||
position: "fixed",
|
||||
@@ -191,6 +163,7 @@ export default function Draggable({
|
||||
display: show ? "block" : "none",
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
@@ -202,6 +175,6 @@ export default function Draggable({
|
||||
{handler}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</DraggableWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Fab from "@mui/material/Fab";
|
||||
import TranslateIcon from "@mui/icons-material/Translate";
|
||||
import ThemeProvider from "../../hooks/Theme";
|
||||
@@ -9,6 +8,8 @@ import Popup from "../Popup";
|
||||
import { debounce } from "../../libs/utils";
|
||||
import { isGm } from "../../libs/client";
|
||||
import Header from "../Popup/Header";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import {
|
||||
DEFAULT_SHORTCUTS,
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
@@ -78,58 +79,54 @@ export default function Action({ translator, fab }) {
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
// 注册菜单
|
||||
const menuCommandIds = [];
|
||||
if (isGm) {
|
||||
try {
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate (Alt+q)",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Style (Alt+c)",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Menu (Alt+k)",
|
||||
(event) => {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Setting (Alt+o)",
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
if (!isGm) {
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isGm) {
|
||||
try {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
}
|
||||
};
|
||||
// 注册菜单
|
||||
try {
|
||||
const menuCommandIds = [];
|
||||
menuCommandIds.push(
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Translate (Alt+q)",
|
||||
(event) => {
|
||||
translator.toggle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Toggle Style (Alt+c)",
|
||||
(event) => {
|
||||
translator.toggleStyle();
|
||||
setShowPopup(false);
|
||||
},
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Menu (Alt+k)",
|
||||
(event) => {
|
||||
setShowPopup((pre) => !pre);
|
||||
},
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
"Open Setting (Alt+o)",
|
||||
(event) => {
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||
},
|
||||
"O"
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
menuCommandIds.forEach((id) => {
|
||||
GM.unregisterMenuCommand(id);
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("[registerMenuCommand]", err);
|
||||
}
|
||||
}, [translator]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -165,7 +162,7 @@ export default function Action({ translator, fab }) {
|
||||
windowSize,
|
||||
width: fabWidth,
|
||||
height: fabWidth,
|
||||
left: fab.x ?? 0,
|
||||
left: fab.x ?? -fabWidth,
|
||||
top: fab.y ?? windowSize.h / 2,
|
||||
};
|
||||
|
||||
@@ -178,17 +175,17 @@ export default function Action({ translator, fab }) {
|
||||
show={showPopup}
|
||||
onStart={handleStart}
|
||||
onMove={handleMove}
|
||||
usePaper
|
||||
handler={
|
||||
<Paper style={{ cursor: "move" }} elevation={3}>
|
||||
<Box style={{ cursor: "move" }}>
|
||||
<Header setShowPopup={setShowPopup} />
|
||||
</Paper>
|
||||
<Divider />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Paper>
|
||||
{showPopup && (
|
||||
<Popup setShowPopup={setShowPopup} translator={translator} />
|
||||
)}
|
||||
</Draggable>
|
||||
<Draggable
|
||||
key="fab"
|
||||
|
||||
@@ -1,44 +1,14 @@
|
||||
import { DEFAULT_COLOR } from "../../config";
|
||||
import { loadingSvg } from "../../libs/svg";
|
||||
|
||||
export default function LoadingIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "1.2em",
|
||||
maxHeight: "1.2em",
|
||||
display: "inline-block",
|
||||
width: "1.2em",
|
||||
height: "1em",
|
||||
}}
|
||||
>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="6" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 15 ; 0 -15; 0 15"
|
||||
repeatCount="indefinite"
|
||||
begin="0.1"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="30" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 10 ; 0 -10; 0 10"
|
||||
repeatCount="indefinite"
|
||||
begin="0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle fill={DEFAULT_COLOR} stroke="none" cx="54" cy="50" r="6">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
type="translate"
|
||||
values="0 5 ; 0 -5; 0 5"
|
||||
repeatCount="indefinite"
|
||||
begin="0.3"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
dangerouslySetInnerHTML={{ __html: loadingSvg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
178
src/views/Options/InputSetting.js
Normal file
178
src/views/Options/InputSetting.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import {
|
||||
OPT_TRANS_ALL,
|
||||
OPT_LANGS_FROM,
|
||||
OPT_LANGS_TO,
|
||||
OPT_INPUT_TRANS_SIGNS,
|
||||
} from "../../config";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import { useInputRule } from "../../hooks/InputRule";
|
||||
import { useCallback } from "react";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { limitNumber } from "../../libs/utils";
|
||||
|
||||
export default function InputSetting() {
|
||||
const i18n = useI18n();
|
||||
const { inputRule, updateInputRule } = useInputRule();
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
let { name, value } = e.target;
|
||||
switch (name) {
|
||||
case "triggerTime":
|
||||
value = limitNumber(value, 10, 1000);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
updateInputRule({
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleShortcutInput = useCallback(
|
||||
(val) => {
|
||||
updateInputRule({ triggerShortcut: val });
|
||||
},
|
||||
[updateInputRule]
|
||||
);
|
||||
|
||||
const {
|
||||
transOpen,
|
||||
translator,
|
||||
fromLang,
|
||||
toLang,
|
||||
triggerShortcut,
|
||||
triggerCount,
|
||||
triggerTime,
|
||||
transSign,
|
||||
} = inputRule;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={3}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
name="transOpen"
|
||||
checked={transOpen}
|
||||
onChange={() => {
|
||||
updateInputRule({ transOpen: !transOpen });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={i18n("input_box_translation")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="translator"
|
||||
value={translator}
|
||||
label={i18n("translate_service")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_TRANS_ALL.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="fromLang"
|
||||
value={fromLang}
|
||||
label={i18n("from_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="toLang"
|
||||
value={toLang}
|
||||
label={i18n("to_lang")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||
<MenuItem key={lang} value={lang}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
name="transSign"
|
||||
value={transSign}
|
||||
label={i18n("input_trans_start_sign")}
|
||||
onChange={handleChange}
|
||||
helperText={i18n("input_trans_start_sign_help")}
|
||||
>
|
||||
<MenuItem value={""}>{i18n("style_none")}</MenuItem>
|
||||
{OPT_INPUT_TRANS_SIGNS.map((item) => (
|
||||
<MenuItem key={item} value={item}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<ShortcutInput
|
||||
value={triggerShortcut}
|
||||
onChange={handleShortcutInput}
|
||||
label={i18n("trigger_trans_shortcut")}
|
||||
helperText={i18n("trigger_trans_shortcut_help")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
name="triggerCount"
|
||||
value={triggerCount}
|
||||
label={i18n("shortcut_press_count")}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((val) => (
|
||||
<MenuItem key={val} value={val}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4} lg={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={i18n("combo_timeout")}
|
||||
type="number"
|
||||
name="triggerTime"
|
||||
defaultValue={triggerTime}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { useI18n } from "../../hooks/I18n";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import ApiIcon from "@mui/icons-material/Api";
|
||||
import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
|
||||
import InputIcon from "@mui/icons-material/Input";
|
||||
|
||||
function LinkItem({ label, url, icon }) {
|
||||
const match = useMatch(url);
|
||||
@@ -38,6 +39,12 @@ export default function Navigator(props) {
|
||||
url: "/rules",
|
||||
icon: <DesignServicesIcon />,
|
||||
},
|
||||
{
|
||||
id: "input_setting",
|
||||
label: i18n("input_setting"),
|
||||
url: "/input",
|
||||
icon: <InputIcon />,
|
||||
},
|
||||
{
|
||||
id: "apis_setting",
|
||||
label: i18n("apis_setting"),
|
||||
|
||||
@@ -12,8 +12,6 @@ import { limitNumber } from "../../libs/utils";
|
||||
import { useI18n } from "../../hooks/I18n";
|
||||
import { useAlert } from "../../hooks/Alert";
|
||||
import { isExt } from "../../libs/client";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {
|
||||
UI_LANGS,
|
||||
@@ -26,57 +24,13 @@ import {
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
} from "../../config";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useShortcut } from "../../hooks/Shortcut";
|
||||
import { shortcutListener } from "../../libs/shortcut";
|
||||
import ShortcutInput from "./ShortcutInput";
|
||||
|
||||
function ShortcutItem({ action, label }) {
|
||||
const { shortcut, setShortcut } = useShortcut(action);
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current.focus();
|
||||
setShortcut([]);
|
||||
|
||||
const clearShortcut = shortcutListener((curkeys, allkeys) => {
|
||||
setShortcut(allkeys);
|
||||
if (curkeys.length === 0) {
|
||||
setDisabled(true);
|
||||
}
|
||||
}, inputRef.current);
|
||||
|
||||
return () => {
|
||||
clearShortcut();
|
||||
};
|
||||
}, [disabled, setShortcut]);
|
||||
|
||||
return (
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
size="small"
|
||||
label={label}
|
||||
name={label}
|
||||
value={shortcut.join(" + ")}
|
||||
fullWidth
|
||||
inputRef={inputRef}
|
||||
disabled={disabled}
|
||||
onBlur={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
{<EditIcon />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,7 +110,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_limit")}
|
||||
type="number"
|
||||
name="fetchLimit"
|
||||
value={fetchLimit}
|
||||
defaultValue={fetchLimit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -165,7 +119,7 @@ export default function Settings() {
|
||||
label={i18n("fetch_interval")}
|
||||
type="number"
|
||||
name="fetchInterval"
|
||||
value={fetchInterval}
|
||||
defaultValue={fetchInterval}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -174,7 +128,7 @@ export default function Settings() {
|
||||
label={i18n("min_translate_length")}
|
||||
type="number"
|
||||
name="minLength"
|
||||
value={minLength}
|
||||
defaultValue={minLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -183,7 +137,7 @@ export default function Settings() {
|
||||
label={i18n("max_translate_length")}
|
||||
type="number"
|
||||
name="maxLength"
|
||||
value={maxLength}
|
||||
defaultValue={maxLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -192,7 +146,7 @@ export default function Settings() {
|
||||
label={i18n("num_of_newline_characters")}
|
||||
type="number"
|
||||
name="newlineLength"
|
||||
value={newlineLength}
|
||||
defaultValue={newlineLength}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -244,32 +198,35 @@ export default function Settings() {
|
||||
<MenuItem value={true}>{i18n("hide")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Grid container rowSpacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_TRANSLATE}
|
||||
label={i18n("toggle_translate_shortcut")}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Grid container spacing={2} columns={12}>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_TRANSLATE}
|
||||
label={i18n("toggle_translate_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_STYLE}
|
||||
label={i18n("toggle_style_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_POPUP}
|
||||
label={i18n("toggle_popup_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_SETTING}
|
||||
label={i18n("open_setting_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_STYLE}
|
||||
label={i18n("toggle_style_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_POPUP}
|
||||
label={i18n("toggle_popup_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={3} lg={3}>
|
||||
<ShortcutItem
|
||||
action={OPT_SHORTCUT_SETTING}
|
||||
label={i18n("open_setting_shortcut")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
56
src/views/Options/ShortcutInput.js
Normal file
56
src/views/Options/ShortcutInput.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { shortcutListener } from "../../libs/shortcut";
|
||||
|
||||
export default function ShortcutInput({ value, onChange, label, helperText }) {
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current.focus();
|
||||
onChange([]);
|
||||
|
||||
const clearShortcut = shortcutListener((curkeys, allkeys) => {
|
||||
onChange(allkeys);
|
||||
if (curkeys.length === 0) {
|
||||
setDisabled(true);
|
||||
}
|
||||
}, inputRef.current);
|
||||
|
||||
return () => {
|
||||
clearShortcut();
|
||||
};
|
||||
}, [disabled, onChange]);
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<TextField
|
||||
size="small"
|
||||
label={label}
|
||||
name={label}
|
||||
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
|
||||
fullWidth
|
||||
inputRef={inputRef}
|
||||
disabled={disabled}
|
||||
onBlur={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
helperText={helperText}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
{<EditIcon />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export default function SyncSetting() {
|
||||
}
|
||||
};
|
||||
|
||||
const { syncUrl, syncKey } = sync;
|
||||
const { syncUrl = "", syncKey = "" } = sync;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { adaptScript } from "../../libs/gm";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Apis from "./Apis";
|
||||
import Webfix from "./Webfix";
|
||||
import InputSetting from "./InputSetting";
|
||||
|
||||
export default function Options() {
|
||||
const [error, setError] = useState("");
|
||||
@@ -82,16 +83,20 @@ export default function Options() {
|
||||
</h2>
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript for Tampermonkey/Violentmonkey 1 (油猴脚本 安装地址 1)
|
||||
Install Userscript for Tampermonkey/Violentmonkey 1 (油猴脚本
|
||||
安装地址 1)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript for Tampermonkey/Violentmonkey 2 (油猴脚本 安装地址 2)
|
||||
Install Userscript for Tampermonkey/Violentmonkey 2 (油猴脚本
|
||||
安装地址 2)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install Userscript for iOS Safari 1 (油猴脚本 iOS Safari专用 安装地址 1)
|
||||
Install Userscript for iOS Safari 1 (油猴脚本 iOS Safari专用
|
||||
安装地址 1)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install Userscript for iOS Safari 2 (油猴脚本 iOS Safari专用 安装地址 2)
|
||||
Install Userscript for iOS Safari 2 (油猴脚本 iOS Safari专用
|
||||
安装地址 2)
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1 (打开设置页面 1)
|
||||
@@ -126,6 +131,7 @@ export default function Options() {
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Setting />} />
|
||||
<Route path="rules" element={<Rules />} />
|
||||
<Route path="input" element={<InputSetting />} />
|
||||
<Route path="apis" element={<Apis />} />
|
||||
<Route path="sync" element={<SyncSetting />} />
|
||||
<Route path="webfix" element={<Webfix />} />
|
||||
|
||||
@@ -21,7 +21,12 @@ export default function Header({ setShowPopup }) {
|
||||
<IconButton onClick={handleHomepage}>
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
}}
|
||||
>
|
||||
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -80,13 +80,12 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
||||
|
||||
const handleSaveRule = async () => {
|
||||
try {
|
||||
let host = window.location.host;
|
||||
let href = window.location.href;
|
||||
if (isExt) {
|
||||
const tab = await getTabInfo();
|
||||
const url = new URL(tab.url);
|
||||
host = url.host;
|
||||
href = tab.url;
|
||||
}
|
||||
saveRule({ ...rule, pattern: host });
|
||||
saveRule({ ...rule, pattern: href });
|
||||
} catch (err) {
|
||||
console.log("[save rule]", err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user