Compare commits

...

43 Commits

Author SHA1 Message Date
Gabe Yuan
189b7f480a v1.7.1 2023-09-15 22:04:30 +08:00
Gabe Yuan
5e3aa7e2d1 update readme 2023-09-15 22:03:50 +08:00
Gabe Yuan
730be678ef input box trans 2023-09-15 21:39:41 +08:00
Gabe Yuan
9293f422f3 input box trans 2023-09-15 20:44:01 +08:00
Gabe Yuan
6e8158bb34 input box trans 2023-09-15 17:58:00 +08:00
Gabe Yuan
3078d3ca91 input box trans 2023-09-15 17:52:06 +08:00
Gabe Yuan
947e1c7f08 input box trans 2023-09-15 17:29:42 +08:00
Gabe Yuan
938c123412 input box trans 2023-09-15 17:25:58 +08:00
Gabe Yuan
e7a57ad3b2 fix svg 2023-09-15 15:45:51 +08:00
Gabe Yuan
1e40f81bf7 input box trans 2023-09-14 16:35:42 +08:00
Gabe Yuan
72b2f44e32 input box trans 2023-09-14 14:45:22 +08:00
Gabe Yuan
76f54461e7 input box trans 2023-09-14 10:59:50 +08:00
Gabe Yuan
14ca13e31d input box trans 2023-09-13 23:24:55 +08:00
Gabe Yuan
556fd71275 Merge branch 'dev' of github.com:fishjar/kiss-translator into dev 2023-09-13 22:12:08 +08:00
Gabe Yuan
a8002bba9f input box trans 2023-09-13 22:11:33 +08:00
Gabe Yuan
ddd9371fbd sync webfix interval 2023-09-13 18:02:51 +08:00
Gabe Yuan
0ea97b73e3 input box trans 2023-09-13 15:53:40 +08:00
Gabe Yuan
f8c8a4ebeb ui fix 2023-09-13 11:16:56 +08:00
Gabe Yuan
5f613ab558 sync webfix interval 2023-09-13 10:26:30 +08:00
Gabe Yuan
56281f9e82 fix save rule 2023-09-12 17:20:56 +08:00
Gabe Yuan
5e8743dbb7 change fab ui 2023-09-12 15:44:30 +08:00
Gabe Yuan
f4e4c84712 popup ui 2023-09-12 11:00:54 +08:00
Gabe Yuan
c57a0a11fa v1.7.0 2023-09-11 23:21:15 +08:00
Gabe Yuan
fa244b2097 subrules sync time 2023-09-11 22:53:04 +08:00
Gabe Yuan
79612f8a1b subrules sync time 2023-09-11 17:56:31 +08:00
Gabe Yuan
2bf79dbc51 rootSlector -> rootSelector 2023-09-11 16:12:37 +08:00
Gabe Yuan
c2658d5dd0 v1.6.12 2023-09-11 14:35:52 +08:00
Gabe Yuan
13684884c7 i18n text 2023-09-11 13:45:26 +08:00
Gabe Yuan
f216a9254e update readme 2023-09-11 13:42:44 +08:00
Gabe Yuan
dbdbcbba2d update readme 2023-09-11 13:37:19 +08:00
Gabe Yuan
2ee4609192 fix sync bug 2023-09-11 11:33:28 +08:00
Gabe Yuan
0d93cf78f7 fix i18n text 2023-09-10 21:59:28 +08:00
Gabe Yuan
3398ca0dd7 fix i18n text 2023-09-10 21:56:06 +08:00
Gabe Yuan
c1778fbcbb v1.6.11 2023-09-10 15:23:50 +08:00
Gabe Yuan
1ef9974c05 fix sync webfix 2023-09-10 15:04:41 +08:00
Gabe Yuan
399c6b6fed add global rule text 2023-09-10 14:51:16 +08:00
Gabe Yuan
62a60eee44 update help text 2023-09-10 14:34:55 +08:00
Gabe Yuan
54339af885 update readme 2023-09-10 14:19:31 +08:00
Gabe Yuan
06cfd33e60 add open options shortcut for ext 2023-09-10 13:56:11 +08:00
Gabe Yuan
08c9d78d2a add save rule button 2023-09-10 13:44:34 +08:00
Gabe Yuan
e7a5e5dce1 add save rule button 2023-09-10 13:09:19 +08:00
Gabe Yuan
3a59a127d1 add save rule button 2023-09-10 12:35:03 +08:00
Gabe Yuan
26f213cad2 add fontsize fixer 2023-09-09 23:32:17 +08:00
40 changed files with 1157 additions and 428 deletions

2
.env
View File

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

View File

@@ -16,7 +16,37 @@ If you also like a little more simplicity, welcome to pick it up.
## Features ## Features
- Keep it simple, smart - [x] Keep it simple, smart
- [x] Open source
- [x] Adapt to common browsers
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [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
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Popup
- `Alt+O` Open Options
- `Alt+I` Input Box Translation
## Download
- [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] GreaseMonkey Script
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## Associated ProjectS ## Associated ProjectS
@@ -38,42 +68,7 @@ If you also like a little more simplicity, welcome to pick it up.
- Supports query of English words, sentences and Chinese characters. - Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections. - Supports history records and word collections.
## Description ## Development Guidelines
### Support shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Menu
## Schedule
- [x] Provide trial installation package
- [x] Adapt browser
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] Support translation services
- [x] Google
- [x] Microsoft
- [x] DeepL
- [x] OpenAI
- [x] Upload to app Store
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
- [x] Open source
- [x] Data Synchronization Function
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## Guide
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git

View File

@@ -16,7 +16,37 @@
## 特点 ## 特点
- 保持简约 - [x] 保持简约
- [x] 开放源代码
- [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi
- [ ] Safari
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI
- [x] 自定义翻译接口
- [x] 支持输入框翻译
- [x] 支持 YouTube 字幕翻译
- [x] 数据同步功能
- [x] 自定义规则 + 规则订阅
- [x] 自定义样式
- [x] 自定义快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开弹窗
- `Alt+O` 打开设置
- `Alt+I` 输入框翻译
## 下载
- [x] 浏览器扩展
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] 油猴脚本
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## 关联项目 ## 关联项目
@@ -38,42 +68,7 @@
- 支持英文单词、句子、汉字的查询。 - 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。 - 支持历史记录、单词收藏。
## 简要说明 ## 开发指引
### 支持快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开菜单
## 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] 支持翻译服务
- [x] Google
- [x] Microsoft
- [x] DeepL
- [x] OpenAI
- [x] 上架应用市场
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码
- [x] 数据同步功能
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
## 指引
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git

View File

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

View File

@@ -10,5 +10,8 @@
}, },
"toggle_style": { "toggle_style": {
"message": "Toggle Style" "message": "Toggle Style"
},
"open_options": {
"message": "Open Options"
} }
} }

View File

@@ -6,9 +6,12 @@
"message": "一个简约的双语网页翻译扩展 & 油猴脚本" "message": "一个简约的双语网页翻译扩展 & 油猴脚本"
}, },
"toggle_translate": { "toggle_translate": {
"message": "切换翻译" "message": "开启翻译"
}, },
"toggle_style": { "toggle_style": {
"message": "切换样式" "message": "切换样式"
},
"open_options": {
"message": "打开设置"
} }
} }

View File

@@ -84,6 +84,11 @@
> >
</p> </p>
</h2> </h2>
<hr />
<input id="input1" style="width: 80%;" />
<hr />
<textarea id="textarea1" style="width: 80%;">test</textarea>
<hr />
<div id="addtitle"></div> <div id="addtitle"></div>
<h2>Shadow 1</h2> <h2>Shadow 1</h2>
<div id="shadow1"></div> <div id="shadow1"></div>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.10", "version": "1.7.1",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -33,6 +33,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["<all_urls>", "storage"], "permissions": ["<all_urls>", "storage"],

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.10", "version": "1.7.1",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -34,6 +34,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage"], "permissions": ["storage"],

View File

@@ -7,6 +7,7 @@ import {
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
} from "./config"; } from "./config";
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage"; import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { trySyncSettingAndRules } from "./libs/sync"; import { trySyncSettingAndRules } from "./libs/sync";
@@ -85,6 +86,9 @@ browser.commands.onCommand.addListener((command) => {
case CMD_TOGGLE_STYLE: case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE); sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break; break;
case CMD_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
default: default:
} }
}); });

View File

@@ -43,6 +43,7 @@ const customApiLangs = `["en", "English - English"],
`; `;
const customApiHelpZH = `/// 自定义翻译源接口说明 const customApiHelpZH = `/// 自定义翻译源接口说明
// 请求Request数据将按下面规范发送 // 请求Request数据将按下面规范发送
{ {
url: {{YOUR_URL}}, url: {{YOUR_URL}},
@@ -70,6 +71,7 @@ ${customApiLangs}
`; `;
const customApiHelpEN = `/// Custom translation source interface description const customApiHelpEN = `/// Custom translation source interface description
// Request data will be sent according to the following specifications // Request data will be sent according to the following specifications
{ {
url: {{YOUR_URL}}, url: {{YOUR_URL}},
@@ -342,8 +344,8 @@ export const I18N = {
en: `Follow the syntax of "CSS"`, en: `Follow the syntax of "CSS"`,
}, },
setting: { setting: {
zh: `设置`, zh: `设置 (Alt+O)`,
en: `Setting`, en: `Setting (Alt+O)`,
}, },
pattern: { pattern: {
zh: `匹配网址`, zh: `匹配网址`,
@@ -527,14 +529,62 @@ export const I18N = {
}, },
hide_fab_button: { hide_fab_button: {
zh: `隐藏悬浮按钮`, zh: `隐藏悬浮按钮`,
en: `"Hide Fab Button`, en: `Hide Fab Button`,
}, },
show: { show: {
zh: `显示`, zh: `显示`,
en: `"Show`, en: `Show`,
}, },
hide: { hide: {
zh: `隐藏`, zh: `隐藏`,
en: `"Hide`, en: `Hide`,
},
save_rule: {
zh: `保存规则`,
en: `Save Rule`,
},
global_rule: {
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"`,
}, },
}; };

View File

@@ -29,6 +29,7 @@ export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle"; export const CMD_TOGGLE_STYLE = "toggleStyle";
export const CMD_OPEN_OPTIONS = "openOptions";
export const CLIENT_WEB = "web"; export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome"; export const CLIENT_CHROME = "chrome";
@@ -136,6 +137,7 @@ export const OPT_LANGS_SPECIAL = {
), ),
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]), [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_NONE = "style_none"; // 无
export const OPT_STYLE_LINE = "under_line"; // 下划线 export const OPT_STYLE_LINE = "under_line"; // 下划线
@@ -197,6 +199,20 @@ export const GLOBLA_RULE = {
textDiyStyle: "", 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 = [ export const DEFAULT_SUBRULES_LIST = [
{ {
@@ -272,6 +288,7 @@ export const DEFAULT_SETTING = {
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译 mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
shortcuts: DEFAULT_SHORTCUTS, // 快捷键 shortcuts: DEFAULT_SHORTCUTS, // 快捷键
hideFab: false, // 是否隐藏按钮 hideFab: false, // 是否隐藏按钮
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];
@@ -284,4 +301,5 @@ export const DEFAULT_SYNC = {
rulesUpdateAt: 0, rulesUpdateAt: 0,
rulesSyncAt: 0, rulesSyncAt: 0,
subRulesSyncAt: 0, // 订阅规则同步时间 subRulesSyncAt: 0, // 订阅规则同步时间
dataCaches: {}, // 缓存同步时间
}; };

18
src/hooks/InputRule.js Normal file
View 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 };
}

View File

@@ -14,7 +14,7 @@ export function useRules() {
const updateRules = useCallback( const updateRules = useCallback(
async (rules) => { async (rules) => {
await save(rules); await save(rules);
trySyncRules(); trySyncRules(false, true);
}, },
[save] [save]
); );

View File

@@ -19,7 +19,7 @@ export function SettingProvider({ children }) {
const syncSetting = useMemo( const syncSetting = useMemo(
() => () =>
debounce(() => { debounce(() => {
trySyncSetting(); trySyncSetting(false, true);
}, [2000]), }, [2000]),
[] []
); );

View File

@@ -31,6 +31,7 @@ export function useStorage(key, defaultVal = null) {
if (val) { if (val) {
setData(val); setData(val);
} else if (defaultVal) { } else if (defaultVal) {
setData(defaultVal);
await storage.setObj(key, defaultVal); await storage.setObj(key, defaultVal);
} }
}, [key, defaultVal]); }, [key, defaultVal]);

View File

@@ -48,7 +48,7 @@ export function useSubRules() {
const addSub = useCallback( const addSub = useCallback(
async (url) => { async (url) => {
const subrulesList = [...list]; const subrulesList = [...list];
subrulesList.push({ url, selected: false, syncAt: Date.now() }); subrulesList.push({ url, selected: false });
await updateSetting({ subrulesList }); await updateSetting({ subrulesList });
}, },
[list, updateSetting] [list, updateSetting]

View File

@@ -1,3 +1,4 @@
import { useCallback } from "react";
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config"; import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import { useStorage } from "./Storage"; import { useStorage } from "./Storage";
@@ -6,6 +7,40 @@ import { useStorage } from "./Storage";
* @returns * @returns
*/ */
export function useSync() { export function useSync() {
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC); const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
return { sync: data, updateSync: update }; return { sync: data, updateSync: update, reloadSync: reload };
}
/**
* caches sync hook
* @param {*} url
* @returns
*/
export function useSyncCaches() {
const { sync, updateSync, reloadSync } = useSync();
const updateDataCache = useCallback(
async (url) => {
const dataCaches = sync.dataCaches || {};
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
},
[sync, updateSync]
);
const deleteDataCache = useCallback(
async (url) => {
const dataCaches = sync.dataCaches || {};
delete dataCaches[url];
await updateSync({ dataCaches });
},
[sync, updateSync]
);
return {
dataCaches: sync.dataCaches || {},
updateDataCache,
deleteDataCache,
reloadSync,
};
} }

View File

@@ -19,3 +19,12 @@ export const sendTabMsg = async (action, args) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true }); const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return browser.tabs.sendMessage(tabs[0].id, { action, args }); return browser.tabs.sendMessage(tabs[0].id, { action, args });
}; };
/**
* 获取当前tab信息
* @returns
*/
export const getTabInfo = async () => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0];
};

View File

@@ -11,6 +11,8 @@ import {
DEFAULT_OW_RULE, DEFAULT_OW_RULE,
} from "../config"; } from "../config";
import { loadOrFetchSubRules } from "./subRules"; import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
/** /**
* 根据href匹配规则 * 根据href匹配规则
@@ -47,9 +49,8 @@ export const matchRule = async (
mixRule[key] = val; mixRule[key] = val;
}); });
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map( let subRules = await loadOrFetchSubRules(selectedSub.url);
(item) => ({ ...item, ...mixRule }) subRules = subRules.map((item) => ({ ...item, ...mixRule }));
);
rules.splice(-1, 0, ...subRules); rules.splice(-1, 0, ...subRules);
} }
} catch (err) { } catch (err) {
@@ -134,3 +135,19 @@ export const checkRules = (rules) => {
return rules; return rules;
}; };
/**
* 保存或更新rule
* @param {*} newRule
*/
export const saveRule = async (newRule) => {
const rules = await getRulesWithDefault();
const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
if (rule && rule.pattern !== GLOBAL_KEY) {
Object.assign(rule, { ...newRule, pattern: rule.pattern });
} else {
rules.unshift(newRule);
}
await setRules(rules);
trySyncRules(false, true);
};

View File

@@ -65,3 +65,48 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
} }
}, target); }, 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);
};

View File

@@ -4,11 +4,21 @@ import {
updateSync, updateSync,
setSubRules, setSubRules,
getSubRules, getSubRules,
updateSetting,
} from "./storage"; } from "./storage";
import { apiFetch } from "../apis"; import { apiFetch } from "../apis";
import { checkRules } from "./rules"; import { checkRules } from "./rules";
import { isAllchar } from "./utils"; import { isAllchar } from "./utils";
import { syncWebfix } from "./webfix";
/**
* 更新缓存同步时间
* @param {*} url
*/
const updateSyncDataCache = async (url) => {
const { dataCaches = {} } = await getSyncWithDefault();
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
};
/** /**
* 同步订阅规则 * 同步订阅规则
@@ -35,6 +45,7 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
for (let subrules of subrulesList) { for (let subrules of subrulesList) {
try { try {
await syncSubRules(subrules.url, isBg); await syncSubRules(subrules.url, isBg);
await updateSyncDataCache(subrules.url);
} catch (err) { } catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err); console.log(`[sync subrule error]: ${subrules.url}`, err);
} }
@@ -52,13 +63,13 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
const now = Date.now(); const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天 const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) { if (now - subRulesSyncAt > interval) {
// 同步订阅规则
await syncAllSubRules(subrulesList, isBg); await syncAllSubRules(subrulesList, isBg);
await updateSync({ subRulesSyncAt: now }); await updateSync({ subRulesSyncAt: now });
// 同步修复规则
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
} }
subrulesList.forEach((item) => {
item.syncAt = now;
});
await updateSetting({ subrulesList });
} catch (err) { } catch (err) {
console.log("[try sync all subrules]", err); console.log("[try sync all subrules]", err);
} }
@@ -70,9 +81,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
* @returns * @returns
*/ */
export const loadOrFetchSubRules = async (url) => { export const loadOrFetchSubRules = async (url) => {
const rules = await getSubRules(url); let rules = await getSubRules(url);
if (rules?.length) { if (!rules || rules.length === 0) {
return rules; rules = await syncSubRules(url);
await updateSyncDataCache(url);
} }
return syncSubRules(url); return rules || [];
}; };

34
src/libs/svg.js Normal file
View 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>
`;

View File

@@ -19,12 +19,21 @@ import { sha256 } from "./utils";
* 同步设置 * 同步设置
* @returns * @returns
*/ */
const syncSetting = async (isBg = false) => { const syncSetting = async (isBg = false, isForce = false) => {
const { syncUrl, syncKey, settingUpdateAt = 0 } = await getSyncWithDefault(); let {
syncUrl,
syncKey,
settingUpdateAt = 0,
settingSyncAt = 0,
} = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
if (isForce) {
settingUpdateAt = Date.now();
}
const setting = await getSettingWithDefault(); const setting = await getSettingWithDefault();
const res = await apiSyncData( const res = await apiSyncData(
syncUrl, syncUrl,
@@ -32,7 +41,7 @@ const syncSetting = async (isBg = false) => {
{ {
key: KV_SETTING_KEY, key: KV_SETTING_KEY,
value: setting, value: setting,
updateAt: settingUpdateAt, updateAt: settingSyncAt === 0 ? 0 : settingUpdateAt,
}, },
isBg isBg
); );
@@ -48,9 +57,9 @@ const syncSetting = async (isBg = false) => {
return res.value; return res.value;
}; };
export const trySyncSetting = async (isBg = false) => { export const trySyncSetting = async (isBg = false, isForce = false) => {
try { try {
return await syncSetting(isBg); return await syncSetting(isBg, isForce);
} catch (err) { } catch (err) {
console.log("[sync setting]", err); console.log("[sync setting]", err);
} }
@@ -60,12 +69,21 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则 * 同步规则
* @returns * @returns
*/ */
const syncRules = async (isBg = false) => { const syncRules = async (isBg = false, isForce = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault(); let {
syncUrl,
syncKey,
rulesUpdateAt = 0,
rulesSyncAt = 0,
} = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (!syncUrl || !syncKey) {
return; return;
} }
if (isForce) {
rulesUpdateAt = Date.now();
}
const rules = await getRulesWithDefault(); const rules = await getRulesWithDefault();
const res = await apiSyncData( const res = await apiSyncData(
syncUrl, syncUrl,
@@ -73,7 +91,7 @@ const syncRules = async (isBg = false) => {
{ {
key: KV_RULES_KEY, key: KV_RULES_KEY,
value: rules, value: rules,
updateAt: rulesUpdateAt, updateAt: rulesSyncAt === 0 ? 0 : rulesUpdateAt,
}, },
isBg isBg
); );
@@ -89,9 +107,9 @@ const syncRules = async (isBg = false) => {
return res.value; return res.value;
}; };
export const trySyncRules = async (isBg = false) => { export const trySyncRules = async (isBg = false, isForce = false) => {
try { try {
return await syncRules(isBg); return await syncRules(isBg, isForce);
} catch (err) { } catch (err) {
console.log("[sync user rules]", err); console.log("[sync user rules]", err);
} }

View File

@@ -9,16 +9,101 @@ import {
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE, OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER, OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; 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 { export class Translator {
_rule = {}; _rule = {};
_inputRule = {};
_setting = {}; _setting = {};
_rootNodes = new Set(); _rootNodes = new Set();
_tranNodes = new Map(); _tranNodes = new Map();
@@ -101,6 +186,11 @@ export class Translator {
if (rule.transOpen === "true") { if (rule.transOpen === "true") {
this._register(); this._register();
} }
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
if (this._inputRule.transOpen) {
this._registerInput();
}
} }
get setting() { 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) => { _handleMouseover = (e) => {
const key = this._setting.mouseKey.slice(3); const key = this._setting.mouseKey.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) { if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {

View File

@@ -179,3 +179,47 @@ export const isSameSet = (a, b) => {
const s = new Set([...a, ...b]); const s = new Set([...a, ...b]);
return s.size === a.size && s.size === b.size; 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);
};

View File

@@ -5,27 +5,34 @@ import { apiFetch } from "../apis";
/** /**
* 修复程序类型 * 修复程序类型
*/ */
const WEBFIX_BR = "br"; const FIXER_BR = "br";
const FIXER_FONTSIZE = "fontSize";
/** /**
* 需要修复的站点列表 * 需要修复的站点列表
* - pattern 匹配网址 * - pattern 匹配网址
* - selector 需要修复的选择器 * - selector 需要修复的选择器
* - rootSlector 需要监听的选择器,可留空 * - rootSelector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数 * - fixer 修复函数,可针对不同网址,选用不同修复函数
*/ */
const DEFAULT_SITES = [ const DEFAULT_SITES = [
{ {
pattern: "www.phoronix.com", pattern: "www.phoronix.com",
selector: ".content", selector: ".content",
rootSlector: "", rootSelector: "",
fixer: WEBFIX_BR, fixer: FIXER_BR,
}, },
{ {
pattern: "t.me/s/*", pattern: "t.me/s/",
selector: ".tgme_widget_message_text", selector: ".tgme_widget_message_text",
rootSlector: ".tgme_channel_history", rootSelector: ".tgme_channel_history",
fixer: WEBFIX_BR, fixer: FIXER_BR,
},
{
pattern: "baidu.com",
selector: "html",
rootSelector: "",
fixer: FIXER_FONTSIZE,
}, },
]; ];
@@ -87,20 +94,29 @@ function brFixer(node) {
node.innerHTML = html; node.innerHTML = html;
} }
/**
* 修复字体大小问题,如 baidu.com
* @param {*} node
*/
function fontSizeFixer(node) {
node.style.cssText += "font-size:1em;";
}
/** /**
* 修复程序映射 * 修复程序映射
*/ */
const fixerMap = { const fixerMap = {
[WEBFIX_BR]: brFixer, [FIXER_BR]: brFixer,
[FIXER_FONTSIZE]: fontSizeFixer,
}; };
/** /**
* 查找、监听节点,并执行修复函数 * 查找、监听节点,并执行修复函数
* @param {*} selector * @param {*} selector
* @param {*} fixer * @param {*} fixer
* @param {*} rootSlector * @param {*} rootSelector
*/ */
function run(selector, fixer, rootSlector) { function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) { var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) { mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) { mutation.addedNodes.forEach(function (addNode) {
@@ -110,8 +126,8 @@ function run(selector, fixer, rootSlector) {
}); });
var rootNodes = [document]; var rootNodes = [document];
if (rootSlector) { if (rootSelector) {
rootNodes = document.querySelectorAll(rootSlector); rootNodes = document.querySelectorAll(rootSelector);
} }
rootNodes.forEach(function (rootNode) { rootNodes.forEach(function (rootNode) {
@@ -165,7 +181,7 @@ export async function webfix(href, { injectWebfix }) {
var site = sites[i]; var site = sites[i];
if (isMatch(href, site.pattern)) { if (isMatch(href, site.pattern)) {
if (fixerMap[site.fixer]) { if (fixerMap[site.fixer]) {
run(site.selector, fixerMap[site.fixer], site.rootSlector); run(site.selector, fixerMap[site.fixer], site.rootSelector);
} }
break; break;
} }

View File

@@ -1,65 +1,51 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import { setFab } from "../../libs/storage"; import { setFab } from "../../libs/storage";
import { debounce } from "../../libs/utils";
import Paper from "@mui/material/Paper";
const getEdgePosition = ( const getEdgePosition = ({
{ x: left, y: top, edge }, x: left,
y: top,
width,
height,
windowWidth, windowWidth,
windowHeight, windowHeight,
width, hover,
height }) => {
) => {
const right = windowWidth - left - width; const right = windowWidth - left - width;
const bottom = windowHeight - top - height; const bottom = windowHeight - top - height;
const min = Math.min(left, top, right, bottom); const min = Math.min(left, top, right, bottom);
switch (min) { switch (min) {
case right: case right:
edge = "right"; left = hover ? windowWidth - width : windowWidth - width / 2;
left = windowWidth - width;
break; break;
case left: case left:
edge = "left"; left = hover ? 0 : -width / 2;
left = 0;
break; break;
case bottom: case bottom:
edge = "bottom"; top = hover ? windowHeight - height : windowHeight - height / 2;
top = windowHeight - height;
break; break;
default: default:
edge = "top"; top = hover ? 0 : -height / 2;
top = 0;
} }
left = limitNumber(left, 0, windowWidth - width); return { x: left, y: top };
top = limitNumber(top, 0, windowHeight - height);
return { x: left, y: top, edge, hide: false };
}; };
const getHidePosition = ( function DraggableWrapper({ children, usePaper, ...props }) {
{ x: left, y: top, edge }, if (usePaper) {
windowWidth, return (
windowHeight, <Paper {...props} elevation={4}>
width, {children}
height </Paper>
) => { );
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;
} }
return { x: left, y: top, edge, hide: true }; return <div {...props}>{children}</div>;
}; }
export default function Draggable({ export default function Draggable({
windowSize, windowSize: { w: windowWidth, h: windowHeight },
width, width,
height, height,
left, left,
@@ -70,66 +56,38 @@ export default function Draggable({
onMove, onMove,
handler, handler,
children, children,
usePaper,
}) { }) {
const [origin, setOrigin] = useState({ const [hover, setHover] = useState(false);
x: left, const [origin, setOrigin] = useState(null);
y: top, const [position, setPosition] = useState({ x: left, y: top });
px: left, const setFabPosition = useMemo(() => debounce(setFab, 500), []);
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 handlePointerDown = (e) => { const handlePointerDown = (e) => {
!isMobile && e.target.setPointerCapture(e.pointerId); !isMobile && e.target.setPointerCapture(e.pointerId);
onStart && onStart(); onStart && onStart();
edgeTimer && clearTimeout(edgeTimer); const { x, y } = position;
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e; const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
setOrigin({ setOrigin({ x, y, clientX, clientY });
x: position.x,
y: position.y,
px: clientX,
py: clientY,
});
}; };
const handlePointerMove = (e) => { const handlePointerMove = (e) => {
onMove && onMove(); onMove && onMove();
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e; const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
if (origin) { if (origin) {
const dx = clientX - origin.px; const dx = clientX - origin.clientX;
const dy = clientY - origin.py; const dy = clientY - origin.clientY;
let x = origin.x + dx; let x = origin.x + dx;
let y = origin.y + dy; let y = origin.y + dy;
const { w, h } = windowSize; x = limitNumber(x, -width / 2, windowWidth - width / 2);
x = limitNumber(x, 0, w - width); y = limitNumber(y, 0, windowHeight - height / 2);
y = limitNumber(y, 0, h - height); setPosition({ x, y });
setPosition({ x, y, edge: null, hide: false });
} }
}; };
const handlePointerUp = (e) => { const handlePointerUp = (e) => {
e.stopPropagation(); e.stopPropagation();
setOrigin(null); setOrigin(null);
if (!snapEdge) {
return;
}
goEdge(windowSize.w, windowSize.h, width, height);
}; };
const handleClick = (e) => { const handleClick = (e) => {
@@ -138,35 +96,48 @@ export default function Draggable({
const handleMouseEnter = (e) => { const handleMouseEnter = (e) => {
e.stopPropagation(); e.stopPropagation();
if (snapEdge && position.hide) { setHover(true);
edgeTimer && clearTimeout(edgeTimer); };
goEdge(windowSize.w, windowSize.h, width, height);
} const handleMouseLeave = (e) => {
e.stopPropagation();
setHover(false);
}; };
useEffect(() => { useEffect(() => {
setOrigin(null); if (!snapEdge || !!origin) {
if (!snapEdge) {
return; return;
} }
goEdge(windowSize.w, windowSize.h, width, height);
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
useEffect(() => { setPosition((pre) => {
if (position.hide) { const edgePosition = getEdgePosition({
setFab({ ...pre,
x: position.x, width,
y: position.y, height,
windowWidth,
windowHeight,
hover,
}); });
} setFabPosition(edgePosition);
}, [position.x, position.y, position.hide]); return edgePosition;
});
}, [
origin,
hover,
width,
height,
windowWidth,
windowHeight,
snapEdge,
setFabPosition,
]);
const opacity = useMemo(() => { const opacity = useMemo(() => {
if (snapEdge) { if (snapEdge) {
return position.hide ? 0.2 : 1; return hover || origin ? 1 : 0.2;
} }
return origin ? 0.8 : 1; return origin ? 0.8 : 1;
}, [origin, snapEdge, position.hide]); }, [origin, snapEdge, hover]);
const touchProps = isMobile const touchProps = isMobile
? { ? {
@@ -181,7 +152,8 @@ export default function Draggable({
}; };
return ( return (
<div <DraggableWrapper
usePaper={usePaper}
style={{ style={{
opacity, opacity,
position: "fixed", position: "fixed",
@@ -191,6 +163,7 @@ export default function Draggable({
display: show ? "block" : "none", display: show ? "block" : "none",
}} }}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick} onClick={handleClick}
> >
<div <div
@@ -202,6 +175,6 @@ export default function Draggable({
{handler} {handler}
</div> </div>
<div>{children}</div> <div>{children}</div>
</div> </DraggableWrapper>
); );
} }

View File

@@ -1,4 +1,3 @@
import Paper from "@mui/material/Paper";
import Fab from "@mui/material/Fab"; import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
@@ -9,6 +8,8 @@ import Popup from "../Popup";
import { debounce } from "../../libs/utils"; import { debounce } from "../../libs/utils";
import { isGm } from "../../libs/client"; import { isGm } from "../../libs/client";
import Header from "../Popup/Header"; import Header from "../Popup/Header";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import { import {
DEFAULT_SHORTCUTS, DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE, OPT_SHORTCUT_TRANSLATE,
@@ -78,58 +79,54 @@ export default function Action({ translator, fab }) {
}, [translator]); }, [translator]);
useEffect(() => { useEffect(() => {
// 注册菜单 if (!isGm) {
const menuCommandIds = []; return;
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);
}
} }
return () => { // 注册菜单
if (isGm) { try {
try { const menuCommandIds = [];
menuCommandIds.forEach((id) => { menuCommandIds.push(
GM.unregisterMenuCommand(id); GM.registerMenuCommand(
}); "Toggle Translate (Alt+q)",
} catch (err) { (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]); }, [translator]);
useEffect(() => { useEffect(() => {
@@ -165,7 +162,7 @@ export default function Action({ translator, fab }) {
windowSize, windowSize,
width: fabWidth, width: fabWidth,
height: fabWidth, height: fabWidth,
left: fab.x ?? 0, left: fab.x ?? -fabWidth,
top: fab.y ?? windowSize.h / 2, top: fab.y ?? windowSize.h / 2,
}; };
@@ -178,17 +175,17 @@ export default function Action({ translator, fab }) {
show={showPopup} show={showPopup}
onStart={handleStart} onStart={handleStart}
onMove={handleMove} onMove={handleMove}
usePaper
handler={ handler={
<Paper style={{ cursor: "move" }} elevation={3}> <Box style={{ cursor: "move" }}>
<Header setShowPopup={setShowPopup} /> <Header setShowPopup={setShowPopup} />
</Paper> <Divider />
</Box>
} }
> >
<Paper> {showPopup && (
{showPopup && ( <Popup setShowPopup={setShowPopup} translator={translator} />
<Popup setShowPopup={setShowPopup} translator={translator} /> )}
)}
</Paper>
</Draggable> </Draggable>
<Draggable <Draggable
key="fab" key="fab"

View File

@@ -1,44 +1,14 @@
import { DEFAULT_COLOR } from "../../config"; import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() { export default function LoadingIcon() {
return ( return (
<svg <div
viewBox="0 0 100 100"
style={{ style={{
maxWidth: "1.2em", display: "inline-block",
maxHeight: "1.2em", width: "1.2em",
height: "1em",
}} }}
> dangerouslySetInnerHTML={{ __html: loadingSvg }}
<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>
); );
} }

View 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>
);
}

View File

@@ -12,6 +12,7 @@ import { useI18n } from "../../hooks/I18n";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import ApiIcon from "@mui/icons-material/Api"; import ApiIcon from "@mui/icons-material/Api";
import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension"; import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
import InputIcon from "@mui/icons-material/Input";
function LinkItem({ label, url, icon }) { function LinkItem({ label, url, icon }) {
const match = useMatch(url); const match = useMatch(url);
@@ -38,6 +39,12 @@ export default function Navigator(props) {
url: "/rules", url: "/rules",
icon: <DesignServicesIcon />, icon: <DesignServicesIcon />,
}, },
{
id: "input_setting",
label: i18n("input_setting"),
url: "/input",
icon: <InputIcon />,
},
{ {
id: "apis_setting", id: "apis_setting",
label: i18n("apis_setting"), label: i18n("apis_setting"),

View File

@@ -48,6 +48,7 @@ import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule"; import OwSubRule from "./OwSubRule";
import ClearAllIcon from "@mui/icons-material/ClearAll"; import ClearAllIcon from "@mui/icons-material/ClearAll";
import HelpButton from "./HelpButton"; import HelpButton from "./HelpButton";
import { useSyncCaches } from "../../hooks/Sync";
function RuleFields({ rule, rules, setShow, setKeyword }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { const initFormValues = rule || {
@@ -362,6 +363,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
} }
function RuleAccordion({ rule, rules }) { function RuleAccordion({ rule, rules }) {
const i18n = useI18n();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const handleChange = (e) => { const handleChange = (e) => {
@@ -376,7 +378,9 @@ function RuleAccordion({ rule, rules }) {
opacity: rules ? 1 : 0.5, opacity: rules ? 1 : 0.5,
}} }}
> >
{rule.pattern} {rule.pattern === GLOBAL_KEY
? `[${i18n("global_rule")}] ${rule.pattern}`
: rule.pattern}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
@@ -621,8 +625,9 @@ function SubRulesItem({
syncAt, syncAt,
selectedUrl, selectedUrl,
delSub, delSub,
updateSub,
setSelectedRules, setSelectedRules,
updateDataCache,
deleteDataCache,
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -630,6 +635,7 @@ function SubRulesItem({
try { try {
await delSub(url); await delSub(url);
await delSubRules(url); await delSubRules(url);
await deleteDataCache(url);
} catch (err) { } catch (err) {
console.log("[del subrules]", err); console.log("[del subrules]", err);
} }
@@ -642,7 +648,7 @@ function SubRulesItem({
if (rules.length > 0 && url === selectedUrl) { if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules); setSelectedRules(rules);
} }
await updateSub(url, { syncAt: Date.now() }); await updateDataCache(url);
} catch (err) { } catch (err) {
console.log("[sync sub rules]", err); console.log("[sync sub rules]", err);
} finally { } finally {
@@ -677,7 +683,7 @@ function SubRulesItem({
); );
} }
function SubRulesEdit({ subList, addSub }) { function SubRulesEdit({ subList, addSub, updateDataCache }) {
const i18n = useI18n(); const i18n = useI18n();
const [inputText, setInputText] = useState(""); const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState(""); const [inputError, setInputError] = useState("");
@@ -712,6 +718,7 @@ function SubRulesEdit({ subList, addSub }) {
throw new Error("empty rules"); throw new Error("empty rules");
} }
await addSub(url); await addSub(url);
await updateDataCache(url);
setShowInput(false); setShowInput(false);
setInputText(""); setInputText("");
} catch (err) { } catch (err) {
@@ -784,7 +791,6 @@ function SubRules({ subRules }) {
const { const {
subList, subList,
selectSub, selectSub,
updateSub,
addSub, addSub,
delSub, delSub,
selectedUrl, selectedUrl,
@@ -792,27 +798,38 @@ function SubRules({ subRules }) {
setSelectedRules, setSelectedRules,
loading, loading,
} = subRules; } = subRules;
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
useSyncCaches();
const handleSelect = (e) => { const handleSelect = (e) => {
const url = e.target.value; const url = e.target.value;
selectSub(url); selectSub(url);
}; };
useEffect(() => {
reloadSync();
}, [selectedRules, reloadSync]);
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<SubRulesEdit subList={subList} addSub={addSub} /> <SubRulesEdit
subList={subList}
addSub={addSub}
updateDataCache={updateDataCache}
/>
<RadioGroup value={selectedUrl} onChange={handleSelect}> <RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => ( {subList.map((item, index) => (
<SubRulesItem <SubRulesItem
key={item.url} key={item.url}
url={item.url} url={item.url}
syncAt={item.syncAt} syncAt={dataCaches[item.url]}
index={index} index={index}
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
delSub={delSub} delSub={delSub}
updateSub={updateSub}
setSelectedRules={setSelectedRules} setSelectedRules={setSelectedRules}
updateDataCache={updateDataCache}
deleteDataCache={deleteDataCache}
/> />
))} ))}
</RadioGroup> </RadioGroup>

View File

@@ -12,8 +12,6 @@ import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { useAlert } from "../../hooks/Alert"; import { useAlert } from "../../hooks/Alert";
import { isExt } from "../../libs/client"; 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 Grid from "@mui/material/Grid";
import { import {
UI_LANGS, UI_LANGS,
@@ -26,57 +24,13 @@ import {
OPT_SHORTCUT_POPUP, OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING, OPT_SHORTCUT_SETTING,
} from "../../config"; } from "../../config";
import { useEffect, useState, useRef } from "react";
import { useShortcut } from "../../hooks/Shortcut"; import { useShortcut } from "../../hooks/Shortcut";
import { shortcutListener } from "../../libs/shortcut"; import ShortcutInput from "./ShortcutInput";
function ShortcutItem({ action, label }) { function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action); 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 ( return (
<Stack direction="row"> <ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
<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>
); );
} }
@@ -156,7 +110,7 @@ export default function Settings() {
label={i18n("fetch_limit")} label={i18n("fetch_limit")}
type="number" type="number"
name="fetchLimit" name="fetchLimit"
value={fetchLimit} defaultValue={fetchLimit}
onChange={handleChange} onChange={handleChange}
/> />
@@ -165,7 +119,7 @@ export default function Settings() {
label={i18n("fetch_interval")} label={i18n("fetch_interval")}
type="number" type="number"
name="fetchInterval" name="fetchInterval"
value={fetchInterval} defaultValue={fetchInterval}
onChange={handleChange} onChange={handleChange}
/> />
@@ -174,7 +128,7 @@ export default function Settings() {
label={i18n("min_translate_length")} label={i18n("min_translate_length")}
type="number" type="number"
name="minLength" name="minLength"
value={minLength} defaultValue={minLength}
onChange={handleChange} onChange={handleChange}
/> />
@@ -183,7 +137,7 @@ export default function Settings() {
label={i18n("max_translate_length")} label={i18n("max_translate_length")}
type="number" type="number"
name="maxLength" name="maxLength"
value={maxLength} defaultValue={maxLength}
onChange={handleChange} onChange={handleChange}
/> />
@@ -192,7 +146,7 @@ export default function Settings() {
label={i18n("num_of_newline_characters")} label={i18n("num_of_newline_characters")}
type="number" type="number"
name="newlineLength" name="newlineLength"
value={newlineLength} defaultValue={newlineLength}
onChange={handleChange} onChange={handleChange}
/> />
@@ -244,32 +198,35 @@ export default function Settings() {
<MenuItem value={true}>{i18n("hide")}</MenuItem> <MenuItem value={true}>{i18n("hide")}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<Grid container rowSpacing={2} columns={12}>
<Grid item xs={12} sm={12} md={3} lg={3}> <Box>
<ShortcutItem <Grid container spacing={2} columns={12}>
action={OPT_SHORTCUT_TRANSLATE} <Grid item xs={12} sm={12} md={3} lg={3}>
label={i18n("toggle_translate_shortcut")} <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>
<Grid item xs={12} sm={12} md={3} lg={3}> </Box>
<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>
</> </>
)} )}
</Stack> </Stack>

View 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>
);
}

View File

@@ -44,7 +44,7 @@ export default function SyncSetting() {
} }
}; };
const { syncUrl, syncKey } = sync; const { syncUrl = "", syncKey = "" } = sync;
return ( return (
<Box> <Box>

View File

@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion"; import Accordion from "@mui/material/Accordion";
@@ -21,14 +21,14 @@ import HelpButton from "./HelpButton";
import { URL_KISS_RULES_NEW_ISSUE } from "../../config"; import { URL_KISS_RULES_NEW_ISSUE } from "../../config";
function ApiFields({ site }) { function ApiFields({ site }) {
const { selector, rootSlector, fixer } = site; const { selector, rootSelector, fixer } = site;
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<TextField <TextField
size="small" size="small"
label={"rootSlector"} label={"rootSelector"}
name="rootSlector" name="rootSelector"
value={rootSlector || "document"} value={rootSelector || "document"}
disabled disabled
/> />
<TextField <TextField
@@ -75,11 +75,17 @@ export default function Webfix() {
const alert = useAlert(); const alert = useAlert();
const { setting, updateSetting } = useSetting(); const { setting, updateSetting } = useSetting();
const loadSites = useCallback(async () => {
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
setSites(sites);
}, []);
const handleSyncTest = async (e) => { const handleSyncTest = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
setLoading(true); setLoading(true);
await syncWebfix(process.env.REACT_APP_WEBFIXURL); await syncWebfix(process.env.REACT_APP_WEBFIXURL);
await loadSites();
alert.success(i18n("sync_success")); alert.success(i18n("sync_success"));
} catch (err) { } catch (err) {
console.log("[sync webfix]", err); console.log("[sync webfix]", err);
@@ -93,15 +99,14 @@ export default function Webfix() {
(async () => { (async () => {
try { try {
setLoading(true); setLoading(true);
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL); await loadSites();
setSites(sites);
} catch (err) { } catch (err) {
console.log("[load webfix]", err.message); console.log("[load webfix]", err.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
})(); })();
}, []); }, [loadSites]);
return ( return (
<Box> <Box>

View File

@@ -19,6 +19,7 @@ import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import Apis from "./Apis"; import Apis from "./Apis";
import Webfix from "./Webfix"; import Webfix from "./Webfix";
import InputSetting from "./InputSetting";
export default function Options() { export default function Options() {
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -36,7 +37,7 @@ export default function Options() {
// 检查版本是否一致 // 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) { if (version !== process.env.REACT_APP_VERSION) {
setError( setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.` `The version is inconsistent, please check whether the script(v${version}) is the latest version(v${process.env.REACT_APP_VERSION}). (版本不一致,请检查脚本(v${version})是否为最新版(v${process.env.REACT_APP_VERSION}))`
); );
break; break;
} }
@@ -53,7 +54,7 @@ export default function Options() {
} }
if (++i > 8) { if (++i > 8) {
setError("Time out."); setError("Time out. (连接超时)");
break; break;
} }
@@ -78,26 +79,30 @@ export default function Options() {
</Divider> </Divider>
<h2> <h2>
Please confirm whether to install or enable KISS Translator Please confirm whether to install or enable KISS Translator
GreaseMonkey script? GreaseMonkey script? (请检查是否安装或启用简约翻译油猴脚本)
</h2> </h2>
<Stack spacing={2}> <Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}> <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1 Install Userscript for Tampermonkey/Violentmonkey 1 (油猴脚本
安装地址 1)
</Link> </Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}> <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2 Install Userscript for Tampermonkey/Violentmonkey 2 (油猴脚本
安装地址 2)
</Link> </Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}> <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1 Install Userscript for iOS Safari 1 (油猴脚本 iOS Safari专用
安装地址 1)
</Link> </Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}> <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2 Install Userscript for iOS Safari 2 (油猴脚本 iOS Safari专用
安装地址 2)
</Link> </Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}> <Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1 Open Options Page 1 (打开设置页面 1)
</Link> </Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}> <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2 Open Options Page 2 (打开设置页面 2)
</Link> </Link>
</Stack> </Stack>
</center> </center>
@@ -126,6 +131,7 @@ export default function Options() {
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Setting />} /> <Route index element={<Setting />} />
<Route path="rules" element={<Rules />} /> <Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="apis" element={<Apis />} /> <Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} /> <Route path="webfix" element={<Webfix />} />

View File

@@ -21,7 +21,12 @@ export default function Header({ setShowPopup }) {
<IconButton onClick={handleHomepage}> <IconButton onClick={handleHomepage}>
<HomeIcon /> <HomeIcon />
</IconButton> </IconButton>
<Box> <Box
sx={{
userSelect: "none",
WebkitUserSelect: "none",
}}
>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`} {`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box> </Box>
</Stack> </Stack>

View File

@@ -5,7 +5,7 @@ import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg"; import { sendTabMsg, getTabInfo } from "../../libs/msg";
import { browser } from "../../libs/browser"; import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client"; import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -24,6 +24,7 @@ import {
CACHE_NAME, CACHE_NAME,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
export default function Popup({ setShowPopup, translator: tran }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
@@ -77,6 +78,19 @@ export default function Popup({ setShowPopup, translator: tran }) {
} }
}; };
const handleSaveRule = async () => {
try {
let href = window.location.href;
if (isExt) {
const tab = await getTabInfo();
href = tab.url;
}
saveRule({ ...rule, pattern: href });
} catch (err) {
console.log("[save rule]", err);
}
};
useEffect(() => { useEffect(() => {
if (!isExt) { if (!isExt) {
return; return;
@@ -218,9 +232,19 @@ export default function Popup({ setShowPopup, translator: tran }) {
/> />
)} )}
<Button variant="text" onClick={handleOpenSetting}> <Stack
{i18n("setting")} direction="row"
</Button> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack> </Stack>
</Box> </Box>
); );